Skip to content

perf(memtrack): reduce serialization bottleneck#436

Open
not-matthias wants to merge 11 commits into
mainfrom
cod-3071-fix-serialization-bottleneck
Open

perf(memtrack): reduce serialization bottleneck#436
not-matthias wants to merge 11 commits into
mainfrom
cod-3071-fix-serialization-bottleneck

Conversation

@not-matthias

Copy link
Copy Markdown
Member

Summary

  • replace derived flattened MemtrackEvent serialization with a byte-identical manual serializer
  • make MemtrackWriter::finish return encoded frame bytes for batching
  • encode contiguous memtrack event batches on worker threads and write sequence-numbered zstd frames in order

Verification

  • cargo test -p runner-shared artifacts::memtrack
  • cargo test -p runner-shared
  • cargo check -p runner-shared
  • cargo test --manifest-path crates/memtrack/Cargo.toml via Nix shell with libclang/build deps; eBPF integration cases compiled but were ignored because GITHUB_ACTIONS was unset
  • cargo bench -p runner-shared --bench memtrack_writer

Notes

  • memtrack_writer bench measures the Phase A single-writer encoder path only, not the full parallel memtrack pipeline.
  • End-to-end memory-mode speedup was not measured in this environment.

@codspeed-hq

codspeed-hq Bot commented Jul 6, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 44.86%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 4 improved benchmarks
✅ 3 untouched benchmarks
🆕 10 new benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation write_events[1000000] 2.1 s 1.4 s +47.8%
Simulation write_events[100000] 207.3 ms 141.3 ms +46.74%
Simulation write_events[10000] 20 ms 13.9 ms +43.69%
Simulation write_events[500000] 1,036.6 ms 733.6 ms +41.31%
🆕 WallTime encode_events_realistic[16] N/A 121.9 ms N/A
🆕 WallTime encode_events_realistic[4] N/A 216.1 ms N/A
🆕 WallTime encode_events_realistic[8] N/A 144.5 ms N/A
🆕 WallTime write_events[10000] N/A 7 ms N/A
🆕 WallTime write_events[100000] N/A 72 ms N/A
🆕 WallTime write_events[1000000] N/A 771.5 ms N/A
🆕 WallTime write_events[500000] N/A 387.4 ms N/A
🆕 Simulation encode_events_realistic[16] N/A 1.2 s N/A
🆕 Simulation encode_events_realistic[8] N/A 1.2 s N/A
🆕 Simulation encode_events_realistic[4] N/A 1.2 s N/A

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing cod-3071-fix-serialization-bottleneck (d06d42e) with main (705245d)

Open in CodSpeed

@not-matthias not-matthias force-pushed the cod-3071-fix-serialization-bottleneck branch from 381bac8 to 9be9b22 Compare July 6, 2026 15:10
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the single-threaded, single-zstd-frame memtrack encoding pipeline with a parallel one: events are batched into 64 K-event windows, each window is compressed as independent zstd frames across a Rayon worker pool, and frames are written in order so the output is fully deterministic. It also migrates the eBPF ring-buffer channel from std::sync::mpsc to crossbeam_channel and simplifies the Tracker ownership model so the poller's lifetime is tied to the struct rather than a detached relay thread.

  • pipeline.rs — new encode_events<S, W> function: collects up to FRAME_EVENTS × WINDOW_FRAMES (1 M) events per window, compresses frames in parallel via rayon, writes in input order, and always emits at least one valid frame for an empty source.
  • writer.rsMemtrackWriter::finish() now returns the compressed Vec<u8> instead of (); compression level lowered to -5 (ultra-fast); BufWriter now buffers serializer output before the encoder rather than after.
  • tracker.rs / poller.rsTracker owns the RingBufferPoller; stop_polling() drops it, which drains ring-buffer stragglers and closes the event channel; the old detached relay thread is gone.

Confidence Score: 5/5

Safe to merge. The core encoding logic is correct, multi-frame decoding works with zstd 0.13.3's default multiple-members behavior, and ordering is verified by the new tests.

The parallel pipeline, ownership changes, and channel migration are all well-reasoned and covered by tests. The two findings are style and benchmark-accuracy nits that don't affect production correctness.

No files require special attention. The benchmark in crates/runner-shared/benches/memtrack_writer.rs has a thread-pool-per-iteration overhead issue, but it only affects benchmark result accuracy, not production behavior.

Important Files Changed

