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 getters to get children, name, scriptName and others. 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).

@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.29 MB
No deduping: 2.29 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-11 13:26:10

Comparing candidate commit fbcdce8 in PR branch ishabi/js-objects-allocations-improvements with baseline commit 7bf5cc6 in branch main.

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

@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".

@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.

@IlyasShabi IlyasShabi marked this pull request as draft February 10, 2026 08:35
};
} // namespace

std::shared_ptr<Node> TranslateAllocationProfileToCpp(
Copy link
Author

Choose a reason for hiding this comment

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

TranslateAllocationProfileToCpp and TranslateAllocationProfileToExternal are ~similar. While templating could reduce duplication, the different string assignment mechanisms would add complexity. Keeping both functions for clarity


export interface AllocationProfileNode extends ProfileNode {
allocations: Allocation[];
children: AllocationProfileNode[];
Copy link
Author

@IlyasShabi IlyasShabi Feb 10, 2026

Choose a reason for hiding this comment

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

Fixing children type bug

@IlyasShabi IlyasShabi force-pushed the ishabi/js-objects-allocations-improvements branch from 7e1921f to b0caf9a Compare February 10, 2026 09:51
@IlyasShabi IlyasShabi marked this pull request as ready for review February 10, 2026 11:53
@nsavoire
Copy link

Currently we have this processing flow for a heap profile:

  • GetAllocationProfile()
    • v8::AllocationProfile (C++)
  • TranslateAllocationProfile()
    • JS profile (node based)
  • serializeHeapProfile
    • pprof-format JS profile (sample based)
  • pprof-format encode
    • protobuf
  • gzip
    • compressed buffer

IIUC the proposed change should decrease the JS memory used during the second step but at the expense of creating a copy of the entire C++ node tree.

It would be nice to have benchmark / test cases that exhibits the behaviour that need fixing and measure the improvement.
Without this, we're essentially optimizing blindly. And I am wary that we might be adding complexity that doesn't solve the actual problem.

@IlyasShabi
Copy link
Author

IlyasShabi commented Feb 11, 2026

I added a benchmark for the second step that:

  • Runs V1 and V2 in separate processes for accurate isolated measurements
  • Generates allocations and traverses the entire tree to load all nodes
  • Measure heapUsed before/after

Results I got from Local and CI across platforms:

  • Memory of only getting data of getAllocationProfile/v2: ~22% of memory usage reduction (V1: ~41KB, V2: ~32KB)
  • Memory with traversing the tree to load lazy children: ~16% of memory usage reduction (V1: ~48KB, V2: ~40KB)

@IlyasShabi IlyasShabi marked this pull request as draft February 11, 2026 13:23
@IlyasShabi IlyasShabi marked this pull request as ready for review February 11, 2026 13:42
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