SERVER-126888: Add a high-resolution timer for perf testing in jstests (#53877)
Co-authored-by: Steve McClure <steve.mcclure@mongodb.com> Co-authored-by: Teo Voinea <teo.voinea@mongodb.com> GitOrigin-RevId: e91b166957c79f6576efe2c6fbf6d796a9636905
This commit is contained in:
parent
0b730ee3c2
commit
41b16e48d9
@ -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: <name> <file>")
|
||||
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:]))
|
||||
|
||||
@ -25,6 +25,7 @@ export default [
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.mongo,
|
||||
internalModule: true,
|
||||
|
||||
// jstests/global.d.ts
|
||||
TestData: true,
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -111,6 +111,7 @@ const expectedGlobalVars = [
|
||||
"getJSHeapLimitMB",
|
||||
"globalThis",
|
||||
"hex_md5",
|
||||
"internalModule",
|
||||
"isFinite",
|
||||
"highWaterMarkResumeTokenType",
|
||||
"isNaN",
|
||||
|
||||
23
jstests/noPassthrough/shell/js/module_loader.js
Normal file
23
jstests/noPassthrough/shell/js/module_loader.js
Normal file
@ -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}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
22
jstests/noPassthrough/shell/js/std_performance.js
Normal file
22
jstests/noPassthrough/shell/js/std_performance.js
Normal file
@ -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");
|
||||
});
|
||||
});
|
||||
16
src/mongo/scripting/mozjs/common/global.d.ts
vendored
16
src/mongo/scripting/mozjs/common/global.d.ts
vendored
@ -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<string, unknown>;
|
||||
|
||||
@ -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",
|
||||
|
||||
86
src/mongo/scripting/mozjs/shell/internal_module_registry.cpp
Normal file
86
src/mongo/scripting/mozjs/shell/internal_module_registry.cpp
Normal file
@ -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
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* 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 <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace mongo::mozjs {
|
||||
namespace {
|
||||
|
||||
using InternalModuleMap = std::unordered_map<std::string, InternalModuleRegistration>;
|
||||
|
||||
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<std::mutex> lock(getInternalModuleMapMutex());
|
||||
getInternalModuleMap(lock)[std::string(moduleName)] =
|
||||
InternalModuleRegistration{std::string(moduleName), initialize, setupFile};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<InternalModuleRegistration> listRegisteredInternalModules() {
|
||||
std::vector<InternalModuleRegistration> registrations;
|
||||
|
||||
std::lock_guard<std::mutex> 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
|
||||
80
src/mongo/scripting/mozjs/shell/internal_module_registry.h
Normal file
80
src/mongo/scripting/mozjs/shell/internal_module_registry.h
Normal file
@ -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
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* 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 <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <js/RootingAPI.h>
|
||||
|
||||
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<InternalModuleRegistration> 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
|
||||
@ -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 <boost/none.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <js/CallArgs.h>
|
||||
#include <js/CharacterEncoding.h>
|
||||
#include <js/CompileOptions.h>
|
||||
#include <js/Context.h>
|
||||
@ -63,6 +65,7 @@
|
||||
#include <js/PropertyAndElement.h>
|
||||
#include <js/PropertyDescriptor.h>
|
||||
#include <js/RootingAPI.h>
|
||||
#include <js/ScriptPrivate.h>
|
||||
#include <js/String.h>
|
||||
#include <js/TypeDecls.h>
|
||||
#include <js/Utility.h>
|
||||
@ -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<JSString*> 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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 = [
|
||||
|
||||
90
src/mongo/shell/std/README.md
Normal file
90
src/mongo/shell/std/README.md
Normal file
@ -0,0 +1,90 @@
|
||||
# `std` Modules
|
||||
|
||||
This directory contains shell standard modules that are imported as `std:<name>` 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 (`<name>.js`) which defines the public API of the module.
|
||||
- an internal C++ binding implementation (`<name>.cpp`) offering bindings between C++ and
|
||||
JavaScript.
|
||||
- a TypeScript declaration file for the public API (`<name>.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 `<name>`, update all of the following:
|
||||
|
||||
1. Add public API file: `src/mongo/shell/std/<name>.js`
|
||||
|
||||
- Export the stable API users import from `std:<name>`.
|
||||
- Use `internalModule("<name>")` only inside this `std:*` module implementation.
|
||||
|
||||
2. Add internal binding file: `src/mongo/shell/std/<name>.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("<name>", initFn, &::mongo::JSFiles::std_<name>)`.
|
||||
|
||||
3. Add public typings: `src/mongo/shell/std/<name>.d.ts`
|
||||
|
||||
- Describe exported symbols from `<name>.js`.
|
||||
|
||||
4. Register the std module name in `src/mongo/shell/BUILD.bazel`
|
||||
|
||||
- Add `("std:<name>", "std/<name>.js")` to `MONGOJS_STD_JS_MODULES`.
|
||||
|
||||
5. Update allowed internal module names in `src/mongo/scripting/mozjs/common/global.d.ts`
|
||||
|
||||
- Add `"<name>"` to the `InternalModuleName` set used by `internalModule()`.
|
||||
|
||||
Name consistency matters:
|
||||
|
||||
- JS import name uses `std:<name>`
|
||||
- internal binding lookup uses `<name>` (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/<name>.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:<name>`) 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("<name>")`.
|
||||
6. Non-`std:*` callers are rejected by `internalModule()` at runtime.
|
||||
|
||||
This split keeps native internals private while exposing a stable, documented JavaScript API.
|
||||
72
src/mongo/shell/std/performance.cpp
Normal file
72
src/mongo/shell/std/performance.cpp
Normal file
@ -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
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* 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 <chrono>
|
||||
|
||||
#include <jsapi.h>
|
||||
|
||||
#include <js/CallArgs.h>
|
||||
#include <js/PropertySpec.h>
|
||||
#include <js/Value.h>
|
||||
|
||||
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::nanoseconds>(
|
||||
std::chrono::steady_clock::now() - kPerformanceProcessStart);
|
||||
return static_cast<double>(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
|
||||
11
src/mongo/shell/std/performance.d.ts
vendored
Normal file
11
src/mongo/shell/std/performance.d.ts
vendored
Normal file
@ -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;
|
||||
7
src/mongo/shell/std/performance.js
Normal file
7
src/mongo/shell/std/performance.js
Normal file
@ -0,0 +1,7 @@
|
||||
const {now: internalNow} = internalModule("performance");
|
||||
|
||||
export const performance = Object.freeze({
|
||||
now() {
|
||||
return internalNow();
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user