Skip to content

Commit d30cccd

Browse files
joyeecheungmarco-ippolito
authored andcommitted
v8: implement v8.queryObjects() for memory leak regression testing
This is similar to the `queryObjects()` console API provided by the Chromium DevTools console. It can be used to search for objects that have the matching constructor on its prototype chain in the entire heap, which can be useful for memory leak regression tests. To avoid surprising results, users should avoid using this API on constructors whose implementation they don't control, or on constructors that can be invoked by other parties in the application. To avoid accidental leaks, this API does not return raw references to the objects found. By default, it returns the count of the objects found. If `options.format` is `'summary'`, it returns an array containing brief string representations for each object. The visibility provided in this API is similar to what the heap snapshot provides, while users can save the cost of serialization and parsing and directly filer the target objects during the search. We have been using this API internally for the test suite, which has been more stable than any other leak regression testing strategies in the CI. With a public implementation we can now use the public API instead. PR-URL: #51927 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Vinícius Lourenço Claro Cardoso <contact@viniciusl.com.br> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
1 parent 956ee74 commit d30cccd

14 files changed

+359
-51
lines changed

doc/api/v8.md

+84
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,89 @@ buffers and external strings.
242242
}
243243
```
244244

245+
## `v8.queryObjects(ctor[, options])`
246+
247+
<!-- YAML
248+
added: REPLACEME
249+
-->
250+
251+
> Stability: 1.1 - Active development
252+
253+
* `ctor` {Function} The constructor that can be used to search on the
254+
prototype chain in order to filter target objects in the heap.
255+
* `options` {undefined|Object}
256+
* `format` {string} If it's `'count'`, the count of matched objects
257+
is returned. If it's `'summary'`, an array with summary strings
258+
of the matched objects is returned.
259+
* Returns: {number|Array<string>}
260+
261+
This is similar to the [`queryObjects()` console API][] provided by the
262+
Chromium DevTools console. It can be used to search for objects that
263+
have the matching constructor on its prototype chain in the heap after
264+
a full garbage collection, which can be useful for memory leak
265+
regression tests. To avoid surprising results, users should avoid using
266+
this API on constructors whose implementation they don't control, or on
267+
constructors that can be invoked by other parties in the application.
268+
269+
To avoid accidental leaks, this API does not return raw references to
270+
the objects found. By default, it returns the count of the objects
271+
found. If `options.format` is `'summary'`, it returns an array
272+
containing brief string representations for each object. The visibility
273+
provided in this API is similar to what the heap snapshot provides,
274+
while users can save the cost of serialization and parsing and directly
275+
filter the target objects during the search.
276+
277+
Only objects created in the current execution context are included in the
278+
results.
279+
280+
```cjs
281+
const { queryObjects } = require('node:v8');
282+
class A { foo = 'bar'; }
283+
console.log(queryObjects(A)); // 0
284+
const a = new A();
285+
console.log(queryObjects(A)); // 1
286+
// [ "A { foo: 'bar' }" ]
287+
console.log(queryObjects(A, { format: 'summary' }));
288+
289+
class B extends A { bar = 'qux'; }
290+
const b = new B();
291+
console.log(queryObjects(B)); // 1
292+
// [ "B { foo: 'bar', bar: 'qux' }" ]
293+
console.log(queryObjects(B, { format: 'summary' }));
294+
295+
// Note that, when there are child classes inheriting from a constructor,
296+
// the constructor also shows up in the prototype chain of the child
297+
// classes's prototoype, so the child classes's prototoype would also be
298+
// included in the result.
299+
console.log(queryObjects(A)); // 3
300+
// [ "B { foo: 'bar', bar: 'qux' }", 'A {}', "A { foo: 'bar' }" ]
301+
console.log(queryObjects(A, { format: 'summary' }));
302+
```
303+
304+
```mjs
305+
import { queryObjects } from 'node:v8';
306+
class A { foo = 'bar'; }
307+
console.log(queryObjects(A)); // 0
308+
const a = new A();
309+
console.log(queryObjects(A)); // 1
310+
// [ "A { foo: 'bar' }" ]
311+
console.log(queryObjects(A, { format: 'summary' }));
312+
313+
class B extends A { bar = 'qux'; }
314+
const b = new B();
315+
console.log(queryObjects(B)); // 1
316+
// [ "B { foo: 'bar', bar: 'qux' }" ]
317+
console.log(queryObjects(B, { format: 'summary' }));
318+
319+
// Note that, when there are child classes inheriting from a constructor,
320+
// the constructor also shows up in the prototype chain of the child
321+
// classes's prototoype, so the child classes's prototoype would also be
322+
// included in the result.
323+
console.log(queryObjects(A)); // 3
324+
// [ "B { foo: 'bar', bar: 'qux' }", 'A {}', "A { foo: 'bar' }" ]
325+
console.log(queryObjects(A, { format: 'summary' }));
326+
```
327+
245328
## `v8.setFlagsFromString(flags)`
246329

247330
<!-- YAML
@@ -1212,6 +1295,7 @@ setTimeout(() => {
12121295
[`deserializer._readHostObject()`]: #deserializer_readhostobject
12131296
[`deserializer.transferArrayBuffer()`]: #deserializertransferarraybufferid-arraybuffer
12141297
[`init` callback]: #initpromise-parent
1298+
[`queryObjects()` console API]: https://developer.chrome.com/docs/devtools/console/utilities#queryObjects-function
12151299
[`serialize()`]: #v8serializevalue
12161300
[`serializer._getSharedArrayBufferId()`]: #serializer_getsharedarraybufferidsharedarraybuffer
12171301
[`serializer._writeHostObject()`]: #serializer_writehostobjectobject

lib/internal/heap_utils.js

+42-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,31 @@
22
const {
33
Symbol,
44
Uint8Array,
5+
ArrayPrototypeMap,
56
} = primordials;
67
const {
78
kUpdateTimer,
89
onStreamRead,
910
} = require('internal/stream_base_commons');
1011
const { owner_symbol } = require('internal/async_hooks').symbols;
1112
const { Readable } = require('stream');
12-
const { validateObject, validateBoolean } = require('internal/validators');
13-
const { kEmptyObject } = require('internal/util');
14-
13+
const {
14+
validateObject,
15+
validateBoolean,
16+
validateFunction,
17+
} = require('internal/validators');
18+
const {
19+
codes: {
20+
ERR_INVALID_ARG_VALUE,
21+
},
22+
} = require('internal/errors');
23+
const { kEmptyObject, emitExperimentalWarning } = require('internal/util');
24+
const {
25+
queryObjects: _queryObjects,
26+
} = internalBinding('internal_only_v8');
27+
const {
28+
inspect,
29+
} = require('internal/util/inspect');
1530
const kHandle = Symbol('kHandle');
1631

1732
function getHeapSnapshotOptions(options = kEmptyObject) {
@@ -50,7 +65,31 @@ class HeapSnapshotStream extends Readable {
5065
}
5166
}
5267

68+
const inspectOptions = {
69+
__proto__: null,
70+
depth: 0,
71+
};
72+
function queryObjects(ctor, options = kEmptyObject) {
73+
validateFunction(ctor, 'constructor');
74+
if (options !== kEmptyObject) {
75+
validateObject(options, 'options');
76+
}
77+
const format = options.format || 'count';
78+
if (format !== 'count' && format !== 'summary') {
79+
throw new ERR_INVALID_ARG_VALUE('options.format', format);
80+
}
81+
emitExperimentalWarning('v8.queryObjects()');
82+
// Matching the console API behavior - just access the .prototype.
83+
const objects = _queryObjects(ctor.prototype);
84+
if (format === 'count') {
85+
return objects.length;
86+
}
87+
// options.format is 'summary'.
88+
return ArrayPrototypeMap(objects, (object) => inspect(object, inspectOptions));
89+
}
90+
5391
module.exports = {
5492
getHeapSnapshotOptions,
5593
HeapSnapshotStream,
94+
queryObjects,
5695
};

lib/internal/test/binding.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
'use strict';
22

33
const {
4+
Error,
5+
StringPrototypeStartsWith,
46
globalThis,
57
} = primordials;
68

79
process.emitWarning(
810
'These APIs are for internal testing only. Do not use them.',
911
'internal/test/binding');
1012

13+
function filteredInternalBinding(id) {
14+
// Disallows internal bindings with names that start with 'internal_only'
15+
// which means it should not be exposed to users even with
16+
// --expose-internals.
17+
if (StringPrototypeStartsWith(id, 'internal_only')) {
18+
// This code is only intended for internal errors and is not documented.
19+
// Do not use the normal error system.
20+
// eslint-disable-next-line no-restricted-syntax
21+
const error = new Error(`No such binding: ${id}`);
22+
error.code = 'ERR_INVALID_MODULE';
23+
throw error;
24+
}
25+
return internalBinding(id);
26+
}
27+
1128
if (module.isPreloading) {
12-
globalThis.internalBinding = internalBinding;
29+
globalThis.internalBinding = filteredInternalBinding;
1330
globalThis.primordials = primordials;
1431
}
1532

16-
module.exports = { internalBinding, primordials };
33+
module.exports = { internalBinding: filteredInternalBinding, primordials };

lib/v8.js

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const {
6060
const {
6161
HeapSnapshotStream,
6262
getHeapSnapshotOptions,
63+
queryObjects,
6364
} = require('internal/heap_utils');
6465
const promiseHooks = require('internal/promise_hooks');
6566
const { getOptionValue } = require('internal/options');
@@ -437,6 +438,7 @@ module.exports = {
437438
serialize,
438439
writeHeapSnapshot,
439440
promiseHooks,
441+
queryObjects,
440442
startupSnapshot,
441443
setHeapSnapshotNearHeapLimit,
442444
GCProfiler,

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
'src/handle_wrap.cc',
8181
'src/heap_utils.cc',
8282
'src/histogram.cc',
83+
'src/internal_only_v8.cc',
8384
'src/js_native_api.h',
8485
'src/js_native_api_types.h',
8586
'src/js_native_api_v8.cc',

src/heap_utils.cc

-36
Original file line numberDiff line numberDiff line change
@@ -474,39 +474,6 @@ void TriggerHeapSnapshot(const FunctionCallbackInfo<Value>& args) {
474474
return args.GetReturnValue().Set(filename_v);
475475
}
476476

477-
class PrototypeChainHas : public v8::QueryObjectPredicate {
478-
public:
479-
PrototypeChainHas(Local<Context> context, Local<Object> search)
480-
: context_(context), search_(search) {}
481-
482-
// What we can do in the filter can be quite limited, but looking up
483-
// the prototype chain is something that the inspector console API
484-
// queryObject() does so it is supported.
485-
bool Filter(Local<Object> object) override {
486-
for (Local<Value> proto = object->GetPrototype(); proto->IsObject();
487-
proto = proto.As<Object>()->GetPrototype()) {
488-
if (search_ == proto) return true;
489-
}
490-
return false;
491-
}
492-
493-
private:
494-
Local<Context> context_;
495-
Local<Object> search_;
496-
};
497-
498-
void CountObjectsWithPrototype(const FunctionCallbackInfo<Value>& args) {
499-
CHECK_EQ(args.Length(), 1);
500-
CHECK(args[0]->IsObject());
501-
Local<Object> proto = args[0].As<Object>();
502-
Isolate* isolate = args.GetIsolate();
503-
Local<Context> context = isolate->GetCurrentContext();
504-
PrototypeChainHas prototype_chain_has(context, proto);
505-
std::vector<Global<Object>> out;
506-
isolate->GetHeapProfiler()->QueryObjects(context, &prototype_chain_has, &out);
507-
args.GetReturnValue().Set(static_cast<uint32_t>(out.size()));
508-
}
509-
510477
void Initialize(Local<Object> target,
511478
Local<Value> unused,
512479
Local<Context> context,
@@ -515,15 +482,12 @@ void Initialize(Local<Object> target,
515482
SetMethod(context, target, "triggerHeapSnapshot", TriggerHeapSnapshot);
516483
SetMethod(
517484
context, target, "createHeapSnapshotStream", CreateHeapSnapshotStream);
518-
SetMethod(
519-
context, target, "countObjectsWithPrototype", CountObjectsWithPrototype);
520485
}
521486

522487
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
523488
registry->Register(BuildEmbedderGraph);
524489
registry->Register(TriggerHeapSnapshot);
525490
registry->Register(CreateHeapSnapshotStream);
526-
registry->Register(CountObjectsWithPrototype);
527491
}
528492

529493
} // namespace heap

src/internal_only_v8.cc

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include "node_binding.h"
2+
#include "node_external_reference.h"
3+
#include "util-inl.h"
4+
#include "v8-profiler.h"
5+
#include "v8.h"
6+
7+
using v8::Array;
8+
using v8::Context;
9+
using v8::FunctionCallbackInfo;
10+
using v8::Global;
11+
using v8::Isolate;
12+
using v8::Local;
13+
using v8::Object;
14+
using v8::Value;
15+
16+
namespace node {
17+
namespace internal_only_v8 {
18+
19+
class PrototypeChainHas : public v8::QueryObjectPredicate {
20+
public:
21+
PrototypeChainHas(Local<Context> context, Local<Object> search)
22+
: context_(context), search_(search) {}
23+
24+
// What we can do in the filter can be quite limited, but looking up
25+
// the prototype chain is something that the inspector console API
26+
// queryObject() does so it is supported.
27+
bool Filter(Local<Object> object) override {
28+
Local<Context> creation_context;
29+
if (!object->GetCreationContext().ToLocal(&creation_context)) {
30+
return false;
31+
}
32+
if (creation_context != context_) {
33+
return false;
34+
}
35+
for (Local<Value> proto = object->GetPrototype(); proto->IsObject();
36+
proto = proto.As<Object>()->GetPrototype()) {
37+
if (search_ == proto) return true;
38+
}
39+
return false;
40+
}
41+
42+
private:
43+
Local<Context> context_;
44+
Local<Object> search_;
45+
};
46+
47+
void QueryObjects(const FunctionCallbackInfo<Value>& args) {
48+
CHECK_EQ(args.Length(), 1);
49+
Isolate* isolate = args.GetIsolate();
50+
if (!args[0]->IsObject()) {
51+
args.GetReturnValue().Set(Array::New(isolate));
52+
return;
53+
}
54+
Local<Object> proto = args[0].As<Object>();
55+
Local<Context> context = isolate->GetCurrentContext();
56+
PrototypeChainHas prototype_chain_has(context, proto.As<Object>());
57+
std::vector<Global<Object>> out;
58+
isolate->GetHeapProfiler()->QueryObjects(context, &prototype_chain_has, &out);
59+
std::vector<Local<Value>> result;
60+
result.reserve(out.size());
61+
for (size_t i = 0; i < out.size(); ++i) {
62+
result.push_back(out[i].Get(isolate));
63+
}
64+
65+
args.GetReturnValue().Set(Array::New(isolate, result.data(), result.size()));
66+
}
67+
68+
void Initialize(Local<Object> target,
69+
Local<Value> unused,
70+
Local<Context> context,
71+
void* priv) {
72+
SetMethod(context, target, "queryObjects", QueryObjects);
73+
}
74+
75+
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
76+
registry->Register(QueryObjects);
77+
}
78+
79+
} // namespace internal_only_v8
80+
} // namespace node
81+
82+
NODE_BINDING_CONTEXT_AWARE_INTERNAL(internal_only_v8,
83+
node::internal_only_v8::Initialize)
84+
NODE_BINDING_EXTERNAL_REFERENCE(
85+
internal_only_v8, node::internal_only_v8::RegisterExternalReferences)

src/node_binding.cc

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
// function. This helps the built-in bindings are loaded properly when
2727
// node is built as static library. No need to depend on the
2828
// __attribute__((constructor)) like mechanism in GCC.
29+
// The binding IDs that start with 'internal_only' are not exposed to the user
30+
// land even from internal/test/binding module under --expose-internals.
2931
#define NODE_BUILTIN_STANDARD_BINDINGS(V) \
3032
V(async_wrap) \
3133
V(blob) \
@@ -46,6 +48,7 @@
4648
V(http2) \
4749
V(http_parser) \
4850
V(inspector) \
51+
V(internal_only_v8) \
4952
V(js_stream) \
5053
V(js_udp_wrap) \
5154
V(messaging) \

src/node_external_reference.h

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class ExternalReferenceRegistry {
109109
V(fs_event_wrap) \
110110
V(handle_wrap) \
111111
V(heap_utils) \
112+
V(internal_only_v8) \
112113
V(messaging) \
113114
V(mksnapshot) \
114115
V(module_wrap) \

0 commit comments

Comments
 (0)