Repository navigation
Preserve Python and Node symbols in memory flamegraphs #556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
not-matthias
merged 8 commits into
main
from
cod-3654-support-memory-flamegraphs-for-pythonnode
Oct 8, 2026
+899
−156
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
9492e55
feat(memory): harvest perf maps and JIT unwind data for memtrack
not-matthias 4a4743c
feat(exec-harness): emit Python and Node perf maps in memory mode
not-matthias 4596091
test(memtrack): cover Python and Node allocations with JIT symbols
not-matthias 316d0a7
fix(memory): keep every placement of a module mapped more than once
not-matthias c3da0d3
fix(exec-harness): only pass --interpreted-frames-native-stack in ana…
not-matthias d87dca3
feat(runner-shared): share the benchmark runtime env and add a node w…
not-matthias 07db865
feat(exec-harness): apply the shared runtime env and node wrapper
not-matthias 8368147
fix(runtime-env): emit the Node perf map in memory mode
not-matthias File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| //! Runtime env for benchmark processes started by exec-harness: the shared | ||
| //! [`runner_shared::runtime_env::env`] vars plus the `node` wrapper on `PATH`. | ||
|
|
||
| use crate::MeasurementMode; | ||
|
|
||
| mod node; | ||
|
|
||
| const PATH_ENV: &str = "PATH"; | ||
|
|
||
| /// Applies the runtime env and the node wrapper to the current process, so | ||
| /// every child inherits them. Existing values are overwritten: `mode` may come | ||
| /// from a CLI flag that differs from an inherited `CODSPEED_RUNNER_MODE`. | ||
| /// | ||
| /// # Safety | ||
| /// Must be called while the process is single-threaded. | ||
| pub(crate) unsafe fn apply(mode: MeasurementMode) -> anyhow::Result<()> { | ||
| for (key, value) in runner_shared::runtime_env::env(mode) { | ||
| unsafe { std::env::set_var(key, value) }; | ||
| } | ||
| let path = std::env::var_os(PATH_ENV).unwrap_or_default(); | ||
| unsafe { std::env::set_var(PATH_ENV, node::path_with_node_wrapper(&path)?) }; | ||
| Ok(()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| //! `node` wrapper for `codspeed exec`. | ||
| //! | ||
| //! The V8 flags we need slow node down, so they should only apply to the | ||
| //! benchmark process. | ||
| //! | ||
| //! In `codspeed run`, the runner's `introspected_nodejs` wrapper does this: | ||
| //! codspeed-node requests the flags through introspection, so non-benchmark | ||
| //! node processes are left alone. | ||
| //! | ||
| //! In `codspeed exec`, there is no codspeed-node and so no way to know which | ||
| //! process is the benchmark. This wrapper therefore adds the flags to every | ||
| //! `node` call. It mirrors codspeed-node's `getV8Flags()`. | ||
| //! | ||
| //! The two wrappers are never on PATH together (`enable_introspection` is | ||
| //! false for exec-harness targets). | ||
|
not-matthias marked this conversation as resolved.
|
||
|
|
||
| use std::ffi::{OsStr, OsString}; | ||
| use std::os::unix::fs::PermissionsExt; | ||
| use std::path::PathBuf; | ||
| use std::sync::Mutex; | ||
|
|
||
| const NODE_WRAPPER_SCRIPT: &str = include_str!("node.sh"); | ||
| const WRAPPER_DIR_PREFIX: &str = "codspeed_node_wrapper"; | ||
| const WRAPPER_FILE_NAME: &str = "node"; | ||
| const EXECUTABLE_MODE: u32 = 0o755; | ||
|
|
||
| /// Folder of the wrapper installed by this process, once it is installed. | ||
| static INSTALLED_DIR: Mutex<Option<PathBuf>> = Mutex::new(None); | ||
|
|
||
| /// Writes the `node` wrapper script and returns its folder. | ||
| /// | ||
| /// The folder is a fresh temp dir with a random name, created exclusively and | ||
| /// writable only by us: a shared, predictable path could be pre-created by | ||
| /// another user, who could then replace the script that ends up first on | ||
| /// `PATH`. It is kept after the process exits, since benchmarks execute the | ||
| /// wrapper until then. | ||
| /// | ||
| /// A process installs at most once. A second writer in the same process would | ||
| /// race with concurrent `fork`+`exec` calls: the child inherits the writable | ||
| /// descriptor and then fails to execute the script with `ETXTBSY`. | ||
| fn install_wrapper() -> std::io::Result<PathBuf> { | ||
| let mut installed = INSTALLED_DIR.lock().unwrap_or_else(|e| e.into_inner()); | ||
| if let Some(dir) = installed.as_ref() { | ||
| return Ok(dir.clone()); | ||
| } | ||
|
|
||
| let dir = tempfile::Builder::new() | ||
| .prefix(WRAPPER_DIR_PREFIX) | ||
| .tempdir()? | ||
| .keep(); | ||
|
not-matthias marked this conversation as resolved.
|
||
| let script = dir.join(WRAPPER_FILE_NAME); | ||
| std::fs::write(&script, NODE_WRAPPER_SCRIPT)?; | ||
| std::fs::set_permissions(&script, std::fs::Permissions::from_mode(EXECUTABLE_MODE))?; | ||
|
|
||
| *installed = Some(dir.clone()); | ||
| Ok(dir) | ||
| } | ||
|
|
||
| /// Installs the `node` wrapper and returns `path` with its folder prepended. | ||
| pub(super) fn path_with_node_wrapper(path: &OsStr) -> anyhow::Result<OsString> { | ||
| let wrapper_dir = install_wrapper()?; | ||
| Ok(std::env::join_paths( | ||
| std::iter::once(wrapper_dir).chain(std::env::split_paths(path)), | ||
| )?) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| #!/usr/bin/env bash | ||
| # CodSpeed `node` wrapper. | ||
| # | ||
| # Installed on PATH in front of the real node. Runs the real node with the V8 | ||
| # flags that the codspeed-node integration would request, based on | ||
| # CODSPEED_RUNNER_MODE and the node major version. | ||
| # | ||
| # Mirrors getV8Flags() and the mode mapping of getInstrumentMode(): | ||
| # https://github.com/CodSpeedHQ/codspeed-node/blob/main/packages/core/src/introspection.ts | ||
| # https://github.com/CodSpeedHQ/codspeed-node/blob/main/packages/core/src/runnerMode.ts | ||
| set -euo pipefail | ||
|
|
||
| # In simulation mode this script runs under valgrind with the CodSpeed preload | ||
| # library in LD_PRELOAD, which reports a benchmark result from every process | ||
| # that loads it. Helper processes must not load it, so it is removed here and | ||
| # restored for the final exec. For the same reason the script avoids command | ||
| # substitutions: a forked subshell would report a result when it exits. | ||
| codspeed_preload="${LD_PRELOAD:-}" | ||
| unset LD_PRELOAD | ||
|
|
||
| wrapper_script="${BASH_SOURCE[0]}" | ||
|
|
||
| # Sets `real_node` to the first `node` on PATH that is not this script. | ||
| find_real_node() { | ||
| local entry | ||
| local IFS=':' | ||
| set -o noglob | ||
| for entry in $PATH; do | ||
| if [[ -z "$entry" || ! -x "$entry/node" || "$entry/node" -ef "$wrapper_script" ]]; then | ||
| continue | ||
| fi | ||
| real_node="$entry/node" | ||
| return 0 | ||
| done | ||
| return 1 | ||
| } | ||
|
|
||
| # Sets `major` from the given node binary's version ("v22.12.0" -> 22). | ||
| node_major_version() { | ||
| local version | ||
| # The only subprocess: node runs without the preload library (see above). | ||
| version="$("$1" --version)" | ||
| version="${version#v}" | ||
| major="${version%%.*}" | ||
| } | ||
|
|
||
| # Flags for simulation and memory mode: make execution deterministic. | ||
| add_analysis_flags() { | ||
| flags+=( | ||
| --hash-seed=1 | ||
| --random-seed=1 | ||
| --no-opt | ||
| --predictable | ||
| --predictable-gc-schedule | ||
| --expose-gc | ||
| --no-concurrent-sweeping | ||
| --max-old-space-size=4096 | ||
| ) | ||
| if (( major < 18 )); then | ||
| flags+=(--no-randomize-hashes) | ||
| fi | ||
| if (( major < 20 )); then | ||
| flags+=(--no-scavenge-task) | ||
| else | ||
| # V8 11.3 renamed --scavenge-task to --minor-gc-task | ||
| flags+=(--no-minor-gc-task) | ||
| fi | ||
| if (( major >= 24 )); then | ||
| # --no-opt only disables TurboFan. Maglev is enabled by default from | ||
| # V8 13.6 and keeps tiering up hot functions. | ||
| flags+=(--no-maglev) | ||
| fi | ||
| } | ||
|
|
||
| # Flags for walltime mode: emit JIT symbols for the profiler. | ||
| add_walltime_flags() { | ||
| flags+=(--perf-prof) | ||
| if [[ -n "${CODSPEED_V8_LOG:-}" ]]; then | ||
| flags+=( | ||
| --log-code | ||
| --no-log-source-code | ||
| --no-logfile-per-isolate | ||
| "--logfile=${CODSPEED_V8_LOG}/codspeed-v8-%p.log" | ||
| ) | ||
| else | ||
| flags+=(--perf-basic-prof) | ||
| fi | ||
| } | ||
|
|
||
| find_real_node || { | ||
| echo "codspeed: node not found in PATH" >&2 | ||
| exit 1 | ||
| } | ||
| node_major_version "$real_node" | ||
|
|
||
| flags=( | ||
| --interpreted-frames-native-stack | ||
| --allow-natives-syntax | ||
| ) | ||
| case "${CODSPEED_RUNNER_MODE:-walltime}" in | ||
| instrumentation | simulation) add_analysis_flags ;; | ||
| memory) | ||
| add_analysis_flags | ||
| # Memory flamegraphs name JS frames through /tmp/perf-<pid>.map. | ||
| flags+=(--perf-basic-prof) | ||
| ;; | ||
| walltime) add_walltime_flags ;; | ||
| *) | ||
| echo "codspeed: unknown CODSPEED_RUNNER_MODE '${CODSPEED_RUNNER_MODE}'" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
|
|
||
| if [[ -n "$codspeed_preload" ]]; then | ||
| export LD_PRELOAD="$codspeed_preload" | ||
| fi | ||
| exec "$real_node" "${flags[@]}" "$@" |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.