perf(storage): compact PieceV2 storage - #292
Conversation
063dea8 to
1145d1f
Compare
|
Benchmark results: Optimised gas reduction versus baseline:
Flat gas used
The optimisation largely removes ballast sensitivity: at 5M slots, batch 32 falls from 2.260B to 200.3M gas (91.1%, or 11.3× lower). Batch 64 is excluded because both versions revert at the Lotus event-size limit. Benchmarks were performed in foc-devnet, ballasting down the PDPVerifier tree with the added dev-only |
| @@ -0,0 +1,359 @@ | |||
| ## Recommendation | |||
There was a problem hiding this comment.
I plan to remove this doc. It was generated as part of LLM task guidance.
|
@rvagg @wjmelements I would appreciate an early review. I don't expect the logic to change much, but it will get way messier when I make this backwards compatible. |
But I think we can fix that here can't we because (a) we're emitting those events with uncapped arrays of piece CIDs and (b) the piece CID we're emitting are the larger legacy format, not the compact 64-byte format; so we could go quite high, at least for the PDPVerifier component (FWSS will impose its own limits), right? |
|
|
||
| offset = paddingOffset; | ||
| require(offset < cid.data.length, "CommPv2 digest is too short"); | ||
| height = uint8(cid.data[offset++]); |
There was a problem hiding this comment.
Solidity does a lot of unnecessary checks unless you tell it not to. For example, did you know offset++ will do a uint256 overflow check?
| offset++; | ||
| offset = multihashOffset; | ||
| uint256 paddingOffset; | ||
| (padding, paddingOffset) = _readUvarint(cid.data, offset); |
There was a problem hiding this comment.
This offset is added to the data's offset, but you can instead iterate using a raw pointer. Then instead of add(add(, it would just be root := mload(offset). I have an example of this with calldata in fvm-solidity (_cdReadArrayHeader) and also with memory (_writeCborArrayHeader).
For example, you would replace height = uint8(cid.data[offset++]) with
height := byte(0, mload(offset))
offset := add(1, offset)
There was a problem hiding this comment.
Thanks for the suggestion. I'm reluctant to implement more of asm optimisations here, as correctness matters and readability suffers significantly.
There was a problem hiding this comment.
Correctness is checked by testing, and you should have good test coverage before doing any optimization.
Coming from C, I think one pointer parameter is more readable than two.
| while (data[offset + i] >= 0x80) { | ||
| // Helper function reading uvarints <= 256 bits. | ||
| // Returns (value, offset) with offset advanced to the following byte. | ||
| function _readUvarint(bytes memory data, uint256 offset) internal pure returns (uint256 value, uint256 newOffset) { |
There was a problem hiding this comment.
I have a prior assembly implementation of a ULEB128 read in _getOwnerActorId in fvm-solidity, (though it can assume the encoded number is a uint64)
There was a problem hiding this comment.
Let's call it that instead of Uvarint so the endianness will be clear in the method name
There was a problem hiding this comment.
varint is term used everywhere within the multihash ecosystem
LEB128 is less specific: https://github.com/multiformats/unsigned-varint
and ULEB128 is not really an established thing
| // Test-only state used to reproduce the deployed contract's storage-tree size on a local devnet. | ||
| mapping(uint256 => uint256) private balastSlots; |
|
I like this a lot. It's a huge improvement. You can perhaps provide a way to permissionlessly migrate, and once we are sure everything is migrated (which can be checked with a script), we can drop the old path. |
I was thinking about leaving old datasets as they are and supporting only new datasets. Otherwise, we would have to keep track of datasets which have been migrated, partial migration states, so on. |
Yeah that might be necessary, but I think it can be piecewise. The backwards compatibility is already going to need to be able to distinguish these states. Distinguishing piecewise might be less overhead than distinguishing by data set for individual pieces, but on the other hand there might be savings if you can assume an entire data set is one way or the other. I didn't notice such a situation in my brief review yesterday though. |
ae229f9 to
9c76672
Compare
|
The code is now backwards compatible through a datasetID cutoff. I did some additional backward-compatibility checks beyond what is in tests.
I didn’t find any backward-compatibility issues in these checks. |
|
I ran a Filecoin snapshot benchmark comparing the deployed legacy storage layout with the optimized compact layout using 1,024-piece datasets. Removals are applied during Removal lifecycle
The 96-piece case is the largest tested with adequate headroom: the legacy implementation already consumes 7.14 billion gas against Filecoin’s 10-billion block limit. Other important measurements
The only notable regressions are the single-piece removal scheduling case at +5.3% and |
Record storage reads, writes, KAMT object locality, and persistent slot growth for additions to an existing data set. This establishes the baseline for the compact piece representation work. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Document the compact PieceV2 design and split the prototype into sequential, agent-ready implementation digests with explicit invariants, acceptance criteria, verification, and handoff requirements. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Reject ambiguous and malformed CID encodings before they can be stored or used for proof verification. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Append the two-slot compact piece mapping and centralize bounded metadata packing before production paths adopt the new storage. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Store newly added pieces in compact slots with their final Fenwick sums, while batching the dataset leaf-count update.\n\nThis removes legacy addition writes ahead of migrating readers and removers. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Serve piece getters, pagination, CID search, and deletion scheduling from compact metadata while preserving canonical CID output and mapping-style getter defaults. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Adapt behavioral and raw-storage tests to compact piece records, including Fenwick sums and cleanup reclamation. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Run generation after layout cleanup so make cannot evaluate generated targets before their files are removed. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Measure isolated 1, 4, 16, and 32-piece additions and lock the observed storage activity. Document the compact baseline, slot reductions, and KAMT alignment behavior. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Keep the consolidated measurement comparison and align the four-piece KAMT baseline with the revised measurement method. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Add deterministic storage population for reproducing the deployed verifier's large state tree during Filecoin gas benchmarks. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Keep compact-storage coverage while preserving the established sum-tree names in tests. Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
Signed-off-by: Jakub Sztandera <oss@kubuxu.com>
d01f0e1 to
691aa6b
Compare
| PieceV2 storage piece = compactPieces[setId][index]; | ||
| piece.metadata = | ||
| index == removedIndex ? piece.metadata.withSum(sum).clearExceptSum() : piece.metadata.withSum(sum); | ||
| } |
There was a problem hiding this comment.
This is a bit ugly because it is where we clear out the piece's metadata, overloading sumTreeRemove to also mark the piece as removed.
|
I also have an additional optimisation of |
Summary
Implementation of the compact two-slot
PieceV2storage prototype, related to #286.New compact-state datasets store pieces in a contiguous per-dataset array:
metadatapacks padding, tree height, leaf count, and the Fenwick partial sum into one storage slot.Changes
PieceCIDv2validation and decoded(padding, height, root)in one pass.PieceV2storage while retaining the existing declarations for physical layout safety.addPiecesstorage activity for 1, 4, 16, and 32-piece batches.Storage measurements
Each scenario creates a fresh dataset, adds one seed piece before recording, then records the measured
addPiecescall.Values are
legacy → compact.Findings
5N → 2Nnewly occupied slots.7Nto3N + 1; reduction grows from 42.9% at one piece to 56.7% at 32 pieces.At 32 pieces, the compact layout occupies 64 slots instead of 160, touches 9 KAMT objects instead of 137, and modifies 5 instead of 132.