feat(jitdump): recover V8 inlined frames from the V8 code log#10
Conversation
V8's jitdump (--perf-prof) records only the innermost inlined source position per PC, so a sampling profiler collapses an inlined chain root->a->b->c down to a single `root` frame for both TurboFan and Maglev (COD-2821; Maglev needs COD-2820). The full inline tree is emitted, with no node/V8 fork, to the code log's `code-source-info` records (positions + inlining_positions + inlined_functions), which use the same runtime code addresses as the jitdump. - v8_inline.rs: parse that log into a per-code-object inline map (keyed on address + size, since V8 reuses freed code addresses over a run) and reconstruct the inline stack via the SourcePosition::InliningStack walk. - jitdump.rs: resolve the producing process's log as $CODSPEED_V8_LOG/codspeed-v8-<pid>.log from the jitdump header pid, then expand a single jitdump frame into the full inline stack, innermost first, with the code object's own frame appended last so a non-inlined hit resolves exactly as before. Falls back to the single-frame path when no log is found. Refs COD-2821 Generated with AI Agent (Claude Code)
not-matthias
left a comment
There was a problem hiding this comment.
LGTM overall. I think the code readability can be improved a bit by:
- Dropping the references to COD-2821 (I didn't comment for all, so pls search for other references)
- Removing 1-3 letter variables by either renaming them or creating a struct (I think I prefer the latter)
| index: JitDumpIndex, | ||
| /// Inline map from the V8 code log of the process that produced this | ||
| /// jitdump, used to expand TurboFan/Maglev inlined frames that the jitdump | ||
| /// itself collapses (COD-2821). `None` when no log was found. |
There was a problem hiding this comment.
I'd drop the (COD-2821) here, it's already included in the commit/branch.
| //! | ||
| //! V8's jitdump records only the *innermost* inlined source position per PC, so | ||
| //! a sampling profiler collapses an inlined chain `root -> a -> b -> c` down to | ||
| //! just `root` (COD-2821). The full inline tree is instead present in the code |
| /// `(code_offset, script_offset, inlining_id)`, sorted ascending by code_offset. | ||
| positions: Vec<(u32, u32, Option<u32>)>, | ||
| /// `code-source-info` inlining tree: `(inlined_function_id, call_site_script_offset, parent_inlining_id)`. | ||
| inl_pos: Vec<(i32, u32, Option<u32>)>, |
There was a problem hiding this comment.
inlined_function_id can be negative? 🤔
There was a problem hiding this comment.
a bit further down below we even check for negative results, maybe just convert it to u32 like the others
| } | ||
| let (_script_off, mut iid) = cur?; | ||
| let mut frames = Vec::new(); | ||
| // Mirror of V8 SourcePosition::InliningStack. |
There was a problem hiding this comment.
nit: link to the function that's referenced could be helpful
| // Source-position table: an entry applies from its code_offset until the | ||
| // next one, so take the last entry with code_offset <= pc_offset. | ||
| let mut cur = None; | ||
| for &(co, so, iid) in &ci.positions { |
There was a problem hiding this comment.
nit: I see the comment above but 2-3 letter var names are a bit hard to follow when reading. I think creating a struct could make sense here.
| /// there is no genuine inlining at that PC; the code object's own frame is | ||
| /// supplied by the caller (the jitdump symbol), so it is *not* included. | ||
| pub fn lookup(&self, code_addr: u64, code_size: u32, pc_offset: u64) -> Option<Vec<V8Frame>> { | ||
| let ci = self.by_key.get(&(code_addr, code_size))?; |
There was a problem hiding this comment.
nit: by_key is already expressed by .get(key) -> could be renamed to code_inlines then it's also clear what ci is
Linked: CodSpeedHQ/codspeed-node#83