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

a few assorted issues #81

Closed
kzc opened this issue May 3, 2020 · 37 comments
Closed

a few assorted issues #81

kzc opened this issue May 3, 2020 · 37 comments

Comments

@kzc
Copy link
Contributor

kzc commented May 3, 2020

esbuild-minify is a dumb wrapper script over esbuild --minify - it's not important other than to demonstrate the following minification issues.

Default argument scope:

$ cat esb1.js 
let a="PASS";((x=a)=>{let a="FAIL";console.log(x)})();

$ cat esb1.js | node
PASS

$ cat esb1.js | esbuild-minify | node
[stdin]:1
let c="PASS";((a=b)=>{let b="FAIL";console.log(a)})();
                 ^
ReferenceError: b is not defined

Switch expression scope:

$ cat esb2.js 
var x="FAIL";switch(x){default:let x="PASS";console.log(x)}

$ cat esb2.js | node
PASS

$ cat esb2.js | esbuild-minify | node
[stdin]:1
var b="FAIL";switch(a){default:let a="PASS";console.log(a)}
                    ^
ReferenceError: a is not defined

Object literal computed property output:

$ cat esb3.js 
var x=1;o={[(++x,5)]:x=>++x};console.log(x,o[5](x),--x);

$ cat esb3.js | node
2 3 1

$ cat esb3.js | esbuild-minify | node
[stdin]:1
var a=1;o={[++a,5]:b=>++b},console.log(a,o[5](a),--a);
               ^
SyntaxError: Unexpected token ,

Class method computed property output:

$ cat esb4.js 
var x=1;o=new class{[(++x,5)](x){return++x}};console.log(x,o[5](x),--x);

$ cat esb4.js | node
2 3 1

$ cat esb4.js | esbuild-minify | node
[stdin]:1
var a=1;o=new class{[++a,5](b){return++b}}(),console.log(a,o[5](a),--a);
                        ^
SyntaxError: Unexpected token ,
@evanw
Copy link
Owner

evanw commented May 3, 2020

Thanks so much for reporting these! Very helpful. I think these all seem pretty straightforward to fix, so they should be fixed soon.

Just curious: how did you come across these? I was wondering if you used some automated means and/or had a test suite that caught these, since that would also be helpful for testing. Manual testing is also very much appreciated of course :)

evanw added a commit that referenced this issue May 3, 2020
evanw added a commit that referenced this issue May 3, 2020
@kzc
Copy link
Contributor Author

kzc commented May 3, 2020

I've encountered and/or fixed similar corner issues in a few ES tools so I thought I'd try them out here.

Take a look at terser's test suite which was derived from uglify-js. You can swap out the minify function for yours and ignore the expected generated code and just match each test's expect_stdout value when run with Node.JS. There's also a very effective ES5 fuzzer in uglify-js that generates complex runnable test cases with expected results. It could be extended for ES2015+ with some effort. Again, you'd have to swap out the minify function. Since uglify-js and its derivative projects have a synchronous minify function you'd have a bit of work to port it to an async API. Rip out the functionality that exercises different sets of minify options, which wouldn't be pertinent to esbuild. You might just use the test cases and rewrite the tools in Go with an embedded JS engine like V8 to verify the results - it'd certainly run faster than forking/exec'ing esbuild for each test case.

Another useful exercise I've found is to minify the generated dist bundle(s) in Rollup and run their mocha test suite as that project uses a fairly decent subset of ES2015+ functionality.

@evanw
Copy link
Owner

evanw commented May 3, 2020

Thanks for the tips! Glad I asked. I'll definitely check those out.

@evanw evanw closed this as completed in 94def59 May 3, 2020
@kzc
Copy link
Contributor Author

kzc commented May 3, 2020

When I think about it, rewriting the expect_stdout test runner in Go would be easier and more performant. To eliminate the fork/exec overhead for each test just have the Go test runner fork a NodeJS sandbox process at startup to run arbitrary JS code and communicate with it using a bidirectional JSON protocol of some kind.

Both terser and uglify-js "compress" tests have valid ES syntax, but are not run directly. Here's a few examples:

