Skip to content
This repository has been archived by the owner on Oct 9, 2020. It is now read-only.

SFX optimization via rollup #205

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/arithmetic.js
Expand Up @@ -216,8 +216,9 @@ function subtractTrees(tree1, tree2) {

// pre-order tree traversal with a visitor and stop condition
exports.traverseTree = traverseTree;
function traverseTree(tree, moduleName, visitor, parent, seen) {
verifyTree(tree);
function traverseTree(tree, moduleName, visitor, traceRuntimePlugin, parent, seen) {
if (!seen)
verifyTree(tree);

seen = seen || [];
seen.push(moduleName);
Expand All @@ -226,7 +227,7 @@ function traverseTree(tree, moduleName, visitor, parent, seen) {
var curNode = tree[moduleName];

if (curNode && visitor(moduleName, parent) !== false)
getLoadDependencies(curNode).forEach(function(dep) {
getLoadDependencies(curNode, traceRuntimePlugin !== false).forEach(function(dep) {
if (seen.indexOf(dep) == -1)
traverseTree(tree, dep, visitor, moduleName, seen);
});
Expand Down
17 changes: 14 additions & 3 deletions lib/builder.js
Expand Up @@ -10,9 +10,11 @@ var extend = require('./utils').extend;
var attachCompilers = require('./compile').attachCompilers;
var compileTree = require('./compile').compileTree;
var compileLoad = require('./compile').compileLoad;

var wrapSFXOutputs = require('./compile').wrapSFXOutputs;
var writeOutputs = require('./output').writeOutputs;

var rollupTree = require('./rollup').rollupTree;

var traceExpression = require('./arithmetic').traceExpression;

var Trace = require('./trace');
Expand Down Expand Up @@ -384,7 +386,10 @@ function processCompileOpts(options, defaults) {

// this shouldn't strictly be a compile option,
// but the cjs compiler needs it to do NODE_ENV optimization
minify: false
minify: false,

// use rollup optimizations
esOptimize: true
};

extend(opts, defaults);
Expand Down Expand Up @@ -524,7 +529,13 @@ Builder.prototype.buildStatic = function(expressionOrTree, outFile, opts) {
// inline conditionals of the trace
return self.tracer.inlineConditions(tree, traceOpts.conditions)
.then(function(inlinedTree) {
return compileTree(self.loader, inlinedTree, compileOpts, outputOpts, self.cache.compile);
// attempt rollup optimizations of ESM entry points
if (compileOpts.esOptimize)
return rollupTree(inlinedTree, [], compileOpts);
return inlinedTree;
})
.then(function(rolledUpTree) {
return compileTree(self.loader, rolledUpTree, compileOpts, outputOpts, self.cache.compile);
})
.then(function(compiled) {
return writeOutputs(compiled.outputs, self.loader.baseURL, outputOpts)
Expand Down
154 changes: 154 additions & 0 deletions lib/rollup.js
@@ -0,0 +1,154 @@
var rollup = require('rollup');
var traverseTree = require('./arithmetic').traverseTree;
var extend = require('./utils').extend;

exports.rollupTree = function(tree, entryPoints, compileOpts) {
// analyze the tree to determine es module subgraphs that can be rolled-up
// entry points are protected from inlining

// determine all the tree entry points
var entryMap = {};
// at the same time build up tree externals list
var externals = [];

// for each module in the tree, we traverse the whole tree
// we then relate each module in the tree to the first traced entry point
Object.keys(tree).forEach(function(entryPoint) {
traverseTree(tree, entryPoint, function(depName, parentName) {
// if we have a entryMap for the given module, then stop
if (entryMap[depName])
return false;

if (!tree[depName] || tree[depName] === true)
externals.push(depName);

if (parentName)
entryMap[depName] = entryPoint;
});
});

// the entry points are then the modules not represented in entryMap
Object.keys(tree).forEach(function(entryPoint) {
if (!entryMap[entryPoint] && entryPoints.indexOf(entryPoint) == -1)
entryPoints.push(entryPoint);
});

// the key property is that no one of these subgraph entry point is contained within the rollup optimization of another
var subGraphEntryPoints = {};

// initially all es modules are considered candidates
Object.keys(tree).forEach(function(moduleName) {
if (tree[moduleName].metadata.format != 'esm')
return;

subGraphEntryPoints[moduleName] = {
externals: []
};
})

// apply filtering to ensure key property above
// traverse down from each entry module to form its esm subgraph
Object.keys(subGraphEntryPoints).forEach(function(entryPoint) {
var entryPointSubGraph = subGraphEntryPoints[entryPoint];

if (!entryPointSubGraph)
return;

traverseTree(tree, entryPoint, function(moduleName, parent) {

// dep outside of tree -> add to externals list
if (!tree[moduleName]) {
entryPointSubGraph.externals.push(moduleName);
return false;
}

// if it is not inlinable, mark as an external and stop traversal
if (tree[moduleName].metadata.format != 'esm' || parent && entryPoints.indexOf(moduleName) != -1) {
entryPointSubGraph.externals.push(moduleName);
return false;
}

// if we have an inlinable dependency that is an entry point
// then note that it is no longer an entry point
// and assume its externals and modules
if (moduleName != entryPoint && subGraphEntryPoints[moduleName]) {
subGraphEntryPoints[moduleName].externals.forEach(function(moduleName) {
entryPointSubGraph.externals.push(moduleName);
});
subGraphEntryPoints[moduleName] = undefined;
return false;
}

}, false);
});

var rolledUpTree = {};
Object.keys(tree).forEach(function(moduleName) {
rolledUpTree[moduleName] = tree[moduleName];
});

// now that we have the full filtered list of entry points
// run rollup against each subgraph
return Promise.all(Object.keys(subGraphEntryPoints).map(function(entryPoint) {
if (!subGraphEntryPoints[entryPoint])
return;

if (entryPoint.indexOf('first.js') != -1)
throw externals;

return rollup.rollup({
entry: entryPoint,
external: externals.concat(subGraphEntryPoints[entryPoint].externals),
plugins: [{
resolveId: function(id, importer, options) {
return importer ? tree[importer].depMap[id] : id;
},
load: function(path, options) {
return tree[path].metadata.originalSource || tree[path].source;
}
}]
})
.then(function(bundle) {
var output = bundle.generate({
sourceMap: compileOpts.sourceMaps
});

var entryPointLoad = tree[entryPoint];

// filter deps to just non-inlined
var deps = entryPointLoad.deps.filter(function(dep) {
var moduleName = entryPointLoad.depMap[dep];
return tree[moduleName].metadata.format != 'esm' || entryPoints.indexOf(moduleName) != -1;
});

// replace the entry point module itself with the inlined subgraph module
rolledUpTree[entryPoint] = extend(extend({}, entryPointLoad), {
deps: deps,
metadata: extend(extend({}, entryPointLoad.metadata), {
originalSource: undefined,
sourceMap: output.map
}),
source: output.code
});
});
}))
.then(function() {
var seenModules = [];
// traverse the whole graph to determine modules orphaned by the inlining operations
entryPoints.forEach(function(entryPoint) {
traverseTree(rolledUpTree, entryPoint, function(moduleName, parent) {
if (seenModules.indexOf(moduleName) != -1)
return false;
seenModules.push(moduleName);
});
});

// remove orphaned loads
Object.keys(rolledUpTree).forEach(function(moduleName) {
if (seenModules.indexOf(moduleName) == -1)
delete rolledUpTree[moduleName];
});

return rolledUpTree;
});
};
10 changes: 5 additions & 5 deletions lib/trace.js
Expand Up @@ -455,7 +455,7 @@ Trace.prototype.getAllLoadRecords = function(canonical, traceAllConditionals, ca

if (load) {
parentStack = parentStack.concat([canonical]);
return Promise.all(Trace.getLoadDependencies(load, traceAllConditionals, canonicalConditionalEnv).map(function(dep) {
return Promise.all(Trace.getLoadDependencies(load, true, traceAllConditionals, canonicalConditionalEnv).map(function(dep) {
return self.getAllLoadRecords(dep, traceAllConditionals, canonicalConditionalEnv, curLoads, parentStack);
}));
}
Expand All @@ -482,12 +482,12 @@ Trace.prototype.getConditionLoadRecords = function(canonical, canonicalCondition
if (load) {
parentStack = parentStack.concat([canonical])
// trace into the conditions themselves
return Promise.all(Trace.getLoadDependencies(load, true, canonicalConditionalEnv, true).map(function(dep) {
return Promise.all(Trace.getLoadDependencies(load, true, true, canonicalConditionalEnv, true).map(function(dep) {
return self.getConditionLoadRecords(dep, canonicalConditionalEnv, true, curLoads, parentStack);
}))
.then(function() {
// trace non-conditions
return Promise.all(Trace.getLoadDependencies(load, true, canonicalConditionalEnv).map(function(dep) {
return Promise.all(Trace.getLoadDependencies(load, true, true, canonicalConditionalEnv).map(function(dep) {
return self.getConditionLoadRecords(dep, canonicalConditionalEnv, inConditionTree, curLoads, parentStack);
}));
});
Expand Down Expand Up @@ -655,7 +655,7 @@ Trace.getConditionalResolutions = function(conditional, traceAllConditionals, co
};

// Returns the ordered immediate dependency array from the trace of a module
Trace.getLoadDependencies = function(load, traceAllConditionals, canonicalConditionalEnv, conditionsOnly) {
Trace.getLoadDependencies = function(load, traceRuntimePlugin, traceAllConditionals, canonicalConditionalEnv, conditionsOnly) {
if (traceAllConditionals !== false)
traceAllConditionals = true;
canonicalConditionalEnv = canonicalConditionalEnv || {};
Expand All @@ -674,7 +674,7 @@ Trace.getLoadDependencies = function(load, traceAllConditionals, canonicalCondit

// trace the plugin as a dependency
var deps = [];
if (load.runtimePlugin)
if (traceRuntimePlugin && load.runtimePlugin)
deps.push(load.plugin);

// add the dependencies
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -7,6 +7,7 @@
"es6-template-strings": "^2.0.0",
"glob": "^5.0.14",
"mkdirp": "^0.5.1",
"rollup": "^0.21.0",
"rsvp": "^3.0.20",
"source-map": "^0.4.4",
"systemjs": "0.19.5",
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/es-tree/a.js
@@ -0,0 +1,2 @@
export {b} from './b.js';
export var a = 'a';
1 change: 1 addition & 0 deletions test/fixtures/es-tree/b.js
@@ -0,0 +1 @@
export var b = 'b';
23 changes: 23 additions & 0 deletions test/static-optimize.js
@@ -0,0 +1,23 @@
var Builder = require('../index');
var builder = new Builder();

var minify = false;

builder.loadConfigSync('./test/fixtures/test-tree.config.js');

builder.config({
transpiler: 'babel',
paths: {
'*': './test/fixtures/es-tree/*'
}
});

suite('SFX Optimizations', function() {
test('All ES6 rollup optimization', function(done) {
builder.buildStatic('a.js', 'test/output/es-sfx.js', { runtime: false, minify: minify })
.then(function(output) {
// TODO actually test
done();
}, done)
});
});