Skip to content
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

Cold start #99

Open
jakobrosenberg opened this issue Jul 4, 2020 · 17 comments
Open

Cold start #99

jakobrosenberg opened this issue Jul 4, 2020 · 17 comments

Comments

@jakobrosenberg
Copy link
Contributor

Is it possible to speed up the cold start? I often get 15s+.

@PepsRyuu
Copy link
Owner

PepsRyuu commented Jul 4, 2020

There's a variety of reasons that can contribute to a slow start, mostly it comes down to the plugins being used (stuff like Babel for example can be really expensive to use for performance). How long does a rebuild take? Is there a some plugins that could be greatly contributing to delays?

If there's reproduction steps I can have a look in more detail as to what's causing the delay and see if there's any quick performance wins. I'm very interested in improving the performance overall.

@PepsRyuu
Copy link
Owner

PepsRyuu commented Jul 4, 2020

Just a couple of other thoughts on this topic:

  • Source map collapsing can definitely be improved. Would be interesting to see what the alternatives there are to source-map and what performance gains can be gained from that.

  • I plan on introducing an option to disable source maps entirely, since it seems like it could be useful in some context of more complex debugging that require looking at the generated code rather than the original code. This would be useful for determining how much of an impact source maps are having on performance.

  • 0.12.0 does a lot more caching compared to the previous versions, but as far as I know there's no other areas for improvement in regards to caching of results, but I could be wrong. Otherwise it looks like the code might have to be rewritten to take advantage of JavaScript micro-optimisations, but I'm not sure how much of a gain could be achieved from that. Would be good to identify other areas for improvement.

@jakobrosenberg
Copy link
Contributor Author

Thanks for the reply. Really insightful!

I have no idea of Nollup's inner workings, so I have little to contribute, but I'm sure there are scenarios where source maps aren't highly necessary. If there's a good performance gain, I'd only use them when debugging.

Could source maps possibly be created in a separate thread and simply be "available when they're available"?

@rixo
Copy link
Contributor

rixo commented Jul 4, 2020

I think sourcemaps can't be spared for Svelters like us.

I also believe the main contributing factor to slow start is silply the size of the project ¯_(ツ)_/¯

Vite somewhat escape it with just in time compilation, which Nollup can't do. Last time I checked, Snowpack was worse than Nollup despite a HDD cold cache. Interestingly, Rollup tends to be (very) marginally faster on initial build.

I'm keen to take a dive into 0.12.0 to see if I can find good targets for caching... Sometime soon.

Otherwise, the main optimization I can think of for cold start is a cold cache written to disk. It would get completely invalidated by any config change, and partially invalidated by files that have changed (I'd check the last modified time for that).

Now there's 2 main reasons for me to restart Nollup: config change or dev server state has been corrupted by an error condition. The first one would benefit nothing from a cold cache, and I'm afraid broken state would be persisted by it 😅 ... Otherwise, for the nominal case of starting and stopping your project in day to day workflow, we could probably go from 10s of seconds to a handful of seconds.

Just my 2 cents.

@PepsRyuu
Copy link
Owner

PepsRyuu commented Jul 4, 2020

For the sake of experimentation, comment out the implementation of combineSourceMapChain in lib/impl/utils and see how much of the 15 seconds timing gets cut. I get double the speed improvement from trying it with the example projects.

Disk cache could be interesting, but would have to carefully determine what should be cached, and what rules determine to invalidate the cache.

@PepsRyuu
Copy link
Owner

PepsRyuu commented Jul 4, 2020

#100

Faster implementation using a WASM version of source-map, would be good to see if this branch helps to reduce the time.

@jakobrosenberg
Copy link
Contributor Author

Did three runs, with and without combineSourceMapChain for routify.dev

With
14973
13940
14079

Without
13727
13655
13402

@PepsRyuu
Copy link
Owner

PepsRyuu commented Jul 5, 2020

Some research notes based on routify.dev:

Compiled main in 2178ms, Modules: 54
Compiled workbox-window.prod.es5 in 8ms, Modules: 1
Compiled _fallback in 25ms, Modules: 28
Compiled _layout in 448ms, Modules: 49
Compiled _layout in 179ms, Modules: 30
Compiled buttons in 436ms, Modules: 38
Compiled code in 36ms, Modules: 20
Compiled colors in 43ms, Modules: 20
Compiled icons in 42ms, Modules: 35
Compiled index in 850ms, Modules: 55
Compiled rich-text in 35ms, Modules: 20
Compiled tabs in 51ms, Modules: 37
Compiled _layout in 17ms, Modules: 10
Compiled comparison-with-sapper in 3422ms, Modules: 249
Compiled index in 29ms, Modules: 28
Compiled migration-guide in 102ms, Modules: 249
Compiled prefetch-and-cache in 80ms, Modules: 249
Compiled request-handling in 16ms, Modules: 10
Compiled roxi in 29ms, Modules: 249
Compiled setup-express-server in 26ms, Modules: 249
Compiled writing-plugins in 24ms, Modules: 10
Compiled index in 219ms, Modules: 33
Compiled _layout in 85ms, Modules: 47
Compiled index in 49ms, Modules: 28
Compiled _layout in 51ms, Modules: 10
Compiled index in 47ms, Modules: 28
Compiled creating-an-app in 145ms, Modules: 66
Compiled install-to-existing-project in 94ms, Modules: 66
Compiled _layout in 20ms, Modules: 10
Compiled index in 19ms, Modules: 28
Compiled getting-started in 35ms, Modules: 66
Compiled structure in 64ms, Modules: 46
Compiled parameters in 36ms, Modules: 34
Compiled layouts in 36ms, Modules: 50
Compiled props in 34ms, Modules: 34
Compiled _layout in 53ms, Modules: 26
Compiled index in 36ms, Modules: 28
Compiled overview in 104ms, Modules: 50
Compiled SSR in 112ms, Modules: 51
Compiled build in 86ms, Modules: 33
Compiled deployment in 87ms, Modules: 50
Compiled configuration in 136ms, Modules: 50
Compiled metadata in 110ms, Modules: 33
Compiled navigation in 118ms, Modules: 51
Compiled bundling in 74ms, Modules: 249
Compiled prefetching in 55ms, Modules: 51
Compiled faq in 48ms, Modules: 249
Compiled _layout in 27ms, Modules: 30
Compiled index in 15ms, Modules: 28
Compiled query-strings in 16ms, Modules: 28
Compiled _layout in 18ms, Modules: 10
Compiled index in 761ms, Modules: 69
Compiled decorators in 36ms, Modules: 50
Compiled transitions in 41ms, Modules: 34
Compiled metadata in 25ms, Modules: 34
Compiled basepath in 37ms, Modules: 18
Compiled _layout in 23ms, Modules: 30
Compiled basics in 53ms, Modules: 37
Compiled deployments in 27ms, Modules: 34
Compiled index in 15ms, Modules: 28
Compiled _layout in 22ms, Modules: 30
Compiled auth in 91ms, Modules: 50
Compiled example-app in 17ms, Modules: 10
Compiled generated-navigation in 39ms, Modules: 18
Compiled index in 17ms, Modules: 28
Compiled _layout in 43ms, Modules: 35
Compiled 1.8-beta in 236ms, Modules: 285
Compiled an-update in 90ms, Modules: 285
Compiled announcing-1.5 in 154ms, Modules: 285
Compiled index in 74ms, Modules: 280
Bundle 0, Files: 70
Compiled in 13663ms.

From what I can tell at the moment, a significant factor is the number of files to be compiled. For example, a standout here is comparison-with-sapper, and the reason for that is because date-fns imports 230 modules. Changing date-fns to use a single pre-compiled module reduces the overall compilation time by 3 seconds. When bundling, the number of modules to compile is far more costly than the size of the modules. One of the things I always check for when using a third party library is if they offer a single-file ESM version that's optimised for browsers. Many libraries don't do this at all, but it's one of the biggest contributing factors to bundling delay.

I tried a few other experiments, like minimising the amount of async/await and disabling magic-string when converting import/export statements, and while there is definitely improvement, it's no silver bullet. I think in general it would be a good idea to look at the specific implementations of functions, and prefer faster native WASM based libraries where possible.

Still investigating.

@jakobrosenberg
Copy link
Contributor Author

Would it be possible to compile dependencies and cache them for future use? Maybe recompile and recache in the background or add a command to Nollup which lets you clear the cache.

Also, completely off topic, where about in Ireland are you located?

@PepsRyuu
Copy link
Owner

PepsRyuu commented Jul 5, 2020

Disk caching I'll have to experiment with and see what kind of results I can get. It feels like it might be feasible, and it certainly warrants investigation. I have my concerns regarding it. For example, part of Nollup's speed comes from the fact it's not writing to disk, so would have to figure out how to minimise the impact (maybe listen to CTRL + C for example and only write to disk then with the latest state).

It's next on my list to investigate. Probably won't have any results on this for a couple of days though!

Based in Dublin. 😎

@jakobrosenberg
Copy link
Contributor Author

Not sure if this is feasible, but could compiled packages be written to a temp folder in node_modules/.nollup? Packages are mostly static and the folder should be deleted automatically when packages are installed/updated.

@rixo
Copy link
Contributor

rixo commented Jul 5, 2020

I also think a cold cache should be written just in time on ctrl-c. Totally unpsyched by the idea of Nollup constantly writing to disk data that would never be used for the most part. Mainly for disk health reasons.

@jakobrosenberg
Copy link
Contributor Author

How would this work with testing where you might have 10 tests running in sequence?

@PepsRyuu
Copy link
Owner

Just some updates on this. I released the fast source map branch in 0.13.6 which cuts build times for the example apps, and should be of some benefit for most people to cut their build time by a couple of seconds.

In regards to the disk cache, I did some experimenting with it, and while it's possible to get this working to some degree without much effort, it's not really compatible with plugins, especially those that output different assets. For example, the CSS plugin I wrote depends on the transform function to keep track of loaded CSS files. If Nollup preloads a cache, that transform function never runs, so no CSS gets outputted because the transform function never runs. This will apply to other plugins as well.

I think this is a problem for plugins themselves to solve rather than Nollup. Plugins like @rollup/plugin-babel could implement a cache to drastically improve cold start times. This is exactly what the Webpack babel-loader does as seen here: https://github.com/babel/babel-loader/blob/master/src/cache.js

@PepsRyuu
Copy link
Owner

For reference, this is the disk caching experiment:

let NollupContext = require('./impl/NollupContext');
let NollupCompiler = require('./impl/NollupCompiler');
let fs = require('fs');
let path = require('path');
let crypto = require('crypto');

async function nollup (options = {}) {
    let queue = [];
    let processing = false;
    let context = await NollupContext.create(options);

    let cacheFileName = context.input.map(i => i.file).join('+');
    cacheFileName = crypto.createHash('md5').update(cacheFileName).digest('hex');

    if (fs.existsSync('node_modules/.nollup/' + cacheFileName)) {
        let content = JSON.parse(fs.readFileSync('node_modules/.nollup/' + cacheFileName, 'utf8'));
        let maxTimeStamp = findLatestModifiedFileTime();
        if (content.timestamp === maxTimeStamp) {
            context.files = content.cache;

            let latestModuleId = 0;
            for (let f in context.files) {
                if (latestModuleId < context.files[f].moduleId) {
                    latestModuleId = context.files[f].moduleId;
                }
            }

            context.moduleIdGenerator = latestModuleId;
        }
    }

    async function generateImpl (resolve, reject) {
        processing = true;

        try {
            resolve(await NollupCompiler.compile(context));
        } catch (e) {
            processing = false;
            reject(e);
        }

        processing = false;

        if (queue.length > 0) {
            queue.shift()();
        }
    }

    function findLatestModifiedFileTime () {
        let maxTimeStamp = 0;

        let impl = function (dir) {
            let files = fs.readdirSync(dir);
            for (let i = 0; i < files.length; i++) {
                let f = files[i];
                let stats = fs.statSync(dir + '/' + f);
                if (stats.isFile()) {
                    if (maxTimeStamp < stats.mtimeMs) {
                        maxTimeStamp = stats.mtimeMs;
                    }
                } else {
                    if (f !== '.git' && f !== 'node_modules') {
                        impl(dir + '/' + f);
                    }
                }
            }
        }
        
        impl(process.cwd());

        return maxTimeStamp;
    }

    function createColdCache () {
        let maxTimeStamp = findLatestModifiedFileTime();

        if (!fs.existsSync('node_modules/.nollup')) {
            fs.mkdirSync('node_modules/.nollup');
        }

        let fileContent = {
            timestamp: maxTimeStamp,
            cache: context.files
        };

        fs.writeFileSync('node_modules/.nollup/' + cacheFileName, JSON.stringify(fileContent));
    }

    process.on('SIGTERM', () => {
        console.log('SIGTERM');
        createColdCache();
    });

     process.on('SIGINT', () => {
        console.log('SIGINT');
        createColdCache();
    });

    return {
        invalidate (file) {
            NollupContext.invalidate(context, file);
        },

        generate (outputOptions = {}) {
            NollupContext.setOutputOptions(context, outputOptions);

            return new Promise((resolve, reject) => {
                if (processing) {
                    queue.push(() => generateImpl(resolve, reject));
                } else {
                    generateImpl(resolve, reject);
                }
            });
        }
    };

};

module.exports = nollup;

@jakobrosenberg
Copy link
Contributor Author

Great job, @PepsRyuu

@PepsRyuu
Copy link
Owner

Further information on this, I have updated all examples to use the newest performance best practices. All examples no longer use rollup-plugin-replace to inject process.env and Babel is configured to exclude node_modules. On my system, the React Refresh example used to take around 6 seconds to cold start, it now takes around 1.3 seconds. This further demonstrates that plugins can be optimised to reduce cold start times significantly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants