Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vm: implement vm.measureMemory() for per-context memory measurement #31824

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,55 @@ console.log(globalVar);
// 1000
```

## `vm.measureMemory([options, [contextifiedObject]])`
jasnell marked this conversation as resolved.
Show resolved Hide resolved

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Measure the memory used by the current execution context or a specified context.
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved

* `options` {Object} Optional.
* `mode` {vm.constants.measureMemory.mode}
**Default:** `vm.constants.measureMemory.mode.SUMMARY`
* `contextifiedObject` {Object} Optional. A [contextified][] object returned
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
by `vm.createContext()`. If not specified, measure the memory usage of the
current context where `vm.measureMemory()` is invoked.
* Returns: {Promise} If the memory is successfully measured the promise will
resolve with an object containing information about the memory usage.

The format of the object that the returned Promise may resolve with is
specific to the V8 engine and may change from one version of V8 to the next.

The returned result is different from the statistics returned by
`v8.GetHeapSpaceStatistics()` in that `vm.measureMemory()` measures
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
the memory reachable from a specific context, while
`v8.GetHeapSpaceStatistics()` measures the memory used by an instance
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
of V8 engine, which can switch among multiple contexts that reference
objects in the heap of one engine.

```js
const vm = require('vm');
// Measure the memory used by the current context and return the result
// in summary.
vm.measureMemory({ mode: vm.constants.measureMemory.mode.SUMMARY })
// Is the same as vm.measureMemory()
.then((result) => {
// The current format is:
// { total: { jsMemoryEstimate: 2211728, jsMemoryRange: [ 0, 2211728 ] } }
console.log(result);
});

