From 41b16e48d982efd2701332badadb1aa521e864aa Mon Sep 17 00:00:00 2001 From: Matt Broadstone Date: Wed, 20 May 2026 13:37:55 -0400 Subject: [PATCH] SERVER-126888: Add a high-resolution timer for perf testing in jstests (#53877) Co-authored-by: Steve McClure Co-authored-by: Teo Voinea GitOrigin-RevId: e91b166957c79f6576efe2c6fbf6d796a9636905 --- buildscripts/jstoh.py | 35 ++- eslint.config.mjs | 1 + jsconfig.json | 10 +- jstests/core/query/js/js_global_scope.js | 1 + .../noPassthrough/shell/js/module_loader.js | 23 ++ .../noPassthrough/shell/js/std_performance.js | 22 ++ src/mongo/scripting/mozjs/common/global.d.ts | 16 ++ src/mongo/scripting/mozjs/shell/BUILD.bazel | 23 ++ .../mozjs/shell/internal_module_registry.cpp | 86 ++++++ .../mozjs/shell/internal_module_registry.h | 80 ++++++ .../scripting/mozjs/shell/module_loader.cpp | 253 +++++++++++++++--- .../scripting/mozjs/shell/module_loader.h | 1 + src/mongo/shell/BUILD.bazel | 38 ++- src/mongo/shell/std/README.md | 90 +++++++ src/mongo/shell/std/performance.cpp | 72 +++++ src/mongo/shell/std/performance.d.ts | 11 + src/mongo/shell/std/performance.js | 7 + 17 files changed, 719 insertions(+), 50 deletions(-) create mode 100644 jstests/noPassthrough/shell/js/module_loader.js create mode 100644 jstests/noPassthrough/shell/js/std_performance.js create mode 100644 src/mongo/scripting/mozjs/shell/internal_module_registry.cpp create mode 100644 src/mongo/scripting/mozjs/shell/internal_module_registry.h create mode 100644 src/mongo/shell/std/README.md create mode 100644 src/mongo/shell/std/performance.cpp create mode 100644 src/mongo/shell/std/performance.d.ts create mode 100644 src/mongo/shell/std/performance.js diff --git a/buildscripts/jstoh.py b/buildscripts/jstoh.py index 28bd1c39673..20351acb7ff 100755 --- a/buildscripts/jstoh.py +++ b/buildscripts/jstoh.py @@ -37,9 +37,7 @@ def jsToHeader(target, source): def lineToChars(s): return ",".join(str(ord(c)) for c in (s.rstrip() + "\n")) + "," - for s in source: - filename = str(s) - objname = os.path.split(filename)[1].split(".")[0] + for module_name, filename, objname in source: stringname = "_jscode_raw_" + objname h.append("constexpr char " + stringname + "[] = {") @@ -53,7 +51,7 @@ def jsToHeader(target, source): h.append("extern const JSFile %s;" % objname) h.append( 'const JSFile %s = { "%s", StringData(%s, sizeof(%s) - 1) };' - % (objname, filename.replace("\\", "/"), stringname, stringname) + % (objname, module_name, stringname, stringname) ) h.append("} // namespace JSFiles") @@ -69,9 +67,36 @@ def jsToHeader(target, source): out.close() +def parse_args(args): + """Parse source files and optional --module name overrides. + + Accepts both forms, which may be mixed: + path/to/foo.js -- module name = file path, C++ var = basename (foo) + --module std:performance foo.js -- module name = std:performance, C++ var = std_performance + """ + entries = [] + i = 0 + while i < len(args): + if args[i] == "--module": + if i + 2 >= len(args): + raise ValueError("--module requires two arguments: ") + module_name = args[i + 1] + filename = args[i + 2] + objname = "".join(c if c.isalnum() else "_" for c in module_name) + entries.append((module_name, filename, objname)) + i += 3 + else: + filename = args[i].replace("\\", "/") + objname = os.path.split(filename)[1].split(".")[0] + entries.append((filename, filename, objname)) + i += 1 + + return entries + + if __name__ == "__main__": if len(sys.argv) < 3: print("Must specify [target] [source] ") sys.exit(1) - jsToHeader(sys.argv[1], sys.argv[2:]) + jsToHeader(sys.argv[1], parse_args(sys.argv[2:])) diff --git a/eslint.config.mjs b/eslint.config.mjs index e5dd8bb57bf..1000f1739d3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,6 +25,7 @@ export default [ languageOptions: { globals: { ...globals.mongo, + internalModule: true, // jstests/global.d.ts TestData: true, diff --git a/jsconfig.json b/jsconfig.json index bc6f802e2fb..035a9da67f5 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -2,7 +2,13 @@ "compilerOptions": { "baseUrl": ".", "disableSizeLimit": true, - "target": "ES2020" + "target": "ES2020", + "moduleResolution": "node", + "paths": { + "std:*": [ + "src/mongo/shell/std/*" + ] + } }, "include": [ "jstests/**/*.js", @@ -11,6 +17,8 @@ "src/mongo/scripting/**/*.d.ts", "src/mongo/shell/*.js", "src/mongo/shell/*.d.ts", + "src/mongo/shell/std/**/*.js", + "src/mongo/shell/std/**/*.d.ts", "src/third_party/fast_check/**/*" ], "exclude": [ diff --git a/jstests/core/query/js/js_global_scope.js b/jstests/core/query/js/js_global_scope.js index 90ce888f134..7dd16474fdc 100644 --- a/jstests/core/query/js/js_global_scope.js +++ b/jstests/core/query/js/js_global_scope.js @@ -111,6 +111,7 @@ const expectedGlobalVars = [ "getJSHeapLimitMB", "globalThis", "hex_md5", + "internalModule", "isFinite", "highWaterMarkResumeTokenType", "isNaN", diff --git a/jstests/noPassthrough/shell/js/module_loader.js b/jstests/noPassthrough/shell/js/module_loader.js new file mode 100644 index 00000000000..a336bf5a10c --- /dev/null +++ b/jstests/noPassthrough/shell/js/module_loader.js @@ -0,0 +1,23 @@ +import {describe, it} from "jstests/libs/mochalite.js"; + +describe("module loader internal binding restrictions", function () { + it("does not allow scripts to import internal bindings as ES modules", async function () { + let importError = null; + try { + await import("performance"); + } catch (error) { + importError = error; + } + + assert.neq(importError, null, "scripts should not import internal bindings as ES modules"); + }); + + it("prevents non-std modules from calling internalModule()", function () { + let callError = assert.throws(() => internalModule("performance")); + assert.neq(callError, null, "non-std modules should not call internalModule()"); + assert( + callError.message.includes("restricted to std:* modules"), + `unexpected internalModule error: ${callError.message}`, + ); + }); +}); diff --git a/jstests/noPassthrough/shell/js/std_performance.js b/jstests/noPassthrough/shell/js/std_performance.js new file mode 100644 index 00000000000..ebae9d4c63c --- /dev/null +++ b/jstests/noPassthrough/shell/js/std_performance.js @@ -0,0 +1,22 @@ +import {describe, it} from "jstests/libs/mochalite.js"; +import {performance} from "std:performance"; + +describe("std:performance with performance internal binding", function () { + it("uses a monotonic high-resolution clock", function () { + const first = performance.now(); + const second = performance.now(); + + assert.gte(first, 0, "performance.now() should be non-negative"); + assert.gte(second, first, "performance.now() should be monotonic"); + + let sawFractionalValue = false; + for (let i = 0; i < 20000; ++i) { + const sample = performance.now(); + if (!Number.isInteger(sample)) { + sawFractionalValue = true; + break; + } + } + assert(sawFractionalValue, "performance.now() should report sub-millisecond precision"); + }); +}); diff --git a/src/mongo/scripting/mozjs/common/global.d.ts b/src/mongo/scripting/mozjs/common/global.d.ts index e5ce68e1774..c1ac581f3a4 100644 --- a/src/mongo/scripting/mozjs/common/global.d.ts +++ b/src/mongo/scripting/mozjs/common/global.d.ts @@ -6,3 +6,19 @@ declare function getJSHeapLimitMB(); declare function print(); declare function sleep(); declare function version(); + +type InternalModuleName = "performance"; + +/** + * Loads a shell-internal module binding for std module bootstrapping. + * + * This API is intended only for implementations of `std:*` modules under + * `src/mongo/shell/std`. Calling this from non-`std:*` modules throws at + * runtime. + * + * JSTests must not call this directly. Instead import the std module: + * e.g.`import {performance} from "std:performance";` + */ +declare function internalModule( + moduleName: InternalModuleName, +): Record; diff --git a/src/mongo/scripting/mozjs/shell/BUILD.bazel b/src/mongo/scripting/mozjs/shell/BUILD.bazel index 716888a3213..30377614e2f 100644 --- a/src/mongo/scripting/mozjs/shell/BUILD.bazel +++ b/src/mongo/scripting/mozjs/shell/BUILD.bazel @@ -29,6 +29,26 @@ mongo_js_library( visibility = ["//visibility:public"], ) +mongo_cc_library( + name = "internal_module_registry", + srcs = ["internal_module_registry.cpp"], + copts = select({ + "@platforms//os:windows": [ + # The default MSVC preprocessor elides commas in some cases as a + # convenience, but this behavior breaks compilation of jspubtd.h. + # Enabling the newer preprocessor fixes the problem. + "/Zc:preprocessor", + "/wd5104", + "/wd5105", + ], + "//conditions:default": [], + }), + deps = [ + "//src/mongo:base", + "//src/third_party/mozjs", + ], +) + mongo_cc_library( name = "mozjs_shell", srcs = glob( @@ -38,6 +58,7 @@ mongo_cc_library( "asan_handles_test.cpp", "implscope_test.cpp", "module_loader_test.cpp", + "internal_module_registry.cpp", ], ) + [ ":scripting_util_gen", @@ -55,11 +76,13 @@ mongo_cc_library( "//conditions:default": [], }), deps = [ + ":internal_module_registry", "//src/mongo/client:clientdriver_network", "//src/mongo/db:service_context", "//src/mongo/db/auth:security_token_auth", "//src/mongo/scripting:scripting_common", "//src/mongo/scripting/mozjs/common:mozjs_common", + "//src/mongo/shell:std_internal_modules", "//src/mongo/util:buildinfo", "//src/mongo/util/concurrency:spin_lock", "//src/third_party/mozjs", diff --git a/src/mongo/scripting/mozjs/shell/internal_module_registry.cpp b/src/mongo/scripting/mozjs/shell/internal_module_registry.cpp new file mode 100644 index 00000000000..781464d6a73 --- /dev/null +++ b/src/mongo/scripting/mozjs/shell/internal_module_registry.cpp @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/scripting/mozjs/shell/internal_module_registry.h" + +#include "mongo/util/concurrency/with_lock.h" + +#include +#include +#include + +namespace mongo::mozjs { +namespace { + +using InternalModuleMap = std::unordered_map; + +InternalModuleMap& getInternalModuleMap(WithLock) { + static InternalModuleMap moduleMap; + return moduleMap; +} + +std::mutex& getInternalModuleMapMutex() { + static std::mutex moduleMapMutex; + return moduleMapMutex; +} + +void addInternalModuleRegistration(std::string_view moduleName, + InternalModuleInitializer initialize, + const ::mongo::JSFile* setupFile) { + if (moduleName.empty() || initialize == nullptr) { + return; + } + + std::lock_guard lock(getInternalModuleMapMutex()); + getInternalModuleMap(lock)[std::string(moduleName)] = + InternalModuleRegistration{std::string(moduleName), initialize, setupFile}; +} + +} // namespace + +std::vector listRegisteredInternalModules() { + std::vector registrations; + + std::lock_guard lock(getInternalModuleMapMutex()); + const auto& moduleMap = getInternalModuleMap(lock); + registrations.reserve(moduleMap.size()); + for (const auto& [_, registration] : moduleMap) { + registrations.push_back(registration); + } + + return registrations; +} + +InternalModuleRegistrar::InternalModuleRegistrar(std::string_view moduleName, + InternalModuleInitializer initialize, + const ::mongo::JSFile* setupFile) { + addInternalModuleRegistration(moduleName, initialize, setupFile); +} + +} // namespace mongo::mozjs diff --git a/src/mongo/scripting/mozjs/shell/internal_module_registry.h b/src/mongo/scripting/mozjs/shell/internal_module_registry.h new file mode 100644 index 00000000000..05c4c5a3a30 --- /dev/null +++ b/src/mongo/scripting/mozjs/shell/internal_module_registry.h @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/util/modules.h" + +#include +#include +#include + +#include + +struct JSContext; + +namespace mongo { +struct JSFile; +} + +namespace mongo::mozjs { + +using InternalModuleInitializer = bool (*)(JSContext* cx, JS::HandleObject target); + +struct InternalModuleRegistration { + std::string moduleName; + InternalModuleInitializer initialize; + const ::mongo::JSFile* setupFile; +}; + +std::vector listRegisteredInternalModules(); + +class MONGO_MOD_PUB InternalModuleRegistrar { +public: + InternalModuleRegistrar(std::string_view moduleName, + InternalModuleInitializer initialize, + const ::mongo::JSFile* setupFile = nullptr); +}; + +} // namespace mongo::mozjs + +#define MONGO_INTERNAL_MODULE_CONCAT_IMPL(X, Y) X##Y +#define MONGO_INTERNAL_MODULE_CONCAT(X, Y) MONGO_INTERNAL_MODULE_CONCAT_IMPL(X, Y) + +#define MONGO_REGISTER_INTERNAL_MODULE(MODULE_NAME, INITIALIZE_FN) \ + namespace { \ + const ::mongo::mozjs::InternalModuleRegistrar MONGO_INTERNAL_MODULE_CONCAT( \ + kInternalModuleRegistrar_, __LINE__)(MODULE_NAME, INITIALIZE_FN, nullptr); \ + } // namespace + +#define MONGO_REGISTER_INTERNAL_MODULE_WITH_SETUP(MODULE_NAME, INITIALIZE_FN, SETUP_FILE) \ + namespace { \ + const ::mongo::mozjs::InternalModuleRegistrar MONGO_INTERNAL_MODULE_CONCAT( \ + kInternalModuleRegistrar_, __LINE__)(MODULE_NAME, INITIALIZE_FN, SETUP_FILE); \ + } // namespace diff --git a/src/mongo/scripting/mozjs/shell/module_loader.cpp b/src/mongo/scripting/mozjs/shell/module_loader.cpp index 3b7afb36674..b0e51efe71d 100644 --- a/src/mongo/scripting/mozjs/shell/module_loader.cpp +++ b/src/mongo/scripting/mozjs/shell/module_loader.cpp @@ -41,6 +41,7 @@ #include "mongo/logv2/log.h" #include "mongo/scripting/mongo_path_util.h" #include "mongo/scripting/mozjs/shell/implscope.h" +#include "mongo/scripting/mozjs/shell/internal_module_registry.h" #include "mongo/scripting/mozjs/shell/module_loader.h" #include "mongo/util/file.h" @@ -53,6 +54,7 @@ #include #include +#include #include #include #include @@ -63,6 +65,7 @@ #include #include #include +#include #include #include #include @@ -75,6 +78,143 @@ namespace mongo { namespace mozjs { +namespace { +constexpr const char* kStdModulePrefix = "std:"; + +enum GlobalAppSlot { + GlobalAppSlotModuleRegistry, + GlobalAppSlotInternalBindingsRegistry, + GlobalAppSlotCount +}; + +bool startsWithPrefix(const char* value, const char* prefix) { + return std::strncmp(value, prefix, std::strlen(prefix)) == 0; +} + +bool getOrCreateGlobalMapInSlot(JSContext* cx, GlobalAppSlot slot, JS::MutableHandleObject mapOut) { + mapOut.set(nullptr); + JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + if (!global) { + return false; + } + + JS::RootedValue value(cx, JS::GetReservedSlot(global, slot)); + if (!value.isUndefined()) { + mapOut.set(&value.toObject()); + return true; + } + + JS::RootedObject map(cx, JS::NewMapObject(cx)); + if (!map) { + return false; + } + + JS::SetReservedSlot(global, slot, JS::ObjectValue(*map)); + mapOut.set(map); + return true; +} + +bool getOrCreateInternalModuleBindingsRegistry(JSContext* cx, + JS::MutableHandleObject bindingsRegistryOut) { + return getOrCreateGlobalMapInSlot( + cx, GlobalAppSlotInternalBindingsRegistry, bindingsRegistryOut); +} + +bool registerInternalModuleBinding(JSContext* cx, + const char* moduleName, + JS::HandleObject bindingObject) { + JS::RootedObject bindingsRegistry(cx); + if (!getOrCreateInternalModuleBindingsRegistry(cx, &bindingsRegistry)) { + return false; + } + + JS::RootedString moduleNameString(cx, JS_NewStringCopyZ(cx, moduleName)); + if (!moduleNameString) { + return false; + } + + JS::RootedValue moduleNameValue(cx, JS::StringValue(moduleNameString)); + JS::RootedValue bindingValue(cx, JS::ObjectValue(*bindingObject)); + return JS::MapSet(cx, bindingsRegistry, moduleNameValue, bindingValue); +} + +bool lookUpInternalModuleBinding(JSContext* cx, + JS::HandleString moduleName, + JS::MutableHandleObject bindingOut) { + bindingOut.set(nullptr); + + JS::RootedObject bindingsRegistry(cx); + if (!getOrCreateInternalModuleBindingsRegistry(cx, &bindingsRegistry)) { + return false; + } + + JS::RootedValue moduleNameValue(cx, JS::StringValue(moduleName)); + JS::RootedValue bindingValue(cx); + if (!JS::MapGet(cx, bindingsRegistry, moduleNameValue, &bindingValue)) { + return false; + } + + if (!bindingValue.isUndefined()) { + bindingOut.set(&bindingValue.toObject()); + } + + return true; +} + +bool internalModuleFunction(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (args.length() < 1 || !args[0].isString()) { + JS_ReportErrorASCII(cx, "internalModule requires a string module name"); + return false; + } + + JS::RootedValue callerPrivate(cx, JS::GetScriptedCallerPrivate(cx)); + if (!callerPrivate.isObject()) { + JS_ReportErrorASCII(cx, "internalModule is restricted to std:* modules"); + return false; + } + + JS::RootedObject callerInfo(cx, &callerPrivate.toObject()); + JS::RootedValue callerPathValue(cx); + if (!JS_GetProperty(cx, callerInfo, "path", &callerPathValue)) { + return false; + } + if (!callerPathValue.isString()) { + JS_ReportErrorASCII(cx, "internalModule is restricted to std:* modules"); + return false; + } + + JS::RootedString callerPath(cx, callerPathValue.toString()); + JS::UniqueChars callerPathChars = JS_EncodeStringToUTF8(cx, callerPath); + if (!callerPathChars) { + return false; + } + if (!startsWithPrefix(callerPathChars.get(), kStdModulePrefix)) { + JS_ReportErrorUTF8(cx, + "internalModule is restricted to std:* modules (called from %s)", + callerPathChars.get()); + return false; + } + + JS::RootedString moduleName(cx, args[0].toString()); + JS::RootedObject binding(cx); + if (!lookUpInternalModuleBinding(cx, moduleName, &binding)) { + return false; + } + if (!binding) { + JS::UniqueChars moduleNameChars = JS_EncodeStringToUTF8(cx, moduleName); + if (!moduleNameChars) { + return false; + } + JS_ReportErrorUTF8(cx, "No such internal module '%s'", moduleNameChars.get()); + return false; + } + + args.rval().setObject(*binding); + return true; +} +} // namespace bool ModuleLoader::init(JSContext* cx, const std::string& loadPath) { _baseUrl = resolveBaseUrl(cx, loadPath); @@ -92,7 +232,17 @@ bool ModuleLoader::init(JSContext* cx, const std::string& loadPath) { JSRuntime* rt = JS_GetRuntime(cx); JS::SetModuleResolveHook(rt, ModuleLoader::moduleResolveHook); JS::SetModuleDynamicImportHook(rt, ModuleLoader::dynamicModuleImportHook); - return true; + + JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + if (!global) { + return false; + } + if (!JS_DefineFunction( + cx, global, "internalModule", internalModuleFunction, 1, JSPROP_PERMANENT)) { + return false; + } + + return preloadInternalModules(cx); } JSObject* ModuleLoader::loadRootModuleFromPath(JSContext* cx, const std::string& path) { @@ -131,6 +281,33 @@ JSObject* ModuleLoader::loadRootModule(JSContext* cx, return resolveImportedModule(cx, referencingPrivate, moduleRequest); } +bool ModuleLoader::preloadInternalModules(JSContext* cx) { + for (const auto& registration : listRegisteredInternalModules()) { + JS::RootedObject binding(cx, JS_NewPlainObject(cx)); + if (!binding) { + return false; + } + if (!registration.initialize(cx, binding)) { + return false; + } + if (!registerInternalModuleBinding(cx, registration.moduleName.c_str(), binding)) { + return false; + } + + if (registration.setupFile) { + JS::RootedObject setupModule(cx, + loadRootModuleFromSource(cx, + registration.setupFile->name, + registration.setupFile->source)); + if (!setupModule) { + return false; + } + } + } + + return true; +} + // static JSObject* ModuleLoader::moduleResolveHook(JSContext* cx, JS::HandleValue referencingPrivate, @@ -235,7 +412,18 @@ JSString* ModuleLoader::resolveAndNormalize(JSContext* cx, return nullptr; } - // check if it's already in the registry + // Root modules loaded from in-memory source (via execSetup) carry a source payload in the + // referencing info. For those loads, keep the existing behavior and bypass file-system lookup. + bool hasSource{false}; + JS::RootedObject referencingInfoObject(cx, &referencingInfo.toObject()); + if (!JS_HasProperty(cx, referencingInfoObject, "source", &hasSource)) { + return nullptr; + } + if (hasSource) { + return specifierString; + } + + // Check if this specifier is already in the in-memory module registry. JS::Rooted path(cx, specifierString); if (!path) { return nullptr; @@ -248,38 +436,32 @@ JSString* ModuleLoader::resolveAndNormalize(JSContext* cx, return specifierString; } - // check if it has a source - bool hasSource; - JS::RootedObject referencingInfoObject(cx, &referencingInfo.toObject()); - if (!JS_HasProperty(cx, referencingInfoObject, "source", &hasSource)) { - return nullptr; - } - if (hasSource) { + JS::UniqueChars specifierChars = JS_EncodeStringToUTF8(cx, specifierString); + uassert(ErrorCodes::JSInterpreterFailure, + "Failed to UTF-8 encode module specifier", + specifierChars); + + // STD modules are identified by module specifier and don't map to filesystem paths. + if (startsWithPrefix(specifierChars.get(), kStdModulePrefix)) { return specifierString; } - // otherwise try to read content from the file system - JS::RootedString refPath(cx); if (!getScriptPath(cx, referencingInfo, &refPath)) { return nullptr; } - if (!refPath) { JS_ReportErrorASCII(cx, "No path set for referencing module"); return nullptr; } - JS::UniqueChars specifierChars = JS_EncodeStringToUTF8(cx, specifierString); - uassert(ErrorCodes::JSInterpreterFailure, - "Failed to UTF-8 encode module specifier", - specifierChars); - boost::filesystem::path specifierPath(specifierChars.get()); - JS::UniqueChars refPathChars = JS_EncodeStringToUTF8(cx, refPath); uassert(ErrorCodes::JSInterpreterFailure, "Failed to UTF-8 encode referencing module path", refPathChars); + + // otherwise try to read content from the file system + boost::filesystem::path specifierPath(specifierChars.get()); boost::filesystem::path refAbsPath(refPathChars.get()); if (is_directory(specifierPath)) { @@ -373,16 +555,18 @@ JSObject* ModuleLoader::loadAndParse(JSContext* cx, return module; } + JS::RootedString source(cx, fetchSource(cx, path, referencingPrivate)); + if (!source) { + return nullptr; + } + JS::UniqueChars filename = JS_EncodeStringToLatin1(cx, path); if (!filename) { return nullptr; } - JS::CompileOptions options(cx); - options.setFileAndLine(filename.get(), 1); - - JS::RootedString source(cx, fetchSource(cx, path, referencingPrivate)); - if (!source) { + JS::RootedObject info(cx, createScriptPrivateInfo(cx, path)); + if (!info) { return nullptr; } @@ -397,16 +581,13 @@ JSObject* ModuleLoader::loadAndParse(JSContext* cx, return nullptr; } + JS::CompileOptions options(cx); + options.setFileAndLine(filename.get(), 1); module = JS::CompileModule(cx, options, srcBuf); if (!module) { return nullptr; } - JS::RootedObject info(cx, createScriptPrivateInfo(cx, path)); - if (!info) { - return nullptr; - } - JS::SetModulePrivate(module, JS::ObjectValue(*info)); if (!addModuleToRegistry(cx, path, module)) { @@ -437,24 +618,12 @@ JSString* ModuleLoader::fetchSource(JSContext* cx, return fileAsString(cx, resolvedPath); } -enum GlobalAppSlot { GlobalAppSlotModuleRegistry, GlobalAppSlotCount }; JSObject* ModuleLoader::getOrCreateModuleRegistry(JSContext* cx) { - JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); - if (!global) { + JS::RootedObject registry(cx); + if (!getOrCreateGlobalMapInSlot(cx, GlobalAppSlotModuleRegistry, ®istry)) { return nullptr; } - JS::RootedValue value(cx, JS::GetReservedSlot(global, GlobalAppSlotModuleRegistry)); - if (!value.isUndefined()) { - return &value.toObject(); - } - - JS::RootedObject registry(cx, JS::NewMapObject(cx)); - if (!registry) { - return nullptr; - } - - JS::SetReservedSlot(global, GlobalAppSlotModuleRegistry, JS::ObjectValue(*registry)); return registry; } diff --git a/src/mongo/scripting/mozjs/shell/module_loader.h b/src/mongo/scripting/mozjs/shell/module_loader.h index 9e628f74be7..812b938b225 100644 --- a/src/mongo/scripting/mozjs/shell/module_loader.h +++ b/src/mongo/scripting/mozjs/shell/module_loader.h @@ -89,6 +89,7 @@ private: JS::HandleValue referencingInfo); JSObject* getOrCreateModuleRegistry(JSContext* cx); JSString* fetchSource(JSContext* cx, JS::HandleString path, JS::HandleValue referencingPrivate); + bool preloadInternalModules(JSContext* cx); bool getScriptPath(JSContext* cx, JS::HandleValue privateValue, JS::MutableHandleString pathOut); diff --git a/src/mongo/shell/BUILD.bazel b/src/mongo/shell/BUILD.bazel index c4d575ecd36..3949159889d 100644 --- a/src/mongo/shell/BUILD.bazel +++ b/src/mongo/shell/BUILD.bazel @@ -290,16 +290,28 @@ MONGOJS_CPP_JSFILES = [ ":error_codes_js", ] -# Converts core JS file content into CPP structures to be loaded directly +# Converts std JS file content into CPP structures to be loaded directly # into the native binary via bytecode upon startup. +MONGOJS_STD_JS_MODULES = [ + ("std:performance", "std/performance.js"), +] + render_template( name = "mongojs_cpp", - srcs = MONGOJS_CPP_JSFILES, + srcs = MONGOJS_CPP_JSFILES + [file for _, file in MONGOJS_STD_JS_MODULES], cmd = [ "$(location mongojs.cpp)", ] + [ "$(location {})".format(file) for file in MONGOJS_CPP_JSFILES + ] + [ + item + for name, file in MONGOJS_STD_JS_MODULES + for item in [ + "--module", + name, + "$(location {})".format(file), + ] ], output = "mongojs.cpp", python_file = "//buildscripts:jstoh.py", @@ -342,6 +354,28 @@ mongo_cc_library( ], ) +mongo_cc_library( + name = "std_internal_modules", + srcs = glob(["std/*.cpp"]), + auto_header = False, + copts = select({ + "@platforms//os:windows": [ + # The default MSVC preprocessor elides commas in some cases as a + # convenience, but this behavior breaks compilation of jspubtd.h. + # Enabling the newer preprocessor fixes the problem. + "/Zc:preprocessor", + "/wd5104", + "/wd5105", + ], + "//conditions:default": [], + }), + deps = [ + ":mongojs", + "//src/mongo/scripting/mozjs/shell:internal_module_registry", + "//src/third_party/mozjs", + ], +) + mongo_cc_library( name = "shell_options_register", srcs = [ diff --git a/src/mongo/shell/std/README.md b/src/mongo/shell/std/README.md new file mode 100644 index 00000000000..a4c376bbf5f --- /dev/null +++ b/src/mongo/shell/std/README.md @@ -0,0 +1,90 @@ +# `std` Modules + +This directory contains shell standard modules that are imported as `std:` from jstests. + +Example: + +```js +import {performance} from "std:performance"; +``` + +## What are `std` modules + +A `std` module is a built-in module shipped with the `mongo` test runner. Each module is comprised +of: + +- a public JavaScript module file (`.js`) which defines the public API of the module. +- an internal C++ binding implementation (`.cpp`) offering bindings between C++ and + JavaScript. +- a TypeScript declaration file for the public API (`.d.ts`), documenting the API for editor + discoverability. + +## What is `internalModule` + +The C++ bindings for internal modules declare methods and class that can be used for interoperate +between the two environments. These bindings are exclusively limited to use in the public JavaScript +API file, so as to not pollute the global namespace. This helps us localize documentation and +improve discoverability. + +## How to Define a New `std` Module + +When adding a new module ``, update all of the following: + +1. Add public API file: `src/mongo/shell/std/.js` + +- Export the stable API users import from `std:`. +- Use `internalModule("")` only inside this `std:*` module implementation. + +2. Add internal binding file: `src/mongo/shell/std/.cpp` + +- Define an initializer with signature `bool init(JSContext*, JS::HandleObject target)`. +- Add functions/properties onto `target`. +- Register with + `MONGO_REGISTER_INTERNAL_MODULE_WITH_SETUP("", initFn, &::mongo::JSFiles::std_)`. + +3. Add public typings: `src/mongo/shell/std/.d.ts` + +- Describe exported symbols from `.js`. + +4. Register the std module name in `src/mongo/shell/BUILD.bazel` + +- Add `("std:", "std/.js")` to `MONGOJS_STD_JS_MODULES`. + +5. Update allowed internal module names in `src/mongo/scripting/mozjs/common/global.d.ts` + +- Add `""` to the `InternalModuleName` set used by `internalModule()`. + +Name consistency matters: + +- JS import name uses `std:` +- internal binding lookup uses `` (without the `std:` prefix) +- all entries above should refer to the same module concept + +## How the Implementation Works Internally + +### Build-time wiring + +1. `src/mongo/shell/BUILD.bazel` lists `MONGOJS_STD_JS_MODULES`. +2. `buildscripts/jstoh.py` consumes these entries via `--module` and generates embedded `JSFile` + entries in `mongojs.cpp`. +3. `std_internal_modules` compiles all `std/*.cpp` files and links them with the internal module + registry. + +### Runtime wiring + +1. Each `std/.cpp` registration macro creates a static `InternalModuleRegistrar`. +2. Registrars populate the internal module registry (`internal_module_registry.*`) at startup. +3. `ModuleLoader::init()` defines a global `internalModule()` function and calls + `preloadInternalModules()`. +4. `preloadInternalModules()`: + +- creates a binding object per registered internal module +- calls each module's C++ initializer to populate it +- stores the binding in an internal registry map keyed by module name +- loads the module setup JS (`std:`) from embedded `JSFile` source + +5. The setup JS module (for example `std/performance.js`) runs as a `std:*` module and is allowed to + call `internalModule("")`. +6. Non-`std:*` callers are rejected by `internalModule()` at runtime. + +This split keeps native internals private while exposing a stable, documented JavaScript API. diff --git a/src/mongo/shell/std/performance.cpp b/src/mongo/shell/std/performance.cpp new file mode 100644 index 00000000000..2731bcfe7f3 --- /dev/null +++ b/src/mongo/shell/std/performance.cpp @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/scripting/mozjs/shell/internal_module_registry.h" + +#include + +#include + +#include +#include +#include + +namespace mongo::JSFiles { +extern const JSFile std_performance; +} // namespace mongo::JSFiles + +namespace mongo::mozjs::std_modules { +namespace { + +constexpr double kNanosPerMillis = 1e6; +const auto kPerformanceProcessStart = std::chrono::steady_clock::now(); + +double performanceNowMillis() { + const auto elapsedNs = std::chrono::duration_cast( + std::chrono::steady_clock::now() - kPerformanceProcessStart); + return static_cast(elapsedNs.count()) / kNanosPerMillis; +} + +bool now(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + args.rval().setDouble(performanceNowMillis()); + return true; +} + +} // namespace + +bool initializePerformanceBinding(JSContext* cx, JS::HandleObject target) { + return JS_DefineFunction(cx, target, "now", now, 0, JSPROP_ENUMERATE); +} + +MONGO_REGISTER_INTERNAL_MODULE_WITH_SETUP("performance", + initializePerformanceBinding, + &::mongo::JSFiles::std_performance); + +} // namespace mongo::mozjs::std_modules diff --git a/src/mongo/shell/std/performance.d.ts b/src/mongo/shell/std/performance.d.ts new file mode 100644 index 00000000000..28bb8444c37 --- /dev/null +++ b/src/mongo/shell/std/performance.d.ts @@ -0,0 +1,11 @@ +export interface PerformanceApi { + /** + * Returns a high-resolution, monotonic timestamp in milliseconds. + * + * The timestamp is relative to shell process startup and is not tied to + * wall-clock time. + */ + now(): number; +} + +export declare const performance: PerformanceApi; diff --git a/src/mongo/shell/std/performance.js b/src/mongo/shell/std/performance.js new file mode 100644 index 00000000000..327ec737d79 --- /dev/null +++ b/src/mongo/shell/std/performance.js @@ -0,0 +1,7 @@ +const {now: internalNow} = internalModule("performance"); + +export const performance = Object.freeze({ + now() { + return internalNow(); + }, +});