Skip to content

Commit a16f2fd

Browse files
perf(graph): reduce allocations on module and chunk graph hot paths
Hoist per-call comparators and usage predicates, drop per-connection Sets and per-call closures in graph hashing and ExportInfo.setUsed*, and skip redundant copies in Chunk, findGraphRoots and buildChunkGraph.
1 parent d2f8f06 commit a16f2fd

6 files changed

Lines changed: 141 additions & 97 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Reduce allocations on module and chunk graph hot paths.

‎lib/graph/Chunk.js‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
"use strict";
77

8-
const { intersect } = require("../util/SetHelpers");
8+
const { first, intersect } = require("../util/SetHelpers");
99
const SortableSet = require("../util/SortableSet");
1010
const StringXor = require("../util/StringXor");
1111
const {
@@ -654,6 +654,18 @@ class Chunk {
654654
}
655655
}
656656

657+
/**
658+
* Gets the chunks shared by every group of this chunk.
659+
* @private
660+
* @returns {Chunks} a new set of the chunks in all groups
661+
*/
662+
_getChunksInAllGroups() {
663+
if (this._groups.size === 1) {
664+
return new Set(/** @type {ChunkGroup} */ (first(this._groups)).chunks);
665+
}
666+
return intersect(Array.from(this._groups, (g) => new Set(g.chunks)));
667+
}
668+
657669
/**
658670
* Gets all async chunks.
659671
* @returns {Chunks} a set of all the async chunks
@@ -664,9 +676,7 @@ class Chunk {
664676
/** @type {Chunks} */
665677
const chunks = new Set();
666678

667-
const initialChunks = intersect(
668-
Array.from(this.groupsIterable, (g) => new Set(g.chunks))
669-
);
679+
const initialChunks = this._getChunksInAllGroups();
670680

