Skip to content

Commit bf92b8d

Browse files
bartlomiejuclaude
andcommitted
fix(ext/node): implement node:v8 heap APIs (#32483)
Implements `v8.getHeapSpaceStatistics()`, `v8.getHeapCodeStatistics()`, `v8.getHeapSnapshot()`, and `v8.writeHeapSnapshot()` from the `node:v8` module Enabled node compat tests: `test-heapdump.js`, `test-v8-getheapsnapshot-twice.js`, `test-v8-stats.js` --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent de1d413 commit bf92b8d

File tree

7 files changed

+148
-23
lines changed

7 files changed

+148
-23
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ deno_task_shell = "=0.29.0"
9898
deno_terminal = "=0.2.3"
9999
deno_unsync = { version = "0.4.4", default-features = false }
100100
deno_whoami = "0.1.0"
101-
v8 = { version = "146.2.0", default-features = false }
101+
v8 = { version = "146.3.0", default-features = false }
102102

103103
denokv_proto = "0.13.0"
104104
denokv_remote = "0.13.0"

ext/node/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ deno_core::extension!(deno_node,
213213
ops::winerror::op_node_sys_to_uv_error,
214214
ops::v8::op_v8_cached_data_version_tag,
215215
ops::v8::op_v8_get_heap_statistics,
216+
ops::v8::op_v8_number_of_heap_spaces,
217+
ops::v8::op_v8_update_heap_space_statistics,
218+
ops::v8::op_v8_get_heap_code_statistics,
219+
ops::v8::op_v8_take_heap_snapshot,
216220
ops::v8::op_v8_get_wire_format_version,
217221
ops::v8::op_v8_new_deserializer,
218222
ops::v8::op_v8_new_serializer,

ext/node/ops/v8.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,51 @@ pub fn op_v8_get_heap_statistics(
3939
buffer[13] = stats.external_memory() as f64;
4040
}
4141

42+
#[op2(fast)]
43+
#[smi]
44+
pub fn op_v8_number_of_heap_spaces(scope: &mut v8::PinScope<'_, '_>) -> u32 {
45+
scope.number_of_heap_spaces() as u32
46+
}
47+
48+
#[op2]
49+
#[string]
50+
pub fn op_v8_update_heap_space_statistics(
51+
scope: &mut v8::PinScope<'_, '_>,
52+
#[buffer] buffer: &mut [f64],
53+
#[smi] space_index: u32,
54+
) -> Option<String> {
55+
let stats = scope.get_heap_space_statistics(space_index as usize)?;
56+
buffer[0] = stats.space_size() as f64;
57+
buffer[1] = stats.space_used_size() as f64;
58+
buffer[2] = stats.space_available_size() as f64;
59+
buffer[3] = stats.physical_space_size() as f64;
60+
Some(stats.space_name().to_string_lossy().into_owned())
61+
}
62+
63+
#[op2]
64+
#[buffer]
65+
pub fn op_v8_take_heap_snapshot(scope: &mut v8::PinScope<'_, '_>) -> Vec<u8> {
66+
let mut buf = Vec::new();
67+
scope.take_heap_snapshot(|chunk| {
68+
buf.extend_from_slice(chunk);
69+
true
70+
});
71+
buf
72+
}
73+
74+
#[op2(fast)]
75+
pub fn op_v8_get_heap_code_statistics(
76+
scope: &mut v8::PinScope<'_, '_>,
77+
#[buffer] buffer: &mut [f64],
78+
) {
79+
if let Some(stats) = scope.get_heap_code_and_metadata_statistics() {
80+
buffer[0] = stats.code_and_metadata_size() as f64;
81+
buffer[1] = stats.bytecode_and_metadata_size() as f64;
82+
buffer[2] = stats.external_script_source_size() as f64;
83+
buffer[3] = stats.cpu_profiler_metadata_size() as f64;
84+
}
85+
}
86+
4287
pub struct Serializer<'a> {
4388
inner: v8::ValueSerializer<'a>,
4489
}

ext/node/polyfills/v8.ts

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import { primordials } from "ext:core/mod.js";
1010
const { ObjectPrototypeToString, SymbolSpecies } = primordials;
1111
import {
1212
op_v8_cached_data_version_tag,
13+
op_v8_get_heap_code_statistics,
1314
op_v8_get_heap_statistics,
1415
op_v8_get_wire_format_version,
1516
op_v8_new_deserializer,
1617
op_v8_new_serializer,
18+
op_v8_number_of_heap_spaces,
1719
op_v8_read_double,
1820
op_v8_read_header,
1921
op_v8_read_raw_bytes,
@@ -22,8 +24,10 @@ import {
2224
op_v8_read_value,
2325
op_v8_release_buffer,
2426
op_v8_set_treat_array_buffer_views_as_host_objects,
27+
op_v8_take_heap_snapshot,
2528
op_v8_transfer_array_buffer,
2629
op_v8_transfer_array_buffer_de,
30+
op_v8_update_heap_space_statistics,
2731
op_v8_write_double,
2832
op_v8_write_header,
2933
op_v8_write_raw_bytes,
@@ -33,21 +37,54 @@ import {
3337
} from "ext:core/ops";
3438

3539
import { Buffer } from "node:buffer";
40+
import { writeFileSync } from "node:fs";
41+
import { Readable } from "node:stream";
3642

3743
import { notImplemented } from "ext:deno_node/_utils.ts";
3844
import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts";
45+
import { getValidatedPath } from "ext:deno_node/internal/fs/utils.mjs";
46+
import { validateObject } from "ext:deno_node/internal/validators.mjs";
3947

4048
export function cachedDataVersionTag() {
4149
return op_v8_cached_data_version_tag();
4250
}
51+
const heapCodeStatisticsBuffer = new Float64Array(4);
52+
4353
export function getHeapCodeStatistics() {
44-
notImplemented("v8.getHeapCodeStatistics");
54+
op_v8_get_heap_code_statistics(heapCodeStatisticsBuffer);
55+
return {
56+
code_and_metadata_size: heapCodeStatisticsBuffer[0],
57+
bytecode_and_metadata_size: heapCodeStatisticsBuffer[1],
58+
external_script_source_size: heapCodeStatisticsBuffer[2],
59+
cpu_profiler_metadata_size: heapCodeStatisticsBuffer[3],
60+
};
4561
}
46-
export function getHeapSnapshot() {
47-
notImplemented("v8.getHeapSnapshot");
62+
export function getHeapSnapshot(options?: Record<string, unknown>) {
63+
if (options !== undefined) {
64+
validateObject(options, "options");
65+
}
66+
const data = op_v8_take_heap_snapshot();
67+
return Readable.from(Buffer.from(data));
4868
}
69+
const heapSpaceStatisticsBuffer = new Float64Array(4);
70+
4971
export function getHeapSpaceStatistics() {
50-
notImplemented("v8.getHeapSpaceStatistics");
72+
const numberOfHeapSpaces = op_v8_number_of_heap_spaces();
73+
const heapSpaceStatistics = new Array(numberOfHeapSpaces);
74+
for (let i = 0; i < numberOfHeapSpaces; i++) {
75+
const spaceName = op_v8_update_heap_space_statistics(
76+
heapSpaceStatisticsBuffer,
77+
i,
78+
);
79+
heapSpaceStatistics[i] = {
80+
space_name: spaceName,
81+
space_size: heapSpaceStatisticsBuffer[0],
82+
space_used_size: heapSpaceStatisticsBuffer[1],
83+
space_available_size: heapSpaceStatisticsBuffer[2],
84+
physical_space_size: heapSpaceStatisticsBuffer[3],
85+
};
86+
}
87+
return heapSpaceStatistics;
5188
}
5289

5390
const buffer = new Float64Array(14);
@@ -89,9 +126,39 @@ export function stopCoverage() {
89126
export function takeCoverage() {
90127
notImplemented("v8.takeCoverage");
91128
}
92-
export function writeHeapSnapshot() {
93-
notImplemented("v8.writeHeapSnapshot");
129+
130+
let heapSnapshotCounter = 0;
131+
132+
export function writeHeapSnapshot(
133+
filename?: string,
134+
options?: Record<string, unknown>,
135+
) {
136+
if (filename !== undefined) {
137+
filename = getValidatedPath(filename) as string;
138+
} else {
139+
const now = new Date();
140+
const year = now.getFullYear();
141+
const month = String(now.getMonth() + 1).padStart(2, "0");
142+
const day = String(now.getDate()).padStart(2, "0");
143+
const hours = String(now.getHours()).padStart(2, "0");
144+
const minutes = String(now.getMinutes()).padStart(2, "0");
145+
const seconds = String(now.getSeconds()).padStart(2, "0");
146+
const pid = globalThis.process?.pid ?? 0;
147+
const thread = 0;
148+
const seq = ++heapSnapshotCounter;
149+
filename =
150+
`Heap.${year}${month}${day}.${hours}${minutes}${seconds}.${pid}.${thread}.${
151+
String(seq).padStart(3, "0")
152+
}.heapsnapshot`;
153+
}
154+
if (options !== undefined) {
155+
validateObject(options, "options");
156+
}
157+
const data = op_v8_take_heap_snapshot();
158+
writeFileSync(filename, data);
159+
return filename;
94160
}
161+
95162
// deno-lint-ignore no-explicit-any
96163
export function serialize(value: any) {
97164
const ser = new DefaultSerializer();

tests/node_compat/config.jsonc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,7 +1503,9 @@
15031503
"parallel/test-uv-binding-constant.js": {},
15041504
"parallel/test-v8-deserialize-buffer.js": {},
15051505
"parallel/test-v8-flag-pool-size-0.js": {},
1506+
"parallel/test-v8-getheapsnapshot-twice.js": {},
15061507
"parallel/test-v8-global-setter.js": {},
1508+
"parallel/test-v8-stats.js": {},
15071509
"parallel/test-vm-access-process-env.js": {},
15081510
"parallel/test-vm-api-handles-getter-errors.js": {},
15091511
"parallel/test-vm-attributes-property-not-on-sandbox.js": {},
@@ -1774,6 +1776,9 @@
17741776
// "sequential/test-diagnostic-dir-heap-prof.js": {},
17751777
"sequential/test-fs-readdir-recursive.js": {},
17761778
"sequential/test-fs-stat-sync-overflow.js": {},
1779+
"sequential/test-heapdump.js": {
1780+
"windows": false
1781+
},
17771782
"sequential/test-http-server-keep-alive-timeout-slow-server.js": {},
17781783
// TODO(bartlomieju): disabled during work on `node:inspector`, this test didn't actualy run before
17791784
// "sequential/test-inspector-open-dispose.mjs": {},

tests/unit_node/v8_test.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,22 @@
11
// Copyright 2018-2026 the Deno authors. MIT license.
2-
import {
3-
cachedDataVersionTag,
4-
deserialize,
5-
getHeapStatistics,
6-
serialize,
7-
setFlagsFromString,
8-
} from "node:v8";
9-
import { assertEquals } from "@std/assert";
2+
import * as v8 from "node:v8";
3+
import { assertEquals, assertThrows } from "@std/assert";
104

115
// https://github.com/nodejs/node/blob/a2bbe5ff216bc28f8dac1c36a8750025a93c3827/test/parallel/test-v8-version-tag.js#L6
126
Deno.test({
137
name: "cachedDataVersionTag success",
148
fn() {
15-
const tag = cachedDataVersionTag();
9+
const tag = v8.cachedDataVersionTag();
1610
assertEquals(typeof tag, "number");
17-
assertEquals(cachedDataVersionTag(), tag);
11+
assertEquals(v8.cachedDataVersionTag(), tag);
1812
},
1913
});
2014

2115
// https://github.com/nodejs/node/blob/a2bbe5ff216bc28f8dac1c36a8750025a93c3827/test/parallel/test-v8-stats.js#L6
2216
Deno.test({
2317
name: "getHeapStatistics success",
2418
fn() {
25-
const s = getHeapStatistics();
19+
const s = v8.getHeapStatistics();
2620
const keys = [
2721
"does_zap_garbage",
2822
"external_memory",
@@ -52,15 +46,25 @@ Deno.test({
5246
Deno.test({
5347
name: "setFlagsFromString",
5448
fn() {
55-
setFlagsFromString("--allow_natives_syntax");
49+
v8.setFlagsFromString("--allow_natives_syntax");
5650
},
5751
});
5852

5953
Deno.test({
6054
name: "serialize deserialize",
6155
fn() {
62-
const s = serialize({ a: 1 });
63-
const d = deserialize(s);
56+
const s = v8.serialize({ a: 1 });
57+
const d = v8.deserialize(s);
6458
assertEquals(d, { a: 1 });
6559
},
6660
});
61+
62+
Deno.test({
63+
name: "writeHeapSnapshot requires write permission",
64+
permissions: { write: false },
65+
fn() {
66+
assertThrows(() => {
67+
v8.writeHeapSnapshot("test.heapsnapshot");
68+
}, Deno.errors.NotCapable);
69+
},
70+
});

0 commit comments

Comments
 (0)