Filename Overview
crates/runner-shared/src/artifacts/memtrack/pipeline.rs New encode_events function: batches events into FRAME_EVENTS-sized windows, compresses frames in parallel via Rayon, writes in order. Logic is correct; order-preservation and empty-stream tests are thorough.
crates/runner-shared/src/artifacts/memtrack/writer.rs MemtrackWriter refactored to return compressed bytes from finish(); BufWriter and encoder layers swapped; compression level changed to -5; stale comment noted as P2.
crates/runner-shared/src/artifacts/memtrack/mod.rs Split into mod.rs + pipeline.rs + writer.rs; decode_streamed unchanged; zstd 0.13.3 enables multiple_members by default so multi-frame decoding works correctly.
crates/memtrack/src/main.rs Old two-stage drain+write pipeline replaced with a single encode_events call; poller lifecycle now tied to Tracker; shutdown sequence (disable → stop_polling → join) is correct.
crates/memtrack/src/ebpf/tracker.rs Tracker now owns the RingBufferPoller; stop_polling() drops it cleanly; old relay-thread workaround removed; cleaner ownership.
crates/runner-shared/benches/memtrack_writer.rs Adds encode_events_realistic bench comparing 4/8/16 workers; Rayon thread pool is created inside bench_local on every iteration, skewing worker-count comparisons with thread lifecycle overhead (P2).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant eBPF as eBPF ring buffer
    participant Poller as RingBufferPoller (thread)
    participant Chan as crossbeam channel (unbounded)
    participant Pipeline as encode_events (main thread)
    participant Rayon as Rayon worker pool
    participant Disk as Output file

    eBPF->>Poller: events (ring buffer)
    Poller->>Chan: tx.send(event) xN
    Note over Pipeline: window.extend(take(1M))
    Chan-->>Pipeline: Receiver iterator (blocking)
    Pipeline->>Rayon: par_chunks(64K) to encode_frame
    Rayon-->>Pipeline: Vec of frames in order
    Pipeline->>Disk: write_all(frame) xWINDOW_FRAMES
    Note over Pipeline: repeat until channel closed
    Note over Poller: stop_polling() shutdown ringbuf.consume()
    Poller->>Chan: tx dropped channel closed
    Chan-->>Pipeline: iterator returns None
    Pipeline->>Disk: flush()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant eBPF as eBPF ring buffer
    participant Poller as RingBufferPoller (thread)
    participant Chan as crossbeam channel (unbounded)
    participant Pipeline as encode_events (main thread)
    participant Rayon as Rayon worker pool
    participant Disk as Output file

    eBPF->>Poller: events (ring buffer)
    Poller->>Chan: tx.send(event) xN
    Note over Pipeline: window.extend(take(1M))
    Chan-->>Pipeline: Receiver iterator (blocking)
    Pipeline->>Rayon: par_chunks(64K) to encode_frame
    Rayon-->>Pipeline: Vec of frames in order
    Pipeline->>Disk: write_all(frame) xWINDOW_FRAMES
    Note over Pipeline: repeat until channel closed
    Note over Poller: stop_polling() shutdown ringbuf.consume()
    Poller->>Chan: tx dropped channel closed
    Chan-->>Pipeline: iterator returns None
    Pipeline->>Disk: flush()
Loading

Reviews (11): Last reviewed commit: "ci: install rust toolchain by explicit c..." | Re-trigger Greptile

Comment thread crates/memtrack/src/main.rs Outdated
Comment thread crates/memtrack/Cargo.toml Outdated
Comment thread crates/memtrack/src/main.rs Outdated
@not-matthias not-matthias force-pushed the cod-3071-fix-serialization-bottleneck branch 2 times, most recently from a225638 to 9561cdd Compare July 7, 2026 17:43
Comment thread crates/runner-shared/src/artifacts/memtrack/pipeline.rs Outdated
@not-matthias not-matthias force-pushed the cod-3071-fix-serialization-bottleneck branch from 9561cdd to 9292b3f Compare July 7, 2026 18:00
@not-matthias not-matthias marked this pull request as ready for review July 7, 2026 18:19
@not-matthias not-matthias force-pushed the cod-3071-fix-serialization-bottleneck branch from 346a162 to 99b9b68 Compare July 14, 2026 10:05

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

lgtm

Callers that encode a frame into an in-memory buffer need the buffer
back after the zstd stream is finalized, so frames can be composed
into a larger artifact stream.

Refs COD-3071
Group events into fixed-size frames and compress them across a rayon
pool, writing windows in input order. Bounds peak memory regardless of
run length and removes the single-threaded compression bottleneck.

Refs COD-3071
…race

Generate a seeded malloc/free/realloc/mmap workload with a live-heap
model instead of uniform random events, and sweep worker counts.

Refs COD-3071
Forward ring buffer events over a crossbeam channel directly instead of
through an extra keep-alive forwarding thread. On shutdown, consume the
ring buffer once more so events emitted after the last poll are not
lost, then close the channel.

Refs COD-3071
Feed the poller's event channel straight into encode_events on one
thread, sized to available parallelism. Shutdown is now deterministic:
stop the poller, which drains stragglers and closes the channel, then
join the pipeline.

Fixes COD-3071
itertools::chunks erases the iterator size hint, so collecting each
window grew by doubling (~29% of encode driver time in reallocation).
A single pre-sized buffer reused across windows removes the growth and
the chunk-adapter bookkeeping: encode_events_realistic median drops
21%/17%/9% at 16/8/4 workers.
@not-matthias not-matthias force-pushed the cod-3071-fix-serialization-bottleneck branch from 99b9b68 to 78d5ae2 Compare July 15, 2026 17:17
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