func_arg_1: {
    options = {
        evaluate: true,
        inline: true,
        passes: 2,
        reduce_funcs: true,
        reduce_vars: true,
        side_effects: true,
        toplevel: true,
        unused: true,
    }
    input: {
        var a = 42;
        !function(a) {
            console.log(a());
        }(function() {
            return a;
        });
    }
    expect: {
        console.log(42);
    }
    expect_stdout: "42"
}
array_forin_1: {
    options = {
        reduce_funcs: true,
        reduce_vars: true,
        toplevel: true,
        unused: true,
    }
    input: {
        var a = [ 1, 2, 3 ];
        for (var b in a)
            console.log(b);
    }
    expect: {
        for (var b in [ 1, 2, 3 ])
            console.log(b);
    }
    expect_stdout: [
        "0",
        "1",
        "2",
    ]
}
iife: {
    options = {
        evaluate: true,
        reduce_funcs: true,
        reduce_vars: true,
    }
    input: {
        !function(a, b, c) {
            b++;
            console.log(a - 1, b * 1, c + 2);
        }(1, 2, 3);
    }
    expect: {
        !function(a, b, c) {
            b++;
            console.log(0, 3, 5);
        }(1, 2, 3);
    }
    expect_stdout: true
}

You would only care about the contents of the input: block and the expect_stdout: value. expect_stdout: true means that the original code when run will produce the same stdout output as the minified code when run. So while expect_stdout with a non-true value is somewhat redundant, it serves as a check that the non-minified original test in the input: block was written correctly. On occasion different versions of NodeJS produce different results, which is why expect_stdout: true is used rather than specifying a concrete result.

Although uglify-js is ES5 only, it is still actively maintained and has a number of tests that terser lacks and vice-versa.

@evanw
Copy link
Owner

evanw commented May 6, 2020

@kzc I have some updates.

Since the above posts, I have added an API called transform() to the JS library that can be used for minification. It starts a single long-lived child process and uses a streaming protocol over stdin/stdout to avoid the overhead of spawning a new process for every minification.

I also just landed a make terser command that runs the terser test suite. I forked the JavaScript test runner and made it call out to the new transform() API instead. The whole test suite runs in around 6 seconds on my machine which is fast enough for me so I don't think it's necessary to port the test runner to Go.

The test suite has lots of helpful failures that I can start to work through. Thank you so much for telling me about it. Some of them are known issues around eval/with/arguments scope stuff, and some of them are unexpected edge cases that are good to know about. I'm still going through the test failures.

I also got esbuild to bundle and minify Rollup from the TypeScript source. It only required adjusting a few import paths because Rollup's build uses some custom path aliases. This exposed a single TypeScript bug which was fixed in f12f6c7. I then ran that build through Rollup's whole test suite and all tests pass.

@kzc
Copy link
Contributor Author

kzc commented May 6, 2020

@evanw Glad you got the terser tests working. I thought porting the test runner was the more difficult route, but clearly that did not present any difficulty once you created the new JS API.

You can repeat the test process with https://github.com/mishoo/UglifyJS/tree/master/test/compress. Although there is a lot of overlap with terser's forked tests, there are a great number of new (and subsequently altered) uglify-js ES5 tests since the terser was forked. Keep in mind that the test filenames may often be the same with different contents.

@kzc
Copy link
Contributor Author

kzc commented May 6, 2020

@evanw It'd be useful to users if the timings to build Rollup from TS sources were shown compared to Rollup itself (time rollup -c --configTest) and display it in https://github.com/evanw/esbuild#typescript-benchmark. The fact that the esbuild bundle is able to successfully run the Rollup test suite is also worth noting in the README.

@evanw
Copy link
Owner

evanw commented May 7, 2020

The readme now mentions that the esbuild bundle is able to successfully run the Rollup test suite.

Rollup builds with esbuild in ~40ms on my machine. Rollup builds itself in ~4820ms. I'm not sure they are exactly equivalent though since the Rollup config file appears to cause other files to be generated too. I didn't take the time to understand what's going on in detail. For future reference, the full commands were:

# For esbuild
esbuild --bundle src/node-entry.ts --outfile=dist/rollup.js --platform=node --target=es2018 --external:fsevents --minify

# For rollup
node_modules/.bin/rollup -c --configTest

