Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple Mapping Storage Primitive #946

Merged
merged 42 commits into from
Nov 15, 2021
Merged

Conversation

HCastano
Copy link
Contributor

@HCastano HCastano commented Sep 29, 2021

An implementation of what's described in #945.

Some things I'm still unsure about:

1. The handling of Mapping's key field
Not sure if this should be user initialized since it's kinda an implementation detail
which is there to make sure we don't have any collisions in storage.

2. The SpreadLayout implementation
So from what I can tell, when we implement this trait we're supposed to indicate how our
structure should be represented in the contract storage. In the case of Mapping, since
it doesn't actually own any data, its really just a proxy for how we represent Key in
storage. Am I thinking about this correctly?

@HCastano HCastano added the A-ink_storage [ink_storage] Work Item label Sep 29, 2021
crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
@HCastano
Copy link
Contributor Author

So this PR isn't quite ready yet, but I'm still at a point where I can use Mapping in the ERC-20 example and have it compile (this doesn't necessarily means it runs correctly on chain). The size improvements are great:

Master:  Original wasm size: 68.0K, Optimized: 28.8K
This PR: Original wasm size: 37.8K, Optimized: 12.6K

They're basically the same as @xgreenx's SimpleHashMap implementation, although this isn't surprising because our implementations are really similar. Gonna need to do some more thinking about where we can lose some extra bytes 😉

@xgreenx
Copy link
Collaborator

xgreenx commented Sep 30, 2021

You are not supporting len and your storage key is not optional, so your implementation takes less space=) From ERC-20 example you can remove usage of Lazy and it will save 1.3 KB=)

I think it is better to return an Option<V> from the get method because in this case, the developer will be available to check if the object exists or not. Because in Solidity in some cases it was a headache.

@Robbepop
Copy link
Collaborator

Robbepop commented Oct 4, 2021

Compiling the adjusted ERC-20 with cargo contract build yields 12.6kB.
Exchanging the Balance type from u128 to u64 or u32 we get down to 12.1kB.
Exchanging the AccountId from [u8; 32] to [u8; 8] we get down to 9.7kB.

This means that a majority of the cluft comes from copying the huge AccountId type around.
I see that especially in calls to the allowance function that takes 2 parameters by value and turns them into a tuple-packed reference.

Ideally we can introduce some traits in order to allow for use cases like these without requiring value types anywhere.
E.g. making &(U, V) equal to (&U, &V) in those cases so we could avoid a copy there.

edit:
With a minor tweak to the current implementation and setting Balance to u32 and AccountId to [u8; 4] (32 bit) the generated Wasm is able to efficiently pack the &(AccountId, Balance) expression leading to a Wasm file size of just 8.6kB. This underlines that the actual offender in Wasm file size is the huge AccountId. If we succeed in using references (32 bit on Wasm) more in places where we currently handling around AccountId instances I think we can win a lot of ground.

Copy link
Collaborator

@Robbepop Robbepop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Mapping is missing some trivial Debug implementation:

impl<K, V> core::fmt::Debug for Mapping<K, V> {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        f.debug_struct("Mapping").finish()
    }
}

crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
crates/storage/src/collections/mapping/mod.rs Outdated Show resolved Hide resolved
HCastano and others added 4 commits November 4, 2021 18:35
* Add `Mapping` storage collection

* Implement `insert` and `get` for `Mapping`

* Implement `SpreadLayout` for `Mapping`

* Fix typo

* Add some basic tests

* Fix some documentation formatting

* Use `PackedLayout` as trait bound instead of `Encode/Decode`

* Avoid using low level `ink_env` functions when interacting with storage

* RustFmt

* Appease Clippy

* Only use single `PhantomData` field

* Change `get` API to take reference to `key`

* Implement `TypeInfo` and `StorageLayout` for `Mapping`

* Properly gate `TypeInfo` and `StorageLayout` impls behind `std`

* Replace `HashMap` with `Mapping` in ERC-20 example

* Return `Option` from `Mapping::get`

* Update ERC-20 to handle `Option` returns

* Change `get` and `key` to use `Borrow`-ed values

* Add `Debug` and `Default` implementations

* Proper spelling

* Change `insert` to only accept borrowed K,V pairs

* Update ERC-20 example accordingly

* Make more explicit what each `key` is referring to

* Try using a `RefCell` instead of passing `Key` around

* Try using `UnsafeCell` instead

* Revert "Try using a `RefCell` instead of passing `Key` around"

This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.

