POC: Memory profiler allocation labels#62649
Draft
rudolf wants to merge 7 commits intonodejs:mainfrom
Draft
Conversation
Add a callback mechanism to V8's SamplingHeapProfiler that allows embedders to attach key-value string labels to allocation samples. The callback receives the ContinuationPreservedEmbedderData (CPED) and writes labels into the sample. This enables per-context memory attribution (e.g., per-HTTP-route) when combined with AsyncLocalStorage. Changes: - AllocationProfile::Sample gains a `labels` field - New HeapProfileSampleLabelsCallback typedef on HeapProfiler - SamplingHeapProfiler invokes the callback in SampleObject() - BuildSamples() copies labels to public Sample structs - Gated behind V8_HEAP_PROFILER_SAMPLE_LABELS (requires CPED) Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
Seven tests covering the labels callback API: - Basic label attribution via callback - No callback returns unlabeled samples - Empty labels callback - Multiple distinct label sets - Labels survive GC with retain flags - Samples removed by GC without flags - Callback unregistration stops labeling Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
C++ bindings for the V8 heap profile labels callback: - RegisterHeapProfileLabels/UnregisterHeapProfileLabels via CPED lookup - HeapProfileLabelsCallback with O(1) address map + GC-move slow path - ProfilingArrayBufferAllocator for per-label Buffer/ArrayBuffer tracking - GetAllocationProfile with labels and externalBytes - Cleanup hooks for environment teardown - Build flag conditioning on v8_enable_continuation_preserved_embedder_data Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
JS API for heap profile label attribution: - withHeapProfileLabels(labels, fn): scoped labels via AsyncLocalStorage with automatic cleanup on sync return, promise settle, or exception - setHeapProfileLabels(labels): enterWith semantics for frameworks where the handler runs after the extension returns (e.g., Hapi) - Expose startSamplingHeapProfiler, stopSamplingHeapProfiler, getAllocationProfile from the v8 binding Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
JS tests: - test-v8-heap-profile-labels: basic labeling, multi-key, JSON round-trip, GC retention/removal, includeCollectedObjects flag - test-v8-heap-profile-labels-async: await boundary propagation, concurrent contexts, Hapi-style setHeapProfileLabels - test-v8-heap-profile-external: Buffer/ArrayBuffer per-label externalBytes tracking, GC cleanup, unlabeled isolation C++ cctests: - Node.js-level label registration, callback integration, cleanup Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
Document the new v8 module APIs: - startSamplingHeapProfiler with includeCollectedObjects option - stopSamplingHeapProfiler - getAllocationProfile with samples and externalBytes - withHeapProfileLabels for scoped label attribution - setHeapProfileLabels for enterWith-style frameworks - Limitations section covering what is and isn't measured Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
Three benchmarks: - v8/heap-profiler-labels: micro benchmark (1M allocations) - http/heap-profiler-labels: single-server with ~150KB mixed workload - http/heap-profiler-realistic: two-server (app + DB) with JSON parse, column aggregation, and Buffer allocation per request Statistical results (20 runs, 1000 rows, Welch t-test): none -> sampling: ~1% overhead sampling -> labels: ~1% overhead none -> labels (total): ~2% overhead Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
Collaborator
|
Review requested:
|
Qard
reviewed
Apr 9, 2026
| // This happens when --experimental-async-context-frame is not set on | ||
| // Node.js 22, causing all contexts to map to Smi::zero() (address 0). | ||
| if (cped.IsEmpty() || cped->IsUndefined()) return; | ||
| uintptr_t addr = node::GetLocalAddress(cped); |
Member
There was a problem hiding this comment.
Storing in binding data by the CPED address won't work at all. Because all AsyncLocalStorage contexts are combined into a single AsyncContextFrame map, any changes to any contexts will change what this value is, even if the particular store you are interested in has not changed at all within that map frame.
You would need to have V8 capture the CPED value at the time of the sample and store that on the heap profile itself alongside the samples, then use that actual AsyncContextFrame instance to look up what the corresponding data was in that frame for the label store.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
This is a POC for initial feedback. If we can get alignment within Node.js I could try to contribute the v8 changes upstream.
Summary
Adds the ability to tag sampling heap profiler allocations with string labels that propagate through async context (via CPED). This enables attributing memory usage to specific HTTP routes, tenants, or operations — something no JS runtime currently supports.
V8 changes
Datadog's Attila Szegedi proposed a similar label mechanism for CPU profiling on v8-dev (July 2025). V8 team (Leszek Swirski) indicated they would review non-invasive patches behind #ifdefs. This PR applies the same approach to heap profiling, which is simpler. Everything runs on the allocation thread with no signal-safety concerns.
Node.js changes:
#62273 landed the SyncHeapProfileHandle API with Symbol.dispose support. The labels API proposed here is complementary, it adds context (which route/tenant) to the samples that SyncHeapProfileHandle already collects. A follow-up could integrate withHeapProfileLabels as a method on the handle.
Motivation
In multi-tenant or multi-route Node.js servers, a memory spike today tells you how much memory grew but not what caused it. Operators resort to code inspection or heap snapshots but these don't scale to collecting data over long timespans for large deployments. With labeled heap/external memory profiling, you can answer "route /api/search accounts for 400MB of the 1.2GB heap" directly from production telemetry (e.g. via OTel).
This mirrors Go's pprof.Labels capability
Overhead
20-run benchmark (two-server realistic HTTP workload):
Test plan