I'm not planning on including it as a benchmark in the readme though because I already have a TypeScript benchmark, and I'm trying to keep the benchmarks clean (one for each topic). The Rome code base is bigger than Rollup so I think it makes for a better TypeScript benchmark.

Also esbuild can't yet build the Rollup code cleanly. I had to make a few changes to Rollup's source code to avoid the need for custom path aliases, since esbuild doesn't support them yet (see #38). So it would be more appropriate as a benchmark once esbuild has support for aliases.

@kzc
Copy link
Contributor Author

kzc commented May 7, 2020

On the topic of benchmarks, the three.js test case is a good example of bundling throughput, but no code can be dropped in that scenario. By design all code in a library is retained. It would be good to also benchmark bundling an end user application like something created with react and material-ui that would better exercise selective code importing, optimization and dead code elimination.

@evanw
Copy link
Owner

evanw commented May 7, 2020

Yeah that'd be great. Do you know of any good sizable open source ones like that? Ideally it'd take at least >30 seconds to be built by other tools.

@kzc
Copy link
Contributor Author

kzc commented May 7, 2020

That's the thing - the largest examples of source code bases are usually the bundlers. I don't know of any off the top of my head.

@kzc
Copy link
Contributor Author

kzc commented May 24, 2020

@evanw Generally libraries are more likely to be open source and web applications are proprietary. But here's a good sized representative example application using react and material-ui:

https://github.com/marmelab/react-admin/tree/master/examples/simple

It's designed to be built with create-react-app and webpack but some quick hacking allows this self-contained example directory to be built with rollup using:

        "rollup": "^2.10.8",
        "@rollup/plugin-node-resolve": "^8.0.0",
        "@rollup/plugin-sucrase": "^3.0.2",
        "@rollup/plugin-commonjs": "^12.0.0",
        "rollup-plugin-terser": "^6.0.1",
node_modules/.bin/rollup src/index.js -d dist-rollup --silent \
  -p node-resolve \
  -p sucrase='{transforms:["typescript","jsx"]}' \
  -p commonjs \
  -p terser='{compress:{global_defs:{"process.env.NODE_ENV":"production"}}}'
