Skip to content

Commit 15ac2dc

Browse files
perf: name an async cycle by a group key instead of a peer list
Every member of a strongly-connected component carried the ids of all its peers, so a cycle of five emitted twenty id slots where five keys do. Each member now emits the component's smallest async member id and the runtime groups the modules under it, cutting the emitted cost of the cycle-error propagation by about 70%. Also stop emitting a blank line and redundant parentheses when output.strictModuleErrorHandling is off, so a default build is byte-identical.
1 parent 27d20c1 commit 15ac2dc

2 files changed

Lines changed: 51 additions & 34 deletions

File tree

‎lib/dependencies/HarmonyCompatibilityDependency.js‎

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,36 @@ makeSerializable(
4747
);
4848

4949
/**
50-
* Ids of the async modules sharing a strongly-connected component with `module`.
51-
* The spec records one `[[EvaluationError]]` for a whole component, so a member
52-
* that already fulfilled still has to report the root's error afterwards.
50+
* Key shared by the async modules of one strongly-connected component. The spec
51+
* records one `[[EvaluationError]]` per component, so a member that already
52+
* fulfilled still has to report the root's error afterwards.
5353
* @param {ModuleGraph} moduleGraph the module graph
5454
* @param {ChunkGraph} chunkGraph the chunk graph
5555
* @param {Module} module the async module
56-
* @returns {ModuleId[] | null} peer ids, or null when it is not in a cycle
56+
* @returns {ModuleId | null} the group key, or null when it is not in an async cycle
5757
*/
58-
const getAsyncCyclePeerIds = (moduleGraph, chunkGraph, module) => {
59-
const { getDeferredCycleModuleIds, getDeferredCycleModules } =
60-
getDeferredCycleHelpers();
58+
const getAsyncCycleGroupId = (moduleGraph, chunkGraph, module) => {
59+
const { getDeferredCycleModules } = getDeferredCycleHelpers();
6160
const peers = getDeferredCycleModules(moduleGraph, module);
6261
if (peers === null) return null;
63-
const asyncPeers = new Set();
62+
// The component's smallest async member id names the group, so every member
63+
// emits the same key and none has to carry the whole peer list.
64+
let groupId = null;
65+
let asyncPeerCount = 0;
6466
for (const peer of peers) {
65-
if (moduleGraph.isAsync(peer)) asyncPeers.add(peer);
67+
if (!moduleGraph.isAsync(peer)) continue;
68+
asyncPeerCount++;
69+
const id = chunkGraph.getModuleId(peer);
70+
if (id !== null && (groupId === null || `${id}` < `${groupId}`)) {
71+
groupId = id;
72+
}
73+
}
74+
if (asyncPeerCount === 0) return null;
75+
const ownId = chunkGraph.getModuleId(module);
76+
if (ownId !== null && (groupId === null || `${ownId}` < `${groupId}`)) {
77+
groupId = ownId;
6678
}
67-
if (asyncPeers.size === 0) return null;
68-
return getDeferredCycleModuleIds(asyncPeers, (m) =>
69-
chunkGraph.getModuleId(m)
70-
);
79+
return groupId;
7180
};
7281

7382
HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate extends (
@@ -133,15 +142,17 @@ HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate
133142
runtimeRequirements.add(RuntimeGlobals.module);
134143
// Only `strictModuleErrorHandling` keeps a recorded error for later
135144
// requires, so the component's peers are worth naming only then.
136-
const cyclePeers = runtimeTemplate.outputOptions.strictModuleErrorHandling
137-
? getAsyncCyclePeerIds(moduleGraph, chunkGraph, module)
145+
const cycleGroupId = runtimeTemplate.outputOptions
146+
.strictModuleErrorHandling
147+
? getAsyncCycleGroupId(moduleGraph, chunkGraph, module)
138148
: null;
139149
const isAsyncBody = /** @type {BuildMeta} */ (module.buildMeta).async;
140-
const hasAwait = cyclePeers
141-
? `, ${isAsyncBody ? 1 : 0}, ${JSON.stringify(cyclePeers)}`
142-
: isAsyncBody
143-
? ", 1"
144-
: "";
150+
const hasAwait =
151+
cycleGroupId === null
152+
? isAsyncBody
153+
? ", 1"
154+
: ""
155+
: `, ${isAsyncBody ? 1 : 0}, ${JSON.stringify(cycleGroupId)}`;
145156
// A module-scope `await using` is disposed when its scope ends, so the body
146157
// needs a block that closes before the module reports completion —
147158
// otherwise dependents run before disposal is awaited.

‎lib/runtime/AsyncModuleRuntimeModule.js‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ class AsyncModuleRuntimeModule extends HelperRuntimeModule {
3333
const fn = RuntimeGlobals.asyncModule;
3434
const defer = this._deferInterop;
3535
const strictErrors = this._strictModuleErrorHandling;
36+
const rejectCall = "reject(promise[webpackError] = err)";
37+
const rejectExpression = strictErrors
38+
? `(markCycle(err), ${rejectCall})`
39+
: rejectCall;
3640
const cst = runtimeTemplate.renderConst();
3741
const lt = runtimeTemplate.renderLet();
3842
const supportsSymbol = runtimeTemplate.supportsSymbol();
@@ -50,6 +54,7 @@ class AsyncModuleRuntimeModule extends HelperRuntimeModule {
5054
defer ? `${RuntimeGlobals.asyncModuleExportSymbol}= ` : ""
5155
}${renderSymbol("webpack exports", RuntimeGlobals.exports)};`,
5256
`${cst} webpackError = ${renderSymbol("webpack error", "__webpack_error__")};`,
57+
...(strictErrors ? [`${cst} cycleGroups = {};`] : []),
5358
defer
5459
? Template.asString([
5560
`${cst} webpackDone = ${RuntimeGlobals.asyncModuleDoneSymbol} = ${renderSymbol("webpack done", "__webpack_done__")};`,
@@ -154,7 +159,7 @@ class AsyncModuleRuntimeModule extends HelperRuntimeModule {
154159
)};`,
155160
`${fn} = ${runtimeTemplate.basicFunction(
156161
strictErrors
157-
? "module, body, hasAwait, cyclePeers"
162+
? "module, body, hasAwait, cycleGroup"
158163
: "module, body, hasAwait",
159164
[
160165
`${lt} queue;`,
@@ -208,20 +213,21 @@ class AsyncModuleRuntimeModule extends HelperRuntimeModule {
208213
])}`,
209214
// One `[[EvaluationError]]` is recorded for a whole strongly-connected
210215
// component, so a peer that already fulfilled must report it too.
211-
strictErrors
212-
? `${cst} markCycle = ${runtimeTemplate.expressionFunction(
213-
`cyclePeers && cyclePeers.forEach(${runtimeTemplate.basicFunction(
214-
"id",
215-
[
216-
`${cst} peer = __webpack_module_cache__[id];`,
217-
"if (peer && peer.error === undefined) peer.error = err;"
218-
]
219-
)})`,
220-
"err"
221-
)};`
222-
: "",
216+
...(strictErrors
217+
? [
218+
`${cst} group = cycleGroup === undefined ? undefined : (cycleGroups[cycleGroup] = cycleGroups[cycleGroup] || []);`,
219+
"group && group.push(module);",
220+
`${cst} markCycle = ${runtimeTemplate.expressionFunction(
221+
`group && group.forEach(${runtimeTemplate.expressionFunction(
222+
"peer.error === undefined && (peer.error = err)",
223+
"peer"
224+
)})`,
225+
"err"
226+
)};`
227+
]
228+
: []),
223229
`${cst} done = ${runtimeTemplate.expressionFunction(
224-
`(err ? (${strictErrors ? "markCycle(err), " : ""}reject(promise[webpackError] = err)) : outerResolve(exports)), resolveQueue(queue)${
230+
`(err ? ${rejectExpression} : outerResolve(exports)), resolveQueue(queue)${
225231
defer
226232
? ", promise[webpackDone] = true, module.evaluatingAsync = false"
227233
: ""

0 commit comments

Comments
 (0)