New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
perf: improve incremental app build times #10805
Conversation
iphone/cli/lib/process-js-task.js
Outdated
// If the file didn't change from previous run, return early | ||
const currentHash = this.generateHash(source); | ||
const previousHash = this.data.contentHashes[from]; | ||
if (previousHash && previousHash === currentHash) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically where all the magic happens. If the source file content did not change BEFORE we run jsanalyze
just skip the whole thing, re-use the existing file and load used Ti symbols from previous run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot skip jsanalyze
. We need to AST parse everything so we can detect all Ti.*
and Titanium.*
symbols and enable the various APIs such as Ti.Media. Same goes for Android (and maybe Windows?).
I highly recommend testing using iOS device builds of KitchenSink. There's a lot of sensitive code here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I store the symbols at the end so they can be re-used on the next run. If nothing in the source file changed i don't see why we would need to run the jsanalyze
again. We can simply load the symbols from the last run and inject them back into the builder.
I tested this with our KitchenSink app using device builds and this works just fine. The symbols for unchanged files will be re-used from the last build and changed files will run jsanalyze
again and have their Ti symbols updated. If a file gets removed then it's stored Ti symbols also get cleared.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I think the concern here is that we need to take into account more than just the original source file changing. Did any options baked into jsanalyze change? minify, transpile, etc. Basically anything that goes into analyzeOptions
. Additionally, and this is implicit and not properly handled as-is, but the user could have a .babelrc/config that will get loaded any modify babel usage. I think @brentonhouse is doing this. We haven't officially supported/fixed handling that yet, but it's an expected use case. (right now I think for example we bomb out if a babel config ignores a file because transform returns null and we expect an object with a code
property).
Second, is obviously we are generating both a transformed file and gathering Ti API usage. You need to handle "storing" both of those and handling merging the Ti API usage symbols stuff back into the build properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The jsanalyze
options are a good point. I can check those before the task starts and fallback to a full run if anything changed. I can also feed in possible .babelrc/config paths and force a full task run if any of those changed. So that's also not a problem once that is officially supported.
Regarding your second point, I'm doing exactly that. The TI API usage symbols are stored per file in the task data that get's written to disk and will be loaded and merged back into the builder on subsequent runs. The transformed file is preserved between builds anyway so it's also kind of "stored".
New dependencies added: p-limitAuthor: Sindre Sorhus Description: Run multiple promise-returning & async functions with limited concurrency Homepage: https://github.com/sindresorhus/p-limit#readme
|
Changes to |
iphone/cli/lib/process-js-task.js
Outdated
|
||
return new Promise((resolve) => { | ||
try { | ||
this.builder.cli.createHook('build.ios.copyResource', this.builder, (from, to, cb) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to break these nesting levels up and try to minimize the mismatches here between Promises and callback functions if we can. Basically extract these callback functions out to their own async methods (using async fs APIs), then have just one method where you interface between the Promises and callbacks (maybe just using util.callbackify
/util.promisify
?)
Forgot to mention: I don't know how to avoid it, but I'm not a huge fan of having to pass in the builder object here and rely on all sorts of properties/values internal to it. In the past I ran into this as well where we pass this massive object holding all the state and it makes it near impossible to test if you want to just hand craft a simple mock object yourself. |
|
||
this.transformAndCopy(source, from, to).then(() => { | ||
this.data.contentHashes[from] = currentHash; | ||
return done(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚠️ cli/lib/tasks/process-js-task.js line 149 – Avoid calling back inside of a promise. (promise/no-callback-in-promise)
this.logger.error(e.codeFrame); | ||
} | ||
|
||
done(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚠️ cli/lib/tasks/process-js-task.js line 156 – Avoid calling back inside of a promise. (promise/no-callback-in-promise)
New dependencies added: p-limitAuthor: Sindre Sorhus Description: Run multiple promise-returning & async functions with limited concurrency Homepage: https://github.com/sindresorhus/p-limit#readme
|
I am seeing the following timings building for KS V2: 8.1.0 (no fix):
CLI:
This PR:
CLI:
@janvennemann , I definitely see an improvement in the timings but do not see a vast improvement as you see in incremental builds. Studio Ver: 5.1.2.201903111843 |
FR Passed. |
@lokeshchdhry Most of the benefits of this PR can be seen on larger projects. We've cut our build times to at least 1/3 of the original build time. |
I was actually working on some of these changes locally in the last few days because it's definitely causing slowness. On my local project (which is small, but has a fairly large amount of node modules) it reduces my build time from ~200s to ~12s. Definitely a good improvement! |
@janvennemann , Can you please resolve the conflicts. Thanks. |
|
||
const i18n = appc.i18n(__dirname); | ||
const __ = i18n.__; | ||
const limit = pLimit(8); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why we need to limit the promises at all? Are we not slowing things down artificially? Or is there some reason we need to do this? If so, why 8 as a limit?
Can someone resolve the merge conflicts and merge this? We need to cherry pick this since months now and it's a pain. |
@sgtcoolguy The merge conflicts come from your env-var changes. I think @janvennemann is on holiday, so can you resolve those? I currently needed to fix the merge problems locally but reverted the env-changes there. |
…_mobile into janvennemann-build-perf
.then(() => { | ||
this.tiSymbols = task.data.tiSymbols; | ||
|
||
return next(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚠️ iphone/cli/commands/_build.js line 5875 – Avoid calling back inside of a promise. (promise/no-callback-in-promise)
I resolved the merge conflicts and this should be good to merge. I'm still curious about why we need |
Packages like I've always put a limit when synchronizing an open-ended number of tasks. I've never done any real world benchmarks, so I can't say what the upper limit is, but it probably depends on the task workload. If you're copying files, 20 may seem low, but 20k, sure seems high. I'd be interested to see some data on it. |
@cb1kenobi sure, but 8 seems pretty low - I agree that tuning it would be nicer (in future). Given that most of the operations are just waiting on IO, there shouldn't be much impact of waiting on a fair bit more than that. |
improve incremental app build times on iOS and Android * move to async/await * let errors bubble up to builder Fixes TIMOB-27043
improve incremental app build times on iOS and Android * move to async/await * let errors bubble up to builder Fixes TIMOB-27043
JIRA: https://jira.appcelerator.org/browse/TIMOB-27043
This is a follow up for #10789 which further improves the build time on incremental builds.
Some timings i made with our mocha test suite (average of three runs each):
The clean build difference can be neglected i guess, that's probably just random due to other system usage. Incremental build time improvements however are huge!
I tried to change as little as possible and just moved the related code to a task that automatically does file change detection. The async stuff is now handled in promises and i refactored the existing code into the two methods
processJsFile
andtransformAndCopy
. The rest is just utility stuff for the task to load and save state and merging data from the previous run with incremental changes back into our builder.This is currently iOS only but once we decide to go for this i'll update the Android build too.