671681
/** @type {Queue} */
672682
const initialQueue = new Set(this.groupsIterable);
@@ -765,9 +775,7 @@ class Chunk {
765775
/** @type {Queue} */
766776
const queue = new Set();
767777

768-
const initialChunks = intersect(
769-
Array.from(this.groupsIterable, (g) => new Set(g.chunks))
770-
);
778+
const initialChunks = this._getChunksInAllGroups();
771779

772780
for (const chunkGroup of this.groupsIterable) {
773781
for (const child of chunkGroup.childrenIterable) {

‎lib/graph/ChunkGraph.js‎

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const createHash = require("../util/createHash");
2121
const {
2222
RuntimeSpecMap,
2323
RuntimeSpecSet,
24-
forEachRuntime,
2524
mergeRuntime,
2625
runtimeToString
2726
} = require("../util/runtime");
@@ -71,6 +70,15 @@ const activeStateToString = (state) => {
7170

7271
const compareModuleIterables = compareIterables(compareModulesByIdentifier);
7372

73+
/** @type {(a: RuntimeModule, b: RuntimeModule) => -1 | 0 | 1} */
74+
const compareRuntimeModulesByStage = concatComparators(
75+
compareSelect(
76+
(module) => /** @type {RuntimeModule} */ (module).stage,
77+
compareIds
78+
),
79+
compareModulesByIdentifier
80+
);
81+
7482
/** @typedef {(c: Chunk, chunkGraph: ChunkGraph) => boolean} ChunkFilterPredicate */
7583
/** @typedef {(m: Module) => boolean} ModuleFilterPredicate */
7684
/** @typedef {[Module, Entrypoint | undefined]} EntryModuleWithChunkGroup */
@@ -846,6 +854,7 @@ class ChunkGraph {
846854
getChunkModuleIdMap(chunk, filterFn, includeAllChunks = false) {
847855
/** @type {ChunkModuleIdMap} */
848856
const chunkModuleIdMap = Object.create(null);
857+
const comparator = compareModulesById(this);
849858

850859
for (const asyncChunk of includeAllChunks
851860
? chunk.getAllReferencedChunks()
@@ -854,7 +863,7 @@ class ChunkGraph {
854863
let array;
855864
for (const module of this.getOrderedChunkModulesIterable(
856865
asyncChunk,
857-
compareModulesById(this)
866+
comparator
858867
)) {
859868
if (filterFn(module)) {
860869
if (array === undefined) {
@@ -886,6 +895,7 @@ class ChunkGraph {
886895
) {
887896
/** @type {ChunkModuleHashMap} */
888897
const chunkModuleHashMap = Object.create(null);
898+
const comparator = compareModulesById(this);
889899

890900
for (const asyncChunk of includeAllChunks
891901
? chunk.getAllReferencedChunks()
@@ -894,7 +904,7 @@ class ChunkGraph {
894904
let idToHashMap;
895905
for (const module of this.getOrderedChunkModulesIterable(
896906
asyncChunk,
897-
compareModulesById(this)
907+
comparator
898908
)) {
899909
if (filterFn(module)) {
900910
if (idToHashMap === undefined) {
@@ -1458,15 +1468,7 @@ class ChunkGraph {
14581468
getChunkRuntimeModulesInOrder(chunk) {
14591469
const cgc = this._getChunkGraphChunk(chunk);
14601470
const array = [...cgc.runtimeModules];
1461-
array.sort(
1462-
concatComparators(
1463-
compareSelect(
1464-
(r) => /** @type {RuntimeModule} */ (r).stage,
1465-
compareIds
1466-
),
1467-
compareModulesByIdentifier
1468-
)
1469-
);
1471+
array.sort(compareRuntimeModulesByStage);
14701472
return array;
14711473
}
14721474

@@ -1890,25 +1892,27 @@ Caller might not support runtime-dependent code generation (opt-out via optimiza
18901892
}
18911893
} else {
18921894
// cspell:word Tnamespace
1895+
// the runtime is sorted once, so every connection reads it in the same order
1896+
runtime.sort();
18931897
for (const connection of connections) {
1894-
/** @type {Set<ConnectionState>} */
1895-
const states = new Set();
1898+
/** @type {ConnectionState | undefined} */
1899+
let firstState;
1900+
let sameState = true;
18961901
let stateInfo = "";
1897-
forEachRuntime(
1898-
runtime,
1899-
(runtime) => {
1900-
const state = connection.getActiveState(runtime);
1901-
states.add(state);
1902-
stateInfo += activeStateToString(state) + runtime;
1903-
},
1904-
true
1905-
);
1906-
if (states.size === 1) {
1907-
const state = first(states);
1908-
if (state === false) continue;
1902+
for (const runtimeName of runtime) {
1903+
const state = connection.getActiveState(runtimeName);
1904+
if (firstState === undefined) {
1905+
firstState = state;
1906+
} else if (state !== firstState) {
1907+
sameState = false;
1908+
}
1909+
stateInfo += activeStateToString(state) + runtimeName;
1910+
}
1911+
if (sameState) {
1912+
if (firstState === false) continue;
19091913
stateInfo = activeStateToString(
19101914
/** @type {ConnectionState} */
1911-
(state)
1915+
(firstState)
19121916
);
19131917
}
19141918
processConnection(connection, stateInfo);
@@ -2003,9 +2007,7 @@ Caller might not support runtime-dependent code generation (opt-out via optimiza
20032007
const chunkGraph = chunkGraphForModuleMap.get(module);
20042008
if (!chunkGraph) {
20052009
throw new Error(
2006-
`${
2007-
deprecateMessage
2008-
}: There was no ChunkGraph assigned to the Module for backward-compat (Use the new API)`
2010+
`${deprecateMessage}: There was no ChunkGraph assigned to the Module for backward-compat (Use the new API)`
20092011
);
20102012
}
20112013
return chunkGraph;
@@ -2061,9 +2063,7 @@ Caller might not support runtime-dependent code generation (opt-out via optimiza
20612063
const chunkGraph = chunkGraphForChunkMap.get(chunk);
20622064
if (!chunkGraph) {
20632065
throw new Error(
2064-
`${
2065-
deprecateMessage
2066-
}There was no ChunkGraph assigned to the Chunk for backward-compat (Use the new API)`
2066+
`${deprecateMessage}There was no ChunkGraph assigned to the Chunk for backward-compat (Use the new API)`
20672067
);
20682068
}
20692069
return chunkGraph;

‎lib/graph/ExportsInfo.js‎

Lines changed: 75 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const { InlinedUsedName } = require("../optimize/InlineExports");
1010
const { equals } = require("../util/ArrayHelpers");
1111
const SortableSet = require("../util/SortableSet");
1212
const makeSerializable = require("../util/makeSerializable");
13-
const { forEachRuntime } = require("../util/runtime");
1413
const { registerLegacyRequest } = require("../util/serialization");
1514

1615
/**
@@ -70,6 +69,59 @@ const UsageState = Object.freeze({
7069

7170
const RETURNS_TRUE = () => true;
7271

72+
/**
73+
* Condition for `setUsedConditionally`, hoisted so no closure is made per call.
74+
* @param {UsageStateType} used usage state
75+
* @returns {boolean} true, when the export is unused
76+
*/
77+
const isUnused = (used) => used === UsageState.Unused;
78+
79+
/**
80+
* Condition for `setUsedConditionally`, hoisted so no closure is made per call.
81+
* @param {UsageStateType} used usage state
82+
* @returns {boolean} true, when the usage is less than unknown
83+
*/
84+
const isBelowUnknown = (used) => used < UsageState.Unknown;
85+
86+
/**
87+
* Sets the usage of one runtime in a per-runtime usage map.
88+
* @param {UsedInRuntime} usedInRuntime usage per runtime
89+
* @param {string} runtime the runtime
90+
* @param {UsageStateType} newValue new usage state
91+
* @param {((condition: UsageStateType) => boolean) | undefined} condition only set when it holds for the old value
92+
* @returns {boolean} true, when the usage changed
93+
*/
94+
const setUsedInRuntime = (usedInRuntime, runtime, newValue, condition) => {
95+
/** @type {UsageStateType | undefined} */
96+
let oldValue = usedInRuntime.get(runtime);
97+
if (oldValue === undefined) oldValue = UsageState.Unused;
98+
if (newValue === oldValue) return false;
99+
if (condition !== undefined && !condition(oldValue)) return false;
100+
if (newValue === UsageState.Unused) {
101+
usedInRuntime.delete(runtime);
102+
} else {
103+
usedInRuntime.set(runtime, newValue);
104+
}
105+
return true;
106+
};
107+
108+
/**
109+
* Creates a per-runtime usage map holding one usage state for every runtime.
110+
* @param {Exclude<RuntimeSpec, undefined>} runtime the runtime
111+
* @param {RuntimeUsageStateType} value usage state
112+
* @returns {UsedInRuntime} usage per runtime
113+
*/
114+
const createUsedInRuntime = (runtime, value) => {
115+
/** @type {UsedInRuntime} */
116+
const usedInRuntime = new Map();
117+
if (typeof runtime === "string") {
118+
usedInRuntime.set(runtime, value);
119+
} else {
120+
for (const name of runtime) usedInRuntime.set(name, value);
121+
}
122+
return usedInRuntime;
123+
};
124+
73125
// Shared empty visited-set for `findTarget`: `_findTarget` only reads
74126
// `alreadyVisited` (`.has`), never mutates it, so a fresh `new Set()` per call
75127
// is pure churn on the per-export reexport hot path. Must stay never-mutated.
@@ -591,7 +643,7 @@ class ExportsInfo {
591643
*/
592644
setUsedForSideEffectsOnly(runtime) {
593645
return this._sideEffectsOnlyInfo.setUsedConditionally(
594-
(used) => used === UsageState.Unused,
646+
isUnused,
595647
UsageState.Used,
596648
runtime
597649
);
@@ -712,8 +764,9 @@ class ExportsInfo {
712764
const inner = this._redirectTo.getProvidedExports();
713765
if (inner === null) return null;
714766
if (inner === true) return true;
767+
const seen = new Set(array);
715768
for (const item of inner) {
716-
if (!array.includes(item)) {
769+
if (!seen.has(item)) {
717770
array.push(item);
718771
}
719772
}
@@ -1261,11 +1314,7 @@ class ExportInfo {
12611314
setUsedInUnknownWay(runtime) {
12621315
let changed = false;
12631316
if (
1264-
this.setUsedConditionally(
1265-
(used) => used < UsageState.Unknown,
1266-
UsageState.Unknown,
1267-
runtime
1268-
)
1317+
this.setUsedConditionally(isBelowUnknown, UsageState.Unknown, runtime)
12691318
) {
12701319
changed = true;
12711320
}
@@ -1392,33 +1441,21 @@ class ExportInfo {
13921441
}
13931442
} else if (this._usedInRuntime === undefined) {
13941443
if (newValue !== UsageState.Unused && condition(UsageState.Unused)) {
1395-
this._usedInRuntime = new Map();
1396-
forEachRuntime(runtime, (runtime) =>
1397-
/** @type {UsedInRuntime} */
1398-
(this._usedInRuntime).set(/** @type {string} */ (runtime), newValue)
1399-
);
1444+
this._usedInRuntime = createUsedInRuntime(runtime, newValue);
14001445
return true;
14011446
}
14021447
} else {
1448+
const usedInRuntime = this._usedInRuntime;
14031449
let changed = false;
1404-
forEachRuntime(runtime, (runtime_) => {
1405-
const runtime = /** @type {string} */ (runtime_);
1406-
const usedInRuntime =
1407-
/** @type {UsedInRuntime} */
1408-
(this._usedInRuntime);
1409-
let oldValue =
1410-
/** @type {UsageStateType} */
1411-
(usedInRuntime.get(runtime));
1412-
if (oldValue === undefined) oldValue = UsageState.Unused;
1413-
if (newValue !== oldValue && condition(oldValue)) {
1414-
if (newValue === UsageState.Unused) {
1415-
usedInRuntime.delete(runtime);
1416-
} else {
1417-
usedInRuntime.set(runtime, newValue);
1450+
if (typeof runtime === "string") {
1451+
changed = setUsedInRuntime(usedInRuntime, runtime, newValue, condition);
1452+
} else {
1453+
for (const name of runtime) {
1454+
if (setUsedInRuntime(usedInRuntime, name, newValue, condition)) {
1455+
changed = true;
14181456
}
1419-
changed = true;
14201457
}
1421-
});
1458+
}
14221459
if (changed) {
14231460
if (this._usedInRuntime.size === 0) this._usedInRuntime = undefined;
14241461
return true;
@@ -1441,33 +1478,21 @@ class ExportInfo {
14411478
}
14421479
} else if (this._usedInRuntime === undefined) {
14431480
if (newValue !== UsageState.Unused) {
1444-
this._usedInRuntime = new Map();
1445-
forEachRuntime(runtime, (runtime) =>
1446-
/** @type {UsedInRuntime} */
1447-
(this._usedInRuntime).set(/** @type {string} */ (runtime), newValue)
1448-
);
1481+
this._usedInRuntime = createUsedInRuntime(runtime, newValue);
14491482
return true;
14501483
}
14511484
} else {
1485+
const usedInRuntime = this._usedInRuntime;
14521486
let changed = false;
1453-
forEachRuntime(runtime, (_runtime) => {
1454-
const runtime = /** @type {string} */ (_runtime);
1455-
const usedInRuntime =
1456-
/** @type {UsedInRuntime} */
1457-
(this._usedInRuntime);
1458-
let oldValue =
1459-
/** @type {UsageStateType} */
1460-
(usedInRuntime.get(runtime));
1461-
if (oldValue === undefined) oldValue = UsageState.Unused;
1462-
if (newValue !== oldValue) {
1463-
if (newValue === UsageState.Unused) {
1464-
usedInRuntime.delete(runtime);
1465-
} else {
1466-
usedInRuntime.set(runtime, newValue);
1487+
if (typeof runtime === "string") {
1488+
changed = setUsedInRuntime(usedInRuntime, runtime, newValue, undefined);
1489+
} else {
1490+
for (const name of runtime) {
1491+
if (setUsedInRuntime(usedInRuntime, name, newValue, undefined)) {
1492+
changed = true;
14671493
}
1468-
changed = true;
14691494
}
1470-
});
1495+
}
14711496
if (changed) {
14721497
if (this._usedInRuntime.size === 0) this._usedInRuntime = undefined;
14731498
return true;

0 commit comments

Comments
 (0)