Skip to content

Commit

Permalink
Code splitting across reexports using symbol data by splitting depend…
Browse files Browse the repository at this point in the history
…encies (#8432)
  • Loading branch information
mischnic committed Sep 9, 2022
1 parent b934741 commit 3092d3c
Show file tree
Hide file tree
Showing 28 changed files with 550 additions and 237 deletions.
2 changes: 1 addition & 1 deletion packages/core/core/src/AssetGraph.js
Expand Up @@ -56,7 +56,7 @@ export function nodeFromDep(dep: Dependency): DependencyNode {
deferred: false,
excluded: false,
usedSymbolsDown: new Set(),
usedSymbolsUp: new Set(),
usedSymbolsUp: new Map(),
usedSymbolsDownDirty: true,
usedSymbolsUpDirtyDown: true,
usedSymbolsUpDirtyUp: true,
Expand Down
193 changes: 160 additions & 33 deletions packages/core/core/src/BundleGraph.js
Expand Up @@ -7,6 +7,7 @@ import type {
TraversalActions,
} from '@parcel/types';
import type {
ContentKey,
ContentGraphOpts,
NodeId,
SerializedContentGraph,
Expand All @@ -31,7 +32,7 @@ import invariant from 'assert';
import nullthrows from 'nullthrows';
import {ContentGraph, ALL_EDGE_TYPES, mapVisitor} from '@parcel/graph';
import {Hash, hashString} from '@parcel/hash';
import {objectSortedEntriesDeep, getRootDir} from '@parcel/utils';
import {DefaultMap, objectSortedEntriesDeep, getRootDir} from '@parcel/utils';

import {Priority, BundleBehavior, SpecifierType} from './types';
import {getBundleGroupId, getPublicId} from './utils';
Expand Down Expand Up @@ -146,7 +147,8 @@ export default class BundleGraph {
assetPublicIds: Set<string> = new Set(),
): BundleGraph {
let graph = new ContentGraph<BundleGraphNode, BundleGraphEdgeType>();
let assetGroupIds = new Set();
let assetGroupIds = new Map();
let dependencies = new Map();
let assetGraphNodeIdToBundleGraphNodeId = new Map<NodeId, NodeId>();

let assetGraphRootNode =
Expand All @@ -168,50 +170,175 @@ export default class BundleGraph {
publicIdByAssetId.set(assetId, publicId);
assetPublicIds.add(publicId);
}
} else if (node.type === 'asset_group') {
assetGroupIds.set(nodeId, assetGraph.getNodeIdsConnectedFrom(nodeId));
}
}

let walkVisited = new Set();
function walk(nodeId) {
if (walkVisited.has(nodeId)) return;
walkVisited.add(nodeId);

let node = nullthrows(assetGraph.getNode(nodeId));
if (node.type === 'dependency' && node.value.symbols != null) {
// asset -> symbols that should be imported directly from that asset
let targets = new DefaultMap<ContentKey, Map<Symbol, Symbol>>(
() => new Map(),
);
let externalSymbols = new Set();
let hasAmbiguousSymbols = false;

for (let [symbol, resolvedSymbol] of node.usedSymbolsUp) {
if (resolvedSymbol) {
targets
.get(resolvedSymbol.asset)
.set(symbol, resolvedSymbol.symbol ?? symbol);
} else if (resolvedSymbol === null) {
externalSymbols.add(symbol);
} else if (resolvedSymbol === undefined) {
hasAmbiguousSymbols = true;
break;
}
}

if (
// Only perform rewriting when there is an imported symbol
// - If the target is side-effect-free, the symbols point to the actual target and removing
// the original dependency resolution is fine
// - Otherwise, keep this dependency unchanged for its potential side effects
node.usedSymbolsUp.size > 0 &&
// Only perform rewriting if the dependency only points to a single asset (e.g. CSS modules)
!hasAmbiguousSymbols &&
// TODO We currently can't rename imports in async imports, e.g. from
// (parcelRequire("...")).then(({ a }) => a);
// to
// (parcelRequire("...")).then(({ a: b }) => a);
// or
// (parcelRequire("...")).then((a)=>a);
// if the reexporting asset did `export {a as b}` or `export * as a`
node.value.priority === Priority.sync
) {
// TODO adjust sourceAssetIdNode.value.dependencies ?
let deps = [
// Keep the original dependency
{
asset: null,
dep: graph.addNodeByContentKey(node.id, {
...node,
value: {
...node.value,
symbols: node.value.symbols
? new Map(
[...node.value.symbols].filter(([k]) =>
externalSymbols.has(k),
),
)
: undefined,
},
usedSymbolsUp: new Map(
[...node.usedSymbolsUp].filter(([k]) =>
externalSymbols.has(k),
),
),
usedSymbolsDown: new Set(),
excluded: externalSymbols.size === 0,
}),
},
...[...targets].map(([asset, target]) => {
let newNodeId = hashString(
node.id + [...target.keys()].join(','),
);
return {
asset,
dep: graph.addNodeByContentKey(newNodeId, {
...node,
id: newNodeId,
value: {
...node.value,
id: newNodeId,
symbols: node.value.symbols
? new Map(
[...node.value.symbols]
.filter(([k]) => target.has(k) || k === '*')
.map(([k, v]) => [target.get(k) ?? k, v]),
)
: undefined,
},
usedSymbolsUp: new Map(
[...node.usedSymbolsUp]
.filter(([k]) => target.has(k) || k === '*')
.map(([k, v]) => [target.get(k) ?? k, v]),
),
usedSymbolsDown: new Set(),
}),
};
}),
];
dependencies.set(nodeId, deps);

// Jump to the dependencies that are used in this dependency
for (let id of targets.keys()) {
walk(assetGraph.getNodeIdByContentKey(id));
}
return;
} else {
// No special handling
let bundleGraphNodeId = graph.addNodeByContentKey(node.id, node);
assetGraphNodeIdToBundleGraphNodeId.set(nodeId, bundleGraphNodeId);
}
}
// Don't copy over asset groups into the bundle graph.
if (node.type === 'asset_group') {
assetGroupIds.add(nodeId);
} else {
else if (node.type !== 'asset_group') {
let bundleGraphNodeId = graph.addNodeByContentKey(node.id, node);
if (node.id === assetGraphRootNode?.id) {
graph.setRootNodeId(bundleGraphNodeId);
}
assetGraphNodeIdToBundleGraphNodeId.set(nodeId, bundleGraphNodeId);
}

for (let id of assetGraph.getNodeIdsConnectedFrom(nodeId)) {
walk(id);
}
}
walk(nullthrows(assetGraph.rootNodeId));

for (let edge of assetGraph.getAllEdges()) {
let fromIds;
if (assetGroupIds.has(edge.from)) {
fromIds = [
...assetGraph.getNodeIdsConnectedTo(
edge.from,
bundleGraphEdgeTypes.null,
),
];
} else {
fromIds = [edge.from];
continue;
}

for (let from of fromIds) {
if (assetGroupIds.has(edge.to)) {
for (let to of assetGraph.getNodeIdsConnectedFrom(
edge.to,
bundleGraphEdgeTypes.null,
)) {
if (dependencies.has(edge.from)) {
// Discard previous edge, insert outgoing edges for all split dependencies
for (let {asset, dep} of nullthrows(dependencies.get(edge.from))) {
if (asset != null) {
graph.addEdge(
nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(from)),
nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(to)),
dep,
nullthrows(
assetGraphNodeIdToBundleGraphNodeId.get(
assetGraph.getNodeIdByContentKey(asset),
),
),
);
}
} else {
graph.addEdge(
nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(from)),
nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(edge.to)),
);
}
continue;
}
if (!assetGraphNodeIdToBundleGraphNodeId.has(edge.from)) {
continue;
}

let to: Array<NodeId> = dependencies.get(edge.to)?.map(v => v.dep) ??
assetGroupIds
.get(edge.to)
?.map(id =>
nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(id)),
) ?? [nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(edge.to))];

for (let t of to) {
graph.addEdge(
nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(edge.from)),
t,
);
}
}

