/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/stats/StatsFactory.js
449 строк
16 KB
Alexander Akait
perf(stats): reduce allocations and comparator-cache churn in stats generation (#21506)
25 июл 2026, 18:56
Не верифицирован
25 июл 2026, 18:56
4ae3518
Код
Авторство
О чём код?
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { HookMap, SyncBailHook, SyncWaterfallHook } = require("tapable"); const smartGrouping = require("../util/smartGrouping"); /** @typedef {import("../Chunk")} Chunk */ /** @typedef {import("../ChunkGroup").OriginRecord} OriginRecord */ /** @typedef {import("../Compilation")} Compilation */ /** @typedef {import("../Compilation").Asset} Asset */ /** @typedef {import("../Compilation").NormalizedStatsOptions} NormalizedStatsOptions */ /** @typedef {import("../Dependency")} Dependency */ /** @typedef {import("../Module")} Module */ /** @typedef {import("../ModuleGraph").ModuleProfile} ModuleProfile */ /** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */ /** @typedef {import("../errors/WebpackError")} WebpackError */ /** @typedef {import("../util/comparators").Comparator<EXPECTED_ANY>} Comparator */ /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ /** * Defines the group config type used by this module. * @template T, R * @typedef {import("../util/smartGrouping").GroupConfig<T, R>} GroupConfig */ /** @typedef {import("./DefaultStatsFactoryPlugin").ChunkGroupInfoWithName} ChunkGroupInfoWithName */ /** @typedef {import("./DefaultStatsFactoryPlugin").ModuleIssuerPath} ModuleIssuerPath */ /** @typedef {import("./DefaultStatsFactoryPlugin").ModuleTrace} ModuleTrace */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsAsset} StatsAsset */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsChunk} StatsChunk */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsChunkGroup} StatsChunkGroup */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsChunkOrigin} StatsChunkOrigin */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsCompilation} StatsCompilation */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsError} StatsError */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModule} StatsModule */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModuleIssuer} StatsModuleIssuer */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModuleReason} StatsModuleReason */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModuleTraceDependency} StatsModuleTraceDependency */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModuleTraceItem} StatsModuleTraceItem */ /** @typedef {import("./DefaultStatsFactoryPlugin").StatsProfile} StatsProfile */ /** * Defines the known stats factory context type used by this module. * @typedef {object} KnownStatsFactoryContext * @property {string} type * @property {Compilation} compilation * @property {(path: string) => string} makePathsRelative * @property {Set<Module>} rootModules * @property {Map<string, Chunk[]>} compilationFileToChunks * @property {Map<string, Chunk[]>} compilationAuxiliaryFileToChunks * @property {RuntimeSpec} runtime * @property {(compilation: Compilation) => Error[]} cachedGetErrors * @property {(compilation: Compilation) => Error[]} cachedGetWarnings */ /** @typedef {KnownStatsFactoryContext & Record<string, EXPECTED_ANY>} StatsFactoryContext */ // StatsLogging StatsLoggingEntry /** * Defines the stats object type used by this module. * @template T * @template F * @typedef {T extends Compilation ? StatsCompilation : T extends ChunkGroupInfoWithName ? StatsChunkGroup : T extends Chunk ? StatsChunk : T extends OriginRecord ? StatsChunkOrigin : T extends Module ? StatsModule : T extends ModuleGraphConnection ? StatsModuleReason : T extends Asset ? StatsAsset : T extends ModuleTrace ? StatsModuleTraceItem : T extends Dependency ? StatsModuleTraceDependency : T extends Error ? StatsError : T extends ModuleProfile ? StatsProfile : F} StatsObject */ /** * Defines the created object type used by this module. * @template T * @template F * @typedef {T extends ChunkGroupInfoWithName[] ? Record<string, StatsObject<ChunkGroupInfoWithName, F>> : T extends (infer V)[] ? StatsObject<V, F>[] : StatsObject<T, F>} CreatedObject */ /** @typedef {EXPECTED_ANY} ObjectForExtract */ /** @typedef {EXPECTED_ANY} FactoryData */ /** @typedef {EXPECTED_ANY} FactoryDataItem */ /** @typedef {EXPECTED_ANY} Result */ /** * Defines the stats factory hooks type used by this module. * @typedef {object} StatsFactoryHooks * @property {HookMap<SyncBailHook<[ObjectForExtract, FactoryData, StatsFactoryContext], void>>} extract * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext, number, number], boolean | void>>} filter * @property {HookMap<SyncBailHook<[Comparator[], StatsFactoryContext], void>>} sort * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext, number, number], boolean | void>>} filterSorted * @property {HookMap<SyncBailHook<[GroupConfig<EXPECTED_ANY, EXPECTED_ANY>[], StatsFactoryContext], void>>} groupResults * @property {HookMap<SyncBailHook<[Comparator[], StatsFactoryContext], void>>} sortResults * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext, number, number], boolean | void>>} filterResults * @property {HookMap<SyncBailHook<[FactoryDataItem[], StatsFactoryContext], Result | void>>} merge * @property {HookMap<SyncBailHook<[Result, StatsFactoryContext], Result>>} result * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext], string | void>>} getItemName * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext], StatsFactory | void>>} getItemFactory */ /** * Represents the stats factory runtime component. * @template T * @typedef {Map<string, T[]>} Caches */ class StatsFactory { constructor() { /** @type {StatsFactoryHooks} */ this.hooks = Object.freeze({ extract: new HookMap( () => new SyncBailHook(["object", "data", "context"]) ), filter: new HookMap( () => new SyncBailHook(["item", "context", "index", "unfilteredIndex"]) ), sort: new HookMap(() => new SyncBailHook(["comparators", "context"])), filterSorted: new HookMap( () => new SyncBailHook(["item", "context", "index", "unfilteredIndex"]) ), groupResults: new HookMap( () => new SyncBailHook(["groupConfigs", "context"]) ), sortResults: new HookMap( () => new SyncBailHook(["comparators", "context"]) ), filterResults: new HookMap( () => new SyncBailHook(["item", "context", "index", "unfilteredIndex"]) ), merge: new HookMap(() => new SyncBailHook(["items", "context"])), result: new HookMap(() => new SyncWaterfallHook(["result", "context"])), getItemName: new HookMap(() => new SyncBailHook(["item", "context"])), getItemFactory: new HookMap(() => new SyncBailHook(["item", "context"])) }); const hooks = this.hooks; this._caches = /** @type {{ [Key in keyof StatsFactoryHooks]: StatsFactoryHooks[Key] extends HookMap<infer H> ? Map<string, H[]> : never }} */ ({}); for (const key of Object.keys(hooks)) { this._caches[/** @type {keyof StatsFactoryHooks} */ (key)] = new Map(); } /** @type {boolean} */ this._inCreate = false; } /** * Get all level hooks. * @template {StatsFactoryHooks[keyof StatsFactoryHooks]} HM * @template {HM extends HookMap<infer H> ? H : never} H * @param {HM} hookMap hook map * @param {Caches<H>} cache cache * @param {string} type type * @returns {H[]} hooks * @private */ _getAllLevelHooks(hookMap, cache, type) { const cacheEntry = cache.get(type); if (cacheEntry !== undefined) { return cacheEntry; } const hooks = /** @type {H[]} */ ([]); const typeParts = type.split("."); for (let i = 0; i < typeParts.length; i++) { const hook = /** @type {H} */ (hookMap.get(typeParts.slice(i).join("."))); if (hook) { hooks.push(hook); } } cache.set(type, hooks); return hooks; } /** * Returns hook. * @template {StatsFactoryHooks[keyof StatsFactoryHooks]} HM * @template {HM extends HookMap<infer H> ? H : never} H * @template {H extends import("tapable").Hook<infer A, infer R> ? R : never} R * @param {HM} hookMap hook map * @param {Caches<H>} cache cache * @param {string} type type * @param {(hook: H) => R | void} fn fn * @returns {R | void} hook * @private */ _forEachLevel(hookMap, cache, type, fn) { for (const hook of this._getAllLevelHooks(hookMap, cache, type)) { const result = fn(/** @type {H} */ (hook)); if (result !== undefined) return result; } } /** * For each level waterfall. * @template {StatsFactoryHooks[keyof StatsFactoryHooks]} HM * @template {HM extends HookMap<infer H> ? H : never} H * @template [D=EXPECTED_ANY] * @param {HM} hookMap hook map * @param {Caches<H>} cache cache * @param {string} type type * @param {D} data data * @param {(hook: H, data: D) => D} fn fn * @returns {D} data * @private */ _forEachLevelWaterfall(hookMap, cache, type, data, fn) { for (const hook of this._getAllLevelHooks(hookMap, cache, type)) { data = fn(/** @type {H} */ (hook), data); } return data; } /** * For each level filter. * @template {StatsFactoryHooks[keyof StatsFactoryHooks]} T * @template {T extends HookMap<infer H> ? H : never} H * @template [D=EXPECTED_ANY] * @param {T} hookMap hook map * @param {Caches<H>} cache cache * @param {string} type type * @param {D[]} items items * @param {(hook: H, item: D, idx: number, i: number) => boolean | void} fn fn * @param {boolean} forceClone force clone * @returns {D[]} result for each level * @private */ _forEachLevelFilter(hookMap, cache, type, items, fn, forceClone) { const hooks = this._getAllLevelHooks(hookMap, cache, type); if (hooks.length === 0) return forceClone ? [...items] : items; let i = 0; return items.filter((item, idx) => { for (const hook of hooks) { const r = fn(/** @type {H} */ (hook), item, idx, i); if (r !== undefined) { if (r) i++; return r; } } i++; return true; }); } /** * Returns created object. * @template FactoryData * @template FallbackCreatedObject * @param {string} type type * @param {FactoryData} data factory data * @param {Omit<StatsFactoryContext, "type">} baseContext context used as base * @returns {CreatedObject<FactoryData, FallbackCreatedObject>} created object */ create(type, data, baseContext) { if (this._inCreate) { return this._create(type, data, baseContext); } try { this._inCreate = true; return this._create(type, data, baseContext); } finally { for (const key of Object.keys(this._caches)) { this._caches[/** @type {keyof StatsFactoryHooks} */ (key)].clear(); } this._inCreate = false; } } /** * Returns created object. * @private * @template FactoryData * @template FallbackCreatedObject * @param {string} type type * @param {FactoryData} data factory data * @param {Omit<StatsFactoryContext, "type">} baseContext context used as base * @returns {CreatedObject<FactoryData, FallbackCreatedObject>} created object */ _create(type, data, baseContext) { const context = /** @type {StatsFactoryContext} */ ({ ...baseContext, type, [type]: data }); if (Array.isArray(data)) { // run filter on unsorted items const items = this._forEachLevelFilter( this.hooks.filter, this._caches.filter, type, data, (h, r, idx, i) => h.call(r, context, idx, i), true ); // sort items /** @type {Comparator[]} */ const comparators = []; this._forEachLevel(this.hooks.sort, this._caches.sort, type, (h) => h.call(comparators, context) ); if (comparators.length > 0) { sortWithOriginalOrder(items, comparators); } // run filter on sorted items const items2 = this._forEachLevelFilter( this.hooks.filterSorted, this._caches.filterSorted, type, items, (h, r, idx, i) => h.call(r, context, idx, i), false ); // reuse one item context; create() spreads it synchronously, so mutating // `_index`/the name key between items is safe and skips a per-item spread /** @type {StatsFactoryContext} */ const itemContext = { ...context }; const itemNameType = `${type}[]`; /** @type {string | void} */ let prevItemName; let resultItems = items2.map((item, i) => { itemContext._index = i; // run getItemName const itemName = this._forEachLevel( this.hooks.getItemName, this._caches.getItemName, itemNameType, (h) => h.call(item, itemContext) ); // drop a previous item's name key before adding this one's if (prevItemName !== undefined && prevItemName !== itemName) { delete itemContext[prevItemName]; } if (itemName) itemContext[itemName] = item; prevItemName = itemName; const innerType = itemName ? `${type}[].${itemName}` : itemNameType; // run getItemFactory const itemFactory = this._forEachLevel( this.hooks.getItemFactory, this._caches.getItemFactory, innerType, (h) => h.call(item, itemContext) ) || this; // run item factory return itemFactory.create(innerType, item, itemContext); }); // sort result items /** @type {Comparator[]} */ const comparators2 = []; this._forEachLevel( this.hooks.sortResults, this._caches.sortResults, type, (h) => h.call(comparators2, context) ); if (comparators2.length > 0) { sortWithOriginalOrder(resultItems, comparators2); } // group result items /** @type {GroupConfig<EXPECTED_ANY, EXPECTED_ANY>[]} */ const groupConfigs = []; this._forEachLevel( this.hooks.groupResults, this._caches.groupResults, type, (h) => h.call(groupConfigs, context) ); if (groupConfigs.length > 0) { resultItems = smartGrouping(resultItems, groupConfigs); } // run filter on sorted result items const finalResultItems = this._forEachLevelFilter( this.hooks.filterResults, this._caches.filterResults, type, resultItems, (h, r, idx, i) => h.call(r, context, idx, i), false ); // run merge on mapped items let result = this._forEachLevel( this.hooks.merge, this._caches.merge, type, (h) => h.call(finalResultItems, context) ); if (result === undefined) result = finalResultItems; // run result on merged items return this._forEachLevelWaterfall( this.hooks.result, this._caches.result, type, result, (h, r) => h.call(r, context) ); } /** @type {ObjectForExtract} */ const object = {}; // run extract on value this._forEachLevel(this.hooks.extract, this._caches.extract, type, (h) => h.call(object, data, context) ); // run result on extracted object return this._forEachLevelWaterfall( this.hooks.result, this._caches.result, type, object, (h, r) => h.call(r, context) ); } } /** * Stable in-place sort applying comparators in order, keeping the original order * for equal items. Inlined instead of `concatComparators(...c, keepOriginalOrder())` * because that combination is single-use per sort and only thrashes the comparator * caches (a fresh tiebreaker closure every call allocates a new cache entry). * @param {EXPECTED_ANY[]} items items to sort in place * @param {Comparator[]} comparators comparators applied in order * @returns {void} */ const sortWithOriginalOrder = (items, comparators) => { // original-index tiebreaker keeps the sort stable on engines without a stable Array.sort /** @type {Map<EXPECTED_ANY, number>} */ const originalOrder = new Map(); for (let i = 0; i < items.length; i++) { originalOrder.set(items[i], i); } const count = comparators.length; items.sort((a, b) => { for (let i = 0; i < count; i++) { const res = comparators[i](a, b); if (res !== 0) return res; } return ( /** @type {number} */ (originalOrder.get(a)) - /** @type {number} */ (originalOrder.get(b)) ); }); }; module.exports = StatsFactory;