[dart2js] Add dartDeferredLibraryMultiLoader API to js_helper.

This will allow users who are implementing their own deferred loading mechanism
to batch all the loads for a specific library. Today in order to do batching users have to gather the URIs passed to the `dartDeferedLibraryLoader` hook in a list and schedule (e.g. via setTimeout) a load for some future task.

The need to schedule a task has been shown to cause delays in IPL. But loading each part file individually can also be very expensive. So this allows for a compromise where bundling can be done synchronously per loaded library.

Adds ~8kB to unminified main files and ~2.5kB to minified main files.

Bug: https://github.com/dart-lang/sdk/issues/54202
Change-Id: I623643b03920988cda8b0f8b297944be35ffa606
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/340480
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Commit-Queue: Nate Biggs <natebiggs@google.com>
This commit is contained in:
Nate Biggs 2023-12-08 21:10:48 +00:00 committed by Commit Queue
parent d2e2ff9b4b
commit f057c39255
3 changed files with 319 additions and 8 deletions

View file

@ -35,6 +35,14 @@ const String HOOKS_API_USAGE = """
// this uri being loaded. The loadPriority argument is the priority the
// library should be loaded with as specified in the code via the
// load-priority annotation (0: normal, 1: high).
// dartDeferredLibraryMultiLoader(uris, successCallback, errorCallback, loadId, loadPriority):
// if this function is defined, it will be called when a deferred library
// is loaded. It should load and eval the javascript of every URI in `uris`,
// and call successCallback. If it fails to do so, it should call
// errorCallback with an error. The loadId argument is the deferred import
// that resulted in this uri being loaded. The loadPriority argument is the
// priority the library should be loaded with as specified in the code via
// the load-priority annotation (0: normal, 1: high).
//
// dartCallInstrumentation(id, qualifiedName):
// if this function is defined, it will be called at each entry of a

View file

@ -2941,6 +2941,29 @@ Future<Null> loadDeferredLibrary(String loadId, int priority) {
}
}
void finalizeLoad() {
initializeSomeLoadedHunks();
// At this point all hunks have been loaded, so there should be no pending
// initializations to do.
assert(nextHunkToInitialize == total);
bool updated = _loadedLibraries.add(loadId);
if (updated && deferredLoadHook != null) {
deferredLoadHook!();
}
}
var deferredLibraryMultiLoader =
JS('', 'self.dartDeferredLibraryMultiLoader');
if (JS('bool', 'typeof # === "function"', deferredLibraryMultiLoader)) {
return _loadAllHunks(
deferredLibraryMultiLoader, uris, hashes, loadId, priority)
.then((_) {
waitingForLoad = List.filled(total, false);
finalizeLoad();
});
}
Future loadAndInitialize(int i) {
final hash = hashes[i];
if (JS('bool', '#(#)', isHunkLoaded, hash)) {
@ -2954,14 +2977,7 @@ Future<Null> loadDeferredLibrary(String loadId, int priority) {
}
return Future.wait(new List.generate(total, loadAndInitialize)).then((_) {
initializeSomeLoadedHunks();
// At this point all hunks have been loaded, so there should be no pending
// initializations to do.
assert(nextHunkToInitialize == total);
bool updated = _loadedLibraries.add(loadId);
if (updated && deferredLoadHook != null) {
deferredLoadHook!();
}
finalizeLoad();
});
}
@ -3103,6 +3119,95 @@ String _computeThisScriptFromTrace() {
throw new UnsupportedError('Cannot extract URI from "$stack"');
}
Future _loadAllHunks(Object loader, List<String> hunkNames, List<String> hashes,
String loadId, int priority) {
var initializationEventLog = JS_EMBEDDED_GLOBAL('', INITIALIZATION_EVENT_LOG);
var isHunkLoaded = JS_EMBEDDED_GLOBAL('', IS_HUNK_LOADED);
_addEvent(part: hunkNames.join(';'), event: 'startLoad', loadId: loadId);
List<String> hunksToLoad = [];
List<String> urisToLoad = [];
List<String> hashesToLoad = [];
List<Future> pendingLoads = [];
for (int i = 0; i < hunkNames.length; ++i) {
final hunkName = hunkNames[i];
final hash = hashes[i];
if (JS('bool', '!#(#)', isHunkLoaded, hash)) {
final completerForHunk = _loadingLibraries[hunkName];
if (completerForHunk != null) {
pendingLoads.add(completerForHunk.future);
_addEvent(part: hunkName, event: 'reuse', loadId: loadId);
} else {
hunksToLoad.add(hunkName);
hashesToLoad.add(hash);
Object trustedScriptUri = _getBasedScriptUrl(hunkName, '');
// [trustedScriptUri] is either a String, in which case `toString()` is
// an identity function, or it is a TrustedScriptURL and `toString()`
// returns the sanitized URL.
urisToLoad.add(JS('', '#.toString()', trustedScriptUri));
}
}
}
if (hunksToLoad.isEmpty) {
return Future.wait(pendingLoads);
}
final loadedHunksString = hunksToLoad.join(';');
Completer<Null> completer = Completer();
hunksToLoad.forEach((hunkName) => _loadingLibraries[hunkName] = completer);
_addEvent(part: loadedHunksString, event: 'downloadMulti', loadId: loadId);
void failure(error, String context, StackTrace? stackTrace) {
_addEvent(
part: loadedHunksString, event: 'downloadFailure', loadId: loadId);
hunksToLoad.forEach((hunkName) => _loadingLibraries[hunkName] = null);
stackTrace ??= StackTrace.current;
completer.completeError(
DeferredLoadException('Loading $loadedHunksString failed: $error\n'
'Context: $context\n'
'event log:\n${_getEventLog()}\n'),
stackTrace);
}
void success() {
List<String> missingHunks = [];
for (int i = 0; i < hashesToLoad.length; i++) {
bool isLoaded = JS('bool', '#(#)', isHunkLoaded, hashesToLoad[i]);
if (!isLoaded) missingHunks.add(hunksToLoad[i]);
}
if (missingHunks.isEmpty) {
_addEvent(
part: loadedHunksString, event: 'downloadSuccess', loadId: loadId);
completer.complete(null);
} else {
failure(
'Success callback invoked but parts ${missingHunks.join(';')} not '
'loaded.',
'',
null);
}
}
var jsSuccess = convertDartClosureToJS(success, 0);
var jsFailure = convertDartClosureToJS((error) {
failure(unwrapException(error), 'js-failure-wrapper',
getTraceFromException(error));
}, 1);
try {
JS('void', '#(#, #, #, #, #)', loader, urisToLoad, jsSuccess, jsFailure,
loadId, priority);
} catch (error, stackTrace) {
failure(error, "invoking dartDeferredLibraryMultiLoader hook", stackTrace);
}
return Future.wait([...pendingLoads, completer.future]);
}
Future<Null> _loadHunk(
String hunkName, String loadId, int priority, String hash, int retryCount) {
const int maxRetries = 3;

View file

@ -0,0 +1,198 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// This test creates a scenario to simulate what happens if hunks are loaded
/// out of order and library loads are interleaved. The compiler should
/// initialize hunks in order and should only request each part file once
/// waiting on an in flight requests to resolve shared part files.
///
/// To create a good number of hunks we created an import graph with 3 deferred
/// imports and 7 libraries, we made pair-wise dependencies to be able to create
/// 2^3 (8) partitions of the program (including the main hunk) that end up
/// corresponding to the libraries themselves. In particular, the import graph
/// looks like this:
///
/// main ---> 1, 2, 3 (deferred)
/// 1 ---> 4, 5, 7
/// 2 ---> 5, 6, 7
/// 3 ---> 4, 6, 7
///
/// So each library maps to a deferred hunk:
/// library 1 = hunk of code only used by 1
/// library 2 = hunk of code only used by 2
/// library 3 = hunk of code only used by 3
/// library 4 = hunk of code shared by 1 & 3
/// library 5 = hunk of code shared by 1 & 2
/// library 6 = hunk of code shared by 2 & 3
/// library 7 = hunk of shared by 1, 2 & 3
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
import 'dart:async';
import 'dart:_foreign_helper' show JS;
import 'load_in_correct_order_lib1.dart' deferred as d1;
import 'load_in_correct_order_lib2.dart' deferred as d2;
import 'load_in_correct_order_lib3.dart' deferred as d3;
// Load the same library three times to ensure we have coverage of the path
// where all parts are 1) in-flight and 2) already loaded.
import 'load_in_correct_order_lib3.dart' deferred as d4;
import 'load_in_correct_order_lib3.dart' deferred as d5;
main() {
asyncStart();
runTest().then((_) => asyncEnd());
}
runTest() async {
setup();
final loadD1 = d1.loadLibrary();
final loadD2 = d2.loadLibrary();
final loadD3 = d3.loadLibrary();
// Now that d3 is loading, load d4 to ensure we cover the case where all parts
// are already loading.
final loadD4 = d4.loadLibrary();
await Future.wait([loadD1, loadD2, loadD3, loadD4]);
// Now that d3 and d4 are loaded, load d5 to ensure we cover the case where
// all parts are already loaded.
await d5.loadLibrary();
Expect.equals(499, d1.c1.a.value);
Expect.equals(500, d2.c2.c.value);
Expect.equals(501, d3.c3.f.value);
Expect.equals(501, d4.c3.f.value);
Expect.equals(501, d5.c3.f.value);
}
void setup() {
JS('', r"""
(function() {
// In d8 we don't have any way to load the content of the file via XHR, but we
// can use the "load" instruction. A hook is already defined in d8 for this
// reason.
self.isD8 = !!self.dartDeferredLibraryLoader;
// This test has 3 loadLibrary calls, this array contains how many hunks will be
// loaded by each call.
self.filesPerLoadLibraryCall = null;
// Number of parts for which a download has been "started" keyed by load ID.
self.incrementCounts = {};
// Number of parts for which we have loaded the JS keyed by load ID.
self.loadedCounts = {};
// Success callback for a library load keyed by load ID.
self.successCallbacks = {};
// URIs passed to the deferred load hook. Used to check there aren't duplicates.
self.providedUris = [];
// JS contents per URI.
self.content = {};
function equal(a, b) {
return a.length === b.length &&
a.every(function (value, index) {
return value === b[index];
});
}
self.initFilesPerLoadLibraryCall = function() {
// We assume we load d1, then d2, then d3. However, we may have integer load
// ids instead of the full load id.
var loadOrder = Object.keys(init.deferredLibraryParts);
var expectedLoadOrder = equal(loadOrder, ['d1', 'd2', 'd3', 'd4', 'd5']) ||
equal(loadOrder, ['1', '2', '3', '4', '5']);
if (!expectedLoadOrder) {
throw 'Unexpected load order ' + loadOrder;
}
var uniques = {};
self.filesPerLoadLibraryCall = {};
for (var i = 0; i < loadOrder.length; i++) {
var filesToLoad = 0;
var parts = init.deferredLibraryParts[loadOrder[i]];
for (var j = 0; j < parts.length; j++) {
if (!uniques.hasOwnProperty(parts[j])) {
uniques[parts[j]] = true;
filesToLoad++;
}
}
self.filesPerLoadLibraryCall[loadOrder[i]] = filesToLoad;
}
};
// Download uri via an XHR
self.download = function(uris, uri, loadId) {
var req = new XMLHttpRequest();
req.addEventListener("load", function() {
self.content[uri] = this.responseText;
self.increment(uris, loadId);
});
req.open("GET", uri);
req.send();
};
// Note that a new hunk is already available to be loaded, wait until all
// expected hunks are available and then evaluate their contents to actually
// load them.
self.increment = function(uris, loadId) {
var count = self.incrementCounts[loadId] = self.incrementCounts[loadId] + 1;
if (count == self.filesPerLoadLibraryCall[loadId]) {
self.doActualLoads(uris, loadId);
}
};
// Hook to control how we load hunks in bulk (we force them to be out of order).
self.dartDeferredLibraryMultiLoader = function(uris, success, error, loadId) {
if (self.filesPerLoadLibraryCall == null) {
self.initFilesPerLoadLibraryCall();
}
self.incrementCounts[loadId] = 0;
self.loadedCounts[loadId] = 0;
self.successCallbacks[loadId] = success;
for (var i = 0; i < uris.length; i++) {
var uri = uris[i];
if (providedUris.some((u) => uri === u)) {
throw 'Requested duplicate uri: ' + uri;
}
providedUris.push(uri);
if (isD8) {
self.increment(uris, loadId);
} else {
self.download(uris, uri, loadId);
}
}
};
// Do the actual load of the hunk and call the corresponding success callback if
// all loads are complete for the provided load ID.
self.doLoad = function(uris, i, loadId) {
self.setTimeout(function () {
var uri = uris[i];
if (self.isD8) {
load(uri);
} else {
eval(self.content[uri]);
}
var loadCount = self.loadedCounts[loadId] + 1;
self.loadedCounts[loadId] = loadCount;
var filesToLoad = self.filesPerLoadLibraryCall[loadId];
if (loadCount == filesToLoad) {
(self.successCallbacks[loadId])();
}
}, 0);
};
// Do all the loads for a load library call. On the first load library call,
// purposely load the hunks out of order.
self.doActualLoads = function(uris, loadId) {
var total = self.incrementCounts[loadId];
if (total >= 1) {
// Load out of order, last first.
self.doLoad(uris, total - 1, loadId);
for (var i = 0; i < total - 1; i++) {
self.doLoad(uris, i, loadId);
}
}
};
})()
""");
}