Bug 1790383 - [devtools] Allow loading ESMs in a distinct loader specific to DevTools. r=arai

Differential Revision: https://phabricator.services.mozilla.com/D157618
This commit is contained in:
Alexandre Poirot 2022-10-03 07:28:09 +00:00
parent 7ad7c7e805
commit 8d2a615b2d
15 changed files with 239 additions and 34 deletions

View File

@ -572,12 +572,30 @@ void ChromeUtils::Import(const GlobalObject& aGlobal,
aRetval.set(exports);
}
static mozJSModuleLoader* GetContextualESLoader(
const Optional<bool>& aLoadInDevToolsLoader, JSObject* aGlobal) {
RefPtr devToolsModuleloader = mozJSModuleLoader::GetDevToolsLoader();
// We should load the module in the DevTools loader if:
// - ChromeUtils.importESModule's `loadInDevToolsLoader` option is true, or,
// - if the callsite is from a module loaded in the DevTools loader and
// `loadInDevToolsLoader` isn't an explicit false.
bool shouldUseDevToolsLoader =
(aLoadInDevToolsLoader.WasPassed() && aLoadInDevToolsLoader.Value()) ||
(devToolsModuleloader && !aLoadInDevToolsLoader.WasPassed() &&
devToolsModuleloader->IsLoaderGlobal(aGlobal));
if (shouldUseDevToolsLoader) {
return mozJSModuleLoader::GetOrCreateDevToolsLoader();
}
return mozJSModuleLoader::Get();
}
/* static */
void ChromeUtils::ImportESModule(const GlobalObject& aGlobal,
const nsAString& aResourceURI,
JS::MutableHandle<JSObject*> aRetval,
ErrorResult& aRv) {
RefPtr moduleloader = mozJSModuleLoader::Get();
void ChromeUtils::ImportESModule(
const GlobalObject& aGlobal, const nsAString& aResourceURI,
const ImportESModuleOptionsDictionary& aOptions,
JS::MutableHandle<JSObject*> aRetval, ErrorResult& aRv) {
RefPtr moduleloader =
GetContextualESLoader(aOptions.mLoadInDevToolsLoader, aGlobal.Get());
MOZ_ASSERT(moduleloader);
NS_ConvertUTF16toUTF8 registryLocation(aResourceURI);
@ -605,6 +623,7 @@ void ChromeUtils::ImportESModule(const GlobalObject& aGlobal,
}
namespace module_getter {
static const size_t SLOT_ID = 0;
static const size_t SLOT_URI = 1;
@ -649,7 +668,12 @@ static bool ModuleGetterImpl(JSContext* aCx, unsigned aArgc, JS::Value* aVp,
}
nsDependentCString uri(bytes.get());
RefPtr moduleloader = mozJSModuleLoader::Get();
RefPtr moduleloader =
aType == ModuleType::JSM
? mozJSModuleLoader::Get()
: GetContextualESLoader(
Optional<bool>(),
JS::GetNonCCWObjectGlobal(js::UncheckedUnwrap(thisObj)));
MOZ_ASSERT(moduleloader);
JS::Rooted<JS::Value> value(aCx);

View File

@ -202,6 +202,7 @@ class ChromeUtils {
static void ImportESModule(const GlobalObject& aGlobal,
const nsAString& aResourceURI,
const ImportESModuleOptionsDictionary& aOptions,
JS::MutableHandle<JSObject*> aRetval,
ErrorResult& aRv);

View File

@ -446,7 +446,7 @@ partial namespace ChromeUtils {
* the same file will not cause the module to be re-evaluated.
*/
[Throws]
object importESModule(DOMString aResourceURI);
object importESModule(DOMString aResourceURI, optional ImportESModuleOptionsDictionary options = {});
/**
* Defines a property on the given target which lazily imports a JavaScript
@ -918,6 +918,15 @@ dictionary CompileScriptOptionsDictionary {
boolean hasReturnValue = false;
};
dictionary ImportESModuleOptionsDictionary {
/**
* If true, a distinct module loader will be used, in the system principal,
* but with a distinct global so that the DevTools can load a distinct set
* of modules and do not interfere with its debuggee.
*/
boolean loadInDevToolsLoader;
};
/**
* A JS object whose properties specify what portion of the heap graph to
* write. The recognized properties are:

View File

@ -107,7 +107,7 @@ nsresult ComponentModuleLoader::StartFetch(ModuleLoadRequest* aRequest) {
JSContext* cx = jsapi.cx();
RootedScript script(cx);
nsresult rv =
mozJSModuleLoader::LoadSingleModuleScript(cx, aRequest, &script);
mozJSModuleLoader::LoadSingleModuleScript(this, cx, aRequest, &script);
MOZ_ASSERT_IF(jsapi.HasException(), NS_FAILED(rv));
MOZ_ASSERT(bool(script) == NS_SUCCEEDED(rv));

View File

@ -272,7 +272,6 @@ mozJSModuleLoader::mozJSModuleLoader()
mInitialized(false),
mLoaderGlobal(dom::RootingCx()),
mServicesObj(dom::RootingCx()) {
MOZ_ASSERT(!sSelf, "mozJSModuleLoader should be a singleton");
}
#define ENSURE_DEP(name) \
@ -413,11 +412,10 @@ mozJSModuleLoader::~mozJSModuleLoader() {
if (mInitialized) {
UnloadModules();
}
sSelf = nullptr;
}
StaticRefPtr<mozJSModuleLoader> mozJSModuleLoader::sSelf;
StaticRefPtr<mozJSModuleLoader> mozJSModuleLoader::sDevToolsLoader;
void mozJSModuleLoader::FindTargetObject(JSContext* aCx,
MutableHandleObject aTargetObject) {
@ -446,21 +444,42 @@ void mozJSModuleLoader::InitStatics() {
RegisterWeakMemoryReporter(sSelf);
}
void mozJSModuleLoader::Unload() {
void mozJSModuleLoader::UnloadLoaders() {
if (sSelf) {
sSelf->UnloadModules();
if (sSelf->mModuleLoader) {
sSelf->mModuleLoader->Shutdown();
sSelf->mModuleLoader = nullptr;
}
sSelf->Unload();
}
if (sDevToolsLoader) {
sDevToolsLoader->Unload();
}
}
void mozJSModuleLoader::Shutdown() {
void mozJSModuleLoader::Unload() {
UnloadModules();
if (mModuleLoader) {
mModuleLoader->Shutdown();
mModuleLoader = nullptr;
}
}
void mozJSModuleLoader::ShutdownLoaders() {
MOZ_ASSERT(sSelf);
UnregisterWeakMemoryReporter(sSelf);
sSelf = nullptr;
if (sDevToolsLoader) {
UnregisterWeakMemoryReporter(sDevToolsLoader);
sDevToolsLoader = nullptr;
}
}
mozJSModuleLoader* mozJSModuleLoader::GetOrCreateDevToolsLoader() {
if (sDevToolsLoader) {
return sDevToolsLoader;
}
sDevToolsLoader = new mozJSModuleLoader();
RegisterWeakMemoryReporter(sDevToolsLoader);
return sDevToolsLoader;
}
// This requires that the keys be strings and the values be pointers.
@ -566,10 +585,12 @@ void mozJSModuleLoader::CreateLoaderGlobal(JSContext* aCx,
MutableHandleObject aGlobal) {
auto backstagePass = MakeRefPtr<BackstagePass>();
RealmOptions options;
auto& creationOptions = options.creationOptions();
options.creationOptions()
.setFreezeBuiltins(true)
.setNewCompartmentInSystemZone();
creationOptions.setFreezeBuiltins(true).setNewCompartmentInSystemZone();
if (IsDevToolsLoader()) {
creationOptions.setInvisibleToDebugger(true);
}
xpc::SetPrefableRealmOptions(options);
// Defer firing OnNewGlobalObject until after the __URI__ property has
@ -622,7 +643,10 @@ void mozJSModuleLoader::CreateLoaderGlobal(JSContext* aCx,
JSObject* mozJSModuleLoader::GetSharedGlobal(JSContext* aCx) {
if (!mLoaderGlobal) {
JS::RootedObject globalObj(aCx);
CreateLoaderGlobal(aCx, "shared JSM global"_ns, &globalObj);
CreateLoaderGlobal(
aCx, IsDevToolsLoader() ? "DevTools global"_ns : "shared JSM global"_ns,
&globalObj);
// If we fail to create a module global this early, we're not going to
// get very far, so just bail out now.
@ -640,8 +664,8 @@ JSObject* mozJSModuleLoader::GetSharedGlobal(JSContext* aCx) {
/* static */
nsresult mozJSModuleLoader::LoadSingleModuleScript(
JSContext* aCx, JS::loader::ModuleLoadRequest* aRequest,
MutableHandleScript aScriptOut) {
ComponentModuleLoader* aModuleLoader, JSContext* aCx,
JS::loader::ModuleLoadRequest* aRequest, MutableHandleScript aScriptOut) {
ModuleLoaderInfo info(aRequest);
nsresult rv = info.EnsureResolvedURI();
NS_ENSURE_SUCCESS(rv, rv);
@ -657,7 +681,13 @@ nsresult mozJSModuleLoader::LoadSingleModuleScript(
NS_ENSURE_SUCCESS(rv, rv);
#ifdef STARTUP_RECORDER_ENABLED
sSelf->RecordImportStack(aCx, aRequest);
if (aModuleLoader == sSelf->mModuleLoader) {
sSelf->RecordImportStack(aCx, aRequest);
} else {
MOZ_ASSERT(sDevToolsLoader);
MOZ_ASSERT(aModuleLoader == sDevToolsLoader->mModuleLoader);
sDevToolsLoader->RecordImportStack(aCx, aRequest);
}
#endif
return NS_OK;
@ -1272,7 +1302,11 @@ nsresult mozJSModuleLoader::GetModuleImportStack(const nsACString& aLocation,
nsACString& retval) {
#ifdef STARTUP_RECORDER_ENABLED
MOZ_ASSERT(nsContentUtils::IsCallerChrome());
MOZ_ASSERT(mInitialized);
// When querying the DevTools loader, it may not be initialized yet
if (!mInitialized) {
return NS_ERROR_FAILURE;
}
ModuleLoaderInfo info(aLocation);
auto str = mImportStacks.Lookup(info.Key());
@ -1698,7 +1732,7 @@ nsresult mozJSModuleLoader::ImportESModule(
aSkipCheck /* = SkipCheckForBrokenURLOrZeroSized::No */) {
using namespace JS::loader;
MOZ_ASSERT(mModuleLoader);
mInitialized = true;
// Called from ChromeUtils::ImportESModule.
nsCString str(aLocation);
@ -1713,6 +1747,9 @@ nsresult mozJSModuleLoader::ImportESModule(
NS_ENSURE_TRUE(globalObj, NS_ERROR_FAILURE);
MOZ_ASSERT(xpc::Scriptability::Get(globalObj).Allowed());
// The module loader should be instantiated when fetching the shared global
MOZ_ASSERT(mModuleLoader);
JSAutoRealm ar(aCx, globalObj);
nsCOMPtr<nsIURI> uri;

View File

@ -57,14 +57,17 @@ class mozJSModuleLoader final : public nsIMemoryReporter {
void FindTargetObject(JSContext* aCx, JS::MutableHandleObject aTargetObject);
static void InitStatics();
static void Unload();
static void Shutdown();
static void UnloadLoaders();
static void ShutdownLoaders();
static mozJSModuleLoader* Get() {
MOZ_ASSERT(sSelf, "Should have already created the module loader");
return sSelf;
}
static mozJSModuleLoader* GetDevToolsLoader() { return sDevToolsLoader; }
static mozJSModuleLoader* GetOrCreateDevToolsLoader();
nsresult ImportInto(const nsACString& aResourceURI,
JS::HandleValue aTargetObj, JSContext* aCx, uint8_t aArgc,
JS::MutableHandleValue aRetval);
@ -110,11 +113,13 @@ class mozJSModuleLoader final : public nsIMemoryReporter {
nsresult IsJSModuleLoaded(const nsACString& aResourceURI, bool* aRetval);
nsresult IsESModuleLoaded(const nsACString& aResourceURI, bool* aRetval);
bool IsLoaderGlobal(JSObject* aObj) { return mLoaderGlobal == aObj; }
bool IsDevToolsLoader() const { return this == sDevToolsLoader; }
// Public methods for use from ComponentModuleLoader.
static bool IsTrustedScheme(nsIURI* aURI);
static nsresult LoadSingleModuleScript(
JSContext* aCx, JS::loader::ModuleLoadRequest* aRequest,
mozilla::loader::ComponentModuleLoader* aModuleLoader, JSContext* aCx,
JS::loader::ModuleLoadRequest* aRequest,
JS::MutableHandleScript aScriptOut);
size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
@ -129,11 +134,15 @@ class mozJSModuleLoader final : public nsIMemoryReporter {
private:
static mozilla::StaticRefPtr<mozJSModuleLoader> sSelf;
static mozilla::StaticRefPtr<mozJSModuleLoader> sDevToolsLoader;
void Unload();
void UnloadModules();
void CreateLoaderGlobal(JSContext* aCx, const nsACString& aLocation,
JS::MutableHandleObject aGlobal);
void CreateDevToolsLoaderGlobal(JSContext* aCx, const nsACString& aLocation,
JS::MutableHandleObject aGlobal);
bool CreateJSServices(JSContext* aCx);

View File

@ -2557,7 +2557,14 @@ nsXPCComponents_Utils::GetLoadedESModules(
NS_IMETHODIMP
nsXPCComponents_Utils::GetModuleImportStack(const nsACString& aLocation,
nsACString& aRetval) {
return mozJSModuleLoader::Get()->GetModuleImportStack(aLocation, aRetval);
nsresult rv =
mozJSModuleLoader::Get()->GetModuleImportStack(aLocation, aRetval);
// Fallback the query to the DevTools loader if not found in the shared loader
if (rv == NS_ERROR_FAILURE && mozJSModuleLoader::GetDevToolsLoader()) {
return mozJSModuleLoader::GetDevToolsLoader()->GetModuleImportStack(
aLocation, aRetval);
}
return rv;
}
/***************************************************************************/

View File

@ -2278,6 +2278,9 @@ void JSReporter::CollectReports(WindowPaths* windowPaths,
mozJSModuleLoader* loader = mozJSModuleLoader::Get();
size_t jsModuleLoaderSize =
loader ? loader->SizeOfIncludingThis(JSMallocSizeOf) : 0;
mozJSModuleLoader* devToolsLoader = mozJSModuleLoader::GetDevToolsLoader();
size_t jsDevToolsModuleLoaderSize =
devToolsLoader ? devToolsLoader->SizeOfIncludingThis(JSMallocSizeOf) : 0;
// This is the second step (see above). First we report stuff in the
// "explicit" tree, then we report other stuff.
@ -2509,6 +2512,8 @@ void JSReporter::CollectReports(WindowPaths* windowPaths,
REPORT_BYTES("explicit/xpconnect/js-module-loader"_ns, KIND_HEAP,
jsModuleLoaderSize, "XPConnect's JS module loader.");
REPORT_BYTES("explicit/xpconnect/js-devtools-module-loader"_ns, KIND_HEAP,
jsDevToolsModuleLoaderSize, "DevTools's JS module loader.");
// Report HelperThreadState.

View File

@ -172,7 +172,7 @@ void nsXPConnect::ReleaseXPConnectSingleton() {
NS_RELEASE2(xpc, cnt);
}
mozJSModuleLoader::Shutdown();
mozJSModuleLoader::ShutdownLoaders();
}
// static

View File

@ -0,0 +1 @@
export const object = { uniqueObjectPerLoader: true };

View File

@ -0,0 +1,29 @@
export let x = 0;
export function increment() {
x++;
};
import { object } from "resource://test/es6module_devtoolsLoader.js";
export const importedObject = object;
const importTrue = ChromeUtils.importESModule("resource://test/es6module_devtoolsLoader.js", { loadInDevToolsLoader : true });
export const importESModuleTrue = importTrue.object;
const importFalse = ChromeUtils.importESModule("resource://test/es6module_devtoolsLoader.js", { loadInDevToolsLoader : false });
export const importESModuleFalse = importFalse.object;
const importNull = ChromeUtils.importESModule("resource://test/es6module_devtoolsLoader.js", {});
export const importESModuleNull = importNull.object;
const importNull2 = ChromeUtils.importESModule("resource://test/es6module_devtoolsLoader.js");
export const importESModuleNull2 = importNull2.object;
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
object: "resource://test/es6module_devtoolsLoader.js",
});
export function importLazy() {
return lazy.object;
}

View File

@ -0,0 +1 @@
export const object = { onlyLoadedFromDevToolsModule: true };

View File

@ -0,0 +1,78 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { addDebuggerToGlobal } = ChromeUtils.import(
"resource://gre/modules/jsdebugger.jsm"
);
addDebuggerToGlobal(this);
const ESM_URL = "resource://test/es6module_devtoolsLoader.sys.mjs";
// Toggle the following pref to enable Cu.getModuleImportStack()
Services.prefs.setBoolPref("browser.startup.record", true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("browser.startup.record");
});
add_task(async function testDevToolsModuleLoader() {
const dbg = new Debugger();
const sharedGlobal = Cu.getGlobalForObject(Services);
const sharedPrincipal = Cu.getObjectPrincipal(sharedGlobal);
info("Test importing in the regular shared loader");
const ns = ChromeUtils.importESModule(ESM_URL);
Assert.equal(ns.x, 0);
ns.increment();
Assert.equal(ns.x, 1);
const nsGlobal = Cu.getGlobalForObject(ns);
const nsPrincipal = Cu.getObjectPrincipal(nsGlobal);
Assert.equal(nsGlobal, sharedGlobal, "Without any parameter, importESModule load in the shared JSM global");
Assert.equal(nsPrincipal, sharedPrincipal);
Assert.ok(nsPrincipal.isSystemPrincipal);
info("Global of ESM loaded in the shared loader can be inspected by the Debugger");
dbg.addDebuggee(nsGlobal);
Assert.ok(true, "The global is accepted by the Debugger API");
const ns1 = ChromeUtils.importESModule(ESM_URL, { loadInDevToolsLoader : false });
Assert.equal(ns1, ns, "Passing loadInDevToolsLoader=false from the shared JSM global is equivalent to regular importESModule");
info("Test importing in the devtools loader");
const ns2 = ChromeUtils.importESModule(ESM_URL, { loadInDevToolsLoader: true });
Assert.equal(ns2.x, 0, "We get a new module instance with a new incremented number");
Assert.notEqual(ns2, ns, "We imported a new instance of the module");
Assert.notEqual(ns2.importedObject, ns.importedObject, "The two module instances expose distinct objects");
Assert.equal(ns2.importESModuleTrue, ns2.importedObject, "When using loadInDevToolsLoader:true from a devtools global, we keep loading in the same loader");
Assert.equal(ns2.importESModuleNull, ns2.importedObject, "When having an undefined loadInDevToolsLoader from a devtools global, we keep loading in the same loader");
Assert.equal(ns2.importESModuleNull2, ns2.importedObject, "When having no optional argument at all, we keep loading in the same loader");
Assert.equal(ns2.importESModuleFalse, ns.importedObject, "When passing an explicit loadInDevToolsLoader:false, we load in the shared global, even from a devtools global");
Assert.equal(ns2.importLazy(), ns2.importedObject, "ChromeUtils.defineESModuleGetters imports will follow the contextual loader");
info("When using the devtools loader, we load in a distinct global, but the same compartment");
const ns2Global = Cu.getGlobalForObject(ns2);
const ns2Principal = Cu.getObjectPrincipal(ns2Global);
Assert.notEqual(ns2Global, sharedGlobal, "The module is loaded in a distinct global");
Assert.equal(ns2Principal, sharedPrincipal, "The principal is still the shared system principal");
Assert.equal(Cu.getGlobalForObject(ns2.importedObject), ns2Global, "Nested dependencies are also loaded in the same devtools global");
Assert.throws(() => dbg.addDebuggee(ns2Global), /TypeError: passing non-debuggable global to addDebuggee/,
"Global os ESM loaded in the devtools loader can't be inspected by the Debugee");
info("Re-import the same module in the devtools loader");
const ns3 = ChromeUtils.importESModule(ESM_URL, { loadInDevToolsLoader: true });
Assert.equal(ns3, ns2, "We import the exact same module");
Assert.equal(ns3.importedObject, ns2.importedObject, "The two module expose the same objects");
info("Import a module only from the devtools loader");
const ns4 = ChromeUtils.importESModule("resource://test/es6module_devtoolsLoader_only.js", { loadInDevToolsLoader: true });
const ns4Global = Cu.getGlobalForObject(ns4);
Assert.equal(ns4Global, ns2Global, "The module is loaded in the same devtools global");
info("Assert the behavior of getModuleImportStack on modules loaded in the devtools loader");
Assert.ok(Cu.getModuleImportStack(ESM_URL).includes("testDevToolsModuleLoader"));
Assert.ok(Cu.getModuleImportStack("resource://test/es6module_devtoolsLoader.js").includes("testDevToolsModuleLoader"));
Assert.ok(Cu.getModuleImportStack("resource://test/es6module_devtoolsLoader.js").includes(ESM_URL));
// Previous import stack were for module loaded via the shared jsm loader.
// Let's also assert that we get stack traces for modules loaded via the devtools loader.
Assert.ok(Cu.getModuleImportStack("resource://test/es6module_devtoolsLoader_only.js").includes("testDevToolsModuleLoader"));
});

View File

@ -29,6 +29,9 @@ support-files =
es6module_cycle_b.js
es6module_cycle_c.js
es6module_top_level_await.js
es6module_devtoolsLoader.js
es6module_devtoolsLoader.sys.mjs
es6module_devtoolsLoader_only.js
esmified-1.sys.mjs
esmified-2.sys.mjs
esmified-3.sys.mjs
@ -202,6 +205,7 @@ head = head_watchdog.js
[test_uawidget_scope.js]
[test_uninitialized_lexical.js]
[test_print_stderr.js]
[test_import_devtools_loader.js]
[test_import_es6_modules.js]
[test_import_shim.js]
[test_defineESModuleGetters.js]

View File

@ -663,7 +663,7 @@ nsresult ShutdownXPCOM(nsIServiceManager* aServMgr) {
// log files. We have to ignore them before we can move
// the mozilla::PoisonWrite call before this point. See bug
// 834945 for the details.
mozJSModuleLoader::Unload();
mozJSModuleLoader::UnloadLoaders();
// Clear the profiler's JS context before cycle collection. The profiler will
// notify the JS engine that it can let go of any data it's holding on to for