prettier-atom
prettier-atom copied to clipboard
Slow Activation Time
Current Behavior
prettier-atom
is showing up as my slowest package, with an activation time nearly 2x as long as the subsequent package.
Expected Behavior
I would expect prettier-atom
to boot closer to 100 ms? It seems to be an outlier, and I would like to understand why.
Debug Info
Atom version: 1.23.1
prettier-atom version: 0.43.1
prettier version: 1.9.2
prettier-eslint version: 8.6.0
prettier-atom configuration: {
"useEslint": false,
"useStylelint": false,
"useEditorConfig": true,
"formatOnSaveOptions": {
"enabled": false,
"respectEslintignore": true,
"showInStatusBar": false,
"javascriptScopes": [
"source.js",
"source.jsx",
"source.js.jsx",
"source.babel",
"source.js-semantic",
"text.html.basic",
"text.html.vue"
],
"typescriptScopes": [
"source.ts",
"source.tsx",
"source.ts.tsx"
],
"cssScopes": [
"source.css",
"source.less",
"source.css.less",
"source.scss",
"source.css.scss",
"source.css.postcss"
],
"jsonScopes": [
"source.json"
],
"graphQlScopes": [
"source.graphql"
],
"markdownScopes": [
"source.md",
"source.gfm",
"text.md"
],
"excludedGlobs": [],
"whitelistedGlobs": [],
"isDisabledIfNotInPackageJson": false,
"isDisabledIfNoConfigFile": false
},
"prettierOptions": {
"singleQuote": false,
"bracketSpacing": true,
"semi": true,
"useTabs": false,
"jsxBracketSameLine": false,
"printWidth": 80,
"tabWidth": "auto",
"trailingComma": "none",
"parser": "babylon"
},
"prettierEslintOptions": {
"prettierLast": false
}
}
OS: MacOS 10.13.2
I also experimented with
atom --profile-startup .
I've attached that profile:
We've done what we can to try and reduce startup time by lazy-loading as much as possible. There's going to be some overhead though because we do register a status tile in the lower left that lets you disable/enable format-on-save. If you or anyone else have any ideas on how to further reduce startup time, I'm happy to give it a shot. I also want to try implementing https://github.com/sindresorhus/import-lazy this instead of our own solution.
Given that context I explored around a bit. As a caveat, I've never worked with Atom plugins so please forgive any naive assumptions I may make 😅 .
- That lazy import looks really interesting. Could definitely be a way to clean up the existing lazy import code.
- I wonder if the status tile could be made even more lazy. It seems currently
createStatusTile
creates the element but also computes the status and imports all ofatomInterface
simply to get thetoggleFormatOnSave
callback. Instead, you could havecreateLazyStatusTile
that injects a disabled tile and adds the status + listener after the package activates. - Speaking of
atomInterface
, that takes 85ms to load. Actually, only 0.1ms of that is self-time. What's really taking time isbabel-runtime/helpers/toConsumableArray
.
Looking at atomInterface
there's no imports but there is this fancy ES6 syntax going on.
const getAllScopes = () => [
...getJavascriptScopes(),
...getTypescriptScopes(),
...getCssScopes(),
...getJsonScopes(),
...getGraphQlScopes(),
...getMarkdownScopes(),
];
It looks like babel handles the spread operator by loading all kinds of ES6 polyfills which is very time consuming for this one usage. Removing the spread syntax might stop that import and give savings.
So those are some ideas :).
Any context I'm missing that might make these futile? #2 might make #3 irrelevant. I'll see if anyone has any thoughts before taking a stab at these changes...
Just got rid of the array spreads in atomInterface
and it seems to have reduced the activation time.
Here's the results from the profile:
And the timecop result:
248ms - 178ms = 70ms
Which seems in line with the time previously taken by babel-runtime
import (which is no longer being required).
Next thing causing long load times - lodash
.
editorInterface
is requiring all of lodash/fp
so that it can use _.flow
.
updateStatusTileScope
and linterInterface
are requiring all of editorInterface
in order to access some helper methods (getCurrentFilePath
and getCurrentScope
). Neither of those helpers use lodash, but they are still blocked on it loading.
Inlining those helper functions gives significant savings.
Got the activation time down to 55ms 🎉
Inlining was my hack way of getting those functions out, so I'll see if I can rework things in a more elegant way...
@haroldtreen This is awesome. Maybe we can try babel-plugin-lodash
for the lodash problem?: https://github.com/lodash/babel-plugin-lodash
Could do. I've actually opened a PR that solves the lodash
problem with lazy loading and importing flow
directly.
https://github.com/prettier/prettier-atom/pull/335
lodash
doesn't appear to be a part of activation anymore, so the gains from that plugin would need to be measured elsewhere.
Still haven't looked at lazy loading the tile callback, so maybe could trim this down more. But I can live with a 55ms activation time 😊 .
Been digging around a tiny bit more. Some thoughts:
-
import-lazy
is nicer syntactically then having many lazy callbacks, but adding it everywhere doesn't necessarily uncover big savings. -
setTimeout
seems to be a promising way of deferring setup work until later.- In
main.js#consumeIndie
there's a call tolinterInterface.set
. By setting the linter interface in asetTimeout
, that module no longer needs to be required foractivate
and that method doesn't need to be run. Saves a bit of time. - I noticed that
atom.commands.add
seemed to be executing the commands? Or at leastdisplayDebugInfo
was being run. That module has some external requires which eat a bit of time. By adding commands after activate usingsetTimeout
, there's also some savings. - In
createStatusTile
you can usesetTimeout
to set thegetFormatOnSaveStatus
and attach the listener. That combined with thelazyImport
saves those two functions from being required right away.
- In
Overall, I've shaved another 15ms off the activation time with these techniques:
But maybe there's race conditions that this would introduce. I'm not super familiar with atom best practices...
Maybe also scratching the bottom of the barrel 😅 .
Looks like other packages use requestIdleCallback
(which is slightly better than using setTimeout
). It looks like that can also be used for installing dependencies. I bet that would shave off a bunch of time.
https://github.com/AtomLinter/linter-eslint/blob/master/src/main.js#L53-L60
I've been trying to blog about these random open source patches. Show libraries I like. Show how welcoming they are to contributions. Show open source isn't scary.
So my last one was about this fix: https://haroldtreen.com/tech/open-source/contributing-to/2018/01/15/contributing-to-prettier-atom/
Thanks again for the awesome project!
@haroldtreen I read your blog post, was super cool! I think all of these ideas sound cool:
- import-lazy: I didn't expect would get us any additional perf over our current from-scratch implementation, but I figured it would clean up the code a bit so I think it's worth doing
- requestIdleCallback sounds like the way to go for those things you mentioned, and I don't see why in any of the cases there would be a race condition we'd need to worry about
I'm going to close this for now because I think our objectives have been accomplished and activation time has been significantly reduced.
Seeing a regression on this one - async
/ await
being used requires a polyfill that adds ~65ms to the startup profile:
@haroldtreen thanks for reporting the regression! I wonder what the proper fix should be? It might be the case that Atom supports this stuff without a polyfill, but I'd have to check that.
I wonder if we could get away with just totally stopping using the Babel precompilation step entirely and we'd avoid these types of problems.
While looking at the .bablerc
env I noticed that electron supports a lot of the features that were triggering polyfill issues in the past (eg. ...
). So hopefully fixing the env will reduce the need for so many babel transforms.
Maybe the best thing to do would remove the stage-2
preset? It's only the new features that trigger heavy polyfills to be added. I think babel will always be needed so flow types get stripped.
The real solution to the above issue will require removing the async/awaits.
I think babel will always be needed so flow types get stripped.
Oh, duh! Good point!
I noticed that electron supports a lot of the features that were triggering polyfill issues in the past... Maybe the best thing to do would remove the stage-2 preset?
Another good point, I think you're right. I will try this out now in a new PR.
Ok done! ^
Looks like this has become a thing again 😅
Oh my gosh, I wish there was some way to lock this down in some kind of automated performance test or something, but I don't think there's an easy way to do that. I'm reopening for now.
Looking at the diff I suspect...
-
require('lodash/fp')
is back in the critical path of loading (In this PR it reduced the activation time by 120ms - https://github.com/prettier/prettier-atom/pull/335) - There's new modules getting required (eg.
src/helpers/isFileFormattable.js
andsrc/helpers/isPrettierProperVersion.js
).
Okay I guess that means I can just go into those new modules and explicitly require or avoid using all of lodash/fp
I currently experience nearly a half second activitation time currently. 😞
![image](https://user-images.githubusercontent.com/438664/49687537-f0cae780-fac9-11e8-88c7-7e0ff8a14dcb.png)
Are you open to a PR replacing all require('lodash/fp')
with specific requires? Do you believe that will mitigate the issue?
Are you open to a PR replacing all require('lodash/fp') with specific requires?
Yes, but just for the files that are requested at activation time, what haroldteen refers to as the "critical path" (most of the code base is only require
d when a format on save or manual save is invoked). I think doing it everywhere would just get kind of annoying to work with, but is worth it if done just on the critical path files?
Do you believe that will mitigate the issue?
I do think that was the culprit last time, so I'd expect to see at least some performance gain from that, yes.
You could also use something like Rollup to turn the package into a single file with minimal I/O.
That's definitely a possibility, but I'm worried that may actually have the opposite effect. Right now, Atom is lazy-loading most of our codebase so we're avoiding a lot of code being on the critical path of Atom's startup routine.
If we put the entire codebase into a single file, we may end up increasing the load time, unless I'm miscalculating?
@robwise You could use code splitting to split the code into what has to be run at startup and what is dynamically loaded later.