Skip to content

Commit ae3929e

Browse files
joyeecheungcodebytere
authored andcommitted
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. PR-URL: #31824 Refs: https://github.com/ulan/performance-measure-memory Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Denys Otrishko <shishugi@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent c5acf0a commit ae3929e

File tree

6 files changed

+208
-2
lines changed

6 files changed

+208
-2
lines changed

doc/api/errors.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,14 @@ STDERR/STDOUT, and the data's length is longer than the `maxBuffer` option.
710710
`Console` was instantiated without `stdout` stream, or `Console` has a
711711
non-writable `stdout` or `stderr` stream.
712712

713+
<a id="ERR_CONTEXT_NOT_INITIALIZED"></a>
714+
### `ERR_CONTEXT_NOT_INITIALIZED`
715+
716+
The vm context passed into the API is not yet initialized. This could happen
717+
when an error occurs (and is caught) during the creation of the
718+
context, for example, when the allocation fails or the maximum call stack
719+
size is reached when the context is created.
720+
713721
<a id="ERR_CONSTRUCT_CALL_REQUIRED"></a>
714722
### `ERR_CONSTRUCT_CALL_REQUIRED`
715723

doc/api/vm.md

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

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

lib/internal/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@ E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded',
758758
RangeError);
759759
E('ERR_CONSOLE_WRITABLE_STREAM',
760760
'Console expects a writable stream instance for %s', TypeError);
761+
E('ERR_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error);
761762
E('ERR_CPU_USAGE', 'Unable to obtain cpu usage %s', Error);
762763
E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED',
763764
'Custom engines not supported by this OpenSSL', Error);

lib/vm.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,21 @@ const {
2525
ArrayIsArray,
2626
ArrayPrototypeForEach,
2727
Symbol,
28+
PromiseReject
2829
} = primordials;
2930