Expand Down Expand Up @@ -1737,16 +1864,15 @@ export default class BundleGraph {
let node = this._graph.getNodeByContentKey(asset.id);
invariant(node && node.type === 'asset');
return this._symbolPropagationRan
? makeReadOnlySet(node.usedSymbols)
? makeReadOnlySet(new Set(node.usedSymbols.keys()))
: null;
}

getUsedSymbolsDependency(dep: Dependency): ?$ReadOnlySet<Symbol> {
let node = this._graph.getNodeByContentKey(dep.id);
invariant(node && node.type === 'dependency');
return this._symbolPropagationRan
? makeReadOnlySet(node.usedSymbolsUp)
: null;
let result = new Set(node.usedSymbolsUp.keys());
return this._symbolPropagationRan ? makeReadOnlySet(result) : null;
}

merge(other: BundleGraph) {
Expand All @@ -1758,6 +1884,7 @@ export default class BundleGraph {

let existingNode = nullthrows(this._graph.getNode(existingNodeId));
// Merge symbols, recompute dep.exluded based on that

if (existingNode.type === 'asset') {
invariant(otherNode.type === 'asset');
existingNode.usedSymbols = new Set([
Expand All @@ -1770,7 +1897,7 @@ export default class BundleGraph {
...existingNode.usedSymbolsDown,
...otherNode.usedSymbolsDown,
]);
existingNode.usedSymbolsUp = new Set([
existingNode.usedSymbolsUp = new Map([
...existingNode.usedSymbolsUp,
...otherNode.usedSymbolsUp,
]);
Expand Down
24 changes: 21 additions & 3 deletions packages/core/core/src/dumpGraphToGraphViz.js
Expand Up @@ -21,11 +21,14 @@ const COLORS = {
};

const TYPE_COLORS = {
// bundle graph
bundle: 'blue',
contains: 'grey',
internal_async: 'orange',
references: 'red',
sibling: 'green',
// asset graph
// request graph
invalidated_by_create: 'green',
invalidated_by_create_above: 'orange',
invalidate_by_update: 'cyan',
Expand Down Expand Up @@ -80,8 +83,13 @@ export default async function dumpGraphToGraphViz(
if (node.type === 'dependency') {
label += node.value.specifier;
let parts = [];
if (node.value.priority !== Priority.sync)
parts.push(node.value.priority);
if (node.value.priority !== Priority.sync) {
parts.push(
Object.entries(Priority).find(
([, v]) => v === node.value.priority,
)?.[0],
);
}
if (node.value.isOptional) parts.push('optional');
if (node.value.specifierType === SpecifierType.url) parts.push('url');
if (node.hasDeferred) parts.push('deferred');
Expand All @@ -103,7 +111,17 @@ export default async function dumpGraphToGraphViz(
label += '\\nweakSymbols: ' + weakSymbols.join(',');
}
if (node.usedSymbolsUp.size > 0) {
label += '\\nusedSymbolsUp: ' + [...node.usedSymbolsUp].join(',');
label +=
'\\nusedSymbolsUp: ' +
[...node.usedSymbolsUp]
.map(([s, sAsset]) =>
sAsset
? `${s}(${sAsset.asset}.${sAsset.symbol ?? ''})`
: sAsset === null
? `${s}(external)`
: `${s}(ambiguous)`,
)
.join(',');
}
if (node.usedSymbolsDown.size > 0) {
label +=
Expand Down
1 change: 0 additions & 1 deletion packages/core/core/src/public/Symbols.js
Expand Up @@ -27,7 +27,6 @@ const EMPTY_ITERATOR = {
const inspect = Symbol.for('nodejs.util.inspect.custom');

let valueToSymbols: WeakMap<Asset, AssetSymbols> = new WeakMap();

export class AssetSymbols implements IAssetSymbols {
/*::
@@iterator(): Iterator<[ISymbol, {|local: ISymbol, loc: ?SourceLocation, meta?: ?Meta|}]> { return ({}: any); }
Expand Down

0 comments on commit 3092d3c

Please sign in to comment.