Skip to content

Conversation

@IlyasShabi
Copy link

@IlyasShabi IlyasShabi commented Feb 9, 2026

What does this PR do?:
Introduces a new lazy heap profiling API profileV2 that reduces memory usage during heap profile serialization. Instead of materializing the entire V8 allocation profile as JS object upfront, the new API traverses the profile root tree on-demand

Motivation:

When collecting heap profiles, the current profile() API calls GetAllocationProfile() to get the profile data, then recursively translate the entire profile tree into JS objects, this creates a full JS objects tree in heap memory which can be significant for large apps.
The new profileV2() API introduces a lazy traversal pattern, getting JS object is done on-demand using the new class AllocationProfileNodeWrapper with methods like getChildrenCount and getChild. On the TS side, we only request for current node JS object which allow us to gradually build the tree and reduce memory usage at this operation.

Additional Notes:

The current implementation copies the V8 allocation profile to C++ heap to keep it alive after GetAllocationProfile() returns. I think that this is necessary because profile object has limited lifetime (HandleScope). While this still a copy it's lightweight compared to creating full JS object since we only store raw profile data (string, number).

Additionally, in this branch I attempted to avoid copying the profile data to C++ by only store profile->GetRootNode(). This can be called on the TS side using v8ProfileV3(), but it crashes when accessing the first child with profile.getChild(0) because node->name is no longer available

To reproduce the error:

yarn prepare && node system-test/busybench-js/src/busybench.js

@IlyasShabi IlyasShabi self-assigned this Feb 9, 2026
@IlyasShabi IlyasShabi added the semver-patch Bug or security fixes, mainly label Feb 9, 2026
@github-actions
Copy link

github-actions bot commented Feb 9, 2026

Overall package size

Self size: 1.91 MB
Deduped: 2.3 MB
No deduping: 2.3 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | source-map | 0.7.6 | 185.63 kB | 185.63 kB | | pprof-format | 2.2.1 | 163.06 kB | 163.06 kB | | p-limit | 3.1.0 | 7.75 kB | 13.78 kB | | delay | 5.0.0 | 11.17 kB | 11.17 kB | | node-gyp-build | 3.9.0 | 8.81 kB | 8.81 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@pr-commenter
Copy link

pr-commenter bot commented Feb 9, 2026

Benchmarks

Benchmark execution time: 2026-02-09 13:54:00

Comparing candidate commit 4c77cf6 in PR branch ishabi/js-objects-allocations-improvements with baseline commit eb9eabd in branch main.

Found 1 performance improvements and 1 performance regressions! Performance is the same for 90 metrics, 28 unstable metrics.

scenario:profiler-idle-no-wall-profiler-18

  • 🟩 cpu_user_time [-5.066ms; -1.441ms] or [-10.065%; -2.863%]

scenario:profiler-light-load-no-wall-profiler-24

  • 🟥 cpu_user_time [+1.118ms; +7.258ms] or [+1.501%; +9.750%]

@IlyasShabi IlyasShabi marked this pull request as ready for review February 9, 2026 09:05
@IlyasShabi IlyasShabi changed the title Improve heap profile memoery usage by lazily loading js objects Improve heap profile memory usage by lazily loading js objects Feb 9, 2026
@nsavoire
Copy link

nsavoire commented Feb 9, 2026

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2982ce329

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 687 to 706
// Handle file:// prefix
let scriptName = node.scriptName;
if (scriptName.startsWith('file://')) {
scriptName = scriptName.slice(7);
}

if (ignoreSamplesPath && scriptName.indexOf(ignoreSamplesPath) > -1) {
continue;
}

const stack = entry.stack;
const location = getLocation(
node,
locations,
locationIdMap,
functions,
functionIdMap,
stringTable,
sourceMapper
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize scriptName before building locations

In serializeHeapProfileV2, the file:// prefix is stripped into a local scriptName only for the ignore-path check, but getLocation(...) still receives the unmodified node whose scriptName retains the file:// prefix. That means the serialized profile will embed file:// paths (and dedup keys) instead of the normalized paths used elsewhere, which is a behavioral change from the existing serializer and can break source-mapping and path-based filtering in downstream tooling that expects plain filesystem paths. Consider mutating node.scriptName or passing the normalized path into getLocation so V2 output matches V1 normalization.

Useful? React with 👍 / 👎.

@szegedi
Copy link

szegedi commented Feb 9, 2026

Some thoughts:

  • It's indeed unfortunate that AllocationProfile::Node stores Local<String> values in a data field. I'm a bit wary about duplication of string values (especially script names) if you convert them to std::string. I do think there's a better way, though: create a Node structure that's very similar to the V8 one, but use Global<String> for these fields. This will prevent duplication.
  • You can then convert those globals to locals when you do populateFields(). This will preserve all the deduplication of those strings; this way you never go from a V8 String to a std::string and back, you just go from Local<String> to Global<String> and back, which are very small pointer operations and just keep referencing the already constructed JS string values on the heap.
  • populateFields will also have the unfortunate side-effect of creating a bunch of new strings for keys too. I guess you could create a bunch of NAN_GETTER() methods that on invocation extract the data from the wrapped Node object.
  • Since the serializer is enqueuing all of the children of the current node, we don't really gain anything by having a separate GetChildrenCount/GetChild API. You could thus declare a NAN_GETTER(Children) that creates the array of children, and I think this'd allow you to keep using the existing serialize method in the profile-serializer.ts and then you wouldn't need serializeHeapProfileV2.
  • the concept of the AllocationProfileHolder might be unnecessary. I'd think it should be enough for AllocationNodeWrapper to have a std::shared_ptr<Node> – since they also keep their children through shared pointers, this should all nicely clean itself up once the wrapper to the root gets garbage collected by V8. You don't need to hold the original profile in memory once our C++ tree of nodes was constructed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver-patch Bug or security fixes, mainly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants