Skip to content

Commit df66a18

Browse files
committed
perf: improve incremental build times
1 parent 42cc21e commit df66a18

File tree

2 files changed

+186
-81
lines changed

2 files changed

+186
-81
lines changed

Alloy/commands/compile/index.js

Lines changed: 42 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ var ejs = require('ejs'),
2323
BuildLog = require('./BuildLog'),
2424
Orphanage = require('./Orphanage');
2525

26+
const MvcCompileTask = require('./tasks/mvc-compile-task');
27+
2628
var alloyRoot = path.join(__dirname, '..', '..'),
2729
viewRegex = new RegExp('\\.' + CONST.FILE_EXT.VIEW + '$'),
2830
controllerRegex = new RegExp('\\.' + CONST.FILE_EXT.CONTROLLER + '$'),
@@ -418,93 +420,52 @@ module.exports = function(args, program) {
418420
CU.models.push(m);
419421
});
420422

421-
// Create a regex for determining which platform-specific
422-
// folders should be used in the compile process
423-
var filteredPlatforms = _.reject(CONST.PLATFORM_FOLDERS_ALLOY, function(p) {
424-
return p === buildPlatform;
425-
});
426-
filteredPlatforms = _.map(filteredPlatforms, function(p) { return p + '[\\\\\\/]'; });
427-
var filterRegex = new RegExp('^(?:(?!' + filteredPlatforms.join('|') + '))');
428-
429-
// don't process XML/controller files inside .svn folders (ALOY-839)
430-
var excludeRegex = new RegExp('(?:^|[\\/\\\\])(?:' + CONST.EXCLUDED_FILES.join('|') + ')(?:$|[\\/\\\\])');
431-
432-
// Process all views/controllers and generate their runtime
433-
// commonjs modules and source maps.
434-
var tracker = {};
435-
_.each(widgetDirs, function(collection) {
436-
// generate runtime controllers from views
437-
var theViewDir = path.join(collection.dir, CONST.DIR.VIEW);
438-
if (fs.existsSync(theViewDir)) {
439-
_.each(walkSync(theViewDir), function(view) {
440-
view = path.normalize(view);
441-
if (viewRegex.test(view) && filterRegex.test(view) && !excludeRegex.test(view)) {
442-
// make sure this controller is only generated once
443-
var theFile = view.substring(0, view.lastIndexOf('.'));
444-
var theKey = theFile.replace(new RegExp('^' + buildPlatform + '[\\/\\\\]'), '');
445-
var fp = path.join(collection.dir, theKey);
446-
if (tracker[fp]) { return; }
447-
448-
// generate runtime controller
449-
logger.info('[' + view + '] ' + (collection.manifest ? collection.manifest.id +
450-
' ' : '') + 'view processing...');
451-
parseAlloyComponent(view, collection.dir, collection.manifest, null, restrictionPath);
452-
tracker[fp] = true;
453-
}
454-
});
455-
}
456-
457-
// generate runtime controllers from any controller code that has no
458-
// corresponding view markup
459-
var theControllerDir = path.join(collection.dir, CONST.DIR.CONTROLLER);
460-
if (fs.existsSync(theControllerDir)) {
461-
_.each(walkSync(theControllerDir), function(controller) {
462-
controller = path.normalize(controller);
463-
if (controllerRegex.test(controller) && filterRegex.test(controller) && !excludeRegex.test(controller)) {
464-
// make sure this controller is only generated once
465-
var theFile = controller.substring(0, controller.lastIndexOf('.'));
466-
var theKey = theFile.replace(new RegExp('^' + buildPlatform + '[\\/\\\\]'), '');
467-
var fp = path.join(collection.dir, theKey);
468-
if (tracker[fp]) { return; }
469-
470-
// generate runtime controller
471-
logger.info('[' + controller + '] ' + (collection.manifest ?
472-
collection.manifest.id + ' ' : '') + 'controller processing...');
473-
parseAlloyComponent(controller, collection.dir, collection.manifest, true, restrictionPath);
474-
tracker[fp] = true;
475-
}
423+
return Promise.resolve()
424+
.then(() => {
425+
const mvcCompileTask = new MvcCompileTask({
426+
incrementalDirectory: path.join(compileConfig.dir.project, 'build', 'alloy', 'incremental', 'mvc'),
427+
logger,
428+
compileConfig,
429+
restrictionPath,
430+
parseAlloyComponent,
431+
sourceCollections: widgetDirs,
432+
targetPlatform: buildPlatform
476433
});
477-
}
478-
});
479-
logger.info('');
480-
481-
generateAppJs(paths, compileConfig, restrictionPath, compilerMakeFile);
482-
483-
// ALOY-905: workaround TiSDK < 3.2.0 iOS device build bug where it can't reference app.js
484-
// in platform-specific folders, so we just copy the platform-specific one to
485-
// the Resources folder.
486-
if (buildPlatform === 'ios' && tiapp.version.lt('3.2.0')) {
487-
U.copyFileSync(path.join(paths.resources, titaniumFolder, 'app.js'), path.join(paths.resources, 'app.js'));
488-
}
434+
return mvcCompileTask.run();
435+
})
436+
.then(() => {
437+
generateAppJs(paths, compileConfig, restrictionPath, compilerMakeFile);
438+
439+
// ALOY-905: workaround TiSDK < 3.2.0 iOS device build bug where it can't reference app.js
440+
// in platform-specific folders, so we just copy the platform-specific one to
441+
// the Resources folder.
442+
if (buildPlatform === 'ios' && tiapp.version.lt('3.2.0')) {
443+
U.copyFileSync(path.join(paths.resources, titaniumFolder, 'app.js'), path.join(paths.resources, 'app.js'));
444+
}
489445

490-
// optimize code
491-
logger.info('----- OPTIMIZING -----');
446+
// optimize code
447+
logger.info('----- OPTIMIZING -----');
492448

493-
if (restrictionSkipOptimize) {
494-
logger.info('Skipping optimize due to file restriction.');
495-
} else {
496-
optimizeCompiledCode(alloyConfig, paths);
497-
}
449+
if (restrictionSkipOptimize) {
450+
logger.info('Skipping optimize due to file restriction.');
451+
} else {
452+
optimizeCompiledCode(alloyConfig, paths);
453+
}
498454

499-
// trigger our custom compiler makefile
500-
if (compilerMakeFile.isActive) {
501-
compilerMakeFile.trigger('post:compile', _.clone(compileConfig));
502-
}
455+
// trigger our custom compiler makefile
456+
if (compilerMakeFile.isActive) {
457+
compilerMakeFile.trigger('post:compile', _.clone(compileConfig));
458+
}
503459

504-
// write out the log for this build
505-
buildLog.write();
460+
// write out the log for this build
461+
buildLog.write();
506462

507-
BENCHMARK('TOTAL', true);
463+
BENCHMARK('TOTAL', true);
464+
})
465+
.catch(e => {
466+
logger.error(e);
467+
U.die(e.message);
468+
});
508469
};
509470

510471

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
const _ = require('lodash');
2+
const { IncrementalFileTask } = require('appc-tasks');
3+
const fs = require('fs');
4+
const path = require('path');
5+
const walkSync = require('walk-sync');
6+
7+
const CONST = require('alloy/Alloy/common/constants');
8+
9+
const viewRegex = new RegExp('\\.' + CONST.FILE_EXT.VIEW + '$');
10+
const controllerRegex = new RegExp('\\.' + CONST.FILE_EXT.CONTROLLER + '$');
11+
const excludeRegex = new RegExp('(?:^|[\\/\\\\])(?:' + CONST.EXCLUDED_FILES.join('|') + ')(?:$|[\\/\\\\])');
12+
13+
/**
14+
* Task to compile controllers/views and their styles
15+
*/
16+
class MvcCompileTask extends IncrementalFileTask {
17+
/**
18+
* Constructs a new MVC compile task.
19+
*
20+
* @param {Object} options Configuration object for this task
21+
*/
22+
constructor(options) {
23+
options.name = options.name || 'mvc-compile';
24+
super(options);
25+
26+
this.targetPlatform = options.targetPlatform;
27+
this.restrictionPath = options.restrictionPath;
28+
this.parseAlloyComponent = options.parseAlloyComponent;
29+
30+
// Create a regex for determining which platform-specific
31+
// folders should be used in the compile process
32+
let filteredPlatforms = _.reject(CONST.PLATFORM_FOLDERS_ALLOY, p => {
33+
return p === this.targetPlatform;
34+
});
35+
filteredPlatforms = _.map(filteredPlatforms, function(p) { return p + '[\\\\\\/]'; });
36+
this.filterRegex = new RegExp('^(?:(?!' + filteredPlatforms.join('|') + '))');
37+
38+
this.processed = {};
39+
this.rawSourceCollections = [];
40+
41+
if (options.sourceCollections) {
42+
this.sourceCollections = options.sourceCollections;
43+
}
44+
}
45+
46+
set sourceCollections(sourceCollections) {
47+
this.inputFiles = new Set();
48+
this.rawSourceCollections = sourceCollections;
49+
const candidateSourcePaths = [CONST.DIR.VIEW, CONST.DIR.CONTROLLER];
50+
_.each(sourceCollections, collection => {
51+
for (const candidateSourcePath of candidateSourcePaths) {
52+
var sourcePath = path.join(collection.dir, candidateSourcePath);
53+
if (!fs.existsSync(sourcePath)) {
54+
continue;
55+
}
56+
57+
_.each(walkSync(sourcePath), componentFilename => {
58+
componentFilename = path.normalize(componentFilename);
59+
const componentRegex = candidateSourcePath === CONST.DIR.VIEW ? viewRegex : controllerRegex;
60+
if (componentRegex.test(componentFilename) && this.filterRegex.test(componentFilename) && !excludeRegex.test(componentFilename)) {
61+
this.addInputFile(path.join(sourcePath, componentFilename));
62+
}
63+
});
64+
}
65+
});
66+
}
67+
68+
/**
69+
* Does a full task run, processing every input file.
70+
*
71+
* @return {Promise}
72+
*/
73+
doFullTaskRun() {
74+
// @todo make this parallel?
75+
for (const inputFile of this.inputFiles) {
76+
this.compileAlloyComponent(inputFile);
77+
}
78+
79+
return Promise.resolve();
80+
}
81+
82+
/**
83+
* Does an incremental task run, processing only changed files.
84+
*
85+
* @param {Map} changedFiles Map of file paths and their current file state (created, changed, deleted)
86+
* @return {Promise}
87+
*/
88+
doIncrementalTaskRun(changedFiles) {
89+
changedFiles.forEach((fileState, filePath) => {
90+
if (fileState === 'deleted') {
91+
// @todo delete associated controller, view and styles
92+
} else {
93+
const collection = this.findSourceCollection(inputFile);
94+
const parseAsCotroller = controllerRegex.test(inputFile);
95+
parseAlloyComponent(view, collection.dir, collection.manifest, parseAsCotroller, restrictionPath);
96+
}
97+
});
98+
99+
return Promise.resolve();
100+
}
101+
102+
/**
103+
* Finds the source collection a file part of.
104+
*
105+
* @param {String} filePath Full path to the file for which to look up the source collection.
106+
*/
107+
findSourceCollection(filePath) {
108+
for (const collection of this.rawSourceCollections) {
109+
if (filePath.startsWith(collection.dir)) {
110+
return collection;
111+
}
112+
}
113+
114+
throw new Error(`Source collection not found for ${filePath}.`);
115+
}
116+
117+
/**
118+
* Compiles an Alloy component (controller/view).
119+
*
120+
* Note that this only needs to be done once for either the view OR the controller.
121+
*
122+
* @param {*} inputFile
123+
*/
124+
compileAlloyComponent(inputFile) {
125+
const collection = this.findSourceCollection(inputFile);
126+
const parseAsCotroller = controllerRegex.test(inputFile);
127+
let relativeComponentPath = inputFile.replace(collection.dir, '');
128+
relativeComponentPath = relativeComponentPath.replace(new RegExp(`^${path.sep}?(views|controllers)${path.sep}?`), '');
129+
130+
var relativeComponentPathWithoutExtension = relativeComponentPath.substring(0, relativeComponentPath.lastIndexOf('.'));
131+
var componentIdentifier = relativeComponentPathWithoutExtension.replace(new RegExp('^' + this.targetPlatform + '[\\/\\\\]'), '');
132+
var fullComponentIdentifier = path.join(collection.dir, componentIdentifier);
133+
if (this.processed[fullComponentIdentifier]) {
134+
return;
135+
}
136+
137+
this.logger.info('[' + relativeComponentPath + '] ' + (collection.manifest ? collection.manifest.id +
138+
' ' : '') + `${parseAsCotroller ? 'controller' : 'view'} processing...`);
139+
this.parseAlloyComponent(relativeComponentPath, collection.dir, collection.manifest, parseAsCotroller, this.restrictionPath);
140+
this.processed[fullComponentIdentifier] = true;
141+
}
142+
}
143+
144+
module.exports = MvcCompileTask;

0 commit comments

Comments
 (0)