Skip to content

Commit 8c15e59

Browse files
avoiding top level await circular (#5843)
* avoiding top level await circular * tweak logic * remove stack and tweak variables name * improve performance * Simplify the isFollowingTopLevelAwait logic --------- Co-authored-by: Lukas Taegert-Atkinson <[email protected]>
1 parent 2212897 commit 8c15e59

33 files changed

+350
-15
lines changed

src/Module.ts

+4
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export default class Module {
240240
shebang: undefined | string;
241241
readonly importers: string[] = [];
242242
readonly includedDynamicImporters: Module[] = [];
243+
readonly includedDirectTopLevelAwaitingDynamicImporters = new Set<Module>();
243244
readonly includedImports = new Set<Variable>();
244245
readonly info: ModuleInfo;
245246
isExecuted = false;
@@ -1365,6 +1366,9 @@ export default class Module {
13651366
if (resolution instanceof Module) {
13661367
if (!resolution.includedDynamicImporters.includes(this)) {
13671368
resolution.includedDynamicImporters.push(this);
1369+
if (node.isFollowingTopLevelAwait) {
1370+
resolution.includedDirectTopLevelAwaitingDynamicImporters.add(this);
1371+
}
13681372
}
13691373

13701374
const importedNames = this.options.treeshake

src/ast/nodes/AwaitExpression.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@ import type { InclusionContext } from '../ExecutionContext';
22
import type { ObjectPath } from '../utils/PathTracker';
33
import ArrowFunctionExpression from './ArrowFunctionExpression';
44
import type * as NodeType from './NodeType';
5+
import { Flag, isFlagSet, setFlag } from './shared/BitFlags';
56
import FunctionNode from './shared/FunctionNode';
67
import { type ExpressionNode, type IncludeChildren, type Node, NodeBase } from './shared/Node';
78

89
export default class AwaitExpression extends NodeBase {
910
declare argument: ExpressionNode;
1011
declare type: NodeType.tAwaitExpression;
1112

13+
get isTopLevelAwait(): boolean {
14+
return isFlagSet(this.flags, Flag.isTopLevelAwait);
15+
}
16+
set isTopLevelAwait(value: boolean) {
17+
this.flags = setFlag(this.flags, Flag.isTopLevelAwait, value);
18+
}
19+
1220
hasEffects(): boolean {
1321
if (!this.deoptimized) this.applyDeoptimizations();
1422
return true;
@@ -22,13 +30,14 @@ export default class AwaitExpression extends NodeBase {
2230
includeNode(context: InclusionContext) {
2331
this.included = true;
2432
if (!this.deoptimized) this.applyDeoptimizations();
25-
checkTopLevelAwait: if (!this.scope.context.usesTopLevelAwait) {
33+
checkTopLevelAwait: {
2634
let parent = this.parent;
2735
do {
2836
if (parent instanceof FunctionNode || parent instanceof ArrowFunctionExpression)
2937
break checkTopLevelAwait;
3038
} while ((parent = (parent as Node).parent as Node));
3139
this.scope.context.usesTopLevelAwait = true;
40+
this.isTopLevelAwait = true;
3241
}
3342
// Thenables need to be included
3443
this.argument.includePath(THEN_PATH, context);

src/ast/nodes/ImportExpression.ts

+4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export default class ImportExpression extends NodeBase {
5858
this.source.bind();
5959
}
6060

61+
get isFollowingTopLevelAwait() {
62+
return this.parent instanceof AwaitExpression && this.parent.isTopLevelAwait;
63+
}
64+
6165
/**
6266
* Get imported variables for deterministic usage, valid cases are:
6367
*

src/ast/nodes/shared/BitFlags.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export const enum Flag {
2525
expression = 1 << 23,
2626
destructuringDeoptimized = 1 << 24,
2727
hasDeoptimizedCache = 1 << 25,
28-
hasEffects = 1 << 26
28+
hasEffects = 1 << 26,
29+
isTopLevelAwait = 1 << 27
2930
}
3031

3132
export function isFlagSet(flags: number, flag: Flag): boolean {

src/utils/chunkAssignment.ts

+81-12
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ export function getChunkAssignments(
161161
allEntries,
162162
dependentEntriesByModule,
163163
dynamicallyDependentEntriesByDynamicEntry,
164-
dynamicImportsByEntry
164+
dynamicImportsByEntry,
165+
dynamicallyDependentEntriesByAwaitedDynamicEntry,
166+
awaitedDynamicImportsByEntry
165167
} = analyzeModuleGraph(entries);
166168

167169
// Each chunk is identified by its position in this array
@@ -177,8 +179,18 @@ export function getChunkAssignments(
177179
dynamicImportsByEntry,
178180
allEntries
179181
);
182+
const awaitedAlreadyLoadedAtomsByEntry = getAlreadyLoadedAtomsByEntry(
183+
staticDependencyAtomsByEntry,
184+
dynamicallyDependentEntriesByAwaitedDynamicEntry,
185+
awaitedDynamicImportsByEntry,
186+
allEntries
187+
);
180188
// This mutates the dependentEntries in chunkAtoms
181-
removeUnnecessaryDependentEntries(chunkAtoms, alreadyLoadedAtomsByEntry);
189+
removeUnnecessaryDependentEntries(
190+
chunkAtoms,
191+
alreadyLoadedAtomsByEntry,
192+
awaitedAlreadyLoadedAtomsByEntry
193+
);
182194
const { chunks, sideEffectAtoms, sizeByAtom } =
183195
getChunksWithSameDependentEntriesAndCorrelatedAtoms(
184196
chunkAtoms,
@@ -241,15 +253,21 @@ function analyzeModuleGraph(entries: Iterable<Module>): {
241253
dependentEntriesByModule: Map<Module, Set<number>>;
242254
dynamicImportsByEntry: readonly ReadonlySet<number>[];
243255
dynamicallyDependentEntriesByDynamicEntry: Map<number, Set<number>>;
256+
awaitedDynamicImportsByEntry: readonly ReadonlySet<number>[];
257+
dynamicallyDependentEntriesByAwaitedDynamicEntry: Map<number, Set<number>>;
244258
} {
245259
const dynamicEntryModules = new Set<Module>();
260+
const awaitedDynamicEntryModules = new Set<Module>();
246261
const dependentEntriesByModule = new Map<Module, Set<number>>();
247262
const allEntriesSet = new Set(entries);
248263
const dynamicImportModulesByEntry: Set<Module>[] = new Array(allEntriesSet.size);
264+
const awaitedDynamicImportModulesByEntry: Set<Module>[] = new Array(allEntriesSet.size);
249265
let entryIndex = 0;
250266
for (const currentEntry of allEntriesSet) {
251267
const dynamicImportsForCurrentEntry = new Set<Module>();
268+
const awaitedDynamicImportsForCurrentEntry = new Set<Module>();
252269
dynamicImportModulesByEntry[entryIndex] = dynamicImportsForCurrentEntry;
270+
awaitedDynamicImportModulesByEntry[entryIndex] = awaitedDynamicImportsForCurrentEntry;
253271
const modulesToHandle = new Set([currentEntry]);
254272
for (const module of modulesToHandle) {
255273
getOrCreate(dependentEntriesByModule, module, getNewSet<number>).add(entryIndex);
@@ -267,6 +285,10 @@ function analyzeModuleGraph(entries: Iterable<Module>): {
267285
dynamicEntryModules.add(resolution);
268286
allEntriesSet.add(resolution);
269287
dynamicImportsForCurrentEntry.add(resolution);
288+
if (resolution.includedDirectTopLevelAwaitingDynamicImporters.has(currentEntry)) {
289+
awaitedDynamicEntryModules.add(resolution);
290+
awaitedDynamicImportsForCurrentEntry.add(resolution);
291+
}
270292
}
271293
}
272294
for (const dependency of module.implicitlyLoadedBefore) {
@@ -279,18 +301,33 @@ function analyzeModuleGraph(entries: Iterable<Module>): {
279301
entryIndex++;
280302
}
281303
const allEntries = [...allEntriesSet];
282-
const { dynamicEntries, dynamicImportsByEntry } = getDynamicEntries(
304+
const {
305+
awaitedDynamicEntries,
306+
awaitedDynamicImportsByEntry,
307+
dynamicEntries,
308+
dynamicImportsByEntry
309+
} = getDynamicEntries(
283310
allEntries,
284311
dynamicEntryModules,
285-
dynamicImportModulesByEntry
312+
dynamicImportModulesByEntry,
313+
awaitedDynamicEntryModules,
314+
awaitedDynamicImportModulesByEntry
286315
);
287316
return {
288317
allEntries,
318+
awaitedDynamicImportsByEntry,
289319
dependentEntriesByModule,
320+
dynamicallyDependentEntriesByAwaitedDynamicEntry: getDynamicallyDependentEntriesByDynamicEntry(
321+
dependentEntriesByModule,
322+
awaitedDynamicEntries,
323+
allEntries,
324+
dynamicEntry => dynamicEntry.includedDirectTopLevelAwaitingDynamicImporters
325+
),
290326
dynamicallyDependentEntriesByDynamicEntry: getDynamicallyDependentEntriesByDynamicEntry(
291327
dependentEntriesByModule,
292328
dynamicEntries,
293-
allEntries
329+
allEntries,
330+
dynamicEntry => dynamicEntry.includedDynamicImporters
294331
),
295332
dynamicImportsByEntry
296333
};
@@ -299,16 +336,42 @@ function analyzeModuleGraph(entries: Iterable<Module>): {
299336
function getDynamicEntries(
300337
allEntries: Module[],
301338
dynamicEntryModules: Set<Module>,
302-
dynamicImportModulesByEntry: Set<Module>[]
339+
dynamicImportModulesByEntry: Set<Module>[],
340+
awaitedDynamicEntryModules: Set<Module>,
341+
awaitedDynamicImportModulesByEntry: Set<Module>[]
303342
) {
304343
const entryIndexByModule = new Map<Module, number>();
305344
const dynamicEntries = new Set<number>();
345+
const awaitedDynamicEntries = new Set<number>();
306346
for (const [entryIndex, entry] of allEntries.entries()) {
307347
entryIndexByModule.set(entry, entryIndex);
308348
if (dynamicEntryModules.has(entry)) {
309349
dynamicEntries.add(entryIndex);
310350
}
351+
if (awaitedDynamicEntryModules.has(entry)) {
352+
awaitedDynamicEntries.add(entryIndex);
353+
}
311354
}
355+
const dynamicImportsByEntry = getDynamicImportsByEntry(
356+
dynamicImportModulesByEntry,
357+
entryIndexByModule
358+
);
359+
const awaitedDynamicImportsByEntry = getDynamicImportsByEntry(
360+
awaitedDynamicImportModulesByEntry,
361+
entryIndexByModule
362+
);
363+
return {
364+
awaitedDynamicEntries,
365+
awaitedDynamicImportsByEntry,
366+
dynamicEntries,
367+
dynamicImportsByEntry
368+
};
369+
}
370+
371+
function getDynamicImportsByEntry(
372+
dynamicImportModulesByEntry: Set<Module>[],
373+
entryIndexByModule: Map<Module, number>
374+
): Set<number>[] {
312375
const dynamicImportsByEntry: Set<number>[] = new Array(dynamicImportModulesByEntry.length);
313376
let index = 0;
314377
for (const dynamicImportModules of dynamicImportModulesByEntry) {
@@ -318,13 +381,14 @@ function getDynamicEntries(
318381
}
319382
dynamicImportsByEntry[index++] = dynamicImports;
320383
}
321-
return { dynamicEntries, dynamicImportsByEntry };
384+
return dynamicImportsByEntry;
322385
}
323386

324387
function getDynamicallyDependentEntriesByDynamicEntry(
325388
dependentEntriesByModule: ReadonlyMap<Module, ReadonlySet<number>>,
326389
dynamicEntries: ReadonlySet<number>,
327-
allEntries: readonly Module[]
390+
allEntries: readonly Module[],
391+
getDynamicImporters: (entry: Module) => Iterable<Module>
328392
): Map<number, Set<number>> {
329393
const dynamicallyDependentEntriesByDynamicEntry = new Map<number, Set<number>>();
330394
for (const dynamicEntryIndex of dynamicEntries) {
@@ -335,7 +399,7 @@ function getDynamicallyDependentEntriesByDynamicEntry(
335399
);
336400
const dynamicEntry = allEntries[dynamicEntryIndex];
337401
for (const importer of concatLazy([
338-
dynamicEntry.includedDynamicImporters,
402+
getDynamicImporters(dynamicEntry),
339403
dynamicEntry.implicitlyLoadedAfter
340404
])) {
341405
for (const entry of dependentEntriesByModule.get(importer)!) {
@@ -444,14 +508,19 @@ function getAlreadyLoadedAtomsByEntry(
444508
*/
445509
function removeUnnecessaryDependentEntries(
446510
chunkAtoms: ModulesWithDependentEntries[],
447-
alreadyLoadedAtomsByEntry: bigint[]
511+
alreadyLoadedAtomsByEntry: bigint[],
512+
awaitedAlreadyLoadedAtomsByEntry: bigint[]
448513
) {
449514
// Remove entries from dependent entries if a chunk is already loaded without
450-
// that entry.
515+
// that entry. Do not remove already loaded atoms where all dynamic imports
516+
// are awaited to avoid cycles in the output.
451517
let chunkMask = 1n;
452518
for (const { dependentEntries } of chunkAtoms) {
453519
for (const entryIndex of dependentEntries) {
454-
if ((alreadyLoadedAtomsByEntry[entryIndex] & chunkMask) === chunkMask) {
520+
if (
521+
(alreadyLoadedAtomsByEntry[entryIndex] & chunkMask) === chunkMask &&
522+
(awaitedAlreadyLoadedAtomsByEntry[entryIndex] & chunkMask) === 0n
523+
) {
455524
dependentEntries.delete(entryIndex);
456525
}
457526
}

test/chunking-form/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ runTestSuiteWithSamples(
2727
const logs = [];
2828
after(() => config.logs && compareLogs(logs, config.logs));
2929

30-
for (const format of FORMATS) {
30+
for (const format of config.formats || FORMATS) {
3131
it('generates ' + format, async () => {
3232
process.chdir(directory);
3333
const warnings = [];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = defineTest({
2+
description: 'avoiding top-level await critical path for chunks',
3+
formats: ['es', 'system']
4+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { g as getInfo } from './generated-lib.js';
2+
3+
function getInfoWithUsed() {
4+
return getInfo() + '_used';
5+
}
6+
7+
export { getInfoWithUsed };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { g as getInfo } from './generated-lib.js';
2+
3+
function getInfoWithVariant() {
4+
return getInfo() + '_variant';
5+
}
6+
7+
export { getInfoWithVariant };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function getInfo() {
2+
return 'info';
3+
}
4+
5+
export { getInfo as g };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { g as getInfo } from './generated-lib.js';
2+
3+
let getCommonInfo = getInfo;
4+
5+
import('./generated-lib-used.js').then(({ getInfoWithUsed }) => {
6+
getCommonInfo = getInfoWithUsed;
7+
});
8+
9+
const { getInfoWithVariant } = await import('./generated-lib-variant.js');
10+
getCommonInfo = getInfoWithVariant;
11+
12+
export { getCommonInfo };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
System.register(['./generated-lib.js'], (function (exports) {
2+
'use strict';
3+
var getInfo;
4+
return {
5+
setters: [function (module) {
6+
getInfo = module.g;
7+
}],
8+
execute: (function () {
9+
10+
exports("getInfoWithUsed", getInfoWithUsed);
11+
12+
function getInfoWithUsed() {
13+
return getInfo() + '_used';
14+
}
15+
16+
})
17+
};
18+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
System.register(['./generated-lib.js'], (function (exports) {
2+
'use strict';
3+
var getInfo;
4+
return {
5+
setters: [function (module) {
6+
getInfo = module.g;
7+
}],
8+
execute: (function () {
9+
10+
exports("getInfoWithVariant", getInfoWithVariant);
11+
12+
function getInfoWithVariant() {
13+
return getInfo() + '_variant';
14+
}
15+
16+
})
17+
};
18+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
System.register([], (function (exports) {
2+
'use strict';
3+
return {
4+
execute: (function () {
5+
6+
exports("g", getInfo);
7+
8+
function getInfo() {
9+
return 'info';
10+
}
11+
12+
})
13+
};
14+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
System.register(['./generated-lib.js'], (function (exports, module) {
2+
'use strict';
3+
var getInfo;
4+
return {
5+
setters: [function (module) {
6+
getInfo = module.g;
7+
}],
8+
execute: (async function () {
9+
10+
let getCommonInfo = exports("getCommonInfo", getInfo);
11+
12+
module.import('./generated-lib-used.js').then(({ getInfoWithUsed }) => {
13+
exports("getCommonInfo", getCommonInfo = getInfoWithUsed);
14+
});
15+
16+
const { getInfoWithVariant } = await module.import('./generated-lib-variant.js');
17+
exports("getCommonInfo", getCommonInfo = getInfoWithVariant);
18+
19+
})
20+
};
21+
}));

0 commit comments

Comments
 (0)