Merge pull request #8468 from webpack/feature/chunk-root-modules

add algorithm to extract graph roots
This commit is contained in:
Tobias Koppers 2018-12-06 22:07:37 +01:00 committed by GitHub
commit a20f621263
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 778 additions and 519 deletions

View File

@ -1267,6 +1267,10 @@ export interface StatsOptions {
* add the origins of chunks and chunk merging info
*/
chunkOrigins?: boolean;
/**
* add root modules information to chunk information
*/
chunkRootModules?: boolean;
/**
* add chunk information
*/

View File

@ -15,6 +15,7 @@ const {
compareSelect,
compareIds
} = require("./util/comparators");
const findGraphRoots = require("./util/findGraphRoots");
/** @typedef {import("./AsyncDependenciesBlock")} AsyncDependenciesBlock */
/** @typedef {import("./Chunk")} Chunk */
@ -155,6 +156,8 @@ class ChunkGraph {
this._blockChunkGroups = new WeakMap();
/** @private @type {ModuleGraph} */
this.moduleGraph = moduleGraph;
this._getGraphRoots = this._getGraphRoots.bind(this);
}
/**
@ -185,6 +188,25 @@ class ChunkGraph {
return c;
}
/**
* @param {SortableSet<Module>} set the sortable Set to get the roots of
* @returns {Module[]} the graph roots
*/
_getGraphRoots(set) {
const { moduleGraph } = this;
return Array.from(
findGraphRoots(set, module => {
return moduleGraph
.getOutgoingConnections(module)
.reduce((arr, connection) => {
const module = connection.module;
if (module) arr.push(module);
return arr;
}, []);
})
).sort(compareModulesByIdentifier);
}
/**
* @param {Chunk} chunk the new chunk
* @param {Module} module the module
@ -534,6 +556,15 @@ class ChunkGraph {
return cgc.modules.getFromUnorderedCache(getModulesSizes);
}
/**
* @param {Chunk} chunk the chunk
* @returns {Module[]} root modules of the chunks (ordered by identifer)
*/
getChunkRootModules(chunk) {
const cgc = this._getChunkGraphChunk(chunk);
return cgc.modules.getFromUnorderedCache(this._getGraphRoots);
}
/**
* @param {Chunk} chunk the chunk
* @param {ChunkSizeOptions} options options object

View File

@ -300,7 +300,7 @@ class ModuleGraph {
* @param {Module} module the module
* @returns {ModuleGraphConnection[]} list of outgoing connections
*/
getOutgoingConnection(module) {
getOutgoingConnections(module) {
const connections = this._getModuleGraphModule(module).outgoingConnections;
return Array.from(connections);
}

View File

@ -170,7 +170,14 @@ class Stats {
!forToString
);
const showChunks = optionOrLocalFallback(options.chunks, !forToString);
const showChunkModules = optionOrLocalFallback(options.chunkModules, true);
const showChunkModules = optionOrLocalFallback(
options.chunkModules,
!forToString
);
const showChunkRootModules = optionOrLocalFallback(
options.chunkRootModules,
forToString ? !showChunkModules : true
);
const showChunkOrigins = optionOrLocalFallback(
options.chunkOrigins,
!forToString
@ -178,7 +185,7 @@ class Stats {
const showModules = optionOrLocalFallback(options.modules, true);
const showNestedModules = optionOrLocalFallback(
options.nestedModules,
true
!forToString
);
const showOrphanModules = optionOrLocalFallback(
options.orphanModules,
@ -793,6 +800,25 @@ class Stats {
);
}
}
if (showChunkRootModules) {
const rootModules = chunkGraph.getChunkRootModules(chunk);
obj.rootModules = rootModules
.slice()
.sort(sortRealModules)
.filter(createModuleFilter("root-of-chunk"))
.map(m => fnModule(m));
obj.filteredRootModules = rootModules.length - obj.rootModules.length;
obj.nonRootModules =
chunkGraph.getNumberOfChunkModules(chunk) - rootModules.length;
if (sortModules) {
obj.rootModules.sort(
concatComparators(
sortByField(sortModules),
keepOriginalOrder(obj.rootModules)
)
);
}
}
if (showChunkOrigins) {
const originsKeySet = new Set();
obj.origins = Array.from(chunk.groupsIterable, g => g.origins)
@ -1059,7 +1085,7 @@ class Stats {
color: getAssetColor(asset, colors.normal)
},
{
value: asset.chunks.join(", "),
value: asset.chunks.map(c => `{${c}}`).join(", "),
color: colors.bold
},
{
@ -1134,21 +1160,6 @@ class Stats {
processChunkGroups(outputChunkGroups, "Chunk Group");
}
const modulesByIdentifier = {};
if (obj.modules) {
for (const module of obj.modules) {
modulesByIdentifier[`$${module.identifier}`] = module;
}
} else if (obj.chunks) {
for (const chunk of obj.chunks) {
if (chunk.modules) {
for (const module of chunk.modules) {
modulesByIdentifier[`$${module.identifier}`] = module;
}
}
}
}
const processSizes = sizes => {
const keys = Object.keys(sizes);
if (keys.length > 1) {
@ -1341,11 +1352,16 @@ class Stats {
newline();
}
if (module.modules) {
processModulesList(module, prefix + "| ");
processModulesList(module, prefix + "| ", "nested module");
}
};
const processModulesList = (obj, prefix) => {
const processModulesList = (
obj,
prefix,
itemType = "module",
dependentItemType = "dependent module"
) => {
if (obj.modules) {
let maxModuleId = 0;
for (const module of obj.modules) {
@ -1397,7 +1413,19 @@ class Stats {
if (obj.modules.length > 0) colors.normal(" + ");
colors.normal(obj.filteredModules);
if (obj.modules.length > 0) colors.normal(" hidden");
colors.normal(obj.filteredModules !== 1 ? " modules" : " module");
colors.normal(` ${itemType}${obj.filteredModules !== 1 ? "s" : ""}`);
newline();
}
if (obj.dependentModules > 0) {
const additional = obj.modules.length > 0 || obj.filteredModules > 0;
colors.normal(prefix);
colors.normal(" ");
if (additional) colors.normal(" + ");
colors.normal(obj.dependentModules);
if (additional) colors.normal(" hidden");
colors.normal(
` ${dependentItemType}${obj.dependentModules !== 1 ? "s" : ""}`
);
newline();
}
}
@ -1473,9 +1501,8 @@ class Stats {
colors.normal("[");
colors.normal(origin.moduleId);
colors.normal("] ");
const module = modulesByIdentifier[`$${origin.module}`];
if (module) {
colors.bold(module.name);
if (origin.moduleName) {
colors.bold(origin.moduleName);
colors.normal(" ");
}
}
@ -1485,7 +1512,21 @@ class Stats {
newline();
}
}
processModulesList(chunk, " ");
const hasRootModules =
chunk.rootModules ||
chunk.filteredRootModules ||
chunk.nonRootModules;
processModulesList(
{
modules: chunk.rootModules,
filteredModules: chunk.filteredRootModules,
dependentModules: chunk.nonRootModules
},
" ",
"root module",
"dependent module"
);
processModulesList(chunk, hasRootModules ? " | " : " ", "chunk module");
}
}
@ -1552,6 +1593,7 @@ class Stats {
modules: false,
chunks: true,
chunkModules: true,
chunkRootModules: false,
chunkOrigins: true,
depth: true,
env: true,
@ -1572,6 +1614,7 @@ class Stats {
chunkGroups: true,
chunks: true,
chunkModules: false,
chunkRootModules: false,
chunkOrigins: true,
depth: true,
usedExports: true,

214
lib/util/findGraphRoots.js Normal file
View File

@ -0,0 +1,214 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const NO_MARKER = 0;
const IN_PROGRESS_MARKER = 1;
const DONE_MARKER = 2;
const DONE_MAYBE_ROOT_CYCLE_MARKER = 3;
const DONE_AND_ROOT_MARKER = 4;
class Node {
constructor(item) {
this.item = item;
this.dependencies = new Set();
this.marker = NO_MARKER;
this.cycle = undefined;
this.incoming = 0;
}
}
class Cycle {
constructor() {
this.nodes = new Set();
}
}
/**
* @typedef {Object} StackEntry
* @property {Node} node
* @property {Node[]} openEdges
*/
/**
* @template T
* @param {Iterable<T>} items list of items
* @param {function(T): Iterable<T>} getDependencies function to get dependencies of an item (items that are not in list are ignored)
* @returns {Iterable<T>} graph roots of the items
*/
module.exports = (items, getDependencies) => {
const itemToNode = new Map();
for (const item of items) {
const node = new Node(item);
itemToNode.set(item, node);
}
// early exit when there is only a single item
if (itemToNode.size <= 1) return items;
// grab all the dependencies
for (const node of itemToNode.values()) {
for (const dep of getDependencies(node.item)) {
const depNode = itemToNode.get(dep);
if (depNode !== undefined) {
node.dependencies.add(depNode);
}
}
}
// Set of current root modules
// items will be removed if a new reference to it has been found
/** @type {Set<Node>} */
const roots = new Set();
// Set of current cycles without references to it
// cycles will be removed if a new reference to it has been found
// that is not part of the cycle
/** @type {Set<Cycle>} */
const rootCycles = new Set();
// For all non-marked nodes
for (const selectedNode of itemToNode.values()) {
if (selectedNode.marker === NO_MARKER) {
// deep-walk all referenced modules
// in a non-recursive way
// start by entering the selected node
selectedNode.marker = IN_PROGRESS_MARKER;
// keep a stack to avoid recursive walk
/** @type {StackEntry[]} */
const stack = [
{
node: selectedNode,
openEdges: Array.from(selectedNode.dependencies)
}
];
// process the top item until stack is empty
while (stack.length > 0) {
const topOfStack = stack[stack.length - 1];
// Are there still edges unprocessed in the current node?
if (topOfStack.openEdges.length > 0) {
// Process one dependency
const dependency = topOfStack.openEdges.pop();
switch (dependency.marker) {
case NO_MARKER:
// dependency has not be visited yet
// mark it as in-progress and recurse
stack.push({
node: dependency,
openEdges: Array.from(dependency.dependencies)
});
dependency.marker = IN_PROGRESS_MARKER;
break;
case IN_PROGRESS_MARKER: {
// It's a in-progress cycle
let cycle = dependency.cycle;
if (!cycle) {
cycle = new Cycle();
cycle.nodes.add(dependency);
dependency.cycle = cycle;
}
// set cycle property for each node in the cycle
// if nodes are already part of a cycle
// we merge the cycles to a shared cycle
for (
let i = stack.length - 1;
stack[i].node !== dependency;
i--
) {
const node = stack[i].node;
if (node.cycle) {
if (node.cycle !== cycle) {
// merge cycles
for (const cycleNode of node.cycle.nodes) {
cycleNode.cycle = cycle;
cycle.nodes.add(cycleNode);
}
}
} else {
node.cycle = cycle;
cycle.nodes.add(node);
}
}
// don't recurse into dependencies
// these are already on the stack
break;
}
case DONE_AND_ROOT_MARKER:
// This node has be visited yet and is currently a root node
// But as this is a new reference to the node
// it's not really a root
// so we have to convert it to a normal node
dependency.marker = DONE_MARKER;
roots.delete(dependency);
break;
case DONE_MAYBE_ROOT_CYCLE_MARKER:
// This node has be visited yet and
// is maybe currently part of a completed root cycle
// we found a new reference to the cycle
// so it's not really a root cycle
// remove the cycle from the root cycles
// and convert it to a normal node
rootCycles.delete(dependency.cycle);
dependency.marker = DONE_MARKER;
break;
// DONE_MARKER: nothing to do, don't recurse into dependencies
}
} else {
// All dependencies of the current node has been visited
// we leave the node
stack.pop();
topOfStack.node.marker = DONE_MARKER;
}
}
const cycle = selectedNode.cycle;
if (cycle) {
for (const node of cycle.nodes) {
node.marker = DONE_MAYBE_ROOT_CYCLE_MARKER;
}
rootCycles.add(cycle);
} else {
selectedNode.marker = DONE_AND_ROOT_MARKER;
roots.add(selectedNode);
}
}
}
// Extract roots from root cycles
// We take the nodes with most incoming edges
// inside of the cycle
for (const cycle of rootCycles) {
let max = 0;
const cycleRoots = new Set();
const nodes = cycle.nodes;
for (const node of nodes) {
for (const dep of node.dependencies) {
if (nodes.has(dep)) {
dep.incoming++;
if (dep.incoming < max) continue;
if (dep.incoming > max) {
cycleRoots.clear();
max = dep.incoming;
}
cycleRoots.add(dep);
}
}
}
for (const cycleRoot of cycleRoots) {
roots.add(cycleRoot);
}
}
// When roots were found, return them
if (roots.size > 0) {
return Array.from(roots, r => r.item);
} else {
throw new Error("Implementation of findGraphRoots is broken");
}
};

View File

@ -1745,6 +1745,10 @@
"description": "add the origins of chunks and chunk merging info",
"type": "boolean"
},
"chunkRootModules": {
"description": "add root modules information to chunk information",
"type": "boolean"
},
"chunks": {
"description": "add chunk information",
"type": "boolean"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
import "./b";

View File

@ -0,0 +1 @@
import "./c";

View File

@ -0,0 +1 @@
import "./index";

View File

@ -0,0 +1 @@
import "./a";

View File

@ -0,0 +1 @@
import "./b";

View File

@ -0,0 +1,2 @@
import "./c";
import "./index";

View File

@ -0,0 +1 @@
import "./index";

View File

@ -0,0 +1 @@
import "./a";

View File

@ -0,0 +1,2 @@
import "./index";
import "./b";

View File

@ -0,0 +1 @@
import "./c";

View File

@ -0,0 +1 @@
import "./index";

View File

@ -0,0 +1 @@
import "./a";

View File

@ -0,0 +1 @@
import "./b";

View File

@ -0,0 +1,2 @@
import "./c";
import "./index";

View File

@ -0,0 +1 @@
import "./index";

View File

@ -0,0 +1 @@
import "./a";

View File

@ -0,0 +1,12 @@
import(/* webpackChunkName: "tree" */ "./tree");
import(/* webpackChunkName: "trees" */ "./trees/1");
import(/* webpackChunkName: "trees" */ "./trees/2");
import(/* webpackChunkName: "trees" */ "./trees/3");
import(/* webpackChunkName: "cycle" */ "./cycle");
import(/* webpackChunkName: "cycle2" */ "./cycle2");
import(/* webpackChunkName: "cycles" */ "./cycles/1");
import(/* webpackChunkName: "cycles" */ "./cycles/2");

View File

@ -0,0 +1 @@
import "./b";

View File

@ -0,0 +1 @@
import "./c";

View File

View File

@ -0,0 +1 @@
import "./a";

View File

@ -0,0 +1 @@
import "./a";

View File

@ -0,0 +1 @@
import "./a";

View File

@ -0,0 +1 @@
import "./b";

View File

@ -0,0 +1 @@
import "./b";

View File

@ -0,0 +1 @@
import "./c";

View File

View File

@ -0,0 +1,14 @@
module.exports = {
mode: "development",
entry: "./index.js",
optimization: {
moduleIds: "natural",
chunkIds: "natural",
splitChunks: false
},
stats: {
all: false,
chunks: true,
chunkRootModules: true
}
};