From 6c04e3c7f6b29f57567c30fa12a495a72b083d61 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sat, 8 Feb 2020 01:49:43 +0800 Subject: [PATCH 1/4] 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. --- doc/api/vm.md | 70 ++++++++++++++++++++++++ lib/vm.js | 30 +++++++++- src/node_contextify.cc | 43 +++++++++++++++ test/parallel/test-vm-measure-memory.js | 73 +++++++++++++++++++++++++ tools/doc/type-parser.js | 1 + 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-vm-measure-memory.js diff --git a/doc/api/vm.md b/doc/api/vm.md index bd64b23484eb24..c01412f567a9ee 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -295,6 +295,55 @@ console.log(globalVar); // 1000 ``` +## `vm.measureMemory([options, [contextifiedObject]])` + + + +> Stability: 1 - Experimental + +Measure the memory used by the current execution context or a specified context. + +* `options` {Object} Optional. + * `mode` {vm.constants.measureMemory.mode} + **Default:** `vm.constants.measureMemory.mode.SUMMARY` +* `contextifiedObject` {Object} Optional. A [contextified][] object returned + 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 +the memory reachable from a specific context, while +`v8.GetHeapSpaceStatistics()` measures the memory used by an instance +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` + +* {Object} An object containing commonly used constants for the vm module. + +### `vm.constants.measureMemory` + + +> 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 @@ -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 diff --git a/lib/vm.js b/lib/vm.js index 1d0715af528c73..0cb12a90cd7fc5 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -30,7 +30,9 @@ const { ContextifyScript, makeContext, isContext: _isContext, - compileFunction: _compileFunction + constants, + compileFunction: _compileFunction, + measureMemory: _measureMemory, } = internalBinding('contextify'); const { ERR_INVALID_ARG_TYPE, @@ -47,7 +49,10 @@ const { validateBuffer, validateObject, } = require('internal/validators'); -const { kVmBreakFirstLineSymbol } = require('internal/util'); +const { + kVmBreakFirstLineSymbol, + emitExperimentalWarning +} = require('internal/util'); const kParsingContext = Symbol('script parsing context'); class Script extends ContextifyScript { @@ -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; + } + validateInt32(mode, 'options.mode', + constants.measureMemory.mode.SUMMARY, + constants.measureMemory.mode.DETAILED); + 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, @@ -365,6 +389,8 @@ module.exports = { runInThisContext, isContext, compileFunction, + measureMemory, + constants, }; if (require('internal/options').getOptionValue('--experimental-vm-modules')) { diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 46a1d7c8ef0691..3704dd26d271ae 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -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; @@ -1200,11 +1203,38 @@ static void WatchdogHasPendingSigint(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ret); } +static void MeasureMemory(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsInt32()); + int32_t mode = args[0].As()->Value(); + Isolate* isolate = args.GetIsolate(); + Environment* env = Environment::GetCurrent(args); + Local context; + if (args[1]->IsUndefined()) { + context = isolate->GetCurrentContext(); + } else { + ContextifyContext* sandbox = + ContextifyContext::ContextFromContextifiedSandbox(env, + args[1].As()); + CHECK_NOT_NULL(sandbox); + context = sandbox->context(); + if (context.IsEmpty()) { // Not yet fully initilaized + return; + } + } + v8::Local promise; + if (!isolate->MeasureMemory(context, static_cast(mode)) + .ToLocal(&promise)) { + return; + } + args.GetReturnValue().Set(promise); +} + void Initialize(Local target, Local unused, Local context, void* priv) { Environment* env = Environment::GetCurrent(context); + Isolate* isolate = env->isolate(); ContextifyContext::Init(env, target); ContextifyScript::Init(env, target); @@ -1221,6 +1251,19 @@ void Initialize(Local target, env->set_compiled_fn_entry_template(tpl->InstanceTemplate()); } + + Local constants = Object::New(env->isolate()); + Local measure_memory = Object::New(env->isolate()); + Local 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 diff --git a/test/parallel/test-vm-measure-memory.js b/test/parallel/test-vm-measure-memory.js new file mode 100644 index 00000000000000..9258d7c1ec80b4 --- /dev/null +++ b/test/parallel/test-vm-measure-memory.js @@ -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); + }); + + 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(); + 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' + }); +} diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 02b59d37ffd278..b65a75d47d419b 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -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', From 60016e878c1b512b3475788c5eea9211b7af2617 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 18 Feb 2020 22:48:57 +0800 Subject: [PATCH 2/4] fixup! vm: implement vm.measureMemory() for per-context memory measurement --- doc/api/errors.md | 8 +++ doc/api/vm.md | 48 +++++---------- lib/internal/errors.js | 1 + lib/vm.js | 37 +++++++----- src/node_contextify.cc | 1 + test/parallel/test-vm-measure-memory.js | 77 ++++++++++++------------- 6 files changed, 83 insertions(+), 89 deletions(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index b186275807aee7..157d840f88da6b 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -710,6 +710,14 @@ STDERR/STDOUT, and the data's length is longer than the `maxBuffer` option. `Console` was instantiated without `stdout` stream, or `Console` has a non-writable `stdout` or `stderr` stream. + +### `ERR_CONTEXT_NOT_INITIALIZED` + +The vm context passed into the API is not yet initialized. This could happen +when an error occurs (and is caught) previously during the creation of the +context, for example, when the allocation fails or the maximum call stack +size is reached when the context is created. + ### `ERR_CONSTRUCT_CALL_REQUIRED` diff --git a/doc/api/vm.md b/doc/api/vm.md index c01412f567a9ee..5f9a33dee18c5c 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -295,7 +295,7 @@ console.log(globalVar); // 1000 ``` -## `vm.measureMemory([options, [contextifiedObject]])` +## `vm.measureMemory([options])` - -* {Object} An object containing commonly used constants for the vm module. - -### `vm.constants.measureMemory` - - -> 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 @@ -1247,7 +1228,6 @@ Constants to be used with the [`vm.measureMemory()`][] method. [`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 diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 392a297070d986..66b42a195392f6 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -754,6 +754,7 @@ E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded', RangeError); E('ERR_CONSOLE_WRITABLE_STREAM', 'Console expects a writable stream instance for %s', TypeError); +E('ERR_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error); E('ERR_CPU_USAGE', 'Unable to obtain cpu usage %s', Error); E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED', 'Custom engines not supported by this OpenSSL', Error); diff --git a/lib/vm.js b/lib/vm.js index 0cb12a90cd7fc5..c2d8908703b396 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -24,6 +24,7 @@ const { ArrayPrototypeForEach, Symbol, + PromiseReject } = primordials; const { @@ -35,7 +36,9 @@ const { measureMemory: _measureMemory, } = internalBinding('contextify'); const { + ERR_CONTEXT_NOT_INITIALIZED, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, } = require('internal/errors').codes; const { isArrayBufferView, @@ -51,7 +54,7 @@ const { } = require('internal/validators'); const { kVmBreakFirstLineSymbol, - emitExperimentalWarning + emitExperimentalWarning, } = require('internal/util'); const kParsingContext = Symbol('script parsing context'); @@ -360,24 +363,29 @@ function compileFunction(code, params, options = {}) { return result.function; } -function measureMemory(options = {}, context) { +const measureMemoryModes = { + summary: constants.measureMemory.mode.SUMMARY, + detailed: constants.measureMemory.mode.DETAILED, +}; + +function measureMemory(options = {}) { emitExperimentalWarning('vm.measureMemory'); validateObject(options, 'options'); - let mode = options.mode; - if (mode === undefined) { - mode = constants.measureMemory.mode.SUMMARY; + const { mode = 'summary', context } = options; + if (mode !== 'summary' && mode !== 'detailed') { + throw new ERR_INVALID_ARG_VALUE( + 'options.mode', options.mode, + 'must be either \'summary\' or \'detailed\''); } - validateInt32(mode, 'options.mode', - constants.measureMemory.mode.SUMMARY, - constants.measureMemory.mode.DETAILED); - if (context === undefined) { - return _measureMemory(mode); + if (context !== undefined && + (typeof context !== 'object' || context === null || !_isContext(context))) { + throw new ERR_INVALID_ARG_TYPE('options.context', 'vm.Context', context); } - if (typeof context !== 'object' || context === null || - !_isContext(context)) { - throw new ERR_INVALID_ARG_TYPE('contextifiedObject', 'vm.Context', context); + const result = _measureMemory(measureMemoryModes[mode], context); + if (result === undefined) { + return PromiseReject(new ERR_CONTEXT_NOT_INITIALIZED()); } - return _measureMemory(mode, context); + return result; } module.exports = { @@ -390,7 +398,6 @@ module.exports = { isContext, compileFunction, measureMemory, - constants, }; if (require('internal/options').getOptionValue('--experimental-vm-modules')) { diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 3704dd26d271ae..6ce478282a5c35 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -1212,6 +1212,7 @@ static void MeasureMemory(const FunctionCallbackInfo& args) { if (args[1]->IsUndefined()) { context = isolate->GetCurrentContext(); } else { + CHECK(args[1]->IsObject()); ContextifyContext* sandbox = ContextifyContext::ContextFromContextifiedSandbox(env, args[1].As()); diff --git a/test/parallel/test-vm-measure-memory.js b/test/parallel/test-vm-measure-memory.js index 9258d7c1ec80b4..7e620304e0a276 100644 --- a/test/parallel/test-vm-measure-memory.js +++ b/test/parallel/test-vm-measure-memory.js @@ -2,36 +2,41 @@ 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'); +// The formats could change when V8 is updated, then the tests should be +// updated accordingly. +function assertSummaryShape(result) { + assert.strictEqual(typeof result, 'object'); + assert.strictEqual(typeof result.total, 'object'); + assert.strictEqual(typeof result.total.jsMemoryEstimate, 'number'); + assert(Array.isArray(result.total.jsMemoryRange)); + assert.strictEqual(typeof result.total.jsMemoryRange[0], 'number'); + assert.strictEqual(typeof result.total.jsMemoryRange[1], 'number'); +} + +function assertDetailedShape(result) { + // For now, the detailed shape is the same as the summary shape. This + // should change in future versions of V8. + return assertSummaryShape(result); +} + // Test measuring memory of the current context { - vm.measureMemory(undefined) - .then((result) => { - assert(result instanceof Object); - }); + vm.measureMemory() + .then(assertSummaryShape); vm.measureMemory({}) - .then((result) => { - assert(result instanceof Object); - }); + .then(assertSummaryShape); - vm.measureMemory({ mode: SUMMARY }) - .then((result) => { - assert(result instanceof Object); - }); + vm.measureMemory({ mode: 'summary' }) + .then(assertSummaryShape); - vm.measureMemory({ mode: DETAILED }) - .then((result) => { - assert(result instanceof Object); - }); + vm.measureMemory({ mode: 'detailed' }) + .then(assertDetailedShape); assert.throws(() => vm.measureMemory(null), { code: 'ERR_INVALID_ARG_TYPE' @@ -39,35 +44,27 @@ common.expectWarning('ExperimentalWarning', assert.throws(() => vm.measureMemory('summary'), { code: 'ERR_INVALID_ARG_TYPE' }); - assert.throws(() => vm.measureMemory({ mode: -1 }), { - code: 'ERR_OUT_OF_RANGE' + assert.throws(() => vm.measureMemory({ mode: 'random' }), { + code: 'ERR_INVALID_ARG_VALUE' }); } // Test measuring memory of the sandbox { - const sandbox = vm.createContext(); - vm.measureMemory(undefined, sandbox) - .then((result) => { - assert(result instanceof Object); - }); + const context = vm.createContext(); + vm.measureMemory({ context }) + .then(assertSummaryShape); - vm.measureMemory({}, sandbox) - .then((result) => { - assert(result instanceof Object); - }); + vm.measureMemory({ mode: 'summary', context },) + .then(assertSummaryShape); - vm.measureMemory({ mode: SUMMARY }, sandbox) - .then((result) => { - assert(result instanceof Object); - }); + vm.measureMemory({ mode: 'detailed', context }) + .then(assertDetailedShape); - vm.measureMemory({ mode: DETAILED }, sandbox) - .then((result) => { - assert(result instanceof Object); - }); - - assert.throws(() => vm.measureMemory({ mode: SUMMARY }, null), { + assert.throws(() => vm.measureMemory({ mode: 'summary', context: null }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => vm.measureMemory({ mode: 'summary', context: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); } From cf0ed0b7d57bfbfa133d114ff68fc3b50f17a21a Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 18 Feb 2020 22:59:51 +0800 Subject: [PATCH 3/4] fixup! doc: move gireeshpunathil to TSC emeritus --- doc/api/vm.md | 2 +- tools/doc/type-parser.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/api/vm.md b/doc/api/vm.md index 5f9a33dee18c5c..ed676414471b8e 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -307,7 +307,7 @@ Measure the memory known to V8 and used by the current execution context or a specified context. * `options` {Object} Optional. - * `mode` {String} Either `'summary'` or `'detailed'`. + * `mode` {string} Either `'summary'` or `'detailed'`. **Default:** `'summary'` * `context` {Object} Optional. A [contextified][] object returned by `vm.createContext()`. If not specified, measure the memory diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index b65a75d47d419b..02b59d37ffd278 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -149,7 +149,6 @@ 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', From 339761df1f987060637a478f7c072caba2ea262b Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 25 Feb 2020 19:09:16 +0800 Subject: [PATCH 4/4] fixup! vm: implement vm.measureMemory() for per-context memory measurement Co-Authored-By: Colin Ihrig --- doc/api/errors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index 157d840f88da6b..4141280fa3f129 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -714,7 +714,7 @@ non-writable `stdout` or `stderr` stream. ### `ERR_CONTEXT_NOT_INITIALIZED` The vm context passed into the API is not yet initialized. This could happen -when an error occurs (and is caught) previously during the creation of the +when an error occurs (and is caught) during the creation of the context, for example, when the allocation fails or the maximum call stack size is reached when the context is created.