3031
const {
3132
ContextifyScript,
3233
makeContext,
3334
isContext: _isContext,
34-
compileFunction: _compileFunction
35+
constants,
36+
compileFunction: _compileFunction,
37+
measureMemory: _measureMemory,
3538
} = internalBinding('contextify');
3639
const {
40+
ERR_CONTEXT_NOT_INITIALIZED,
3741
ERR_INVALID_ARG_TYPE,
42+
ERR_INVALID_ARG_VALUE,
3843
} = require('internal/errors').codes;
3944
const {
4045
isArrayBufferView,
@@ -44,7 +49,10 @@ const {
4449
validateUint32,
4550
validateString
4651
} = require('internal/validators');
47-
const { kVmBreakFirstLineSymbol } = require('internal/util');
52+
const {
53+
kVmBreakFirstLineSymbol,
54+
emitExperimentalWarning,
55+
} = require('internal/util');
4856
const kParsingContext = Symbol('script parsing context');
4957

5058
class Script extends ContextifyScript {
@@ -401,6 +409,30 @@ function compileFunction(code, params, options = {}) {
401409
return result.function;
402410
}
403411

412+
const measureMemoryModes = {
413+
summary: constants.measureMemory.mode.SUMMARY,
414+
detailed: constants.measureMemory.mode.DETAILED,
415+
};
416+
417+
function measureMemory(options = {}) {
418+
emitExperimentalWarning('vm.measureMemory');
419+
validateObject(options, 'options');
420+
const { mode = 'summary', context } = options;
421+
if (mode !== 'summary' && mode !== 'detailed') {
422+
throw new ERR_INVALID_ARG_VALUE(
423+
'options.mode', options.mode,
424+
'must be either \'summary\' or \'detailed\'');
425+
}
426+
if (context !== undefined &&
427+
(typeof context !== 'object' || context === null || !_isContext(context))) {
428+
throw new ERR_INVALID_ARG_TYPE('options.context', 'vm.Context', context);
429+
}
430+
const result = _measureMemory(measureMemoryModes[mode], context);
431+
if (result === undefined) {
432+
return PromiseReject(new ERR_CONTEXT_NOT_INITIALIZED());
433+
}
434+
return result;
435+
}
404436

405437
module.exports = {
406438
Script,
@@ -411,6 +443,7 @@ module.exports = {
411443
runInThisContext,
412444
isContext,
413445
compileFunction,
446+
measureMemory,
414447
};
415448

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

src/node_contextify.cc

Lines changed: 44 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;
@@ -1203,11 +1206,39 @@ static void WatchdogHasPendingSigint(const FunctionCallbackInfo<Value>& args) {
12031206
args.GetReturnValue().Set(ret);
12041207
}
12051208

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

@@ -1224,6 +1255,19 @@ void Initialize(Local<Object> target,
12241255

12251256
env->set_compiled_fn_entry_template(tpl->InstanceTemplate());
12261257
}
1258+
1259+
Local<Object> constants = Object::New(env->isolate());
1260+
Local<Object> measure_memory = Object::New(env->isolate());
1261+
Local<Object> memory_mode = Object::New(env->isolate());
1262+
MeasureMemoryMode SUMMARY = MeasureMemoryMode::kSummary;
1263+
MeasureMemoryMode DETAILED = MeasureMemoryMode::kDetailed;
1264+
NODE_DEFINE_CONSTANT(memory_mode, SUMMARY);
1265+
NODE_DEFINE_CONSTANT(memory_mode, DETAILED);
1266+
READONLY_PROPERTY(measure_memory, "mode", memory_mode);
1267+
READONLY_PROPERTY(constants, "measureMemory", measure_memory);
1268+
target->Set(context, env->constants_string(), constants).Check();
1269+
1270+
env->SetMethod(target, "measureMemory", MeasureMemory);
12271271
}
12281272

12291273
} // namespace contextify
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const vm = require('vm');
5+
6+
common.expectWarning('ExperimentalWarning',
7+
'vm.measureMemory is an experimental feature. ' +
8+
'This feature could change at any time');
9+
10+
// The formats could change when V8 is updated, then the tests should be
11+
// updated accordingly.
12+
function assertSummaryShape(result) {
13+
assert.strictEqual(typeof result, 'object');
14+
assert.strictEqual(typeof result.total, 'object');
15+
assert.strictEqual(typeof result.total.jsMemoryEstimate, 'number');
16+
assert(Array.isArray(result.total.jsMemoryRange));
17+
assert.strictEqual(typeof result.total.jsMemoryRange[0], 'number');
18+
assert.strictEqual(typeof result.total.jsMemoryRange[1], 'number');
19+
}
20+
21+
function assertDetailedShape(result) {
22+
// For now, the detailed shape is the same as the summary shape. This
23+
// should change in future versions of V8.
24+
return assertSummaryShape(result);
25+
}
26+
27+
// Test measuring memory of the current context
28+
{
29+
vm.measureMemory()
30+
.then(assertSummaryShape);
31+
32+
vm.measureMemory({})
33+
.then(assertSummaryShape);
34+
35+
vm.measureMemory({ mode: 'summary' })
36+
.then(assertSummaryShape);
37+
38+
vm.measureMemory({ mode: 'detailed' })
39+
.then(assertDetailedShape);
40+
41+
assert.throws(() => vm.measureMemory(null), {
42+
code: 'ERR_INVALID_ARG_TYPE'
43+
});
44+
assert.throws(() => vm.measureMemory('summary'), {
45+
code: 'ERR_INVALID_ARG_TYPE'
46+
});
47+
assert.throws(() => vm.measureMemory({ mode: 'random' }), {
48+
code: 'ERR_INVALID_ARG_VALUE'
49+
});
50+
}
51+
52+
// Test measuring memory of the sandbox
53+
{
54+
const context = vm.createContext();
55+
vm.measureMemory({ context })
56+
.then(assertSummaryShape);
57+
58+
vm.measureMemory({ mode: 'summary', context },)
59+
.then(assertSummaryShape);
60+
61+
vm.measureMemory({ mode: 'detailed', context })
62+
.then(assertDetailedShape);
63+
64+
assert.throws(() => vm.measureMemory({ mode: 'summary', context: null }), {
65+
code: 'ERR_INVALID_ARG_TYPE'
66+
});
67+
assert.throws(() => vm.measureMemory({ mode: 'summary', context: {} }), {
68+
code: 'ERR_INVALID_ARG_TYPE'
69+
});
70+
}

0 commit comments

Comments
 (0)