Closed Bug 1790383 Opened 3 years ago Closed 3 years ago

Allow loading ESM in a distinct system compartment

Categories

(DevTools :: General, enhancement)

enhancement

Tracking

(firefox107 fixed)

RESOLVED FIXED
107 Branch
Tracking Status
firefox107 --- fixed

People

(Reporter: ochameau, Assigned: ochameau)

References

(Blocks 2 open bugs)

Details

Attachments

(1 file)

In order to convert backend modules of DevTools to ESM we would need a way to load these modules in a distinct system compartment.

For now we are using either freshCompartment: true or invisibleToDebugger: true arguments of Sandbox in order to load all CommonJS modules in a distinct system compartment.
This is only important in the case of the Browser Toolbox, i.e. when we debug the browser.
When we debug the browser itself, we have to:

  • load DevTools modules independantly of the ones loaded for regular DevTools. We don't want to share any possible state.
  • have a way to distinguish the two set of modules and ignore the one related to the brower toolbox. This is especially important for the debugger in order to avoid pausing in code related to the browser toolbox.

For now, the CommonJS modules are loaded via loadSubScript in hand-crafted JS Sandbox().

We could possibly use a regumar import method from a hand-crafted document, using the right principal. Similaryl to attachment 9041865 [details].
But I suspect that any inner usage of ChromeUtils.defineESModuleGetters is going to fallback to load the ESM in the shared system global instead of the one from the caller...
(There is also possible usage of ChromeUtils.import, but we could say we have to convert them to regular import x from;)

I see two possible call for actions here:

  • make ChromeUtils.defineESModuleGetters try to use the callsite's module loader, if called from a ESM. Does that sound feasible/reasonable?
  • may be also expose an API to easily load a ESM in this "distinct system compartment". So that it is simplier to load the very first devtools backend JSM.

But I'm open to suggestions and alternative plan here.
I think that WebExtension might need something alongside these lines. Any bug already opened?

Flags: needinfo?(arai.unmht)

(In reply to Alexandre Poirot [:ochameau] from comment #0)

But I suspect that any inner usage of ChromeUtils.defineESModuleGetters is going to fallback to load the ESM in the shared system global instead of the one from the caller...

Yes, currently there's only one shared global and module loader, and ChromeUtils.importESModule and ChromeUtils.defineESModuleGetters use them, regardless of the caller.

https://searchfox.org/mozilla-central/rev/b94f0c157227dadd3ecf119ce271035c52ed237b/js/xpconnect/loader/mozJSModuleLoader.cpp#564-566,584-587,614-617

void mozJSModuleLoader::CreateLoaderGlobal(JSContext* aCx,
                                           const nsACString& aLocation,
                                           MutableHandleObject aGlobal) {
...
  nsresult rv = xpc::InitClassesWithNewWrappedGlobal(
      aCx, static_cast<nsIGlobalObject*>(backstagePass),
      nsContentUtils::GetSystemPrincipal(), xpc::DONT_FIRE_ONNEWGLOBALHOOK,
      options, &global);
...
  MOZ_ASSERT(!mModuleLoader);
  RefPtr<ComponentScriptLoader> scriptLoader = new ComponentScriptLoader;
  mModuleLoader = new ComponentModuleLoader(scriptLoader, backstagePass);
  backstagePass->InitModuleLoader(mModuleLoader);
  • make ChromeUtils.defineESModuleGetters try to use the callsite's module loader, if called from a ESM. Does that sound feasible/reasonable?

We could add support for multiple shared globals and module loaders,
and a map from global to loader, and use corresponding loader for each
ChromeUtils.importESModule and ChromeUtils.defineESModuleGetters call.

https://searchfox.org/mozilla-central/rev/b94f0c157227dadd3ecf119ce271035c52ed237b/js/xpconnect/loader/mozJSModuleLoader.h#247,250

class mozJSModuleLoader final : public nsIMemoryReporter {
...
  JS::PersistentRooted<JSObject*> mLoaderGlobal;
...
  RefPtr<mozilla::loader::ComponentModuleLoader> mModuleLoader;

I'll look into the design.

  • may be also expose an API to easily load a ESM in this "distinct system compartment". So that it is simplier to load the very first devtools backend JSM.

Adding extra options parameter would work.

e.g.

const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs", { newGlobal: true }
);
ChromeUtils.defineESModuleGetters(lazy, {
  UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
}, { newGlobal: true });

If there'll be only one distinct global other than the normal one, the option can be one of:

  • import into the current shared global (default)
  • import into the normal shared global (maybe unnecessary)
  • import into the distinct shared global

If there can be multiple distinct globals other than the normal one, the option would need to be ID or something.

I think that WebExtension might need something alongside these lines. Any bug already opened?

So far no bugs are opened, and iiuc migrating the subscript was not planned when we were discussing the APIs.

Flags: needinfo?(arai.unmht)

perhaps adding yet another mozJSModuleLoader instance might be simpler

https://searchfox.org/mozilla-central/rev/7b36c8b83337c4b4cdfd4ccc2168f3491a86811b/js/xpconnect/loader/mozJSModuleLoader.h#129

static mozilla::StaticRefPtr<mozJSModuleLoader> sSelf;

a problem I can think of is, the edge case around how to associate the global/loader.

inside the system module, the current global tells which loader it corresponds to, given there's only one shared global per each loader (or loader's context or whatever).
So, once we add entry point to use different shared global, any module-import inside system module loads the module into the current module loader.

however, once the execution goes out of the system module's global, for example, by running sandboxed code, the current global doesn't tell the correct loader, or even, the "correct loader" needs definition.

for example, if the following code is executed inside a module loaded into the different shared global, where should the A.sys.mjs be loaded?

var sandbox = Cu.Sandbox(...);
Cu.evalInSandbox(`
ChromeUtils.importESModule("A.sys.mjs");
`, sandbox);

possible options are:

  • (a) load into the regular shared global
  • (b) look into the call stack or something, and load into the innermost shared global if found

(a) is the default behavior when we don't add special case, except for using current global's loader.

(b) would work as expected in some case, but it may need yet another way to leave the different shared global.

in the browser toolbox's case, what's the interaction between the code inside the distinct system compartment and others?
can there be a case like the above example?

Flags: needinfo?(poirot.alex)

(In reply to Tooru Fujisawa [:arai] from comment #3)

a problem I can think of is, the edge case around how to associate the global/loader.

inside the system module, the current global tells which loader it corresponds to, given there's only one shared global per each loader (or loader's context or whatever).
So, once we add entry point to use different shared global, any module-import inside system module loads the module into the current module loader.

This sounds exactly like what we would need!
Any import foo from "foo.js"; would be loaded in the same loader/global/principal.
In theory, we wouldn't use ChromeUtils.importESModule. Consider that we will replace all of them with regular ES import.

But there is lazy imports, which have to be going through ChromeUtils.defineESModuleGetters.
We should make it so that this magic method reuse the contextual loader. The loader of the module where we called the function.

however, once the execution goes out of the system module's global, for example, by running sandboxed code, the current global doesn't tell the correct loader, or even, the "correct loader" needs definition.

for example, if the following code is executed inside a module loaded into the different shared global, where should the A.sys.mjs be loaded?

var sandbox = Cu.Sandbox(...);
Cu.evalInSandbox(`
ChromeUtils.importESModule("A.sys.mjs");
`, sandbox);

possible options are:

  • (a) load into the regular shared global
  • (b) look into the call stack or something, and load into the innermost shared global if found

(a) is the default behavior when we don't add special case, except for using current global's loader.

(b) would work as expected in some case, but it may need yet another way to leave the different shared global.

We do not need anything special here. importESModule should keep its current behavior, so (a).

May be we should be conservative here and only consider the very specific special of DevTools:

  • ChromeUtils.importESModule isn't contextual, but accepts a parameter to be able to load the very first ESM in the devtool's specific loader.
    Any other usage of this method, without the new param, will load in the regular shared global.
  • Only tweak the behavior of ChromeUtils.defineESModuleGetters to be contextual, and only for the precise usecase of devtools:
    • only if the callsite is from a module using the devtools specific loader,
    • then load all lazy symbol in the devtools specific loader

WDYT?

Flags: needinfo?(poirot.alex)

(In reply to Tooru Fujisawa [:arai] from comment #4)

in the browser toolbox's case, what's the interaction between the code inside the distinct system compartment and others?
can there be a case like the above example?

Overall, the typical scenarios is that ESM/JSM of the regular shared global will use ChromeUtils.importESModule with a new flag to load some modules in the special devtool global. They will set the flag explicitely. Typically when we start the DevToolsServer for the Browser Toolbox.
For example:
https://searchfox.org/mozilla-central/rev/9769b513e38ee4f5df9d5d1eff55ff7cdc8cbf81/devtools/client/framework/browser-toolbox/Launcher.sys.mjs#132-140
could become:

    const { DevToolsServer } = ChromeUtils.importESModule(
      "resource://devtools/server/devtools-server.sys.mjs", { devtoolsGlobal: true } // Just a proposal, there might be better API/name than "devtoolsGlobal".
    );

Then, any ESM loaded in this special devtools global should load its dependencies in the same devtools global (via import foo from "foo.js", or ChromeUtils.importESModule).
Leftovers of ChromeUtils.import and ChromeUtils.importESModule will load from the regular shared global.
That's fine. And it might actually be useful in order to escape the devtools global.
We might have cases where we would like to use a module from the shared global, then, we would use ChromeUtils.importESModule.

I can't think about any scenario where we are using another Sandbox.
But if we do have one, and we would like to load a ESM in the devtools global, we would use the new param of ChromeUtils.importESModule.

sorry, my example about the sandbox was misleading.

  • it's also about ChromeUtils.defineESModuleGetters inside the sandbox case, or any other case where the execution goes out of the devtools shared global
  • the target code can be non-module script, and in that case static import is not available

So, the question is that, is there any case that following happens?

  • a code "A" inside "devtools shared global" runs another code "B" in another global (for example, calls function, performs eval, loads subscript, etc)
  • the code "B" (or any other code called/executed from there) calls ChromeUtils.importESModule or ChromeUtils.defineESModuleGetters
  • the code "A" expects the ChromeUtils.importESModule or ChromeUtils.defineESModuleGetters inside code "B" to use the same shared global as "A"

In the current code, I guess the almost-equivalent question would be, is there any case that the this.#loader in the above link is implicitly used by a code outside of the special global?

if this never happens, then we don't have to worry about module loading outside of system global.

(In reply to Alexandre Poirot [:ochameau] from comment #5)

(In reply to Tooru Fujisawa [:arai] from comment #3)
In theory, we wouldn't use ChromeUtils.importESModule. Consider that we will replace all of them with regular ES import.

static import is available only at the top-level of the module script.
So if there's ChromeUtils.import inside a block (conditionally imported) or inside a function (import only when called), that should be rewritten with ChromeUtils.importESModule, not the static import (or dynamic import() which is not supported yet).

Thus, I think we should also make ChromeUtils.importESModule contextual, to avoid confusion.

(In reply to Alexandre Poirot [:ochameau] from comment #6)

Leftovers of ChromeUtils.import and ChromeUtils.importESModule will load from the regular shared global.
That's fine. And it might actually be useful in order to escape the devtools global.
We might have cases where we would like to use a module from the shared global, then, we would use ChromeUtils.importESModule.

Given above, if we need a way to escape the devtools global, that will also need special flag, maybe { devtoolsGlobal: false }.

IMO, it's better making the boundary explicit.

I can't think about any scenario where we are using another Sandbox.
But if we do have one, and we would like to load a ESM in the devtools global, we would use the new param of ChromeUtils.importESModule.

okay, if all cases can be explicit about "non-devtools-global to devtools-global" import, it sounds good :)

(In reply to Tooru Fujisawa [:arai] from comment #7)

So, the question is that, is there any case that following happens?

  • a code "A" inside "devtools shared global" runs another code "B" in another global (for example, calls function, performs eval, loads subscript, etc)

We do use Sandbox:
https://searchfox.org/mozilla-central/search?q=Sandbox%28&path=devtools%2Fserver%2F&case=false&regexp=false
But none of them are meant to load ESM. The main usage of Sandbox is to evaluate code provided by the user in a safe sandbox.
(ignore the first usage, this code should be removed)

We use loadSubScript only in the loader (which will go away once we move to ESM):
https://searchfox.org/mozilla-central/search?q=loadSubScript&path=devtools%2Fserver%2F&case=false&regexp=false

We don't do any straight "eval()". We do eval in the web page, typically, for the web console. But again, this won't imply loading any devtools ESM.
We might use import, but this is meant to use the web page's loader.

  • the code "B" (or any other code called/executed from there) calls ChromeUtils.importESModule or ChromeUtils.defineESModuleGetters

I think we stop here. We will only use these two methods from ESM/JSM. The ESM callsites will be either loaded via ChromeUtils methods -or- via regular import. It can be ESM loaded from the regular shared global -or- the special devtools global.

(In reply to Alexandre Poirot [:ochameau] from comment #5)

(In reply to Tooru Fujisawa [:arai] from comment #3)
In theory, we wouldn't use ChromeUtils.importESModule. Consider that we will replace all of them with regular ES import.

static import is available only at the top-level of the module script.
So if there's ChromeUtils.import inside a block (conditionally imported) or inside a function (import only when called), that should be rewritten with ChromeUtils.importESModule, not the static import (or dynamic import() which is not supported yet).

Thus, I think we should also make ChromeUtils.importESModule contextual, to avoid confusion.

Ah yes, I forgot about imports done from a block/function.
I considered that imports are either done from the top level, or using the lazy API.
But yes, that sounds fine making importESModule contextual.

(In reply to Alexandre Poirot [:ochameau] from comment #6)

Leftovers of ChromeUtils.import and ChromeUtils.importESModule will load from the regular shared global.
That's fine. And it might actually be useful in order to escape the devtools global.
We might have cases where we would like to use a module from the shared global, then, we would use ChromeUtils.importESModule.

Given above, if we need a way to escape the devtools global, that will also need special flag, maybe { devtoolsGlobal: false }.

IMO, it's better making the boundary explicit.

Yes, that's a good idea.

I can't think about any scenario where we are using another Sandbox.
But if we do have one, and we would like to load a ESM in the devtools global, we would use the new param of ChromeUtils.importESModule.

okay, if all cases can be explicit about "non-devtools-global to devtools-global" import, it sounds good :)

Yes, I think both "non-devtools-global to devtools-global" and "devtools-global to non-devtools-global" will/could be explicit.

Note that I'm not really a C++ developer, but I'd be happy to be mentored if you are busy with other things.

I imagine the starting point would be conditionaly use a distinct loader over there:
https://searchfox.org/mozilla-central/rev/9769b513e38ee4f5df9d5d1eff55ff7cdc8cbf81/dom/base/ChromeUtils.cpp#580

- RefPtr moduleloader = mozJSModuleLoader::Get();
+ RefPtr moduleloader = devtoolsGlobal ? mozJSModuleLoader::GetDevToolsLoader() : mozJSModuleLoader::Get();

Then, this second mozJSModuleLoader would actualy spawn another distinct shared global:
https://searchfox.org/mozilla-central/source/js/xpconnect/loader/mozJSModuleLoader.cpp#622-639
We may only tweak this function to create a global with another name over there:
https://searchfox.org/mozilla-central/rev/9769b513e38ee4f5df9d5d1eff55ff7cdc8cbf81/js/xpconnect/loader/mozJSModuleLoader.cpp#625

- CreateLoaderGlobal(aCx, "shared JSM global"_ns, &globalObj);
+ CreateLoaderGlobal(aCx, "DevTools server global"_ns, &globalObj);

And if necessary tweak realms settings:
https://searchfox.org/mozilla-central/rev/9769b513e38ee4f5df9d5d1eff55ff7cdc8cbf81/js/xpconnect/loader/mozJSModuleLoader.cpp#570-573

Then, making the API contextual is still unclear to me.
https://searchfox.org/mozilla-central/rev/9769b513e38ee4f5df9d5d1eff55ff7cdc8cbf81/dom/base/ChromeUtils.cpp#576

void ChromeUtils::ImportESModule(const GlobalObject& aGlobal,
                                 const nsAString& aResourceURI,
                                 JS::MutableHandle<JSObject*> aRetval,
                                 ErrorResult& aRv) {
  RefPtr moduleloader = mozJSModuleLoader::Get();

I imagine we should query aGlobal?

void ChromeUtils::ImportESModule(const GlobalObject& aGlobal,
                                 const nsAString& aResourceURI,
                                 bool devtoolsGlobal, // We probably benefit from having a better API than just a boolean, but let's say we pass a boolean
                                 JS::MutableHandle<JSObject*> aRetval,
                                 ErrorResult& aRv) {
 // Either we explicitely requested to load from devtools gloal, or we are called from a devtools global.
 // We would also have to handle the case where we pass an explicit devtoolsGlobal = false and should load from the shared global.
 RefPtr moduleloader = devtoolsGlobal || mozJSModuleLoader::GetDevToolsLoader()->GetSharedGlobal(aGlobal.Context()) == aGlobal  ? mozJSModuleLoader::GetDevToolsLoader() : mozJSModuleLoader::Get();

And then apply something similar to defineESModuleGetters.

Okay, sounds good :)
I'm happy to mentor.

(In reply to Alexandre Poirot [:ochameau] from comment #8)

I imagine the starting point would be conditionaly use a distinct loader over there:
https://searchfox.org/mozilla-central/rev/9769b513e38ee4f5df9d5d1eff55ff7cdc8cbf81/dom/base/ChromeUtils.cpp#580

- RefPtr moduleloader = mozJSModuleLoader::Get();
+ RefPtr moduleloader = devtoolsGlobal ? mozJSModuleLoader::GetDevToolsLoader() : mozJSModuleLoader::Get();

Yes, that's for ChromeUtils.importESModule, and the following for ChromeUtils.defineESModuleGetters.

https://searchfox.org/mozilla-central/rev/0948667bc62415d48abff27e1405fb4ab4d65d75/dom/base/ChromeUtils.cpp#633-634,652

static bool ModuleGetterImpl(JSContext* aCx, unsigned aArgc, JS::Value* aVp,
                             ModuleType aType) {
...
  RefPtr moduleloader = mozJSModuleLoader::Get();

This function is shared between ChromeUtils.defineModuleGetter (aType == ModuleType::JSM) and ChromeUtils.defineESModuleGetters (aType == ModuleType::ESM).

Here's the full list of the places the loader is used, and corresponding API

Callsite API Note
ChromeUtils::Import ChromeUtils.import
ChromeUtils::ImportESModule ChromeUtils.importESModule
module_getter::ModuleGetterImpl ChromeUtils.defineModuleGetter and ChromeUtils.defineESModuleGetters
JSActorManager::GetActor ChromeUtils.registerWindowActor and ChromeUtils.registerProcessActor unrelated if window/process actor is not used in devtools global
OSFileConstantsService::Init OS.File unnecessary as long as OS.File is not used in devtools global
EvalStencil and mozJSSubScriptLoader::DoLoadSubScriptWithOptions Services.scriptloader.loadSubScript unnecessary if subscript loader is unused
loader::ImportModule - unrelated
nsXPCComponents_Utils::IsModuleLoaded Cu.isModuleLoaded unnecessary is there's no consumer in devtools global
nsXPCComponents_Utils::IsJSModuleLoaded Cu.isJSModuleLoaded (same)
nsXPCComponents_Utils::IsESModuleLoaded Cu.isESModuleLoaded (same)
nsXPCComponents_Utils::GetLoadedModules Cu.loadedModules (same)
nsXPCComponents_Utils::GetLoadedJSModules Cu.loadedJSModules (same)
nsXPCComponents_Utils::GetLoadedESModules Cu.loadedESModules (same)
nsXPCComponents_Utils::GetModuleImportStack Cu.getModuleImportStack might be nice to support, for debugging purpose
nsXPCComponents_Utils::Unload Cu.unload unnecessary as long as JSM is not used
xpc::JSReporter::CollectReports Memory reporter This needs to report both regular one and devtools one
XPCJSRuntime::LoaderGlobal ? this is used by many places and needs special attention
XPCWrappedNativeScope::AttachJSServices Cu.Sandbox and other sandbox APIs unnecessary if all sandboxes are going away. otherwise Services in the sandbox is mixed up
xpc::InitGlobalObject creating global or document unnecessary as long as either: (a) devtools global does not create any global, or (b) devtools global creates global but does not expect it to use its Services
ctypes::Module::Call ctypes.jsm unnecessary as long as ctypes is not used
ConstructJSMOrESMComponent static component loader unrelated

About XPCJSRuntime::LoaderGlobal, we might have to check all the following consumers to see if any of them are explicitly/implicitly used from devtools global:

Then, this second mozJSModuleLoader would actualy spawn another distinct shared global:
https://searchfox.org/mozilla-central/source/js/xpconnect/loader/mozJSModuleLoader.cpp#622-639
We may only tweak this function to create a global with another name over there:
https://searchfox.org/mozilla-central/rev/9769b513e38ee4f5df9d5d1eff55ff7cdc8cbf81/js/xpconnect/loader/mozJSModuleLoader.cpp#625

- CreateLoaderGlobal(aCx, "shared JSM global"_ns, &globalObj);
+ CreateLoaderGlobal(aCx, "DevTools server global"_ns, &globalObj);

Yes, exactly.

Then, making the API contextual is still unclear to me.
https://searchfox.org/mozilla-central/rev/9769b513e38ee4f5df9d5d1eff55ff7cdc8cbf81/dom/base/ChromeUtils.cpp#576

void ChromeUtils::ImportESModule(const GlobalObject& aGlobal,
                                 const nsAString& aResourceURI,
                                 JS::MutableHandle<JSObject*> aRetval,
                                 ErrorResult& aRv) {
  RefPtr moduleloader = mozJSModuleLoader::Get();

I imagine we should query aGlobal?

void ChromeUtils::ImportESModule(const GlobalObject& aGlobal,
                                 const nsAString& aResourceURI,
                                 bool devtoolsGlobal, // We probably benefit from having a better API than just a boolean, but let's say we pass a boolean
                                 JS::MutableHandle<JSObject*> aRetval,
                                 ErrorResult& aRv) {
 // Either we explicitely requested to load from devtools gloal, or we are called from a devtools global.
 // We would also have to handle the case where we pass an explicit devtoolsGlobal = false and should load from the shared global.
 RefPtr moduleloader = devtoolsGlobal || mozJSModuleLoader::GetDevToolsLoader()->GetSharedGlobal(aGlobal.Context()) == aGlobal  ? mozJSModuleLoader::GetDevToolsLoader() : mozJSModuleLoader::Get();

And then apply something similar to defineESModuleGetters.

Yes, using the GlobalObject or underlying JSObject* as a key would work.

But I think it's better not to call GetSharedGlobal there, given it will create the global even if the global is not used in the process.
So, just comparing against the possibly-null member would be nice.

https://searchfox.org/mozilla-central/rev/0948667bc62415d48abff27e1405fb4ab4d65d75/js/xpconnect/loader/mozJSModuleLoader.h#247

 JS::PersistentRooted<JSObject*> mLoaderGlobal;
+JS::PersistentRooted<JSObject*> mDevToolsLoaderGlobal;

and

>  RefPtr moduleloader = devtoolsGlobal || aGlobal.Get() == mDevToolsLoaderGlobal ? mozJSModuleLoader::GetDevToolsLoader() : mozJSModuleLoader::Get();

I still need to go through the table of comment 9, but I'd appreciate feedback on this first patch.
Consider that I'm not a C++ developer and feel free to suggest any major modification!

Typically, I'm not sure the following practice is acceptable?

  if (this == sSelf) {
    sSelf = nullptr;
  } else if (this == sDevToolsLoader) {
    sDevToolsLoader = nullptr;
  }

(In reply to Tooru Fujisawa [:arai] from comment #9

Here's the full list of the places the loader is used, and corresponding API

Out of curiosity, this table is super handy... are you using a particular tool to built such table??

Callsite API Note
ChromeUtils::Import ChromeUtils.import Don't want to impact JSM
ChromeUtils::ImportESModule ChromeUtils.importESModule Migrated
module_getter::ModuleGetterImpl ChromeUtils.defineModuleGetter and ChromeUtils.defineESModuleGetters Migrated
JSActorManager::GetActor ChromeUtils.registerWindowActor and ChromeUtils.registerProcessActor Would be a nice to have. Will open a followup.
OSFileConstantsService::Init OS.File The is one usage left, I'll try to get rid of it!
EvalStencil and mozJSSubScriptLoader::DoLoadSubScriptWithOptions Services.scriptloader.loadSubScript subscript loader is only used to implement the common js module loader. Or this is used in the frontend where we want to use the regular shared global.
loader::ImportModule - unrelated
nsXPCComponents_Utils::IsModuleLoaded Cu.isModuleLoaded Only used to check is a JSM is loaded to avoid loading it. This JSM is one from WebExtension, so we only care about the shared loader.
nsXPCComponents_Utils::IsJSModuleLoaded Cu.isJSModuleLoaded (same)
nsXPCComponents_Utils::IsESModuleLoaded Cu.isESModuleLoaded (same)
nsXPCComponents_Utils::GetLoadedModules Cu.loadedModules Not used
nsXPCComponents_Utils::GetLoadedJSModules Cu.loadedJSModules (same)
nsXPCComponents_Utils::GetLoadedESModules Cu.loadedESModules (same)
nsXPCComponents_Utils::GetModuleImportStack Cu.getModuleImportStack might be nice to support, for debugging purpose
nsXPCComponents_Utils::Unload Cu.unload We won't be using JSM. The goal is to migrate JSM and CommonJS to ESM.
xpc::JSReporter::CollectReports Memory reporter This needs to report both regular one and devtools one
XPCJSRuntime::LoaderGlobal ? Investigated below
XPCWrappedNativeScope::AttachJSServices Cu.Sandbox and other sandbox APIs Sandboxes are mostly used to implement the CommonJS loader. But there is three usage of Sandbox in the server. None of them use wantComponents. So we are all good?
xpc::InitGlobalObject creating global or document This seems to be used only for backstagepass or DOM Windows. We shouldn't be instantiating DOM documents related to the special devtools global.
ctypes::Module::Call ctypes.jsm We aren't using ctypes directly. May be via external deps. We do use SubProcess, but this isn't loaded via the DevTools loader. We load it via JSM and it would be fine loading via the shared JSM global as ESM.
ConstructJSMOrESMComponent static component loader unrelated

Note that we don't need to load all DevTools modules in the special compartment flagged with invisibleToDebugger.
We only need to load server modules which are loaded when a Browser Toolbox debug firefox itself and we don't want to see or interupt these server modules while debugging.
This is why this feature and patch applies only to a subset of the devtools/ folder. Mostly the devtools/server/ folder and some stuff from devtools/shared/.

About XPCJSRuntime::LoaderGlobal, we might have to check all the following consumers to see if any of them are explicitly/implicitly used from devtools global:

If we do care to spawn Sandbox in the same compartment as DevTools, it might be interesting looking into
https://searchfox.org/mozilla-central/rev/6f9fdc5c3b164a46004c98a5baaf55b05e2ad329/js/xpconnect/src/Sandbox.cpp#1328
But I'm not sure it is really useful.

TBH, I don't know what to think about this one AutoJSAPI::ReportException()

Same about JSActor. I would like to followup to allow loading JSWindowActor in the devtools global. But it sounds like the aGlobal will be passed.
Looking at test coverage it is very rare when we fallback to the priviledged scope. Might be something to verify while following up on JSActors?

Otherwise, the vast majority of the usages of PrivilegedJunk is to create JS Objects.
It sounds fine creating the objects in the JSM global. Typically all the following pattern xpc::NativeGlobal(xpc::PrivilegedJunkScope()); looks fine.

I don't know what to think about the ScriptPreloader.
I imagine it is fine not having any preload for modules loaded by the browser toolbox.
But could that be an issue if we expect the scripts to be preloaded in the right devtools loader scope?

Otherwise CompilationScope is used only for mesage manager, xul and subscript loader. All this sounds fine being specific to the shared JSM loader.

Assignee: nobody → poirot.alex

(In reply to Alexandre Poirot [:ochameau] from comment #12)

(In reply to Tooru Fujisawa [:arai] from comment #9

Here's the full list of the places the loader is used, and corresponding API

Out of curiosity, this table is super handy... are you using a particular tool to built such table??

Mostly manually, with searching each consumer in searchfox.

If we do care to spawn Sandbox in the same compartment as DevTools, it might be interesting looking into
https://searchfox.org/mozilla-central/rev/6f9fdc5c3b164a46004c98a5baaf55b05e2ad329/js/xpconnect/src/Sandbox.cpp#1328
But I'm not sure it is really useful.

If we're not going to spawn sandboxes from devtools global, it would be nice to add assertion to related code that checks the current global is not devtools global.
If we're going to spawn sandboxes from devtools global but it's supposed to be using shared global, then the assertion isn't necessary.

TBH, I don't know what to think about this one AutoJSAPI::ReportException()

This doesn't seem to require consistency between shared global vs devtools global, but just need to some random global to enter, and it should be fine to keep using shared global unconditionally.

Same about JSActor. I would like to followup to allow loading JSWindowActor in the devtools global. But it sounds like the aGlobal will be passed.
Looking at test coverage it is very rare when we fallback to the priviledged scope. Might be something to verify while following up on JSActors?

If this path is not used right now in devtools global, I think it's fine to address this in a followup bug.
(optional) it might be nice to add assertion here as well to check if this is really not used by devtools global.

Otherwise, the vast majority of the usages of PrivilegedJunk is to create JS Objects.
It sounds fine creating the objects in the JSM global. Typically all the following pattern xpc::NativeGlobal(xpc::PrivilegedJunkScope()); looks fine.

Okay, sounds good :)

I don't know what to think about the ScriptPreloader.
I imagine it is fine not having any preload for modules loaded by the browser toolbox.
But could that be an issue if we expect the scripts to be preloaded in the right devtools loader scope?

The scope is used mostly for getting realm options, for decoding cache, and as long as those options are same between shared global and devtools global, which is true in the current patch, it doesn't matter which one to use.
Also, this part doesn't actually allocate objects in the give scope/global, it's not necessary to handle devtools global here.

Also, we're going to remove JSContext-dependency from cache and decoding API in long term.

Otherwise CompilationScope is used only for mesage manager, xul and subscript loader. All this sounds fine being specific to the shared JSM loader.

Thank you for investigating!

(In reply to Tooru Fujisawa [:arai] from comment #13)

If we're not going to spawn sandboxes from devtools global, it would be nice to add assertion to related code that checks the current global is not devtools global.
If we're going to spawn sandboxes from devtools global but it's supposed to be using shared global, then the assertion isn't necessary.

We do spawn 3 sandboxes.
A first one in the content process targets actor. We expect it to relate to the content process privileged scope, so the shared global in each content process.
Then the two others are only used to fetch symbols out of the Sandbox object (indexedDB, and list of native global names).
Theoritically, we probably expect these symbols to be related to the debugee, which can be a content document, or the privileged scope (=shared jsm global). But at the end, I don't think binding them to one global or the other will have much impact.

So, we do spawn sandboxes. And at least one, the one in content-process.js is expected to be using the shared global for its imports.

TBH, I don't know what to think about this one AutoJSAPI::ReportException()

This doesn't seem to require consistency between shared global vs devtools global, but just need to some random global to enter, and it should be fine to keep using shared global unconditionally.

I'm relieved as I didn't know how to handle this one!

Same about JSActor. I would like to followup to allow loading JSWindowActor in the devtools global. But it sounds like the aGlobal will be passed.
Looking at test coverage it is very rare when we fallback to the priviledged scope. Might be something to verify while following up on JSActors?

If this path is not used right now in devtools global, I think it's fine to address this in a followup bug.
(optional) it might be nice to add assertion here as well to check if this is really not used by devtools global.

I opened bug 1792683 to followup on this.
Do you mean to add an assertion against aGlobal?
Something like this in JSActor's contructor:

  if (aGlobal) {
    mozJSModuleLoader* loader = mozJSModuleLoader::Get();
    MOZ_ASSERT(loader->IsLoaderGlobal(aGlobal));
  }

Or something asserting the contextual global of the callsite?

(In reply to Alexandre Poirot [:ochameau] from comment #14)

I opened bug 1792683 to followup on this.
Do you mean to add an assertion against aGlobal?

Yes, but checks if the global is not devtools loader's one if it's already instantiated.

Also, JSActor class doesn't necessarily know the current global.
So I think it should be placed at the consumer of JSActor, maybe around the following:

https://searchfox.org/mozilla-central/rev/d45dd05bf412e7468b3770a52519e9d546d6325c/dom/ipc/jsactor/JSActorManager.cpp#23-25

already_AddRefed<JSActor> JSActorManager::GetActor(JSContext* aCx,
                                                   const nsACString& aName,
                                                   ErrorResult& aRv) {

Something like:

MOZ_ASSERT(mozJSModuleLoader::IsNotDevToolsGlobal(JS::CurrentGlobalOrNull(aCx)));

With:

static bool IsNotDevToolsGlobal(JSObject* aGlobal) {
  if (!sDevToolsLoader) {
    return true;
  }
  return aGlobal != sDevToolsLoader->mLoaderGlobal;
}

(In reply to Tooru Fujisawa [:arai] from comment #15)

(In reply to Alexandre Poirot [:ochameau] from comment #14)

I opened bug 1792683 to followup on this.
Do you mean to add an assertion against aGlobal?

Yes, but checks if the global is not devtools loader's one if it's already instantiated.

Also, JSActor class doesn't necessarily know the current global.
So I think it should be placed at the consumer of JSActor, maybe around the following:

https://searchfox.org/mozilla-central/rev/d45dd05bf412e7468b3770a52519e9d546d6325c/dom/ipc/jsactor/JSActorManager.cpp#23-25

Ahh ok. But then, we can't add such assertion.

DevTools do use getActor extensively, and will do so from the DevTools ESMs.
For now JSWindow Actors are buggy in the browser toolbox and setting breakpoint in them can easily break/freeze the browser toolbox.
That's because they aren't running in the CommonJS DevTools loader and so aren't running in the invisibleToDebugger global.
Bug 1792683 will actually finally be able to fix that by loading the ESM in the DevTools loader.

In bug 1792683, I was considering introducing a new flag in ChromeUtils.registerWindowActor / WindowActorOptions in order to force loading a particular JSWindowActor into the DevTools loader.
But with your suggested assertion... that makes me wonder if we can actually make that contextual based on getActor() callsite.
That would be even easier!

So. No need of an assertion in Sandbox, nor here in getActor. In bug 1792683, I'll make either make it contextual, or allow to load in a precise module loader based on WindowActorOptions. In any case getActor will be called from the two loaders.
Does that mean I am go to go with this patch?

But I will at least wait for bug 1792404 to land and rebase to support the devtools loader in import stack.

Okay, then I think the current patch is ready, once it's rebased onto bug 1792404.

Attachment #9295221 - Attachment description: Bug 1790383 - [devtools] Allow loader ESMs in a distinct loader specific to DevTools. → Bug 1790383 - [devtools] Allow loading ESMs in a distinct loader specific to DevTools.
Attachment #9295221 - Attachment description: Bug 1790383 - [devtools] Allow loading ESMs in a distinct loader specific to DevTools. → Bug 1790383 - [devtools] Allow loader ESMs in a distinct loader specific to DevTools.
Attachment #9295221 - Attachment description: Bug 1790383 - [devtools] Allow loader ESMs in a distinct loader specific to DevTools. → Bug 1790383 - [devtools] Allow loading ESMs in a distinct loader specific to DevTools.
Pushed by apoirot@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/12df9b383a32 [devtools] Allow loading ESMs in a distinct loader specific to DevTools. r=arai
Status: NEW → RESOLVED
Closed: 3 years ago
Resolution: --- → FIXED
Target Milestone: --- → 107 Branch
Regressions: 1793602
See Also: → 1837394
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: