Skip to content

Commit 6c04e3c

Browse files
committed
vm: implement vm.measureMemory() for per-context memory measurement
This patch implements `vm.measureMemory()` with the new `v8::Isolate::MeasureMemory()` API to measure per-context memory usage. This should be experimental, since detailed memory measurement requires further integration with the V8 API that should be available in a future V8 update.
1 parent 4c746a6 commit 6c04e3c

File tree

5 files changed

+215
-2
lines changed

5 files changed

+215
-2
lines changed

doc/api/vm.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,55 @@ console.log(globalVar);
295295
// 1000
296296
```
297297

298+
## `vm.measureMemory([options, [contextifiedObject]])`
299+
300+
<!-- YAML
301+
added: REPLACEME
302+
-->
303+
304+
> Stability: 1 - Experimental
305+
306+
Measure the memory used by the current execution context or a specified context.
307+
308+
* `options` {Object} Optional.
309+
* `mode` {vm.constants.measureMemory.mode}
310+
**Default:** `vm.constants.measureMemory.mode.SUMMARY`
311+
* `contextifiedObject` {Object} Optional. A [contextified][] object returned
312+
by `vm.createContext()`. If not specified, measure the memory usage of the
313+
current context where `vm.measureMemory()` is invoked.
314+
* Returns: {Promise} If the memory is successfully measured the promise will
315+
resolve with an object containing information about the memory usage.
316+
317+
The format of the object that the returned Promise may resolve with is
318+
specific to the V8 engine and may change from one version of V8 to the next.
319+
320+
The returned result is different from the statistics returned by
321+
`v8.GetHeapSpaceStatistics()` in that `vm.measureMemory()` measures
322+
the memory reachable from a specific context, while
323+
`v8.GetHeapSpaceStatistics()` measures the memory used by an instance
324+
of V8 engine, which can switch among multiple contexts that reference
325+
objects in the heap of one engine.
326+
327+
```js
328+
const vm = require('vm');
329+
// Measure the memory used by the current context and return the result
330+
// in summary.
331+
vm.measureMemory({ mode: vm.constants.measureMemory.mode.SUMMARY })
332+
// Is the same as vm.measureMemory()
333+
.then((result) => {
334+
// The current format is:
335+
// { total: { jsMemoryEstimate: 2211728, jsMemoryRange: [ 0, 2211728 ] } }
336+
console.log(result);
337+
});
338+
339+
const context = vm.createContext({});
340+
vm.measureMemory({ mode: vm.constants.measureMemory.mode.DETAILED }, context)
341+
.then((result) => {
342+
// At the moment the DETAILED format is the same as the SUMMARY one.
343+
console.log(result);
344+
});
345+
```
346+
298347
## Class: `vm.Module`
299348
<!-- YAML
300349
added: v13.0.0
@@ -1169,6 +1218,26 @@ the `process.nextTick()` and `queueMicrotask()` functions.
11691218
This issue occurs because all contexts share the same microtask and nextTick
11701219
queues.
11711220

1221+
## `vm.constants`
1222+
<!-- YAML
1223+
added: REPLACEME
1224+
-->
1225+
1226+
* {Object} An object containing commonly used constants for the vm module.
1227+
1228+
### `vm.constants.measureMemory`
1229+
<!-- YAML
1230+
added: REPLACEME
1231+
-->
1232+
1233+
> Stability: 1 - Experimental
1234+
1235+
Constants to be used with the [`vm.measureMemory()`][] method.
1236+
1237+
* `mode` {Object}
1238+
* `SUMMARY` {integer} Return the measured memory in summary.
1239+
* `DETAILED` {integer} Return the measured memory in detail.
1240+
11721241
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
11731242
[`ERR_VM_MODULE_STATUS`]: errors.html#ERR_VM_MODULE_STATUS
11741243
[`Error`]: errors.html#errors_class_error
@@ -1178,6 +1247,7 @@ queues.
11781247
[`script.runInThisContext()`]: #vm_script_runinthiscontext_options
11791248
[`url.origin`]: url.html#url_url_origin
11801249
[`vm.createContext()`]: #vm_vm_createcontext_contextobject_options
1250+
[`vm.measureMemory()`]: #vm_vm_measurememory_options_contextifiedobject
11811251
[`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedobject_options
11821252
[`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options
11831253
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records

lib/vm.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ const {
3030
ContextifyScript,
3131
makeContext,
3232
isContext: _isContext,
33-
compileFunction: _compileFunction
33+
constants,
34+
compileFunction: _compileFunction,
35+
measureMemory: _measureMemory,
3436
} = internalBinding('contextify');
3537
const {
3638
ERR_INVALID_ARG_TYPE,
@@ -47,7 +49,10 @@ const {
4749
validateBuffer,
4850
validateObject,
4951
} = require('internal/validators');
50-
const { kVmBreakFirstLineSymbol } = require('internal/util');
52+
const {
53+
kVmBreakFirstLineSymbol,
54+
emitExperimentalWarning
55+
} = require('internal/util');
5156
const kParsingContext = Symbol('script parsing context');
5257

5358
class Script extends ContextifyScript {
@@ -355,6 +360,25 @@ function compileFunction(code, params, options = {}) {
355360
return result.function;
356361
}
357362

363+
function measureMemory(options = {}, context) {
364+
emitExperimentalWarning('vm.measureMemory');
365+
validateObject(options, 'options');
366+
let mode = options.mode;
367+
if (mode === undefined) {
368+
mode = constants.measureMemory.mode.SUMMARY;
369+
}
370+
validateInt32(mode, 'options.mode',
371+
constants.measureMemory.mode.SUMMARY,
372+
constants.measureMemory.mode.DETAILED);
373+
if (context === undefined) {
374+
return _measureMemory(mode);
375+
}
376+
if (typeof context !== 'object' || context === null ||
377+
!_isContext(context)) {
378+
throw new ERR_INVALID_ARG_TYPE('contextifiedObject', 'vm.Context', context);
379+
}
380+
return _measureMemory(mode, context);
381+
}
358382

359383
module.exports = {
360384
Script,
@@ -365,6 +389,8 @@ module.exports = {
365389
runInThisContext,
366390
isContext,
367391
compileFunction,
392+
measureMemory,
393+
constants,
368394
};
369395

370396
if (require('internal/options').getOptionValue('--experimental-vm-modules')) {

src/node_contextify.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,20 @@ using v8::FunctionCallbackInfo;
4747
using v8::FunctionTemplate;
4848
using v8::HandleScope;
4949
using v8::IndexedPropertyHandlerConfiguration;
50+
using v8::Int32;
5051
using v8::Integer;
5152
using v8::Isolate;
5253
using v8::Local;
5354
using v8::Maybe;
5455
using v8::MaybeLocal;
56+
using v8::MeasureMemoryMode;
5557
using v8::Name;
5658
using v8::NamedPropertyHandlerConfiguration;
5759
using v8::Number;
5860
using v8::Object;
5961
using v8::ObjectTemplate;
6062
using v8::PrimitiveArray;
63+
using v8::Promise;
6164
using v8::PropertyAttribute;
6265
using v8::PropertyCallbackInfo;
6366
using v8::PropertyDescriptor;
@@ -1200,11 +1203,38 @@ static void WatchdogHasPendingSigint(const FunctionCallbackInfo<Value>& args) {
12001203
args.GetReturnValue().Set(ret);
12011204
}
12021205

1206+
static void MeasureMemory(const FunctionCallbackInfo<Value>& args) {
1207+
CHECK(args[0]->IsInt32());
1208+
int32_t mode = args[0].As<v8::Int32>()->Value();
1209+
Isolate* isolate = args.GetIsolate();
1210+
Environment* env = Environment::GetCurrent(args);
1211+
Local<Context> context;
1212+
if (args[1]->IsUndefined()) {
1213+
context = isolate->GetCurrentContext();
1214+
} else {
1215+
ContextifyContext* sandbox =
1216+
ContextifyContext::ContextFromContextifiedSandbox(env,
1217+
args[1].As<Object>());
1218+
CHECK_NOT_NULL(sandbox);
1219+
context = sandbox->context();
1220+
if (context.IsEmpty()) { // Not yet fully initilaized
1221+
return;
1222+
}
1223+
}
1224+
v8::Local<v8::Promise> promise;
1225+
if (!isolate->MeasureMemory(context, static_cast<v8::MeasureMemoryMode>(mode))
1226+
.ToLocal(&promise)) {
1227+
return;
1228+
}
1229+
args.GetReturnValue().Set(promise);
1230+
}
1231+
12031232
void Initialize(Local<Object> target,
12041233
Local<Value> unused,
12051234
Local<Context> context,
12061235
void* priv) {
12071236
Environment* env = Environment::GetCurrent(context);
1237+
Isolate* isolate = env->isolate();
12081238
ContextifyContext::Init(env, target);
12091239
ContextifyScript::Init(env, target);
12101240

@@ -1221,6 +1251,19 @@ void Initialize(Local<Object> target,
12211251

12221252
env->set_compiled_fn_entry_template(tpl->InstanceTemplate());
12231253
}
1254+
1255+
Local<Object> constants = Object::New(env->isolate());
1256+
Local<Object> measure_memory = Object::New(env->isolate());
1257+
Local<Object> memory_mode = Object::New(env->isolate());
1258+
MeasureMemoryMode SUMMARY = MeasureMemoryMode::kSummary;
1259+
MeasureMemoryMode DETAILED = MeasureMemoryMode::kDetailed;
1260+
NODE_DEFINE_CONSTANT(memory_mode, SUMMARY);
1261+
NODE_DEFINE_CONSTANT(memory_mode, DETAILED);
1262+
READONLY_PROPERTY(measure_memory, "mode", memory_mode);
1263+
READONLY_PROPERTY(constants, "measureMemory", measure_memory);
1264+
target->Set(context, env->constants_string(), constants).Check();
1265+
1266+
env->SetMethod(target, "measureMemory", MeasureMemory);
12241267
}
12251268

12261269
} // namespace contextify
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const vm = require('vm');
5+
const {
6+
SUMMARY,
7+
DETAILED
8+
} = vm.constants.measureMemory.mode;
9+
10+
common.expectWarning('ExperimentalWarning',
11+
'vm.measureMemory is an experimental feature. ' +
12+
'This feature could change at any time');
13+
14+
// Test measuring memory of the current context
15+
{
16+
vm.measureMemory(undefined)
17+
.then((result) => {
18+
assert(result instanceof Object);
19+
});
20+
21+
vm.measureMemory({})
22+
.then((result) => {
23+
assert(result instanceof Object);
24+
});
25+
26+
vm.measureMemory({ mode: SUMMARY })
27+
.then((result) => {
28+
assert(result instanceof Object);
29+
});
30+
31+
vm.measureMemory({ mode: DETAILED })
32+
.then((result) => {
33+
assert(result instanceof Object);
34+
});
35+
36+
assert.throws(() => vm.measureMemory(null), {
37+
code: 'ERR_INVALID_ARG_TYPE'
38+
});
39+
assert.throws(() => vm.measureMemory('summary'), {
40+
code: 'ERR_INVALID_ARG_TYPE'
41+
});
42+
assert.throws(() => vm.measureMemory({ mode: -1 }), {
43+
code: 'ERR_OUT_OF_RANGE'
44+
});
45+
}
46+
47+
// Test measuring memory of the sandbox
48+
{
49+
const sandbox = vm.createContext();
50+
vm.measureMemory(undefined, sandbox)
51+
.then((result) => {
52+
assert(result instanceof Object);
53+
});
54+
55+
vm.measureMemory({}, sandbox)
56+
.then((result) => {
57+
assert(result instanceof Object);
58+
});
59+
60+
vm.measureMemory({ mode: SUMMARY }, sandbox)
61+
.then((result) => {
62+
assert(result instanceof Object);
63+
});
64+
65+
vm.measureMemory({ mode: DETAILED }, sandbox)
66+
.then((result) => {
67+
assert(result instanceof Object);
68+
});
69+
70+
assert.throws(() => vm.measureMemory({ mode: SUMMARY }, null), {
71+
code: 'ERR_INVALID_ARG_TYPE'
72+
});
73+
}

tools/doc/type-parser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ const customTypesMap = {
149149

150150
'vm.Module': 'vm.html#vm_class_vm_module',
151151
'vm.SourceTextModule': 'vm.html#vm_class_vm_sourcetextmodule',
152+
'vm.constants.measureMemory.mode': 'vm.html#vm_vm_constants_measurememory',
152153

153154
'MessagePort': 'worker_threads.html#worker_threads_class_messageport',
154155

0 commit comments

Comments
 (0)