$ wc -c dist-rollup/*.js
    5081 dist-rollup/fr-d02bda44.js
 1299889 dist-rollup/index.js

and with esbuild 0.3.9:

node_modules/.bin/esbuild src/index.js \
  --bundle --minify --loader:.js=tsx --target=es2019 \
  '--define:process.env.NODE_ENV="production"' \
  --outdir=dist-esbuild
$ wc -c dist-esbuild/*.js
 1801918 dist-esbuild/index.js

I can't attest to the correctness of the builds - I haven't tried running either and may have missed something. There's likely some create-react-app configuration I'm not aware of to get it working.

Rollup produced two chunks, and esbuild just one. The esbuild bundle is roughly 39% larger than the aggregate size of the rollup chunks, possibly due to these factors.

@kzc
Copy link
Contributor Author

kzc commented May 26, 2020

https://github.com/marmelab/react-admin/tree/master/examples/simple with esbuild 0.4.0:

 1631079 dist-esbuild/index.js

@kzc
Copy link
Contributor Author

kzc commented Jun 5, 2020

fwiw, here's https://github.com/marmelab/react-admin/tree/master/examples/simple with esbuild 0.4.7:

 1368131 dist-esbuild/index.js

It's in the same ballpark as rollup+terser:

    5081 dist-rollup/fr-d02bda44.js
 1299889 dist-rollup/index.js

@evanw
Copy link
Owner

evanw commented Jun 5, 2020

Thanks! Yeah I tested that as well. I was able to get the build running in the browser too and everything seemed functional to me.

@kzc
Copy link
Contributor Author

kzc commented Jun 5, 2020

Can you share how to run the resultant bundle in a browser? I couldn't figure it out.

@evanw
Copy link
Owner

evanw commented Jun 5, 2020

Sure:

cd examples/simple
npm install
npm install esbuild
npm run build
npx esbuild --bundle --minify --loader:.js=jsx --define:process.env.NODE_ENV='"production"' --define:global=window --outfile=dist/main.js src/index.js
python -m SimpleHTTPServer

Then open http://localhost:8000/dist/ in a browser.

@kzc
Copy link
Contributor Author

kzc commented Jun 5, 2020

Thanks! It's the darnedest thing - I thought I tried that. Wait - I was missing --define:global=window - that must be it.

In my opinion the artificial synthetic bench/three should be replaced with https://github.com/marmelab/react-admin/tree/master/examples/simple since it's more a kin to a real life web application with dead code eliminated.

@kzc
Copy link
Contributor Author

kzc commented Jun 5, 2020

If you decide to use react-admin/examples/simple as a bench, the rollup terser plugin would be:

  -p terser='{compress:{global_defs:{"process.env.NODE_ENV":"production","@global":"window"}}}'

It requires the use of a @ prefix before the key when replacing a symbol with another symbol - otherwise it assumes the replacement is a JS literal string. Alternatively, rollup-plugin-replace could be used instead of terser's global_defs for variable substitution.

@kzc
Copy link
Contributor Author

kzc commented Jun 12, 2020

@evanw There's a bundle size regression in react-admin/examples/simple from 0.5.0 forward...

esbuild --bundle --minify --loader:.js=jsx --define:process.env.NODE_ENV='"production"' --define:global=window --outfile=dist/main.js src/index.js

0.4.7: 1368131 dist/main.js
0.4.9: 1368064 dist/main.js
0.5.0: 1446700 dist/main.js
0.5.3: 1446467 dist/main.js

All versions appear to work correctly in the browser.

@evanw
Copy link
Owner

evanw commented Jun 12, 2020

Thanks for the heads up. It looks like the bundle size regression happened in 0.4.12 (vs 0.4.11). That release included some changes to how export * from statements work with CommonJS modules. I'll investigate.

@kzc
Copy link
Contributor Author

kzc commented Jun 12, 2020

You might double check esbuild 0.5.3 against the latest version of https://github.com/mischnic/tree-shaking-example. It resolves some other bundler issues in your fork.

@evanw
Copy link
Owner

evanw commented Jun 12, 2020

I have investigated the size regression in react-admin/examples/simple. Here's what happens:

  • First @material-ui/icons/utils/createSvgIcon.js uses require() to import @material-ui/core/esm/SvgIcon/index.js. This causes @material-ui/core/esm/SvgIcon/index.js to be considered a CommonJS module because it needs to be importable using require().

  • Then @material-ui/core/esm/index.js becomes a CommonJS module because it contains export * from './SvgIcon' and @material-ui/core/esm/SvgIcon/index.js is now a CommonJS module. Using export * from to re-export exports from a CommonJS module requires generating a call to __exportStar() to bind re-exports dynamically at run time.

  • Since @material-ui/core/esm/index.js is a CommonJS module, all re-exports in that file become included. This is because CommonJS modules export a single exports object instead of individual exports, and it is assumed that code using a CommonJS exports object may attempt to access any exported property.

This library is especially strange because the export * from statements appear to be totally unnecessary. The @material-ui/core/esm/index.js file contains lots of repeated code that looks like this:

export { default as SvgIcon } from './SvgIcon';
export * from './SvgIcon';
export { default as SwipeableDrawer } from './SwipeableDrawer';
export * from './SwipeableDrawer';
// ... many more like this ...

Each file seems to just have two exports, default and styles. Exports named default are always ignored by export * from and multiple re-exports of the same name styles become an ambiguous re-export and cancel out (both rules are part of the ES6 module spec). So as far as I can tell the export * from statements are entirely useless? This sort of seems like a problem with the material-ui library.

Right now modules in esbuild are either CommonJS or not (a binary choice). I suppose I could try to address this by giving modules a third state that is both CommonJS and not CommonJS, in the sense that it must be CommonJS because code needs to require() it but we do actually know the names of all of the exports for use with export * from re-exports because the module was originally not CommonJS. But the export binding system is already very complicated and I'm not sure if this additional complexity is worth it (e.g. it might cause more bugs).

You might double check esbuild 0.5.3 against the latest version of https://github.com/mischnic/tree-shaking-example. It resolves some other bundler issues in your fork.

I tried running esbuild 0.5.3 and the bundle sizes didn't change. So it looks like there are no esbuild regressions in those benchmarks.

I did find what seems like another issue with that repo though. Rollup generates warnings about missing exports for prop-types and react-dom. Fixing those warnings makes Rollup's bundle size increase a bit (85.9kb to 86.1kb). I assume this fixes correctness issues and the previous bundle wasn't an accurate measurement? I pushed my changes to my fork: https://github.com/evanw/tree-shaking-example.

@kzc
Copy link
Contributor Author

kzc commented Jun 12, 2020

What a deep dive. I thought @material-ui had a purely ES module variant using only import rather than require, but I trust your analysis.

Just curious why esbuild 0.4.11 and earlier versions produced a smaller apparently correct working bundle.

Rollup generates warnings about missing exports for prop-types and react-dom

If the modules were ultimately not needed to produce the correct answer in the test verification step, then one could argue that the warning is not important.

But the configuration for rollup to deal with the react CJS module is indeed cumbersome and error prone. It involves a lot of user configuration that other bundlers appear to figure out on their own. This could be remedied by the react project releasing an ES module version of their library, or better heuristics in the commonjs rollup plugin.

@evanw
Copy link
Owner

evanw commented Jun 12, 2020

I thought @material-ui had a purely ES module variant using only import rather than require

This is interesting. I took a deeper look and there is indeed an esm variant, but it's not getting picked. Both @material-ui/icons/utils/createSvgIcon.js and @material-ui/icons/esm/utils/createSvgIcon.js exist but @material-ui/icons/utils/createSvgIcon.js is the one picked by esbuild.

This is one such import chain leading to that decision:

// src/Layout.js
import { Layout, AppBar, UserMenu, useLocale, useSetLocale } from 'react-admin';

// node_modules/react-admin/esm/index.js
import AdminRouter from './AdminRouter';

// node_modules/react-admin/esm/AdminRouter.js
import { Loading } from 'ra-ui-materialui';

// node_modules/ra-ui-materialui/esm/index.js
export * from './layout';

// node_modules/ra-ui-materialui/esm/layout/index.js
import UserMenu from './UserMenu';

// node_modules/ra-ui-materialui/esm/layout/UserMenu.js
import AccountCircle from '@material-ui/icons/AccountCircle';

// node_modules/@material-ui/icons/AccountCircle.js
var _createSvgIcon = _interopRequireDefault(require("./utils/createSvgIcon"));

// node_modules/@material-ui/icons/utils/createSvgIcon.js
var _SvgIcon = _interopRequireDefault(require("@material-ui/core/SvgIcon"));

This switches over from ES6 to CommonJS in UserMenu.js which does a nested import from @material-ui/icons/AccountCircle.js instead of just importing AccountCircle from @material-ui/icons. That bypasses the "module": "./esm/index.js" field @material-ui/icons/package.json and the CommonJS version is picked instead of the ES6 version.

So it looks like this is potentially a bug in the ra-ui-materialui library? I don't think this is a bug in esbuild since I don't think esbuild has enough information in package.json to know that it should select the esm version. I think esbuild is following the "module" spec correctly, although it'd be good to get confirmation of that. I'm also not aware of any features of existing bundlers that allow library authors to avoid this footgun. Are there any?

Just curious why esbuild 0.4.11 and earlier versions produced a smaller apparently correct working bundle.

Before version 0.4.12, esbuild would silently ignore star re-exports from CommonJS modules and generate potentially incorrect code with missing re-exports. Starting with version 0.4.12 esbuild now generates code that is always correct¹ because it handles star re-exports from CommonJS modules.

The build was still correct with the old version of esbuild because these star re-exports happened to be meaningless and didn't have an observable effect. But that's not going to be the case for other libraries so esbuild can't revert back to the old behavior of ignoring star re-exports from CommonJS modules.

¹ At least as correct as is reasonably possible without Proxy. The star re-export code for CommonJS modules iterates over the exports once immediately after doing a require() and copies the properties over at that point, so it won't include exported properties that are dynamically added later. I'm not aware of any CommonJS modules that do this but I wouldn't be surprised if there are some in the wild that do this. It's technically allowed by the CommonJS spec and I could imagine it accidentally happening due to dependency cycles.

@kzc
Copy link
Contributor Author

kzc commented Jun 13, 2020

I think esbuild is following the "module" spec correctly

After some debugging, rollup appears to resolve to the same material-ui source paths that esbuild does. I don't know whether its smaller bundle size is attributable to rollup's general tree shaking heuristics, or how it implements "sideEffects": false.

So it looks like this is potentially a bug in the ra-ui-materialui library?

It does seem odd that the esm sources explicitly reference CJS source paths in the same library.

I'm also not aware of any features of existing bundlers that allow library authors to avoid this footgun. Are there any?

Not that I know of.

What would be the heuristic? If an ES module imports a CJS module from the same library emit a warning? Or only emit if the warning if there's a plausible esm equivalent path?

@kzc
Copy link
Contributor Author

kzc commented Aug 16, 2020

@evanw I noticed a 25% speed regression in esbuild-wasm --minify in recent releases for a number of bundles. Are the benchmark timings in the README still representative of the latest version of the native version of esbuild?

I was curious to see how react-admin/examples/simple fares using esbuild-wasm with the commands provided in #81 (comment).

Here's a baseline reference using an older version - it finishes in just under 11 seconds:

$ esbuild-wasm --version
0.5.26

$ time esbuild-wasm --bundle --minify --loader:.js=jsx --define:process.env.NODE_ENV='"production"' --define:global=window --outfile=dist/main.js src/index.js
node_modules/@testing-library/dom/dist/@testing-library/dom.esm.js:3:7: warning: Import "default" will always be undefined
import MutationObserver from '@sheerun/mutationobserver-shim';
       ~~~~~~~~~~~~~~~~
1 warning

real	0m10.835s
user	0m19.940s
sys	0m1.042s

It produces a 1.4M output bundle.

However, the latest version of esbuild-wasm fails after 2 minutes with an out of memory error...

$ esbuild-wasm --version
0.6.24

$ time esbuild-wasm --bundle --minify --loader:.js=jsx --define:process.env.NODE_ENV='"production"' --define:global=window --outfile=dist/main.js src/index.js
node_modules/ra-language-french/tsconfig.json:2:15: warning: Cannot find base config file "../../tsconfig.json"
    "extends": "../../tsconfig.json",
               ~~~~~~~~~~~~~~~~~~~~~
runtime: out of memory: cannot allocate 4194304-byte block (916455424 in use)
fatal error: out of memory

...

real	2m17.802s
user	3m2.898s
sys	0m10.542s

@evanw
Copy link
Owner

evanw commented Aug 16, 2020

Thanks for the heads up. That's concerning. I periodically test benchmark performance as I work to make sure there haven't been any regressions, but only on native. The WebAssembly version is already so slow (and isn't really designed to be fast anyway) that I haven't been bothering to test it as I work. Looks like I should probably start testing it periodically too.

The regression was introduced between version 0.5.26 and version 0.6.0. I narrowed down the cause of the out-of-memory error to this commit: 0a8bbfd. This change moves path resolution from the main thread to the parse thread. Path resolution means going through all of the imports in a file and resolving the path text to the absolute path of the actual file on disk. I moved it from the main thread to the per-file parser thread since it's a per-file task, so it shouldn't be blocking the main thread.

This will be especially important when plugins are introduced because path resolving may involve running resolver plugins, which may end up running JavaScript in the parent process via IPC. Builds will be much slower if that work is done serially on the main thread instead of in parallel for each file. However, it does mean that there will now be multiple simultaneous path resolutions going on in parallel (by design). It looks like this is having a significant effect on the behavior of the build. I haven't figured out why that is yet.

@kzc
Copy link
Contributor Author

kzc commented Aug 16, 2020

Might c25c607 be detrimental to wasm CLI performance?

@evanw
Copy link
Owner

evanw commented Aug 16, 2020

I am thinking about that change, yeah. It could make sense to only do that in 64-bit environments in which case the WebAssembly environment would start running the GC. However, it seems intuitively like that would just make things run slower instead of faster since it's doing more work. FWIW I've already tried enabling the GC again and that avoids the OOM but it's still nowhere near as fast as it was before.

The thing I don't understand yet is that the serial vs. parallel path resolution change should be doing approximately the same amount of work as before, so it shouldn't be using a lot more memory. Parallel vs. serial on a single-threaded runtime with the GC disabled should end up with around the same memory usage at the end either way I'd think. I'd like to understand why this happens and implement the correct fix instead of just making a change that hides the problem. I'm still investigating this. Will keep you posted.

@evanw
Copy link
Owner

evanw commented Aug 16, 2020

Is there a reason why you're using the WebAssembly version on the command line by the way? The native version is equivalent and faster, so I can't think of a reason to do this myself. I think the WebAssembly implementation is really only useful in the browser, where running native code isn't an option. The performance and memory issues you're calling out here are only a problem with bundling, and the bundling API isn't exposed in the browser. So these problems shouldn't come up in the normal WebAssembly use case.

@kzc
Copy link
Contributor Author

kzc commented Aug 17, 2020

The Go runtime doesn't work on my old version of Mac OS, and I like the idea of using the same portable wasm package on all platforms. A 10 second build is adequate for what I need.

@kzc
Copy link
Contributor Author

kzc commented Aug 17, 2020

The thing I don't understand yet is that the serial vs. parallel path resolution change should be doing approximately the same amount of work as before, so it shouldn't be using a lot more memory.

If I had to guess not knowing much about Go or the esbuild code base, the IO bound parallel resolution within the parsing goroutines might cause them to repeatedly context switch out between each other due to the single-threaded wasm runtime producing a larger working set of memory.

@evanw
Copy link
Owner

evanw commented Aug 17, 2020

After a lot of debugging, I've discovered that part of the problem is the @material-ui/icons folder has 11,000 files in it. Some optimizations I've made that work great for directories with a small number of files work poorly for this edge case. This is also a problem with the native build too (it's also slower and has higher memory usage). So I think the problem is mainly about this particular package, not really about the WebAssembly build. Thanks for pointing this out. This will be great to fix.

@evanw
Copy link
Owner

evanw commented Aug 17, 2020

Ok, give version 0.6.25 a try. I believe I have fixed the issues in this test case.

Path resolving still originates from separate threads but the resolver itself now has a single lock around it. That should allow resolver plugins to still run concurrently but the resolver fallback will now run serially. This change fixes the memory issues and is responsible for most of the performance improvement. I also am no longer eagerly calling stat for each entry in parent directories, which is an additional small speed improvement. GC is still off when esbuild is invoked from the command line.

And I discovered an additional WebAssembly-related performance improvement (in the release notes) which means the command-line WebAssembly build should now be faster than before. Version 0.5.26 (the version before the major regression) builds the react-admin benchmark in around 6.81s for me while version 0.6.25 builds it in 3.97s.

I have added the react-admin benchmark to my main rotation and will now keep an eye on it as I develop.

@kzc
Copy link
Contributor Author

kzc commented Aug 17, 2020

react-admin/examples/simple is indeed much faster in esbuild-wasm@0.6.25 - real 5.561s, user 14.003s, sys 0.575s on my machine. Likewise, esbuild-wasm --minify is now 2.2 times faster than version 0.5.26 for my assortment of JS files, more than making up for the recent speed regression.

The advantage of testing against a slower thread poor platform like wasm is that minute inefficiencies are exaggerated.

@kzc
Copy link
Contributor Author

kzc commented Aug 25, 2020

Here's a how-to for the handful of people on earth using an unsupported version of Mac OSX and wish to run the native version of esbuild or the go compiler:

git clone https://github.com/macports/macports-legacy-support.git
cd macports-legacy-support
make FORCE_ARCH=x86_64

This produces a shared library lib/libMacportsLegacySupport.dylib that provides the missing newer system call(s) needed by the go runtime.

Set these environmental variables either in ~/.profile or on a per-command basis and you're good to go:

# needed for native `esbuild` and `go` runtime
export DYLD_INSERT_LIBRARIES=/path/to/libMacportsLegacySupport.dylib

# needed for `go` binary - might need to be customized for your system C compiler
export CGO_CPPFLAGS=-Wno-unknown-warning-option

Optional: Install the official Go release by untarring https://golang.org/dl/go1.15.darwin-amd64.tar.gz.

Assuming esbuild and/or go are in your PATH, you can verify it works:

$ esbuild --version
0.6.27
$ go version
go version go1.15 darwin/amd64

A couple of bugs in esbuild have been discovered in this process and will be filed in another issue.

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

2 participants