* Clean up some of the documentation

* adjust the Mapping type for the new SpreadAllocate trait

* adjust ERC-20 example for changes in Mapping type

* remove commented out code

* add doc comment to new_init

* make it possible to use references in more cases with Mapping

* use references in more cases for ERC-20 example contract

* remove unnecessary references in Mapping methods

* refactor/improve pull_packed_root_opt utility method slightly

* fix ERC-20 example contract

The problem with *self.total_supply is that it may implicitly read from storage in case it has not yet read a value from storage whereas Lazy::set just writes the value to the Lazy instance.

Co-authored-by: Hernando Castano <hernando@hcastano.com>
Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>
@codecov-commenter
Copy link

codecov-commenter commented Nov 9, 2021

Codecov Report

Merging #946 (5e5a918) into master (c7cc828) will decrease coverage by 0.15%.
The diff coverage is 89.47%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #946      +/-   ##
==========================================
- Coverage   78.86%   78.71%   -0.16%     
==========================================
  Files         245      246       +1     
  Lines        9246     9260      +14     
==========================================
- Hits         7292     7289       -3     
- Misses       1954     1971      +17     
Impacted Files Coverage Δ
crates/storage/src/lazy/mod.rs 85.29% <ø> (ø)
crates/storage/src/traits/optspec.rs 90.00% <66.66%> (-6.56%) ⬇️
crates/storage/src/lazy/mapping.rs 100.00% <100.00%> (ø)
...tes/storage/src/collections/bitstash/fuzz_tests.rs 40.00% <0.00%> (-60.00%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c7cc828...5e5a918. Read the comment docs.

@@ -97,6 +97,7 @@ impl Storage<'_> {
derive(::ink_storage::traits::StorageLayout)
)]
#[derive(::ink_storage::traits::SpreadLayout)]
#[derive(::ink_storage::traits::SpreadAllocate)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First: You cannot do this since not all #[ink(storage)] structs should implement SpreadAllocate which is why it is separate from SpreadLayout and has not been integrated into the already existing traits.
Second: If this was actually useful (which it isn't) it should reside in another PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ink! codegen to automatically implement SpreadAllocate optionally we'd need support for Rust trivial trait bounds which currently are unstable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. I thought that the idea behind merging #995 before the Mapping was so we could derive SpreadAllocate for storage types in the codegen instead of manually deriving it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Rust had trivial trait bounds we actually could do exactly that ... :(

@@ -59,20 +61,19 @@ mod erc20 {
/// Creates a new ERC-20 contract with the specified initial supply.
#[ink(constructor)]
pub fn new(initial_supply: Balance) -> Self {
ink_lang::codegen::initialize_contract(|contract| Self::new_init(contract, initial_supply))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Robbepop using the codegen module here feels wrong, is there a better way to expose this to users?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual plan is still that we provide some useful and well designed syntactic sugar so that this line of code won't be necessary but maybe that's not well thought through and maybe writing out this line of code actually isn't so bad. Then we could put this into some other module or into root.

@HCastano
Copy link
Contributor Author

HCastano commented Nov 9, 2021

FYI, with the additional bloat from #998 we're currently sitting at 12.1K.

@Robbepop
Copy link
Collaborator

Robbepop commented Nov 9, 2021

FYI, with the additional bloat from #998 we're currently sitting at 12.1K.

The bloat can easily be fixed with another PR and more intelligent codegen.

@xgreenx
Copy link
Collaborator

xgreenx commented Nov 9, 2021

I tried to build this change and #999.

Erc20 with `Lazy` for `total_supply`:
Original wasm size: 34.1K, Optimized: 10.9K
Erc20 without `Lazy`:
Original wasm size: 33.6K, Optimized: 10.5K

@xgreenx
Copy link
Collaborator

xgreenx commented Nov 10, 2021

Some other tests:

This + use-ink/cargo-contract#358: Original wasm size: 35.4K, Optimized: 11.7K
This + use-ink/cargo-contract#358 + #1012: Original wasm size: 34.3K, Optimized: 10.6K
This + #1012: Original wasm size: 35.2K, Optimized: 11.0K
This + #999 + use-ink/cargo-contract#358: Original wasm size: 33.2K, Optimized: 10.4K. In this case, #1012 doesn't cause any effect on Erc20.

@HCastano
Copy link
Contributor Author

@xgreenx I haven't had time to loop back into this, so thanks for your experiments!

@HCastano HCastano marked this pull request as ready for review November 11, 2021 17:49
@HCastano
Copy link
Contributor Author

I think we can probably merge this as-is, and fix anything in follow ups (like not using the codegen module directly). Having this in master would allow us to start doing more experiments with the Mapping

@Robbepop
Copy link
Collaborator

I would like to see the new Mapping type under ink_storage::lazy instead of ink_storage::collections module before we merge this.

push_packed_root,
ExtKeyPtr,
KeyPtr,
PackedLayout,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@HCastano HCastano merged commit 7432565 into master Nov 15, 2021
@HCastano HCastano deleted the hc-simple-mapping-primitive branch November 15, 2021 10:59
xgreenx pushed a commit to Supercolony-net/ink that referenced this pull request Feb 8, 2022
* Add `Mapping` storage collection

* Implement `insert` and `get` for `Mapping`

* Implement `SpreadLayout` for `Mapping`

* Fix typo

* Add some basic tests

* Fix some documentation formatting

* Use `PackedLayout` as trait bound instead of `Encode/Decode`

* Avoid using low level `ink_env` functions when interacting with storage

* RustFmt

* Appease Clippy

* Only use single `PhantomData` field

* Change `get` API to take reference to `key`

* Implement `TypeInfo` and `StorageLayout` for `Mapping`

* Properly gate `TypeInfo` and `StorageLayout` impls behind `std`

* Replace `HashMap` with `Mapping` in ERC-20 example

* Return `Option` from `Mapping::get`

* Update ERC-20 to handle `Option` returns

* Change `get` and `key` to use `Borrow`-ed values

* Add `Debug` and `Default` implementations

* Proper spelling

* Change `insert` to only accept borrowed K,V pairs

* Update ERC-20 example accordingly

* Make more explicit what each `key` is referring to

* Try using a `RefCell` instead of passing `Key` around

* Try using `UnsafeCell` instead

* Revert "Try using a `RefCell` instead of passing `Key` around"

This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.

* Clean up some of the documentation

* Simple Mapping type improvements (use-ink#979)

* Add `Mapping` storage collection

* Implement `insert` and `get` for `Mapping`

* Implement `SpreadLayout` for `Mapping`

* Fix typo

* Add some basic tests

* Fix some documentation formatting

* Use `PackedLayout` as trait bound instead of `Encode/Decode`

* Avoid using low level `ink_env` functions when interacting with storage

* RustFmt

* Appease Clippy

* Only use single `PhantomData` field

* Change `get` API to take reference to `key`

* Implement `TypeInfo` and `StorageLayout` for `Mapping`

* Properly gate `TypeInfo` and `StorageLayout` impls behind `std`

* Replace `HashMap` with `Mapping` in ERC-20 example

* Return `Option` from `Mapping::get`

* Update ERC-20 to handle `Option` returns

* Change `get` and `key` to use `Borrow`-ed values

* Add `Debug` and `Default` implementations

* Proper spelling

* Change `insert` to only accept borrowed K,V pairs

* Update ERC-20 example accordingly

* Make more explicit what each `key` is referring to

* Try using a `RefCell` instead of passing `Key` around

* Try using `UnsafeCell` instead

* Revert "Try using a `RefCell` instead of passing `Key` around"

This reverts commit cede033.

Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than
what we have now, and it introduced `unsafe` code. We believe the
limiting factor here is the `Key` type definition anyways.

* Clean up some of the documentation

* adjust the Mapping type for the new SpreadAllocate trait

* adjust ERC-20 example for changes in Mapping type

* remove commented out code

* add doc comment to new_init

* make it possible to use references in more cases with Mapping

* use references in more cases for ERC-20 example contract

* remove unnecessary references in Mapping methods

* refactor/improve pull_packed_root_opt utility method slightly

* fix ERC-20 example contract

The problem with *self.total_supply is that it may implicitly read from storage in case it has not yet read a value from storage whereas Lazy::set just writes the value to the Lazy instance.

Co-authored-by: Hernando Castano <hernando@hcastano.com>
Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>

* Use new `initialize_contract()` function

* Derive `SpreadAllocate` for `ink(storage)` structs

* Stop manually implementing SpreadAllocate for ERC-20

* Stop implementing `SpreadAllocate` in the storage codegen

* Derive `SpreadAllocate` manually for ERC-20

* RustFmt example

* Move `Mapping` from `collections` to `lazy`

* Remove extra `0` in docs

Co-authored-by: Robin Freyler <robin.freyler@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ink_storage [ink_storage] Work Item
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants