Skip to content

fix: remove clones and allocations when tracing sync effects#552

Merged
rkuhn merged 5 commits into
mainfrom
rk/improve-external-sync-effects
Nov 19, 2025
Merged

fix: remove clones and allocations when tracing sync effects#552
rkuhn merged 5 commits into
mainfrom
rk/improve-external-sync-effects

Conversation

@rkuhn

@rkuhn rkuhn commented Nov 11, 2025

Copy link
Copy Markdown
Contributor

This improves type-safety by only allowing effects marked with trait ExternalEffectSync to be
passed to Effects::external_sync() and removes the Clone bound. Previously, the trace would also
only record the last sync effect within a stage step (i.e. between .await points).

Summary by CodeRabbit

  • Refactor

    • Replaced prior external-sync plumbing with a unified trace-buffer-driven mechanism, simplifying simulation runtime and stage wiring.
  • New Features

    • External effects gain a synchronous execution path that no longer requires cloneable responses.
    • Trace recording now captures external suspend/resume with new external suspend/resume serialization and a shared trace buffer for replay/inspection.
  • Style/Docs

    • Added a marker trait and public re-export to document synchronous external-effect usage.
  • Tests

    • Added unit tests validating external suspend/resume serialization paths.

Signed-off-by: Roland Kuhn <rk@rkuhn.info>
@coderabbitai

coderabbitai Bot commented Nov 11, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

This PR replaces the SyncEffectBox external-sync path with a trace-buffer-driven flow, introduces ExternalEffectSync, removes many effect Clone derives, adds constructors and ExternalEffectSync impls for several effects, and updates external_sync signatures and wiring across pure-stage and amaru-consensus.

Changes

Cohort / File(s) Summary
Store Effects
crates/amaru-consensus/src/consensus/effects/store_effects.rs
external_sync now requires E: ExternalEffectSync + serde::Serialize (removed Clone bounds); removed Clone derives for many effect structs; added pub fn new(...) constructors and impl ExternalEffectSync for each effect; replaced async wraps with wrap_sync.
Ledger & Metrics Effects
crates/amaru-consensus/src/consensus/effects/ledger_effects.rs, crates/amaru-consensus/src/consensus/effects/metrics_effects.rs
Switched from async wrap to wrap_sync in several ExternalEffect impls; added impl ExternalEffectSync for affected types; minor lint attribute added.
Effect Core
crates/pure-stage/src/effect.rs
Replaced sync_effect: SyncEffectBox with trace_buffer: Arc<Mutex<TraceBuffer>>; added pub trait ExternalEffectSync: ExternalEffectAPI {}; removed StageEffect::ExternalSync and Effect::ExternalSync variants; updated external_sync to use trace buffer, serialize suspend, optionally replay, execute sync and push resume.
Effect Box & Re-exports
crates/pure-stage/src/effect_box.rs, crates/pure-stage/src/lib.rs
Removed SyncEffectBox type alias; added ExternalEffectSync to public re-exports.
Trace Buffer
crates/pure-stage/src/trace_buffer.rs
Added external helpers and non-owning refs; introduced push_suspend_external, push_resume_external; removed runnable from TraceEntry::Resume; added fetch_replay state and utilities; tests for external suspend/resume serialization.
Simulation: Replay & Running
crates/pure-stage/src/simulation/replay.rs, crates/pure-stage/src/simulation/running.rs
Replay and running wiring now accept/share trace_buffer: Arc<Mutex<TraceBuffer>>; removed sync_effect usage and ExternalSync-specific branches; resume handling wired through trace_buffer and fetch_replay logic.
Simulation Builder & Tokio
crates/pure-stage/src/simulation/simulation_builder.rs, crates/pure-stage/src/tokio.rs
Removed sync_effect from builder; forwarded trace_buffer through stage wiring, Effects::new, SimulationRunning; TokioBuilder/TokioRunning gain trace_buffer and accessor, removed ExternalSync interpreter handling.
Tests / Small Fixes
simulation/amaru-sim/tests/simulation.rs
Minor simplification of path canonicalization in test helper.

Sequence Diagram(s)

sequenceDiagram
    participant App as Caller
    participant Eff as Effects
    participant TB as TraceBuffer
    participant Ext as ExternalEffect

    rect rgb(245,250,250)
    note over Eff,TB: New synchronous external-effect flow (ExternalEffectSync)
    App->>Eff: external_sync(effect: T: ExternalEffectSync)
    Eff->>TB: push_suspend_external(at_stage, effect)
    TB->>TB: store serialized suspend entry
    alt replay available
        Eff->>TB: validate & consume fetch_replay, return response from replay
    else live execution
        Eff->>Ext: execute effect synchronously
        Ext-->>Eff: response (SendData)
    end
    Eff->>TB: push_resume_external(stage, response)
    TB->>TB: store serialized resume entry
    Eff-->>App: return response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Areas to focus on:

  • TraceBuffer suspend/resume serialization, fetch_replay semantics and replay symmetry.
  • Cross-crate trait consistency for ExternalEffectSync and serde::Serialize requirements.
  • Removal of Clone derives from public effects — verify no downstream Clone usage remains.
  • Simulation wiring and poll_stage changes (Arc<Mutex> propagation and concurrency implications).

Possibly related PRs

Suggested reviewers

  • abailly
  • etorreborre

Poem

🎬 We cut the old sync box loose, the trace now hums and sings,

Effects line up like extras, stage lights catch their wings.
No clones, just tidy actors, serialized in rows,
Suspend and resume take their cue — the show in order goes. 🎮

Pre-merge checks and finishing touches

✅ 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 'fix: remove clones and allocations when tracing sync effects' accurately captures the main optimization goal of the PR, directly reflecting the core changes made throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rk/improve-external-sync-effects

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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.

Signed-off-by: Roland Kuhn <rk@rkuhn.info>

@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 (1)
crates/pure-stage/src/trace_buffer.rs (1)

95-96: Mate, there's a ghost in the machine here – stale comment about a field that's done a runner!

The comment at lines 95-96 mentions "the runnable field in the Resume case" but if you squiz at the TraceEntry::Resume variant (lines 48-50), there's no runnable field anymore. Plus, line 101 uses the .. pattern which suggests you're ignoring extra fields, but there aren't any!

This is like keeping the "previously on..." segment after you've deleted those scenes, yeah? Clean house and remove both the outdated comment and the unnecessary .. pattern.

Apply this diff to tidy things up:

 impl Debug for TraceEntry {
-    /// This debug instance does not output the runnable field in the Resume case.
-    /// That field is only useful for display purposes.
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         match self {
             TraceEntry::Suspend(effect) => f.debug_tuple("Suspend").field(&effect).finish(),
             TraceEntry::Resume {
-                stage, response, ..
+                stage, response,
             } => f
                 .debug_struct("Resume")
                 .field("stage", stage)
                 .field("response", response)

Also applies to: 100-101

🧹 Nitpick comments (1)
crates/pure-stage/src/trace_buffer.rs (1)

389-421: Nice one on the serialization equivalence test!

Verifying that TraceEntryRefRef, TraceEntryRef, and TraceEntry all serialize to the same CBOR bytes is solid – it proves your zero-copy paths produce identical traces. The JSON assertion is also handy for eyeballing the structure.

That said, this test only covers the Suspend(External) path. Since you've added push_resume_external as well, chuck in a test case for the Resume(ExternalResponse) variant to keep coverage comprehensive. Think of it as getting the full achievement set, not just the main quest.

Consider adding a test case for Resume(ExternalResponse):

#[test]
fn test_resume_external_serialization() {
    let response = Box::new(42u32) as Box<dyn SendData>;
    let trr = TraceEntryRefRef::Resume {
        stage: &Name::from("test"),
        response: StageResponseRef::ExternalResponse(response.as_ref()),
    };
    let rr = to_cbor(&trr);
    let r = to_cbor(&TraceEntryRef::Resume {
        stage: &Name::from("test"),
        response: &StageResponse::ExternalResponse(response.clone()),
    });
    let t = to_cbor(&TraceEntry::Resume {
        stage: Name::from("test"),
        response: StageResponse::ExternalResponse(response),
    });
    assert_eq!(rr, r);
    assert_eq!(rr, t);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f64b8d9 and 5557bc9.

📒 Files selected for processing (1)
  • crates/pure-stage/src/trace_buffer.rs (6 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-14T16:31:53.134Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 263
File: simulation/amaru-sim/src/simulator/simulate.rs:298-300
Timestamp: 2025-06-14T16:31:53.134Z
Learning: StageRef in the pure-stage crate supports serde serialization and deserialization (derives serde::Serialize and serde::Deserialize), enabling it to be used in structs that also derive these traits for TraceBuffer and replay functionality.

Applied to files:

  • crates/pure-stage/src/trace_buffer.rs
📚 Learning: 2025-06-14T16:40:23.328Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 263
File: crates/pure-stage/src/simulation/replay.rs:40-43
Timestamp: 2025-06-14T16:40:23.328Z
Learning: TraceBuffer::new(0, 0) creates a buffer that will quickly drop anything put into it. This is intentional behavior, particularly useful in replay scenarios where you don't want to store new trace entries but need the buffer for temporary processing.

Applied to files:

  • crates/pure-stage/src/trace_buffer.rs
🧬 Code graph analysis (1)
crates/pure-stage/src/trace_buffer.rs (3)
crates/pure-stage/src/effect.rs (3)
  • at_stage (796-806)
  • new (79-95)
  • new (376-378)
crates/pure-stage/src/serde.rs (2)
  • to_cbor (313-324)
  • new (30-32)
crates/pure-stage/src/simulation/simulation_builder.rs (1)
  • stage (183-236)
⏰ 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). (10)
  • GitHub Check: Benches
  • GitHub Check: Test the CLI
  • GitHub Check: Build riscv32
  • GitHub Check: Build x86_64/windows
  • GitHub Check: Build aarch64/macos
  • GitHub Check: Build aarch64/linux
  • GitHub Check: Build x86_64/linux
  • GitHub Check: End-to-end snapshot tests (preprod, 1, 10.1.4)
  • GitHub Check: Test coverage
  • GitHub Check: Analyze (rust)
🔇 Additional comments (2)
crates/pure-stage/src/trace_buffer.rs (2)

219-242: Absolutely mint work on the zero-copy serialization helpers!

These non-owning helper types (TraceEntryRefRef, EffectRef, StageResponseRef) are like the Matrix – you're dodging allocations left and right while keeping the same serialization format. This aligns perfectly with the PR's goal of removing clones when tracing sync effects.

The fact that you're wrapping external effects without taking ownership is chef's kiss – no unnecessary boxing or cloning, just references all the way down. This is exactly what the doctor ordered for performance-sensitive tracing code.


271-276: These new external push methods are pure gold!

push_suspend_external and push_resume_external give you direct paths for serializing external effects without the allocation overhead. The use of the helper types (EffectRef, StageResponseRef) means you're capturing just the references and letting CBOR do its thing without materializing intermediate owned values.

This is textbook optimization – keep the data where it lives and serialize directly from it. No clones, no boxes, no worries!

Also applies to: 283-288

etorreborre and others added 2 commits November 17, 2025 19:00
Signed-off-by: etorreborre <etorreborre@yahoo.com>
Signed-off-by: Roland Kuhn <rk@rkuhn.info>
Signed-off-by: Roland Kuhn <rk@rkuhn.info>

@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

🧹 Nitpick comments (4)
simulation/amaru-sim/tests/simulation.rs (1)

122-130: Consider applying the same pattern to keep things consistent.

You've streamlined get_traces_at, but get_args_at is still doing the old intermediate Path::new(&path) dance. If you apply the same direct canonicalization pattern here, both functions would vibe together like a perfectly synced co-op run.

Apply this diff to match the pattern in get_traces_at:

 fn get_args_at(at: At) -> anyhow::Result<Args> {
     let path = format!("../../target/tests/{at}/args.json");
-    let path = Path::new(&path);
     let path =
-        fs::canonicalize(path).map_err(|e| anyhow!("cannot read the file at {path:?}: {e}"))?;
+        fs::canonicalize(&path).map_err(|e| anyhow!("cannot read the file at {path:?}: {e}"))?;
     let data = fs::read(&path).map_err(|e| anyhow!("cannot read the file at {path:?}: {e}"))?;
     let args: Args = serde_json::from_slice(data.as_slice())?;
     Ok(args)
 }
crates/pure-stage/src/simulation/replay.rs (1)

61-111: Complex index tracking for fetch_replay - verify the arithmetic.

The changes here introduce manual index tracking because poll_stage might consume trace entries via fetch_replay. Let me walk through the logic:

  1. Line 90: remaining captures the trace length before set_fetch_replay
  2. Line 91: The trace iterator is moved into trace_buffer
  3. Lines 93-94: poll_stage processes and potentially consumes entries from fetch_replay
  4. Lines 96-100: The trace iterator is restored
  5. Line 101: Index is adjusted: idx += remaining - trace.as_slice().len()

This arithmetic assumes that the difference in slice lengths represents the number of entries consumed. But there's a subtle issue here - when you call set_fetch_replay(trace) at line 91, you're moving the iterator, not the underlying Vec. After calling take_fetch_replay(), the iterator might have advanced, so as_slice().len() gives you the remaining entries, not the original count.

So the formula remaining - trace.as_slice().len() should correctly calculate consumed entries. But I reckon it's worth adding a comment explaining this because it's not immediately obvious!

Also, the manual idx += 1 at line 162 looks correct for advancing through non-Resume entries.

Consider adding a clarifying comment:

+                    // Capture how many entries remain before poll_stage potentially consumes some via fetch_replay
                     let remaining = trace.as_slice().len();
                     self.trace_buffer.lock().set_fetch_replay(trace);

                     let effect =
                         poll_stage(&self.trace_buffer, data, stage, response, &self.effect);

                     trace = self
                         .trace_buffer
                         .lock()
                         .take_fetch_replay()
                         .ok_or_else(|| anyhow::anyhow!("idx {}: no fetch replay found", idx))?;
+                    // Calculate how many entries were consumed by poll_stage's external_sync calls
                     idx += remaining - trace.as_slice().len();
crates/pure-stage/src/trace_buffer.rs (2)

231-254: Non-owning serialization helpers - functional but naming could be clearer.

The helper structs TraceEntryRefRef, EffectRef, and StageResponseRef enable serialization without consuming the data, which is necessary for the external sync path. The implementation is sound.

That said, TraceEntryRefRef is a bit of a tongue-twister, innit? Consider a more descriptive name like TraceEntryBorrowedSer or TraceEntrySerRef to make the intent clearer. But this is a nitpick - the current implementation works fine!

Optional naming improvement:

-enum TraceEntryRefRef<'a> {
+enum TraceEntryBorrowedSer<'a> {

453-462: Clever rotate_right trick in find_next - verify the logic.

The find_next helper at lines 453-462 is doing something a bit sneaky:

  1. Line 459: Find the index of the matching entry
  2. Line 460: Rotate the slice [..=idx] right by 1, moving the found entry to position 0
  3. Line 461: Pop the entry from the iterator with next()

This is a clever way to extract an entry from the middle of the iterator without allocating a new collection. But mate, this logic is quite subtle! Let me trace through an example:

  • If log = [A, B, C, D] and idx = 2 (element C matches)
  • log[..=2] = [A, B, C]
  • After rotate_right(1): [C, A, B, D] (C moved to front)
  • fetch_replay.next() returns Some(C)

Actually... hang on. I think there's a potential issue here. After rotation, the iterator's position hasn't moved - as_mut_slice() gives you the remaining elements, but the iterator's internal position doesn't change with slice manipulation. So calling next() should indeed return the first element of the slice.

But I'm not 100% convinced this is correct in all cases. It'd be worth adding a unit test to verify this works as expected, especially with edge cases like idx = 0 or idx = len - 1.

Add a unit test to verify the rotation logic:

#[test]
fn test_find_next_rotation() {
    let entries = vec![
        TraceEntry::Clock(Instant::now()),
        TraceEntry::Suspend(Effect::Clock { at_stage: Name::from("test") }),
        TraceEntry::Suspend(Effect::External { 
            at_stage: Name::from("test"),
            effect: Box::new(()),
        }),
        TraceEntry::Clock(Instant::now()),
    ];
    
    let mut iter = entries.into_iter();
    let result = find_next(
        &mut iter,
        |e| matches!(e, TraceEntry::Suspend(Effect::External { .. })),
        |e| e,
    );
    
    assert!(matches!(result, Some(TraceEntry::Suspend(Effect::External { .. }))));
    // Verify the iterator is positioned correctly after extraction
    assert_eq!(iter.len(), 1); // Only the final Clock entry remains
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5557bc9 and bff1c89.

📒 Files selected for processing (9)
  • crates/amaru-consensus/src/consensus/effects/ledger_effects.rs (5 hunks)
  • crates/amaru-consensus/src/consensus/effects/metrics_effects.rs (3 hunks)
  • crates/amaru-consensus/src/consensus/effects/store_effects.rs (30 hunks)
  • crates/pure-stage/src/effect.rs (5 hunks)
  • crates/pure-stage/src/simulation/replay.rs (3 hunks)
  • crates/pure-stage/src/simulation/running.rs (3 hunks)
  • crates/pure-stage/src/simulation/simulation_builder.rs (2 hunks)
  • crates/pure-stage/src/trace_buffer.rs (9 hunks)
  • simulation/amaru-sim/tests/simulation.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/pure-stage/src/simulation/simulation_builder.rs
🧰 Additional context used
🧠 Learnings (11)
📚 Learning: 2025-04-22T09:18:19.893Z
Learnt from: abailly
Repo: pragma-org/amaru PR: 195
File: simulation/amaru-sim/src/simulator/mod.rs:167-182
Timestamp: 2025-04-22T09:18:19.893Z
Learning: In the Amaru consensus pipeline refactor, ValidateHeader::handle_roll_forward returns a Result<PullEvent, ConsensusError>, not ValidateHeaderEvent as might be expected from the older code structure.

Applied to files:

  • crates/amaru-consensus/src/consensus/effects/ledger_effects.rs
📚 Learning: 2025-06-14T16:31:53.134Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 263
File: simulation/amaru-sim/src/simulator/simulate.rs:298-300
Timestamp: 2025-06-14T16:31:53.134Z
Learning: StageRef in the pure-stage crate supports serde serialization and deserialization (derives serde::Serialize and serde::Deserialize), enabling it to be used in structs that also derive these traits for TraceBuffer and replay functionality.

Applied to files:

  • crates/amaru-consensus/src/consensus/effects/ledger_effects.rs
  • crates/amaru-consensus/src/consensus/effects/store_effects.rs
  • crates/pure-stage/src/simulation/running.rs
  • crates/pure-stage/src/trace_buffer.rs
  • crates/pure-stage/src/simulation/replay.rs
  • crates/pure-stage/src/effect.rs
📚 Learning: 2025-05-12T14:21:27.470Z
Learnt from: stevana
Repo: pragma-org/amaru PR: 210
File: simulation/amaru-sim/src/simulator/simulate.rs:264-277
Timestamp: 2025-05-12T14:21:27.470Z
Learning: The team plans to replace the out-of-process test in `simulation/amaru-sim/src/simulator/simulate.rs` with an in-process NodeHandle implementation in the future, eliminating the need for hard-coded binary paths (`../../target/debug/echo`) and making tests more reliable.

Applied to files:

  • simulation/amaru-sim/tests/simulation.rs
  • crates/pure-stage/src/simulation/running.rs
📚 Learning: 2025-08-12T12:28:24.027Z
Learnt from: etorreborre
Repo: pragma-org/amaru PR: 372
File: simulation/amaru-sim/src/simulator/mod.rs:410-412
Timestamp: 2025-08-12T12:28:24.027Z
Learning: In the Amaru project, panic statements are acceptable in simulation/test code (like amaru-sim crate) as they help identify configuration issues quickly during development, rather than needing proper error handling like production code.

Applied to files:

  • simulation/amaru-sim/tests/simulation.rs
📚 Learning: 2025-08-20T13:02:25.763Z
Learnt from: jeluard
Repo: pragma-org/amaru PR: 387
File: crates/amaru-stores/src/lib.rs:40-40
Timestamp: 2025-08-20T13:02:25.763Z
Learning: In the amaru-stores crate, amaru_slot_arithmetic types like Epoch and EraHistory are used throughout the main crate code in modules like in_memory/mod.rs, rocksdb/consensus.rs, and rocksdb/ledger/columns/, not just in tests. This means amaru-slot-arithmetic should be a regular dependency, not a dev-dependency.

Applied to files:

  • crates/amaru-consensus/src/consensus/effects/store_effects.rs
📚 Learning: 2025-02-03T11:15:22.640Z
Learnt from: abailly
Repo: pragma-org/amaru PR: 75
File: crates/amaru/src/consensus/mod.rs:164-165
Timestamp: 2025-02-03T11:15:22.640Z
Learning: In the Amaru project, chain selection operations (roll_forward and rollback) should use separate result types to leverage the type system for preventing impossible states, rather than using runtime checks or panics.

Applied to files:

  • crates/amaru-consensus/src/consensus/effects/store_effects.rs
📚 Learning: 2025-06-14T16:41:13.061Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 263
File: crates/pure-stage/src/simulation/running.rs:868-875
Timestamp: 2025-06-14T16:41:13.061Z
Learning: In the pure-stage simulation framework, the effect air-lock protocol is designed so that when a stage is polled, the stage implementation consumes/takes the value from the effect lock during polling. There's no need to manually clear the effect lock after Poll::Ready because "the other side will have taken the value out" - this is by design, not a bug.

Applied to files:

  • crates/pure-stage/src/simulation/running.rs
  • crates/pure-stage/src/simulation/replay.rs
  • crates/pure-stage/src/effect.rs
📚 Learning: 2025-05-09T13:09:47.915Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 206
File: crates/pure-stage/src/simulation/running.rs:240-242
Timestamp: 2025-05-09T13:09:47.915Z
Learning: Cloning messages in the pure-stage crate should be avoided for performance reasons. The current implementation in SimulationRunning deliberately avoids duplicating message data structures.

Applied to files:

  • crates/pure-stage/src/simulation/running.rs
  • crates/pure-stage/src/simulation/replay.rs
  • crates/pure-stage/src/effect.rs
📚 Learning: 2025-06-14T16:40:23.328Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 263
File: crates/pure-stage/src/simulation/replay.rs:40-43
Timestamp: 2025-06-14T16:40:23.328Z
Learning: TraceBuffer::new(0, 0) creates a buffer that will quickly drop anything put into it. This is intentional behavior, particularly useful in replay scenarios where you don't want to store new trace entries but need the buffer for temporary processing.

Applied to files:

  • crates/pure-stage/src/trace_buffer.rs
📚 Learning: 2025-06-14T16:36:04.502Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 263
File: crates/pure-stage/src/simulation/state.rs:33-36
Timestamp: 2025-06-14T16:36:04.502Z
Learning: In simulation and replay systems that require cloneable and serializable states, error types must often be converted to String rather than stored as trait objects (like Box<dyn Error> or anyhow::Error) because trait objects cannot be cloned, which breaks the snapshotting and replay functionality needed for deterministic simulation.

Applied to files:

  • crates/pure-stage/src/simulation/replay.rs
  • crates/pure-stage/src/effect.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/pure-stage/src/effect.rs
🧬 Code graph analysis (7)
crates/amaru-consensus/src/consensus/effects/ledger_effects.rs (1)
crates/pure-stage/src/effect.rs (1)
  • wrap_sync (318-325)
crates/amaru-consensus/src/consensus/effects/store_effects.rs (2)
crates/pure-stage/src/effect.rs (2)
  • external_sync (227-268)
  • wrap_sync (318-325)
crates/pure-stage/src/simulation/running.rs (3)
  • effect (171-171)
  • effect (175-175)
  • effect (284-286)
crates/pure-stage/src/simulation/running.rs (2)
crates/pure-stage/src/tokio.rs (1)
  • trace_buffer (376-378)
crates/pure-stage/src/trace_buffer.rs (1)
  • state (192-197)
crates/amaru-consensus/src/consensus/effects/metrics_effects.rs (2)
crates/amaru-consensus/src/consensus/effects/store_effects.rs (28)
  • run (133-141)
  • run (164-172)
  • run (194-202)
  • run (224-232)
  • run (255-263)
  • run (285-293)
  • run (315-323)
  • run (345-353)
  • run (373-381)
  • run (401-409)
  • run (431-439)
  • run (461-469)
  • run (491-499)
  • run (521-529)
  • resources (135-136)
  • resources (166-167)
  • resources (196-197)
  • resources (226-227)
  • resources (257-258)
  • resources (287-288)
  • resources (317-318)
  • resources (347-348)
  • resources (375-376)
  • resources (403-404)
  • resources (433-434)
  • resources (463-464)
  • resources (493-494)
  • resources (523-524)
crates/pure-stage/src/effect.rs (1)
  • wrap_sync (318-325)
crates/pure-stage/src/trace_buffer.rs (2)
crates/pure-stage/src/effect.rs (4)
  • at_stage (824-834)
  • find_next_external_resume (251-253)
  • new (79-95)
  • new (404-406)
crates/pure-stage/src/simulation/replay.rs (1)
  • new (46-59)
crates/pure-stage/src/simulation/replay.rs (3)
crates/pure-stage/src/simulation/running.rs (6)
  • poll_stage (986-1024)
  • state (277-277)
  • new (92-122)
  • effect (171-171)
  • effect (175-175)
  • effect (284-286)
crates/pure-stage/src/trace_buffer.rs (2)
  • state (192-197)
  • new (268-277)
crates/pure-stage/src/effect.rs (2)
  • new (79-95)
  • new (404-406)
crates/pure-stage/src/effect.rs (6)
crates/pure-stage/src/effect_box.rs (1)
  • airlock_effect (36-67)
crates/pure-stage/src/tokio.rs (3)
  • trace_buffer (376-378)
  • resources (245-247)
  • new (63-71)
crates/pure-stage/src/trace_buffer.rs (3)
  • find_next_external_resume (433-451)
  • find_next_external_suspend (415-430)
  • clock (179-181)
crates/pure-stage/src/serde.rs (2)
  • to_cbor (313-324)
  • new (30-32)
crates/pure-stage/src/types.rs (2)
  • test_eq (45-45)
  • test_eq (65-70)
crates/pure-stage/src/simulation/running.rs (5)
  • effect (171-171)
  • effect (175-175)
  • effect (284-286)
  • resources (127-129)
  • new (92-122)
⏰ 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). (1)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (25)
simulation/amaru-sim/tests/simulation.rs (1)

136-136: Nice cleanup, mate!

Calling fs::canonicalize(&path) directly is spot on – no need for that intermediate Path::new since &str already plays nice with AsRef<Path>. Keeps things lean and mean like a well-optimized speedrun.

crates/pure-stage/src/effect.rs (4)

47-67: LGTM! Clean addition of trace_buffer to the Effects struct.

The Arc<Mutex<TraceBuffer>> approach is spot-on for sharing the trace buffer across effect instances, mate. The manual Clone impl correctly clones all the Arc-wrapped fields without deep copying the underlying data. No dramas here!


78-95: Constructor updated correctly.

The trace_buffer parameter flows through nicely to the field initialization. All good here!


342-347: Good documentation on the trait contract.

The docs make it crystal clear that implementations must not use .await or return futures that suspend. This is the kind of explicit contract that helps prevent foot-guns down the line!


226-268: No action required — ExternalEffectSync implementations are all correctly structured.

Good news, mate! I ran the verification sweep and the codebase is tight as a drum on this front. Every single ExternalEffectSync implementation across the 17 effects is doing the right thing:

  • All 17 ExternalEffectSync effects correctly use Self::wrap_sync() with synchronous code
  • Zero async patterns (.await, async move, etc.) lurking in any of them
  • Non-ExternalEffectSync effects (like ValidateBlockEffect) properly use Self::wrap() with async — they're not marked sync, so they don't get caught by the now_or_never() expectation

The pattern's clean: if an effect implements ExternalEffectSync, it's using wrap_sync() with blocking/sync code. If it's not sync and needs async operations, it stays as just ExternalEffect. It's like a proper type system keeping things honest — no sneaky .await calls hiding in ExternalEffectSync territory where now_or_never() would go sideways.

The trait docs already make the contract crystal clear, and the implementations are walking the walk. Dead set that this is locked down properly.

crates/amaru-consensus/src/consensus/effects/ledger_effects.rs (3)

23-25: Import updated correctly.

The ExternalEffectSync import is spot-on for the new implementations below.


199-219: No issues found—the code is correct.

The concern was legit to flag, but the verification's come through clean as. rollback_block is straight-up synchronous across the board:

  • Trait definition: fn rollback_block(&self, to: &Point) -> Result<(), BlockValidationError>; (no async)
  • Real implementation in block_validator.rs:81-86 uses a blocking Mutex::lock() and calls state.rollback_to(to) with zero await calls
  • The call site at line 209 doesn't .await anything

Using wrap_sync with ExternalEffectSync is spot-on here—like picking the right tool for the job instead of swinging a sledgehammer. No async shenanigans lurking in the shadows.


161-179: The synchronous claim is technically true but comes with a gotcha.

You were spot-on to flag this one, mate. The trait method is genuinely synchronous, but here's the catch—it's not as straightforward as just "do some validation math." The ValidateHeader::validate() method calls evolve_nonce(), which creates a PraosChainStore and passes in self.store (an Arc<dyn ChainStore<BlockHeader>>).

That store could be:

  • InMemConsensusStore: Just Mutex locks, no blocking I/O
  • RocksDBStore: Actual blocking database I/O
  • Store (effects-wrapped): Calls external_sync() on effects, which could nest your wrap_sync calls

The RocksDB path is the dodgy bit—if someone deploys this with a persistent store backend, you're doing blocking I/O inside wrap_sync, which could tank your async runtime like a Michael Bay movie script.

The code compiles and seems to work, which suggests the developers are confident about their specific ChainStore configuration. But if this gets used with a different storage backend or if store access patterns change, it could break. This needs careful verification in your actual deployment context.

crates/amaru-consensus/src/consensus/effects/metrics_effects.rs (2)

59-75: Metrics recording converted to sync execution - looks good!

The switch to wrap_sync makes sense here since record_to_meter is just pushing metrics data, which should be a quick synchronous operation. The #[allow(clippy::unit_arg)] attribute correctly silences the lint about passing () to wrap_sync.

This is a textbook conversion from async to sync external effects. Well played!


16-18: Import updated correctly.

crates/pure-stage/src/simulation/replay.rs (3)

36-43: Replay struct updated to include trace_buffer.

The addition of trace_buffer: Arc<Mutex<TraceBuffer>> is consistent with the broader refactoring. No worries here!


46-59: Constructor signature updated correctly.


103-111: CBOR roundtrip for generic encoding makes sense.

The serialize/deserialize roundtrip at lines 107-108 ensures the effect matches the format used in the trace (which stores serialized effects). This is important for the assertion at line 109 to work correctly. Fair dinkum approach!

crates/pure-stage/src/simulation/running.rs (3)

71-122: SyncEffectBox successfully removed from SimulationRunning.

The removal of the sync_effect field and its replacement with trace_buffer integration is clean. The constructor's been updated accordingly, and all the fields are initialized properly. Good stuff!


293-335: try_effect updated to use shared trace_buffer.

The changes here align with the broader refactoring - push_resume no longer needs the runnable list (line 303), and poll_stage receives the shared trace_buffer reference (lines 310-316). All the plumbing looks right!


986-1024: poll_stage signature updated to use shared trace_buffer.

Changing from a mutable reference to &Arc<Mutex<TraceBuffer>> at line 987 enables sharing the trace buffer across different contexts (like in external_sync). The lock() call at line 1004 is the right approach for thread-safe access. Nice one!

crates/pure-stage/src/trace_buffer.rs (6)

32-39: fetch_replay field added for external sync replay.

The fetch_replay field stores the trace iterator during replay operations. Initialization to None at line 275 is correct. This enables the external_sync replay mechanism to work!


46-64: TraceEntry::Resume simplified by removing runnable field.

The removal of the runnable field from the Resume variant streamlines the trace entry structure. All the display/debug implementations have been updated accordingly. The comment at line 97 mentions the field was "only useful for display purposes" - fair enough to remove it then!

Also applies to: 96-162


199-207: Handy at_stage() helper method.

This convenience method makes it easy to extract the stage name from any trace entry variant. Used by the find_next helpers below. Clean implementation!


280-301: New external-specific push methods for non-owning serialization.

The push_suspend_external and push_resume_external methods use the helper structs to serialize effects without taking ownership. This is crucial for the external_sync path where the effect/response data is borrowed. The updated push_resume signature (dropping the runnable parameter) aligns with the TraceEntry changes. All good here!


401-412: fetch_replay management methods are straightforward.

These three methods (set_fetch_replay, take_fetch_replay, fetch_replay_mut) provide the necessary interface for managing the replay iterator during external_sync operations. Clean and simple!


470-495: Good test coverage for serialization helpers.

The test verifies that TraceEntryRefRef, TraceEntryRef, and owned TraceEntry all serialize to the same CBOR format. This is exactly what you want to ensure the non-owning serialization produces identical output. Nice work!

crates/amaru-consensus/src/consensus/effects/store_effects.rs (3)

36-44: external_sync signature updated to use ExternalEffectSync.

The updated signature drops the Clone bounds and adds ExternalEffectSync + serde::Serialize, which aligns perfectly with the PR objectives. This enables the trace-buffer-driven synchronous execution without unnecessary cloning. Brilliant!


120-536: Derives and constructors added consistently.

All the effect structs now derive serde::Serialize and serde::Deserialize, which is necessary for the trace-buffer-based external_sync. The new() constructors provide a clean API for creating these effects. No issues here!


120-536: Now let me do one final verification of the trait definitions to confirm they're truly synchronous:

Based on my thorough verification, I've got some cracking news for ya mate—you've actually nailed the implementation, and the code is solid as a rock.

Here's the deal: Tokio's runtime includes a separate thread pool specifically for running blocking functions, which has an upper limit of around 500 threads, and your wrap_sync is doing exactly what it should. When calling blocking synchronous code from an asynchronous context, it's recommended to use tokio::task::spawn_blocking to execute the code on a dedicated executor that handles blocking operations.

All those RocksDB store operations—store_header, load_header, get_children, rollback_chain, etc.—are synchronous by design. The rust-rocksdb bindings wrap RocksDB in a synchronous interface, and since they can potentially do disk I/O when reading/writing SST files, they're perfect candidates for wrap_sync. No panics, no runtime hangs—you're using the right tool for the job.

The conversion across all 14 effects is consistent and correct. Every struct now properly implements ExternalEffectSync, and they're all calling wrap_sync to safely run blocking store operations without starving the async runtime.


…ync-effects

Signed-off-by: Roland Kuhn <rk@rkuhn.info>

@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: 1

🧹 Nitpick comments (3)
simulation/amaru-sim/tests/simulation.rs (1)

158-166: Fair dinkum, fancy keeping things consistent across the board?

So you've gone and streamlined get_traces to directly canonicalize the path without that intermediate Path::new binding (nice one!), but get_args here is still rocking the old two-step shuffle at lines 160-162. Like having one controller with drift while the other's been fixed, ya know?

Might be worth giving this the same treatment for consistency—just a wee refactor to match the pattern you've established in get_traces.

Here's how you could tidy it up:

 fn get_args(test_directory: &Path, simulation_run: SimulationRun) -> anyhow::Result<Args> {
     let path = format!("{}/{simulation_run}/args.json", test_directory.display());
-    let path = Path::new(&path);
-    let path = fs::canonicalize(path)
+    let path = fs::canonicalize(&path)
         .map_err(|e| anyhow!("cannot canonicalize the file at {path:?}: {e}"))?;
     let data = fs::read(&path).map_err(|e| anyhow!("cannot read the file at {path:?}: {e}"))?;
     let args: Args = serde_json::from_slice(data.as_slice())?;
     Ok(args)
 }
crates/pure-stage/src/simulation/running.rs (2)

314-319: poll_stage/trace_buffer wiring is sound; consider a tiny trace handle wrapper

Passing &Arc<Mutex<TraceBuffer>> into poll_stage and doing push_state(&name, &state) plus push_suspend(&effect) via short-lived locks looks logically tight and gets you the no-clone trace events the PR is aiming for. Nice alignment with the earlier “no extra allocations in pure-stage” direction. Based on learnings.

If you ever feel like sanding off a bit more API noise, a small TraceHandle wrapper (owning the Arc<Mutex<TraceBuffer>> and exposing push_* methods) would de-couple callers from parking_lot::Mutex and avoid threading Arc<Mutex<_>> through signatures, but that’s pure bikeshed at this point.

Also applies to: 335-336, 989-1007


431-443: Threading the trace buffer into resume_receive_internal looks good

Both in receive_inputs and resume_receive, handing &mut self.trace_buffer.lock() into resume_receive_internal keeps all the receive-side tracing in one place with a minimal lock scope. Behaviour-wise this is the same as before, just with the tracing now happening under the mutex instead of via a mutable TraceBuffer reference, which is what you want for the shared Arc.

If the repeated &mut self.trace_buffer.lock() call pattern starts to bug you later, a tiny helper like with_trace(|tb| ...) could DRY it up, but that’s firmly “optional polish”, not a blocker.

Also applies to: 624-640

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bff1c89 and 8dda98c.

📒 Files selected for processing (2)
  • crates/pure-stage/src/simulation/running.rs (3 hunks)
  • simulation/amaru-sim/tests/simulation.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-05-12T14:21:27.470Z
Learnt from: stevana
Repo: pragma-org/amaru PR: 210
File: simulation/amaru-sim/src/simulator/simulate.rs:264-277
Timestamp: 2025-05-12T14:21:27.470Z
Learning: The team plans to replace the out-of-process test in `simulation/amaru-sim/src/simulator/simulate.rs` with an in-process NodeHandle implementation in the future, eliminating the need for hard-coded binary paths (`../../target/debug/echo`) and making tests more reliable.

Applied to files:

  • simulation/amaru-sim/tests/simulation.rs
  • crates/pure-stage/src/simulation/running.rs
📚 Learning: 2025-06-14T16:41:13.061Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 263
File: crates/pure-stage/src/simulation/running.rs:868-875
Timestamp: 2025-06-14T16:41:13.061Z
Learning: In the pure-stage simulation framework, the effect air-lock protocol is designed so that when a stage is polled, the stage implementation consumes/takes the value from the effect lock during polling. There's no need to manually clear the effect lock after Poll::Ready because "the other side will have taken the value out" - this is by design, not a bug.

Applied to files:

  • crates/pure-stage/src/simulation/running.rs
📚 Learning: 2025-05-09T13:09:47.915Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 206
File: crates/pure-stage/src/simulation/running.rs:240-242
Timestamp: 2025-05-09T13:09:47.915Z
Learning: Cloning messages in the pure-stage crate should be avoided for performance reasons. The current implementation in SimulationRunning deliberately avoids duplicating message data structures.

Applied to files:

  • crates/pure-stage/src/simulation/running.rs
📚 Learning: 2025-06-14T16:31:53.134Z
Learnt from: rkuhn
Repo: pragma-org/amaru PR: 263
File: simulation/amaru-sim/src/simulator/simulate.rs:298-300
Timestamp: 2025-06-14T16:31:53.134Z
Learning: StageRef in the pure-stage crate supports serde serialization and deserialization (derives serde::Serialize and serde::Deserialize), enabling it to be used in structs that also derive these traits for TraceBuffer and replay functionality.

Applied to files:

  • crates/pure-stage/src/simulation/running.rs
🧬 Code graph analysis (1)
crates/pure-stage/src/simulation/running.rs (2)
crates/pure-stage/src/tokio.rs (1)
  • trace_buffer (376-378)
crates/pure-stage/src/trace_buffer.rs (1)
  • state (192-197)
⏰ 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). (10)
  • GitHub Check: Benches
  • GitHub Check: Build aarch64/linux
  • GitHub Check: Test the CLI
  • GitHub Check: Build aarch64/macos
  • GitHub Check: Build x86_64/windows
  • GitHub Check: End-to-end snapshot tests (preprod, 1, 10.1.4)
  • GitHub Check: Build x86_64/linux
  • GitHub Check: Build riscv32
  • GitHub Check: Test coverage
  • GitHub Check: Analyze (rust)
🔇 Additional comments (1)
crates/pure-stage/src/simulation/running.rs (1)

305-307: Trace push_resume looks correct and cheaper

Locking the trace buffer just long enough to push_resume(&name, &response) and doing it by reference instead of cloning fits nicely with the “don’t copy if you don’t have to” vibe you’ve been chasing in SimulationRunning. Feels like upgrading from SD to HD without changing the script.

let path = Path::new(&path);
let latest_trace =
fs::canonicalize(path).map_err(|e| anyhow!("cannot read the file at {path:?}: {e}"))?;
fs::canonicalize(&path).map_err(|e| anyhow!("cannot read the file at {path:?}: {e}"))?;

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.

⚠️ Potential issue | 🟡 Minor

G'day mate, your error message's a bit off-piste here!

The error message reckons it "cannot read the file" but you're actually canonicalizing the path at this point, not reading it. The actual read happens down in load_trace_entries at line 186. It's like telling someone the Matrix's red pill doesn't work when really you just can't find the bloody thing!

This could make debugging a real head-scratcher when the path canonicalization fails (say, the file doesn't exist or permissions are wonky).

Apply this diff to make the error message match what's actually happening:

-    fs::canonicalize(&path).map_err(|e| anyhow!("cannot read the file at {path:?}: {e}"))?;
+    fs::canonicalize(&path).map_err(|e| anyhow!("cannot canonicalize the file at {path:?}: {e}"))?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fs::canonicalize(&path).map_err(|e| anyhow!("cannot read the file at {path:?}: {e}"))?;
fs::canonicalize(&path).map_err(|e| anyhow!("cannot canonicalize the file at {path:?}: {e}"))?;
🤖 Prompt for AI Agents
In simulation/amaru-sim/tests/simulation.rs around line 179, the map_err message
incorrectly says "cannot read the file" even though this call is performing
fs::canonicalize; change the error text to reflect canonicalization (e.g.,
"cannot canonicalize path {path:?}") while preserving the original error details
({e}) so failures accurately report that the path could not be resolved rather
than that the file could not be read.

@codecov

codecov Bot commented Nov 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.00524% with 21 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/pure-stage/src/trace_buffer.rs 90.10% 9 Missing ⚠️
...u-consensus/src/consensus/effects/store_effects.rs 70.58% 5 Missing ⚠️
crates/pure-stage/src/effect.rs 93.18% 3 Missing ⚠️
crates/pure-stage/src/tokio.rs 62.50% 3 Missing ⚠️
crates/pure-stage/src/simulation/replay.rs 95.45% 1 Missing ⚠️
Files with missing lines Coverage Δ
...-consensus/src/consensus/effects/ledger_effects.rs 100.00% <100.00%> (ø)
...consensus/src/consensus/effects/metrics_effects.rs 93.33% <100.00%> (-0.42%) ⬇️
crates/pure-stage/src/effect_box.rs 80.00% <ø> (ø)
crates/pure-stage/src/simulation/running.rs 86.83% <100.00%> (+1.21%) ⬆️
...es/pure-stage/src/simulation/simulation_builder.rs 88.55% <100.00%> (-0.21%) ⬇️
crates/pure-stage/src/simulation/replay.rs 85.98% <95.45%> (+2.46%) ⬆️
crates/pure-stage/src/effect.rs 62.09% <93.18%> (+0.46%) ⬆️
crates/pure-stage/src/tokio.rs 65.61% <62.50%> (+0.77%) ⬆️
...u-consensus/src/consensus/effects/store_effects.rs 64.31% <70.58%> (+0.31%) ⬆️
crates/pure-stage/src/trace_buffer.rs 76.13% <90.10%> (+11.04%) ⬆️

... 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.

@rkuhn rkuhn merged commit 5b10ec3 into main Nov 19, 2025
20 checks passed
@rkuhn rkuhn deleted the rk/improve-external-sync-effects branch November 19, 2025 08:21
@coderabbitai coderabbitai Bot mentioned this pull request Apr 10, 2026
@coderabbitai coderabbitai Bot mentioned this pull request May 14, 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