Skip to content

Commit 5ff86d7

Browse files
committed
perf(runner-shared): stream each memtrack frame when finding module events
Decompressing a whole frame before filtering it ties a worker's memory to the frame's decoded size, which the event count per frame does not bound: a frame of captured stacks decodes to hundreds of MB, held by every rayon worker at once. Stream each frame through the existing event decoder instead, so a worker holds about one zstd window and one event. Frames are still decoded in parallel and events keep their artifact order. This trades speed for bounded memory: on a 32-thread machine the 100M event search takes about 1.85 s instead of 1.0 s, still well ahead of the single-threaded streamed decoder.
1 parent 7bb6a1d commit 5ff86d7

1 file changed

Lines changed: 15 additions & 26 deletions

File tree

  • crates/runner-shared/src/artifacts/memtrack

‎crates/runner-shared/src/artifacts/memtrack/mod.rs‎

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,21 @@ impl MemtrackArtifact {
4040
/// and the forks and execs that inherit or drop them. Offline stack
4141
/// attribution needs these few events out of the whole artifact.
4242
///
43-
/// Every frame the encoder writes is a self-contained zstd frame, so frames
44-
/// are decoded in parallel; events keep their artifact order. A truncated
45-
/// last frame cannot be split off and is streamed instead, so the events
46-
/// before the cut are still found.
43+
/// Events are returned in artifact order. A truncated artifact yields the
44+
/// events before the cut.
4745
pub fn decode_module_events(artifact: &[u8]) -> anyhow::Result<Vec<MemtrackEvent>> {
4846
use rayon::prelude::*;
4947

48+
// The encoder writes self-contained zstd frames: split on them and
49+
// decode the frames in parallel. Collecting in frame order keeps the
50+
// artifact order.
5051
let (frames, tail) = split_zstd_frames(artifact);
5152
let mut events = frames
5253
.par_iter()
53-
.map_init(Vec::new, |msgpack, frame| {
54-
module_events_in_frame(frame, msgpack)
55-
})
54+
.map(|frame| module_events_in_frame(frame))
5655
.collect::<anyhow::Result<Vec<_>>>()?
5756
.concat();
57+
// A truncated last frame cannot be split off; stream it on its own.
5858
events.extend(Self::decode_streamed(tail)?.filter(|event| event.kind.is_module_event()));
5959
Ok(events)
6060
}
@@ -78,25 +78,14 @@ fn split_zstd_frames(mut artifact: &[u8]) -> (Vec<&[u8]>, &[u8]) {
7878
(frames, artifact)
7979
}
8080

81-
/// Decompress one frame into `msgpack` and return its module events. Decoding
82-
/// from the buffer lets strings and stack payloads be read in place instead of
83-
/// copied out of a stream first. Like [`MemtrackEventStream`], reading stops at
84-
/// the first event that fails to decode.
85-
fn module_events_in_frame(
86-
frame: &[u8],
87-
msgpack: &mut Vec<u8>,
88-
) -> anyhow::Result<Vec<MemtrackEvent>> {
89-
msgpack.clear();
90-
zstd::stream::copy_decode(frame, &mut *msgpack)?;
91-
92-
let mut deserializer = rmp_serde::Deserializer::from_read_ref(msgpack.as_slice());
93-
let mut events = Vec::new();
94-
while let Ok(event) = MemtrackEvent::deserialize(&mut deserializer) {
95-
if event.kind.is_module_event() {
96-
events.push(event);
97-
}
98-
}
99-
Ok(events)
81+
/// Stream one frame and return its module events, holding one decoded event
82+
/// at a time however large the frame decompresses. Like
83+
/// [`MemtrackEventStream`], reading stops at the first event that fails to
84+
/// decode.
85+
fn module_events_in_frame(frame: &[u8]) -> anyhow::Result<Vec<MemtrackEvent>> {
86+
Ok(MemtrackArtifact::decode_streamed(frame)?
87+
.filter(|event| event.kind.is_module_event())
88+
.collect())
10089
}
10190

10291
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]

0 commit comments

Comments
 (0)