const context = vm.createContext({});
vm.measureMemory({ mode: vm.constants.measureMemory.mode.DETAILED }, context)
.then((result) => {
// At the moment the DETAILED format is the same as the SUMMARY one.
console.log(result);
});
```

## Class: `vm.Module`
<!-- YAML
added: v13.0.0
Expand Down Expand Up @@ -1169,6 +1218,26 @@ the `process.nextTick()` and `queueMicrotask()` functions.
This issue occurs because all contexts share the same microtask and nextTick
queues.

## `vm.constants`
<!-- YAML
added: REPLACEME
-->

* {Object} An object containing commonly used constants for the vm module.

### `vm.constants.measureMemory`
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Constants to be used with the [`vm.measureMemory()`][] method.

* `mode` {Object}
* `SUMMARY` {integer} Return the measured memory in summary.
* `DETAILED` {integer} Return the measured memory in detail.

[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
[`ERR_VM_MODULE_STATUS`]: errors.html#ERR_VM_MODULE_STATUS
[`Error`]: errors.html#errors_class_error
Expand All @@ -1178,6 +1247,7 @@ queues.
[`script.runInThisContext()`]: #vm_script_runinthiscontext_options
[`url.origin`]: url.html#url_url_origin
[`vm.createContext()`]: #vm_vm_createcontext_contextobject_options
[`vm.measureMemory()`]: #vm_vm_measurememory_options_contextifiedobject
[`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedobject_options
[`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records
Expand Down
30 changes: 28 additions & 2 deletions lib/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ const {
ContextifyScript,
makeContext,
isContext: _isContext,
compileFunction: _compileFunction
constants,
compileFunction: _compileFunction,
measureMemory: _measureMemory,
} = internalBinding('contextify');
const {
ERR_INVALID_ARG_TYPE,
Expand All @@ -47,7 +49,10 @@ const {
validateBuffer,
validateObject,
} = require('internal/validators');
const { kVmBreakFirstLineSymbol } = require('internal/util');
const {
kVmBreakFirstLineSymbol,
emitExperimentalWarning
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
} = require('internal/util');
const kParsingContext = Symbol('script parsing context');

class Script extends ContextifyScript {
Expand Down Expand Up @@ -355,6 +360,25 @@ function compileFunction(code, params, options = {}) {
return result.function;
}

function measureMemory(options = {}, context) {
emitExperimentalWarning('vm.measureMemory');
validateObject(options, 'options');
let mode = options.mode;
if (mode === undefined) {
mode = constants.measureMemory.mode.SUMMARY;
}
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
validateInt32(mode, 'options.mode',
constants.measureMemory.mode.SUMMARY,
constants.measureMemory.mode.DETAILED);
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
if (context === undefined) {
return _measureMemory(mode);
}
if (typeof context !== 'object' || context === null ||
!_isContext(context)) {
throw new ERR_INVALID_ARG_TYPE('contextifiedObject', 'vm.Context', context);
}
return _measureMemory(mode, context);
}

module.exports = {
Script,
Expand All @@ -365,6 +389,8 @@ module.exports = {
runInThisContext,
isContext,
compileFunction,
measureMemory,
constants,
};

if (require('internal/options').getOptionValue('--experimental-vm-modules')) {
Expand Down
43 changes: 43 additions & 0 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,20 @@ using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::IndexedPropertyHandlerConfiguration;
using v8::Int32;
using v8::Integer;
using v8::Isolate;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::MeasureMemoryMode;
using v8::Name;
using v8::NamedPropertyHandlerConfiguration;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::PrimitiveArray;
using v8::Promise;
using v8::PropertyAttribute;
using v8::PropertyCallbackInfo;
using v8::PropertyDescriptor;
Expand Down Expand Up @@ -1200,11 +1203,38 @@ static void WatchdogHasPendingSigint(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(ret);
}

static void MeasureMemory(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsInt32());
int32_t mode = args[0].As<v8::Int32>()->Value();
Isolate* isolate = args.GetIsolate();
Environment* env = Environment::GetCurrent(args);
Local<Context> context;
if (args[1]->IsUndefined()) {
context = isolate->GetCurrentContext();
} else {
ContextifyContext* sandbox =
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
ContextifyContext::ContextFromContextifiedSandbox(env,
args[1].As<Object>());
CHECK_NOT_NULL(sandbox);
context = sandbox->context();
if (context.IsEmpty()) { // Not yet fully initilaized
return;
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
}
}
v8::Local<v8::Promise> promise;
if (!isolate->MeasureMemory(context, static_cast<v8::MeasureMemoryMode>(mode))
gengjiawen marked this conversation as resolved.
Show resolved Hide resolved
.ToLocal(&promise)) {
return;
}
args.GetReturnValue().Set(promise);
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
ContextifyContext::Init(env, target);
ContextifyScript::Init(env, target);

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

env->set_compiled_fn_entry_template(tpl->InstanceTemplate());
}

Local<Object> constants = Object::New(env->isolate());
Local<Object> measure_memory = Object::New(env->isolate());
Local<Object> memory_mode = Object::New(env->isolate());
MeasureMemoryMode SUMMARY = MeasureMemoryMode::kSummary;
MeasureMemoryMode DETAILED = MeasureMemoryMode::kDetailed;
NODE_DEFINE_CONSTANT(memory_mode, SUMMARY);
NODE_DEFINE_CONSTANT(memory_mode, DETAILED);
READONLY_PROPERTY(measure_memory, "mode", memory_mode);
READONLY_PROPERTY(constants, "measureMemory", measure_memory);
target->Set(context, env->constants_string(), constants).Check();

env->SetMethod(target, "measureMemory", MeasureMemory);
}

} // namespace contextify
Expand Down
73 changes: 73 additions & 0 deletions test/parallel/test-vm-measure-memory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const vm = require('vm');
const {
SUMMARY,
DETAILED
} = vm.constants.measureMemory.mode;

common.expectWarning('ExperimentalWarning',
'vm.measureMemory is an experimental feature. ' +
'This feature could change at any time');

// Test measuring memory of the current context
{
vm.measureMemory(undefined)
.then((result) => {
assert(result instanceof Object);
});

vm.measureMemory({})
.then((result) => {
assert(result instanceof Object);
});

vm.measureMemory({ mode: SUMMARY })
.then((result) => {
assert(result instanceof Object);
});

vm.measureMemory({ mode: DETAILED })
.then((result) => {
assert(result instanceof Object);
});
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
lundibundi marked this conversation as resolved.
Show resolved Hide resolved

assert.throws(() => vm.measureMemory(null), {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => vm.measureMemory('summary'), {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => vm.measureMemory({ mode: -1 }), {
code: 'ERR_OUT_OF_RANGE'
});
}

// Test measuring memory of the sandbox
{
const sandbox = vm.createContext();
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
vm.measureMemory(undefined, sandbox)
.then((result) => {
assert(result instanceof Object);
});

vm.measureMemory({}, sandbox)
.then((result) => {
assert(result instanceof Object);
});

vm.measureMemory({ mode: SUMMARY }, sandbox)
.then((result) => {
assert(result instanceof Object);
});

vm.measureMemory({ mode: DETAILED }, sandbox)
.then((result) => {
assert(result instanceof Object);
});

assert.throws(() => vm.measureMemory({ mode: SUMMARY }, null), {
code: 'ERR_INVALID_ARG_TYPE'
});
}
1 change: 1 addition & 0 deletions tools/doc/type-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ const customTypesMap = {

'vm.Module': 'vm.html#vm_class_vm_module',
'vm.SourceTextModule': 'vm.html#vm_class_vm_sourcetextmodule',
'vm.constants.measureMemory.mode': 'vm.html#vm_vm_constants_measurememory',

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

Expand Down