Skip to content

Commit

Permalink
Extract module execution order analysis from Graph
Browse files Browse the repository at this point in the history
  • Loading branch information
lukastaegert committed Oct 15, 2018
1 parent 3be7f64 commit dbbd52d
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 149 deletions.
158 changes: 16 additions & 142 deletions src/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,10 @@ import {
Watcher
} from './rollup/types';
import { finaliseAsset } from './utils/assetHooks';
import {
randomUint8Array,
Uint8ArrayEqual,
Uint8ArrayToHexString,
Uint8ArrayXor
} from './utils/entryHashing';
import { Uint8ArrayToHexString } from './utils/entryHashing';
import error from './utils/error';
import { sortByExecutionOrder } from './utils/execution-order';
import { isRelative, relative, resolve } from './utils/path';
import { analyzeModuleExecution, sortByExecutionOrder } from './utils/execution-order';
import { isRelative, resolve } from './utils/path';
import { createPluginDriver, PluginDriver } from './utils/pluginDriver';
import relativeId, { getAliasName } from './utils/relativeId';
import { timeEnd, timeStart } from './utils/timers';
Expand Down Expand Up @@ -376,12 +371,24 @@ export default class Graph {

this.link();

const { orderedModules, dynamicImports, dynamicImportAliases } = this.analyseExecution(
const {
orderedModules,
dynamicImports,
dynamicImportAliases,
cyclePaths
} = analyzeModuleExecution(
entryModules,
!preserveModules && !inlineDynamicImports,
inlineDynamicImports,
manualChunkModules
);
for (const cyclePath of cyclePaths) {
this.warn({
code: 'CIRCULAR_DEPENDENCY',
importer: cyclePath[0],
message: `Circular dependency: ${cyclePath.join(' -> ')}`
});
}

if (entryModuleAliases) {
for (let i = entryModules.length - 1; i >= 0; i--) {
Expand Down Expand Up @@ -495,139 +502,6 @@ export default class Graph {
);
}

private analyseExecution(
entryModules: Module[],
graphColouring: boolean,
inlineDynamicImports: boolean,
chunkModules?: Record<string, Module[]>
) {
let curEntry: Module, curEntryHash: Uint8Array;
const allSeen: { [id: string]: boolean } = {};

const orderedModules: Module[] = [];
let analyzedModuleCount = 0;

const dynamicImports: Module[] = [];
const dynamicImportAliases: string[] = [];

let parents: { [id: string]: string };

const visit = (module: Module) => {
// Track entry point graph colouring by tracing all modules loaded by a given
// entry point and colouring those modules by the hash of its id. Colours are mixed as
// hash xors, providing the unique colouring of the graph into unique hash chunks.
// This is really all there is to automated chunking, the rest is chunk wiring.
if (graphColouring) {
if (!curEntry.chunkAlias) {
Uint8ArrayXor(module.entryPointsHash, curEntryHash);
} else {
// manual chunks are indicated in this phase by having a chunk alias
// they are treated as a single colour in the colouring
// and aren't divisable by future colourings
module.chunkAlias = curEntry.chunkAlias;
module.entryPointsHash = curEntryHash;
}
}

for (const depModule of module.dependencies) {
if (depModule instanceof ExternalModule) {
depModule.execIndex = analyzedModuleCount++;
continue;
}

if (depModule.id in parents) {
if (!allSeen[depModule.id]) {
this.warnCycle(depModule.id, module.id, parents);
}
continue;
}

parents[depModule.id] = module.id;
if (!depModule.isEntryPoint && !depModule.chunkAlias) visit(depModule);
}

for (const dynamicModule of module.dynamicImportResolutions) {
if (!(dynamicModule.resolution instanceof Module)) continue;
// If the parent module of a dynamic import is to a child module whose graph
// colouring is the same as the parent module, then that dynamic import does
// not need to be treated as a new entry point as it is in the static graph
if (
!graphColouring ||
(!dynamicModule.resolution.chunkAlias &&
!Uint8ArrayEqual(dynamicModule.resolution.entryPointsHash, curEntry.entryPointsHash))
) {
if (dynamicImports.indexOf(dynamicModule.resolution) === -1) {
dynamicImports.push(dynamicModule.resolution);
dynamicImportAliases.push(dynamicModule.alias);
}
}
}

if (allSeen[module.id]) return;
allSeen[module.id] = true;

module.execIndex = analyzedModuleCount++;
orderedModules.push(module);
};

if (graphColouring && chunkModules) {
for (const chunkName of Object.keys(chunkModules)) {
curEntryHash = randomUint8Array(10);

for (curEntry of chunkModules[chunkName]) {
if (curEntry.chunkAlias) {
error({
code: 'INVALID_CHUNK',
message: `Cannot assign ${relative(
process.cwd(),
curEntry.id
)} to the "${chunkName}" chunk as it is already in the "${curEntry.chunkAlias}" chunk.
Try defining "${chunkName}" first in the manualChunks definitions of the Rollup configuration.`
});
}
curEntry.chunkAlias = chunkName;
parents = { [curEntry.id]: null };
visit(curEntry);
}
}
}

for (curEntry of entryModules) {
curEntry.isEntryPoint = true;
curEntryHash = randomUint8Array(10);
parents = { [curEntry.id]: null };
visit(curEntry);
}

// new items can be added during this loop
for (curEntry of dynamicImports) {
if (curEntry.isEntryPoint) continue;
if (!inlineDynamicImports) curEntry.isEntryPoint = true;
curEntryHash = randomUint8Array(10);
parents = { [curEntry.id]: null };
visit(curEntry);
}

return { orderedModules, dynamicImports, dynamicImportAliases };
}

private warnCycle(id: string, parentId: string, parents: { [id: string]: string | null }) {
const path = [relativeId(id)];
let curId = parentId;
while (curId !== id) {
path.push(relativeId(curId));
curId = parents[curId];
if (!curId) break;
}
path.push(path[0]);
path.reverse();
this.warn({
code: 'CIRCULAR_DEPENDENCY',
importer: path[0],
message: `Circular dependency: ${path.join(' -> ')}`
});
}

private fetchModule(id: string, importer: string): Promise<Module> {
// short-circuit cycles
const existingModule = this.moduleById.get(id);
Expand Down
137 changes: 137 additions & 0 deletions src/utils/execution-order.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import ExternalModule from '../ExternalModule';
import Module from '../Module';
import { randomUint8Array, Uint8ArrayEqual, Uint8ArrayXor } from './entryHashing';
import error from './error';
import { relative } from './path';
import relativeId from './relativeId';

interface OrderedExecutionUnit {
execIndex: number;
}
Expand All @@ -8,3 +15,133 @@ const compareExecIndex = <T extends OrderedExecutionUnit>(unitA: T, unitB: T) =>
export function sortByExecutionOrder(units: OrderedExecutionUnit[]) {
units.sort(compareExecIndex);
}

export function analyzeModuleExecution(
entryModules: Module[],
generateChunkHashes: boolean,
inlineDynamicImports: boolean,
manualChunkModules: Record<string, Module[]>
) {
let curEntry: Module, curEntryHash: Uint8Array;
const cyclePaths: string[][] = [];
const allSeen: { [id: string]: boolean } = {};

const orderedModules: Module[] = [];
let analyzedModuleCount = 0;

const dynamicImports: Module[] = [];
const dynamicImportAliases: string[] = [];

let parents: { [id: string]: string };

const visit = (module: Module) => {
// Track entry point graph colouring by tracing all modules loaded by a given
// entry point and colouring those modules by the hash of its id. Colours are mixed as
// hash xors, providing the unique colouring of the graph into unique hash chunks.
// This is really all there is to automated chunking, the rest is chunk wiring.
if (generateChunkHashes) {
if (!curEntry.chunkAlias) {
Uint8ArrayXor(module.entryPointsHash, curEntryHash);
} else {
// manual chunks are indicated in this phase by having a chunk alias
// they are treated as a single colour in the colouring
// and aren't divisable by future colourings
module.chunkAlias = curEntry.chunkAlias;
module.entryPointsHash = curEntryHash;
}
}

for (const depModule of module.dependencies) {
if (depModule instanceof ExternalModule) {
depModule.execIndex = analyzedModuleCount++;
continue;
}

if (depModule.id in parents) {
if (!allSeen[depModule.id]) {
cyclePaths.push(getCyclePath(depModule.id, module.id, parents));
}
continue;
}

parents[depModule.id] = module.id;
if (!depModule.isEntryPoint && !depModule.chunkAlias) visit(depModule);
}

for (const dynamicModule of module.dynamicImportResolutions) {
if (!(dynamicModule.resolution instanceof Module)) continue;
// If the parent module of a dynamic import is to a child module whose graph
// colouring is the same as the parent module, then that dynamic import does
// not need to be treated as a new entry point as it is in the static graph
if (
!generateChunkHashes ||
(!dynamicModule.resolution.chunkAlias &&
!Uint8ArrayEqual(dynamicModule.resolution.entryPointsHash, curEntry.entryPointsHash))
) {
if (dynamicImports.indexOf(dynamicModule.resolution) === -1) {
dynamicImports.push(dynamicModule.resolution);
dynamicImportAliases.push(dynamicModule.alias);
}
}
}

if (allSeen[module.id]) return;
allSeen[module.id] = true;

module.execIndex = analyzedModuleCount++;
orderedModules.push(module);
};

if (generateChunkHashes && manualChunkModules) {
for (const chunkName of Object.keys(manualChunkModules)) {
curEntryHash = randomUint8Array(10);

for (curEntry of manualChunkModules[chunkName]) {
if (curEntry.chunkAlias) {
error({
code: 'INVALID_CHUNK',
message: `Cannot assign ${relative(
process.cwd(),
curEntry.id
)} to the "${chunkName}" chunk as it is already in the "${curEntry.chunkAlias}" chunk.
Try defining "${chunkName}" first in the manualChunks definitions of the Rollup configuration.`
});
}
curEntry.chunkAlias = chunkName;
parents = { [curEntry.id]: null };
visit(curEntry);
}
}
}

for (curEntry of entryModules) {
curEntry.isEntryPoint = true;
curEntryHash = randomUint8Array(10);
parents = { [curEntry.id]: null };
visit(curEntry);
}

// new items can be added during this loop
for (curEntry of dynamicImports) {
if (curEntry.isEntryPoint) continue;
if (!inlineDynamicImports) curEntry.isEntryPoint = true;
curEntryHash = randomUint8Array(10);
parents = { [curEntry.id]: null };
visit(curEntry);
}

return { orderedModules, dynamicImports, dynamicImportAliases, cyclePaths };
}

function getCyclePath(id: string, parentId: string, parents: { [id: string]: string | null }) {
const path = [relativeId(id)];
let curId = parentId;
while (curId !== id) {
path.push(relativeId(curId));
curId = parents[curId];
if (!curId) break;
}
path.push(path[0]);
path.reverse();
return path;
}
20 changes: 13 additions & 7 deletions test/function/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,26 @@ function runSingleFileTest(code, configContext) {
}

function runCodeSplitTest(output, configContext) {
function requireFromOutput(fileName) {
const outputId = path.relative('.', fileName);
const requireFromOutputVia = importer => importee => {
const outputId = path.join(path.dirname(importer), importee);
const chunk = output[outputId];
if (chunk) {
return requireWithContext(chunk.code, context);
return requireWithContext(
chunk.code,
Object.assign({ require: requireFromOutputVia(outputId) }, context)
);
} else {
return require(fileName);
return require(importee);
}
}
};

const context = Object.assign({ require: requireFromOutput, assert }, configContext);
const context = Object.assign({ assert }, configContext);
let exports;
try {
exports = requireWithContext(output['main.js'].code, context);
exports = requireWithContext(
output['main.js'].code,
Object.assign({ require: requireFromOutputVia('main.js') }, context)
);
} catch (error) {
return { error, exports: error.exports };
}
Expand Down

0 comments on commit dbbd52d

Please sign in to comment.