Skip to content

perf: simplify decoding logic#632

Merged
KtorZ merged 2 commits into
mainfrom
eh2406/decode
Jan 9, 2026
Merged

perf: simplify decoding logic#632
KtorZ merged 2 commits into
mainfrom
eh2406/decode

Conversation

@Eh2406

@Eh2406 Eh2406 commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

The first commit was noticed while doing memory profiling. missing_field was preemptively constructing the error message in case a field was missing, even though most of the time it turned out to be present. format! is surprisingly expensive, so reducing its use on the hot path was a clear win.

With that fixed, the Box<fn ...> in the PartialDecoder also looked suspicious. It was being used only to track whether the fields of a Struct had been read from CBOR yet and whether a missing field should be an error or a default value. Option can work keeping track of this info. Using the Builder Pattern might be an even more idiomatic solution in this case, but this change seemed the least invasive.

These kinds of short lived allocations sometimes anger allocators. Having now made these changes and tested the result, no connection. These changes do not make a measurable difference to peak memory usage or throughput. 🙁 It does reduce the number of allocations and thereby the overhead of using memory profiling tools. It should not be too hard to collect quantitative results if requested.

Summary by CodeRabbit

  • Refactor

    • Reworked transaction-output decoding internals to use explicit per-field optional values and removed legacy partial-decoder helpers; public APIs adjusted accordingly.
    • Error handling now yields concrete decode errors immediately rather than deferred error creators.
    • Strengthened validation to enforce presence of required transaction attributes.
  • Documentation

    • Updated examples and module docs to reflect the new option-based decoding approach and API surface.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: Jacob Finkelman <YeomanYaacov@gmail.com>
@coderabbitai

coderabbitai Bot commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Switches from a boxed-closure PartialDecoder pattern to an Option-per-field decoding model for heterogeneous CBOR maps; updates decoding sites to assign Some(...) for address, value, datum, and script, and converts missing-field handling to return concrete CBOR decode errors by field index.

Changes

Cohort / File(s) Summary
CBOR Decode API Refactor
crates/amaru-minicbor-extra/src/decode.rs
Removes PartialDecoder<A>, decode_chunk, and with_default_value; changes missing_field to take a u8 and return cbor::decode::Error; refactors heterogeneous_map example and public API to use Option per-field instead of boxed decoders.
Transaction Output Decoding Update
crates/amaru-kernel/src/memoized/transaction_output.rs
Replaces default/with_default_value initialization with None-based fields; assigns Some(...) directly for address, value, datum, and script during decoding; uses ok_or_else(missing_field) for required fields and removes import/usage of with_default_value.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

✨ Boxes popped, options bloom in line,
Fields now show up — no phantom design,
Errors named by index, tidy and neat,
Decoding walks steady, rhythm and beat,
🎮 Cheers to cleaner builds — roll camera, let’s ship!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf: simplify decoding logic' directly summarizes the main change: refactoring the decoding mechanism to reduce allocations and complexity by replacing PartialDecoder with Option-based tracking.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f102f56 and 8569655.

📒 Files selected for processing (2)
  • crates/amaru-kernel/src/memoized/transaction_output.rs
  • crates/amaru-minicbor-extra/src/decode.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/amaru-kernel/src/memoized/transaction_output.rs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-16T21:32:37.668Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 584
File: crates/amaru-network/src/handshake/tests.rs:40-47
Timestamp: 2025-12-16T21:32:37.668Z
Learning: In Rust, shadowing a binding with a new let does not drop the previous binding until the end of the scope. All shadowed bindings in a scope are dropped in reverse-declaration order when the scope ends. Therefore, multiple let _guard = register_*() calls will keep all guards alive until the end of the function (or the surrounding scope). When reviewing code, be mindful that resources tied to shadowed bindings persist longer than the most recent binding; to release early, constrain the lifetime in an inner block or explicitly drop guards when appropriate.

Applied to files:

  • crates/amaru-minicbor-extra/src/decode.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Build aarch64/macos
  • GitHub Check: Build riscv32
  • GitHub Check: Test the CLI
  • GitHub Check: Analyze (rust)
  • GitHub Check: Build wasm32
  • GitHub Check: Build x86_64/windows
  • GitHub Check: Test coverage
  • GitHub Check: Benches
  • GitHub Check: clippy
🔇 Additional comments (4)
crates/amaru-minicbor-extra/src/decode.rs (4)

81-100: Ripper documentation, mate!

The example code is spot-on and shows the new Option-based pattern clear as day. Love how it demonstrates the state mutation approach with Some(...) assignments - makes it dead simple for folks to grok how to use this API. No dramas here!


288-303: Beauty! This refactor is clean as a whistle.

The new Option-based pattern is much easier to follow than the old PartialDecoder approach. The ok_or_else + missing_field combo at lines 301-302 perfectly demonstrates the lazy error construction - no wasted allocations unless something's actually missing. This is the kind of code that makes memory profilers happy, mate!


314-326: G'day, this is brilliantly simple!

The default values pattern is even cleaner than I expected. Just bung the defaults in the initial state (line 314), let field decoding overwrite when present, and Bob's your uncle - no need for ok_or_else ceremony at all. Line 326 shows the payoff: direct field access, no unwrapping, no boxing. This is like going from a Rube Goldberg machine to a straight pipe - and I'm here for it!


126-133: Legend status: confirmed, mate!

The lazy eval pattern absolutely slaps here—format! now only fires when a field is genuinely missing, which keeps those hot-path allocations down. Proper stuff.

And yeah, the field_tag restriction to u8 is spot on. All the call sites are already passing u8 literals (0, 1, etc.)—in Foo, MemoizedTransactionOutput, and across the board. It's not limiting future use cases; it's matching how CBOR maps actually work in this codebase. Think of it like locking down the API to what it actually needs, like how Dark Souls doesn't let you parry with a katana in both hands—sometimes constraints make the design cleaner.

No red flags here. Ship it.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/amaru-minicbor-extra/src/decode.rs (1)

284-305: Hold up—missing the Some wrapper here, chief.

Lines 292-293 show direct assignments like state.0 = d.decode_with(ctx)?, but your initial state at line 288 is (None::<u8>, None::<u8>), meaning both fields are Option<u8>. To set an Option to a value, you need Some(...) wrapping, not direct assignment.

Looking at the other file (transaction_output.rs line 105), the pattern is state.0 = Some(decode_address(...)), which is the correct approach.

🐛 Proposed fix: Wrap decoded values in Some
                    match field {
-                       0 => state.0 = d.decode_with(ctx)?,
-                       1 => state.1 = d.decode_with(ctx)?,
+                       0 => state.0 = Some(d.decode_with(ctx)?),
+                       1 => state.1 = Some(d.decode_with(ctx)?),
                        _ => return unexpected_field::<Foo, _>(field),
                    }
crates/amaru-kernel/src/memoized/transaction_output.rs (1)

99-122: Decoding logic is solid, but there's some redundant syntax to clean up.

The Option-based approach here is implemented correctly:

  • Required fields (address, value) start as None, get wrapped in Some(...), then validated with ok_or_else
  • The datum field uses MemoizedDatum::None as its default and gets assigned directly
  • Optional script field follows the same None/Some pattern

However, the pipeline caught some redundant field names in your struct initialization at lines 119-120. When the variable name matches the field name, Rust lets you use the shorthand syntax.

✨ Proposed fix: Use field init shorthand
    Ok(MemoizedTransactionOutput {
        is_legacy: false,
        address: address.ok_or_else(|| missing_field::<MemoizedTransactionOutput, Address>(0))?,
        value: value.ok_or_else(|| missing_field::<MemoizedTransactionOutput, Value>(1))?,
-       datum: datum,
-       script: script,
+       datum,
+       script,
    })
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4bc1d6d and f102f56.

📒 Files selected for processing (2)
  • crates/amaru-kernel/src/memoized/transaction_output.rs
  • crates/amaru-minicbor-extra/src/decode.rs
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-08-08T14:35:35.562Z
Learnt from: KtorZ
Repo: pragma-org/amaru PR: 370
File: crates/amaru-kernel/src/transaction_pointer.rs:36-44
Timestamp: 2025-08-08T14:35:35.562Z
Learning: In the amaru project, when decoding CBOR arrays, prefer using minicbor_extra::heterogenous_array with the expected length to validate definite-length arrays and correctly handle indefinite-length arrays. Example: crates/amaru-kernel/src/transaction_pointer.rs Decode should use heterogenous_array(d, 2, …) instead of ignoring the length from d.array().

Applied to files:

  • crates/amaru-minicbor-extra/src/decode.rs
  • crates/amaru-kernel/src/memoized/transaction_output.rs
📚 Learning: 2025-08-08T14:34:06.105Z
Learnt from: KtorZ
Repo: pragma-org/amaru PR: 370
File: crates/minicbor-extra/src/lib.rs:50-55
Timestamp: 2025-08-08T14:34:06.105Z
Learning: Project uses Rust 1.88 stable; using Result::is_err_and is acceptable and should not be flagged as unstable. In particular, crates/minicbor-extra/src/lib.rs relies on is_err_and when checking end_of_input; future reviews should not suggest replacing it for stability reasons.

Applied to files:

  • crates/amaru-minicbor-extra/src/decode.rs
📚 Learning: 2025-08-23T15:44:44.318Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 392
File: crates/ouroboros-traits/src/is_header/fake.rs:52-88
Timestamp: 2025-08-23T15:44:44.318Z
Learning: In the Amaru project, FakeHeader in crates/ouroboros-traits/src/is_header/fake.rs is specifically designed for testing purposes, and panic behavior (like copy_from_slice() panicking on malformed hex) is the preferred approach rather than graceful error handling, as it helps identify test data issues quickly during development.

Applied to files:

  • crates/amaru-minicbor-extra/src/decode.rs
📚 Learning: 2025-04-20T17:57:23.233Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 149
File: crates/amaru/src/stages/consensus/chain_forward/test_infra.rs:272-285
Timestamp: 2025-04-20T17:57:23.233Z
Learning: In test infrastructure code, rkuhn prefers explicit panics (using .unwrap() or similar) over returning Result types, as test failures should be immediate and obvious.

Applied to files:

  • crates/amaru-minicbor-extra/src/decode.rs
📚 Learning: 2025-12-16T21:32:37.668Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 584
File: crates/amaru-network/src/handshake/tests.rs:40-47
Timestamp: 2025-12-16T21:32:37.668Z
Learning: In Rust, shadowing a binding with a new let does not drop the previous binding until the end of the scope. All shadowed bindings in a scope are dropped in reverse-declaration order when the scope ends. Therefore, multiple let _guard = register_*() calls will keep all guards alive until the end of the function (or the surrounding scope). When reviewing code, be mindful that resources tied to shadowed bindings persist longer than the most recent binding; to release early, constrain the lifetime in an inner block or explicitly drop guards when appropriate.

Applied to files:

  • crates/amaru-minicbor-extra/src/decode.rs
  • crates/amaru-kernel/src/memoized/transaction_output.rs
🪛 GitHub Actions: Coding Practices
crates/amaru-kernel/src/memoized/transaction_output.rs

[error] 119-119: redundant field names in struct initialization. replace 'datum: datum' with 'datum'.


[error] 120-120: redundant field names in struct initialization. replace 'script: script' with 'script'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: End-to-end snapshot tests (preprod, 1, 182, 15, 10.5.3)
  • GitHub Check: End-to-end snapshot tests (preview, 2, 680, true, 15, 10.5.3)
  • GitHub Check: Build aarch64/macos
  • GitHub Check: Build x86_64/linux
  • GitHub Check: Test the CLI
  • GitHub Check: Build aarch64/linux
  • GitHub Check: Build riscv32
  • GitHub Check: Build x86_64/windows
  • GitHub Check: Benches
  • GitHub Check: Build wasm32
  • GitHub Check: Test coverage
  • GitHub Check: Analyze (rust)
🔇 Additional comments (5)
crates/amaru-minicbor-extra/src/decode.rs (4)

81-100: Documentation's spot on, mate!

The updated example clearly shows the new Option-based flow. It's like going from a convoluted Rube Goldberg machine to a straight shot—much cleaner for future devs reading this.


150-153: Test imports cleaned up nicely.

Gone are the old PartialDecoder helpers, in with the simplified API. Like Marie Kondo-ing your imports—only keeping what sparks joy (and compiles).


310-328: Default values pattern looks bang on.

This test shows the alternative approach: start with concrete defaults (14_u8, 42_u8) and overwrite them during decoding. No Options needed since you've got fallback values from the jump. Clean implementation for optional fields with defaults.


126-133: The API narrowing concern doesn't hold—all call sites are already passing u8 values.

Fair point to double-check, but you can rest easy on this one. I've traced all the call sites of missing_field across the codebase, and they're all feeding it u8 literals (0, 1, etc.). Two instances in the test suite and two more in transaction_output.rs—all u8, all day. No strings, no fancy enums, no shenanigans.

So while the parameter type restriction is technically narrower on paper, it's not a practical breaking change for the existing code. The optimization lands cleanly without tripping up the rest of the codebase.

crates/amaru-kernel/src/memoized/transaction_output.rs (1)

21-21: Imports aligned with the new decoding API.

Out with the old PartialDecoder helpers, in with the streamlined Option-based approach. Straightforward migration.

@codecov

codecov Bot commented Jan 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
...es/amaru-kernel/src/memoized/transaction_output.rs 72.32% <100.00%> (-0.51%) ⬇️
crates/amaru-minicbor-extra/src/decode.rs 97.31% <100.00%> (-0.11%) ⬇️

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@KtorZ KtorZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

sigh ... So it's all indeed a lie when they say Rust is functional programming language; because it always resort to mutating structures rather than composing functions :(

Thanks for ALSO updating the docs 🫶

@KtorZ KtorZ merged commit b246ee0 into main Jan 9, 2026
22 checks passed
@KtorZ KtorZ deleted the eh2406/decode branch January 9, 2026 08:20
@coderabbitai coderabbitai Bot mentioned this pull request Jan 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants