diff --git a/pkg/dev_compiler/lib/js/legacy/dart_library.js b/pkg/dev_compiler/lib/js/legacy/dart_library.js index feb748c7832..321f4fe97b7 100644 --- a/pkg/dev_compiler/lib/js/legacy/dart_library.js +++ b/pkg/dev_compiler/lib/js/legacy/dart_library.js @@ -28,23 +28,55 @@ if (!dart_library) { dart_library.libraryImports = libraryImports; const _metrics = Symbol('metrics'); - const _logMetrics = false; // Returns a map from module name to various metrics for module. - function metrics() { + function moduleMetrics() { const map = {}; const keys = Array.from(_libraries.keys()); for (const key of keys) { const lib = _libraries.get(key); - map[lib._name] = lib._library[_metrics]; + map[lib._name] = lib.firstLibraryValue[_metrics]; } return map; } - dart_library.metrics = metrics; + dart_library.moduleMetrics = moduleMetrics; + + // Returns an application level overview of the module metrics. + function appMetrics() { + const metrics = moduleMetrics(); + let dartSize = 0; + let jsSize = 0; + let sourceMapSize = 0; + let evaluatedModules = 0; + const keys = Array.from(_libraries.keys()); + + let firstLoadStart = Number.MAX_VALUE; + let lastLoadEnd = Number.MIN_VALUE; + + for (const module of keys) { + let data = metrics[module]; + if (data != null) { + evaluatedModules++; + dartSize += data.dartSize; + jsSize += data.jsSize; + sourceMapSize += data.sourceMapSize; + firstLoadStart = Math.min(firstLoadStart, data.loadStart); + lastLoadEnd = Math.max(lastLoadEnd, data.loadEnd); + } + } + return { + 'dartSize': dartSize, + 'jsSize': jsSize, + 'sourceMapSize': sourceMapSize, + 'evaluatedModules': evaluatedModules, + 'loadTimeMs': lastLoadEnd - firstLoadStart + }; + } + dart_library.appMetrics = appMetrics; function _sortFn(key1, key2) { - const t1 = _libraries.get(key1)._library[_metrics].loadTime; - const t2 = _libraries.get(key2)._library[_metrics].loadTime; + const t1 = _libraries.get(key1).firstLibraryValue[_metrics].loadStart; + const t2 = _libraries.get(key2).firstLibraryValue[_metrics].loadStart; return t1 - t2; } @@ -52,18 +84,19 @@ if (!dart_library) { // in CSV format. function metricsCsv() { let buffer = - 'Module, JS Size, Dart Size, Load Time, Cumulative JS Size\n'; + 'Module, JS Size, Dart Size, Load Start, Load End, Cumulative JS Size\n'; const keys = Array.from(_libraries.keys()); keys.sort(_sortFn); let cumulativeJsSize = 0; for (const key of keys) { const lib = _libraries.get(key); - const jsSize = lib._library[_metrics].jsSize; + const jsSize = lib.firstLibraryValue[_metrics].jsSize; cumulativeJsSize += jsSize; - const dartSize = lib._library[_metrics].dartSize; - const loadTime = lib._library[_metrics].loadTime; + const dartSize = lib.firstLibraryValue[_metrics].dartSize; + const loadStart = lib.firstLibraryValue[_metrics].loadStart; + const loadEnd = lib.firstLibraryValue[_metrics].loadEnd; buffer += '"' + lib._name + '", ' + jsSize + ', ' + dartSize + ', ' + - loadTime + ', ' + cumulativeJsSize + '\n'; + loadStart + ', ' + loadEnd + ', ' + cumulativeJsSize + '\n'; } return buffer; } @@ -103,11 +136,32 @@ if (!dart_library) { let _reverseImports = new Map(); - // Set of libraries that were not only loaded on the page but also executed. - let _executedLibraries = new Set(); + // App name to set of libraries that were not only loaded on the page but + // also executed. + const _executedLibraries = new Map(); + dart_library.executedLibraryCount = function() { + let count = 0; + _executedLibraries.forEach(function(executedLibraries, _) { + count += executedLibraries.size; + }); + return count; + }; + + // Library instance that is going to be loaded or has been loaded. + class LibraryInstance { + constructor(libraryValue) { + this.libraryValue = libraryValue; + // Cyclic import detection + this.loadingState = LibraryLoader.NOT_LOADED; + } + + get isNotLoaded() { + return this.loadingState == LibraryLoader.NOT_LOADED; + } + } class LibraryLoader { - constructor(name, defaultValue, imports, loader, data) { + constructor(name, defaultLibraryValue, imports, loader, data) { imports.forEach(function(i) { let deps = _reverseImports.get(i); if (!deps) { @@ -117,41 +171,71 @@ if (!dart_library) { deps.add(name); }); this._name = name; - this._library = defaultValue ? defaultValue : {}; + this._defaultLibraryValue = + defaultLibraryValue ? defaultLibraryValue : {}; this._imports = imports; this._loader = loader; data.jsSize = loader.toString().length; - data.loadTime = Infinity; + data.loadStart = NaN; + data.loadEnd = NaN; this._metrics = data; - // Cyclic import detection - this._state = LibraryLoader.NOT_LOADED; + // First loaded instance for supporting logic that assumes there is only + // one app. + // TODO(b/204209941): Remove _firstLibraryInstance after debugger and + // metrics support multiple apps. + this._firstLibraryInstance = + new LibraryInstance(this._deepCopyDefaultValue()); + this._firstLibraryInstanceUsed = false; + + // App name to instance map. + this._instanceMap = new Map(); } - loadImports() { - let results = []; - for (let name of this._imports) { - results.push(import_(name)); + /// First loaded value for supporting logic that assumes there is only + /// one app. + get firstLibraryValue() { + return this._firstLibraryInstance.libraryValue; + } + + /// The loaded instance value for the given `appName`. + libraryValueInApp(appName) { + return this._instanceMap.get(appName).libraryValue; + } + + load(appName) { + let instance = this._instanceMap.get(appName); + if (!instance && !this._firstLibraryInstanceUsed) { + // If `_firstLibraryInstance` is already assigned to an app, creates a + // new instance clone (with deep copy) and assigns it the given app. + // Otherwise, reuse `_firstLibraryInstance`. + instance = this._firstLibraryInstance; + this._firstLibraryInstanceUsed = true; + this._instanceMap.set(appName, instance); + } + if (!instance) { + instance = new LibraryInstance(this._deepCopyDefaultValue()); + this._instanceMap.set(appName, instance); } - return results; - } - load() { // Check for cycles - if (this._state == LibraryLoader.LOADING) { + if (instance.loadingState == LibraryLoader.LOADING) { throwLibraryError('Circular dependence on library: ' + this._name); - } else if (this._state >= LibraryLoader.READY) { - return this._library; + } else if (instance.loadingState >= LibraryLoader.READY) { + return instance.libraryValue; } - _executedLibraries.add(this._name); - this._state = LibraryLoader.LOADING; + if (!_executedLibraries.has(appName)) { + _executedLibraries.set(appName, new Set()); + } + _executedLibraries.get(appName).add(this._name); + instance.loadingState = LibraryLoader.LOADING; // Handle imports - let args = this.loadImports(); + let args = this._loadImports(appName); // Load the library let loader = this; - let library = this._library; + let library = instance.libraryValue; library[libraryImports] = this._imports; library[loadedModule] = library; @@ -160,40 +244,50 @@ if (!dart_library) { if (this._name == 'dart_sdk') { // Eagerly load the SDK. - if (!!self.performance) { - library[_metrics].loadTime = self.performance.now(); + if (!!self.performance && !!self.performance.now) { + library[_metrics].loadStart = self.performance.now(); } - if (_logMetrics) console.time('Load ' + this._name); this._loader.apply(null, args); - if (_logMetrics) console.timeEnd('Load ' + this._name); + if (!!self.performance && !!self.performance.now) { + library[_metrics].loadEnd = self.performance.now(); + } } else { // Load / parse other modules on demand. let done = false; - this._library = new Proxy(library, { + instance.libraryValue = new Proxy(library, { get: function(o, name) { if (name == _metrics) { return o[name]; } if (!done) { done = true; - if (!!self.performance) { - library[_metrics].loadTime = self.performance.now(); + if (!!self.performance && !!self.performance.now) { + library[_metrics].loadStart = self.performance.now(); } - if (_logMetrics) console.time('Load ' + loader._name); loader._loader.apply(null, args); - if (_logMetrics) console.timeEnd('Load ' + loader._name); + if (!!self.performance && !!self.performance.now) { + library[_metrics].loadEnd = self.performance.now(); + } } return o[name]; } }); } - this._state = LibraryLoader.READY; - return this._library; + instance.loadingState = LibraryLoader.READY; + return instance.libraryValue; } - stub() { - return this._library; + _loadImports(appName) { + let results = []; + for (let name of this._imports) { + results.push(import_(name, appName)); + } + return results; + } + + _deepCopyDefaultValue() { + return JSON.parse(JSON.stringify(this._defaultLibraryValue)); } } LibraryLoader.NOT_LOADED = 0; @@ -208,7 +302,7 @@ if (!dart_library) { dart_library.debuggerLibraries = function() { let debuggerLibraries = []; _libraries.forEach(function(value, key, map) { - debuggerLibraries.push(value.load()); + debuggerLibraries.push(value.load(_firstStartedAppName)); }); debuggerLibraries.__proto__ = null; return debuggerLibraries; @@ -217,21 +311,24 @@ if (!dart_library) { // Invalidate a library and all things that depend on it function _invalidateLibrary(name) { let lib = _libraries.get(name); - if (lib._state == LibraryLoader.NOT_LOADED) return; - lib._state = LibraryLoader.NOT_LOADED; - lib._library = {}; + if (lib._instanceMap.size === 0) return; + lib._firstLibraryInstance = + new LibraryInstance(lib._deepCopyDefaultValue()); + lib._firstLibraryInstanceUsed = false; + lib._instanceMap.clear(); let deps = _reverseImports.get(name); if (!deps) return; deps.forEach(_invalidateLibrary); } - function library(name, defaultValue, imports, loader, data = {}) { + function library(name, defaultLibraryValue, imports, loader, data = {}) { let result = _libraries.get(name); if (result) { console.log('Re-loading ' + name); _invalidateLibrary(name); } - result = new LibraryLoader(name, defaultValue, imports, loader, data); + result = + new LibraryLoader(name, defaultLibraryValue, imports, loader, data); _libraries.set(name, result); return result; } @@ -240,20 +337,33 @@ if (!dart_library) { // Store executed modules upon reload. if (!!self.addEventListener && !!self.localStorage) { self.addEventListener('beforeunload', function(event) { - let libraryCache = { - 'time': new Date().getTime(), - 'modules': Array.from(_executedLibraries.keys()) - }; - self.localStorage.setItem( - 'dartLibraryCache', JSON.stringify(libraryCache)); + _nameToApp.forEach(function(_, appName) { + if (!_executedLibraries.get(appName)) { + return; + } + let libraryCache = { + 'time': new Date().getTime(), + 'modules': Array.from(_executedLibraries.get(appName).keys()), + }; + self.localStorage.setItem( + `dartLibraryCache:${appName}`, JSON.stringify(libraryCache)); + }); }); } - // Map from module name to corresponding proxy library. + // Map from module name to corresponding app to proxy library map. let _proxyLibs = new Map(); - function import_(name) { - let proxy = _proxyLibs.get(name); + function import_(name, appName) { + // For backward compatibility. + if (!appName && _lastStartedSubapp) { + appName = _lastStartedSubapp.appName; + } + + let proxy; + if (_proxyLibs.has(name)) { + proxy = _proxyLibs.get(name).get(appName); + } if (proxy) return proxy; let proxyLib = new Proxy({}, { get: function(o, p) { @@ -266,9 +376,21 @@ if (!dart_library) { xhr.open('GET', sourceURL, false); xhr.withCredentials = true; xhr.send(); + // Add inline policy to make eval() call Trusted Types compatible + // when running in a TT compatible browser + let policy = { + createScript: function(script) { + return script; + } + }; + if (self.trustedTypes && self.trustedTypes.createPolicy) { + policy = self.trustedTypes.createPolicy( + 'dartDdcModuleLoading#dart_library', policy); + } // Append sourceUrl so the resource shows up in the Chrome // console. - eval(xhr.responseText + '//@ sourceURL=' + sourceURL); + eval(policy.createScript( + xhr.responseText + '//@ sourceURL=' + sourceURL)); lib = _libraries.get(name); } } @@ -277,10 +399,13 @@ if (!dart_library) { } // Always load the library before accessing a property as it may have // been invalidated. - return lib.load()[p]; + return lib.load(appName)[p]; } }); - _proxyLibs.set(name, proxyLib); + if (!_proxyLibs.has(name)) { + _proxyLibs.set(name, new Map()); + } + _proxyLibs.get(name).set(appName, proxyLib); return proxyLib; } dart_library.import = import_; @@ -297,7 +422,12 @@ if (!dart_library) { let _debuggerInitialized = false; - // Called to initiate a hot restart of the application. + // Caches the last N runIds to prevent hot reload requests from the same + // runId from executing more than once. + const _hotRestartRunIdCache = new Array(); + + // Called to initiate a hot restart of the application for a given uuid. If + // it is not set, the last started application will be hot restarted. // // "Hot restart" means all application state is cleared, the newly compiled // modules are loaded, and `main()` is called. @@ -316,100 +446,190 @@ if (!dart_library) { // 3. Call dart:_runtime's `hotRestart()` function to clear any state that // `dartdevc` is tracking, such as initialized static fields and type // caches. - // 4. Call `window.$dartWarmReload()` (provided by the HTML page) to reload - // the relevant JS modules, passing a callback that will invoke `main()`. - // 5. `$dartWarmReload` calls the callback to rerun main. + // 4. Call `self.$dartReloadModifiedModules()` (provided by the HTML page) + // to reload the relevant JS modules, passing a callback that will invoke + // `main()`. + // 5. `$dartReloadModifiedModules` calls the callback to rerun main. // - function reload(clearState) { - // TODO(jmesserly): once we've rolled out `clearState` make it the - // default, and eventually remove the parameter. - if (clearState == null) clearState = true; - - - // TODO(jmesserly): we may want to change these APIs to use the - // "hot restart" terminology for consistency with Flutter. In Flutter, - // "hot reload" refers to keeping the application state and attempting to - // patch the code for the application while it is executing - // (https://flutter.io/hot-reload/), whereas "hot restart" refers to what - // dartdevc supports: tear down the app, update the code, and rerun the - // app. - if (!self || !self.$dartWarmReload) { + async function hotRestart(config) { + if (!self || !self.$dartReloadModifiedModules) { console.warn('Hot restart not supported in this environment.'); return; } - // Call the application's `onReloadStart()` function, if provided. - let result; - if (_lastLibrary && _lastLibrary.onReloadStart) { - result = _lastLibrary.onReloadStart(); + // If `config.runId` is set (e.g. a unique build ID that represent the + // current build and shared by multiple subapps), skip the following runs + // with the same id. + if (config && config.runId) { + if (_hotRestartRunIdCache.indexOf(config.runId) >= 0) { + // The run has already started (by other subapp or app) + return; + } + _hotRestartRunIdCache.push(config.runId); + + // Only cache the runIds for the last N runs. We assume that there are + // less than N requests with different runId can happen in a very short + // period of time (e.g. 1 second). + if (_hotRestartRunIdCache.length > 10) { + _hotRestartRunIdCache.shift(); + } } - let sdk = _libraries.get('dart_sdk'); + self.console.clear(); + const sdk = _libraries.get('dart_sdk'); - /// Once the `onReloadStart()` completes, this finishes the restart. - function finishHotRestart() { - self.console.clear(); - if (clearState) { - // This resets all initialized fields and clears type caches and other - // temporary data structures used by the compiler/SDK. - sdk._library.dart.hotRestart(); + // Finds out what apps and their subapps should be hot restarted in + // their starting order. + const dirtyAppNames = new Array(); + const dirtySubapps = new Array(); + if (config && config.runId) { + _nameToApp.forEach(function(app, appName) { + dirtySubapps.push(...app.uuidToSubapp.values()); + dirtyAppNames.push(appName); + }); + } else { + dirtySubapps.push(_lastStartedSubapp); + dirtyAppNames.push(_lastStartedSubapp.appName); + } + + // Invokes onReloadStart for each subapp in reversed starting order. + const onReloadStartPromises = new Array(); + for (const subapp of dirtySubapps.reverse()) { + // Call the application's `onReloadStart()` function, if provided. + if (subapp.library && subapp.library.onReloadStart) { + const result = subapp.library.onReloadStart(); + if (result && result.then) { + let resolve; + onReloadStartPromises.push(new Promise(function(res, _) { + resolve = res; + })); + const dart = sdk.libraryValueInApp(subapp.appName).dart; + result.then(dart.dynamic, function() { + resolve(); + }); + } } + } + // Reverse the subapps back to starting order. + dirtySubapps.reverse(); + + await Promise.all(onReloadStartPromises); + + // Invokes SDK `hotRestart` to reset all initialized fields and clears + // type caches and other temporary data structures used by the + // compiler/SDK. + for (const appName of dirtyAppNames) { + sdk.libraryValueInApp(appName).dart.hotRestart(); + } + + // Starts the subapps in their starting order. + for (const subapp of dirtySubapps) { // Call the module loader to reload the necessary modules. - self.$dartWarmReload(() => { + self.$dartReloadModifiedModules(subapp.appName, function() { // Once the modules are loaded, rerun `main()`. - start(_lastModuleName, _lastLibraryName, true); + start( + subapp.appName, subapp.uuid, subapp.moduleName, + subapp.libraryName, true); }); } + } + dart_library.reload = hotRestart; - if (result && result.then) { - result.then(sdk._library.dart.Dynamic)(finishHotRestart); - } else { - finishHotRestart(); + /// An App contains one or multiple Subapps, all of the subapps share the + /// same memory copy of library instances, and as a result they share state + /// in Dart statics and top-level fields. There can be one or multiple Apps + /// in a browser window, all of the Apps are isolated from each other + /// (i.e. they create different instances even for the same module). + class App { + constructor(name) { + this.name = name; + + // Subapp's uuid to subapps in initial starting order. + // (ES6 preserves iteration order) + this.uuidToSubapp = new Map(); } } - dart_library.reload = reload; + class Subapp { + constructor(uuid, appName, moduleName, libraryName, library) { + this.uuid = uuid; + this.appName = appName; + this.moduleName = moduleName; + this.libraryName = libraryName; + this.library = library; - let _lastModuleName; - let _lastLibraryName; - let _lastLibrary; - let _originalBody; + this.originalBody = null; + } + } - function start(moduleName, libraryName, isReload) { + // App name to App map in initial starting order. + // (ES6 preserves iteration order) + const _nameToApp = new Map(); + let _firstStartedAppName; + let _lastStartedSubapp; + + /// Starts a subapp that is identified with `uuid`, `moduleName`, and + /// `libraryName` inside a parent app that is identified by `appName`. + function start(appName, uuid, moduleName, libraryName, isReload) { + console.info( + `DDC: Subapp Module [${appName}:${moduleName}:${uuid}] is starting`); if (libraryName == null) libraryName = moduleName; - _lastModuleName = moduleName; - _lastLibraryName = libraryName; - let library = import_(moduleName)[libraryName]; - _lastLibrary = library; - let dart_sdk = import_('dart_sdk'); + const library = import_(moduleName, appName)[libraryName]; + + let app = _nameToApp.get(appName); + if (!isReload) { + if (!app) { + app = new App(appName); + _nameToApp.set(appName, app); + } + + let subapp = app.uuidToSubapp.get(uuid); + if (!subapp) { + subapp = new Subapp(uuid, appName, moduleName, libraryName, library); + app.uuidToSubapp.set(uuid, subapp); + } + + _lastStartedSubapp = subapp; + if (!_firstStartedAppName) { + _firstStartedAppName = appName; + } + } + + const subapp = app.uuidToSubapp.get(uuid); + const sdk = import_('dart_sdk', appName); if (!_debuggerInitialized) { // This import is only needed for chrome debugging. We should provide an // option to compile without it. - dart_sdk._debugger.registerDevtoolsFormatter(); + sdk._debugger.registerDevtoolsFormatter(); // Create isolate. _debuggerInitialized = true; } if (isReload) { + // subapp may have been modified during reload, `subapp.library` needs + // to always point to the latest data. + subapp.library = library; + if (library.onReloadEnd) { library.onReloadEnd(); return; } else { if (!!self.document) { - // Note: we expect _originalBody to be undefined in non-browser + // Note: we expect originalBody to be undefined in non-browser // environments, but in that case so is the body. - if (!_originalBody && !!self.document.body) { + if (!subapp.originalBody && !!self.document.body) { self.console.warn('No body saved to update on reload'); } else { - self.document.body = _originalBody; + self.document.body = subapp.originalBody; } } } } else { - // If not a reload then store the initial html to reset it on reload. - if (!!self.document && !!self.document.body) { - _originalBody = self.document.body.cloneNode(true); + // If not a reload and `onReloadEnd` is not defined, store the initial + // html to reset it on reload. + if (!library.onReloadEnd && !!self.document && !!self.document.body) { + subapp.originalBody = self.document.body.cloneNode(true); } } library.main([]); diff --git a/pkg/dev_compiler/test/expression_compiler/expression_compiler_e2e_suite.dart b/pkg/dev_compiler/test/expression_compiler/expression_compiler_e2e_suite.dart index 811796a5801..9fd709cc24c 100644 --- a/pkg/dev_compiler/test/expression_compiler/expression_compiler_e2e_suite.dart +++ b/pkg/dev_compiler/test/expression_compiler/expression_compiler_e2e_suite.dart @@ -350,6 +350,8 @@ class TestDriver { var bootstrapFile = File(htmlBootstrapper.toFilePath())..createSync(); var moduleName = compiler.metadata!.name; var mainLibraryName = compiler.metadataForLibraryUri(input).name; + var appName = p.relative( + p.withoutExtension(compiler.metadataForLibraryUri(input).importUri)); switch (setup.moduleFormat) { case ModuleFormat.ddc: @@ -368,6 +370,9 @@ class TestDriver { var dartLibraryPath = escaped(p.join(ddcPath, 'lib', 'js', 'legacy', 'dart_library.js')); var outputPath = output.toFilePath(); + // This is used in the DDC module system for multiapp workflows and is + // stubbed here. + var uuid = '00000000-0000-0000-0000-000000000000'; bootstrapFile.writeAsStringSync(''' @@ -384,7 +389,8 @@ class TestDriver { sdk.dart.nonNullAsserts(true); sdk.dart.nativeNonNullAsserts(true); sdk._debugger.registerDevtoolsFormatter(); - dart_library.start('$moduleName', '$mainLibraryName'); + dart_library.start('$appName', '$uuid', '$moduleName', '$mainLibraryName', + false); '''); break; diff --git a/pkg/dev_compiler/tool/ddb b/pkg/dev_compiler/tool/ddb index f73026e8211..4c95bf42acf 100755 --- a/pkg/dev_compiler/tool/ddb +++ b/pkg/dev_compiler/tool/ddb @@ -136,6 +136,14 @@ void main(List args) async { var libname = js_names.pathToJSIdentifier(p.relative(p.withoutExtension(entry))); + // This is used in the DDC module system and usually corresponds to the + // entrypoint's path. + var appname = p.relative(p.withoutExtension(entry)); + + // This is used in the DDC module system for multiapp workflows and is + // stubbed in ddb. + var uuid = "00000000-0000-0000-0000-000000000000"; + // By default (no `-d`), we use the `dartdevc` binary on the user's path to // compute the SDK we use for execution. I.e., we assume that `dart` is // under `$DART_SDK/bin/dart` and use that to find `dartdevc` and related @@ -353,7 +361,7 @@ sdk.dart.nativeNonNullAsserts($nativeNonNullAsserts); // Invoke main through the d8 preamble to ensure the code is running // within the fake event loop. self.dartMainRunner(function () { - dart_library.start("$basename", "$libname"); + dart_library.start("$appname", "$uuid", "$basename", "$libname", false); }); '''; var dart2jsD8Preamble =