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

support asynchronous test cases properly #4529

Merged
merged 1 commit into from
Jan 19, 2021
Merged

Conversation

alexlamsl
Copy link
Collaborator

@kzc you may find this interesting 😉

Caveats:

  • when async is in use, anything relying on Unicode will break (at least on Windows)
  • it is very slow 🐢

test/sandbox.js Outdated
} while (prev !== stdout);
return stdout;
} : run_code;
} : semver.satisfies(process.version, "<8") ? run_code_vm : function(code, toplevel, timeout) {
return (/async function/.test(code) ? run_code_exec : run_code_vm)(code, toplevel, timeout);
Copy link
Contributor

@kzc kzc Jan 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the fact that it differentiates between async test cases and regular ones for perf reasons, but that regex won't catch async arrow functions or when the tokens span multiple lines.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I forgot to append [WIP] to this PR − not planning to merge this, just thought I'd clean up the stage a bit so you can have a go when you want to do so 😉

@kzc
Copy link
Contributor

kzc commented Jan 9, 2021

Very cool.

Can we force certain tests to be async even without an async or await keyword? For example - a classic ES5 test invoking setTimeout? The test could have a property async: true (or perhaps spawn: true) for such tests to force the use of run_code_exec.

@kzc
Copy link
Contributor

kzc commented Jan 9, 2021

when async is in use, anything relying on Unicode will break (at least on Windows)

That's very odd. Must be a Windows thing. Can you force a certain Windows unicode code page or something like that?

it is very slow 🐢

No surprise there - but at least it works - that's the important thing.

It should be possible to squeeze some extra performance from it with a reusable child process and a wire protocol from the main program to avoid the process spawning overhead.

@alexlamsl alexlamsl changed the title support async test cases properly [WIP] support async test cases properly Jan 9, 2021
@alexlamsl
Copy link
Collaborator Author

Can you force a certain Windows unicode code page or something like that?

I tried to no avail − it's a Node.js bug, as I can see and print Unicode in Command Prompt without issues.

Node.js v15 also screws up the ANSI colour codes, so no surprises there. I have tried to make Node.js use Buffer instead of strings for stdio, but that doesn't seem to make a difference.

@alexlamsl
Copy link
Collaborator Author

It should be possible to squeeze some extra performance from it with a reusable child process and a wire protocol from the main program to avoid the process spawning overhead.

Since some test cases creates and modifies global variables, will need to make sure everything's cleaned up properly.

And then there's the problem of using any asynchronous API calls which requires extensive rewrites of all the users of sandbox.run_code() − that I'm not thrilled about in the slightest...

@alexlamsl
Copy link
Collaborator Author

Can we force certain tests to be async even without an async or await keyword?

Could do − as I said earlier, this PR is more of a proof of concept at this stage, as there will be kinks to work out in order for it to work across all Node.js versions and OS platforms.

@alexlamsl
Copy link
Collaborator Author

BTW child_process.execSync() is only available in Node.js v0.12+, so we can't really test on a ES5 runtime for things like setTimeout() − but then I guess that's not a major issue.

@kzc
Copy link
Contributor

kzc commented Jan 9, 2021

Great proof of concept in any event.

I don't recall whether function*/yield test cases were also limited by the vm sandbox relying on next tick.

Node.js v15 also screws up the ANSI colour codes

I hate that new useless color feature in NodeJS. Should just disable it with that env var NO_COLOR and call it a day.

@alexlamsl alexlamsl force-pushed the sandbox branch 2 times, most recently from b1da9db to 96ef385 Compare January 9, 2021 21:50
@alexlamsl
Copy link
Collaborator Author

Right, let's see if my workaround for the newly discovered quirk on Node.js v0.12 works.

I've expanded the regex to cover all forms of async and the timer functions as well, so I guess we don't need a special flag for that.

I don't recall whether function*/yield test cases were also limited by the vm sandbox relying on next tick

Anything that gets scheduled on a queue would escape vm sandbox 😓

I hate that new useless color feature in NodeJS.

At least you only hate that one feature... 😉

@kzc
Copy link
Contributor

kzc commented Jan 9, 2021

I've expanded the regex to cover all forms of async and the timer functions as well, so I guess we don't need a special flag for that.

The dozen non-asynchronous tests using async variable and function names would be slowed down by exec needlessly. Sometimes would be useful to override behavior.

@alexlamsl
Copy link
Collaborator Author

Fair point − I was more concerned with ufuzz not being able to use such a feature flag, but yes if we do have a sizable volume of synchronous compress tests that run via execSync the slowdown would be noticable.

@alexlamsl
Copy link
Collaborator Author

Looks like what happened to Node.js v0.12 back in #4495 is happening to Node.js v6 as well 👻

@alexlamsl
Copy link
Collaborator Author

alexlamsl commented Jan 10, 2021

test.js

if (0) (function() {
/*
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 * abcdefghijklmnopqrstuvwxyz
 */
})();
(function({
    [function() {
        console.log(typeof f);
    }()]: a,
}) {})(0);
$ node -v
v6.17.1
$ cat test.js | node

$ node test.js
undefined
 
$ node -v
v8.17.0
$ cat test.js | node
undefined

$ node test.js
undefined
 

@kzc
Copy link
Contributor

kzc commented Jan 10, 2021

Buffer overflow in Node v6?

@alexlamsl
Copy link
Collaborator Author

Given the fact that it relies on the destructuring function parameter with computed key to trigger, I'd lean towards some parser bug on the "slow" path, routed by input byte size, which isn't present on the "fast" path.

@alexlamsl alexlamsl force-pushed the sandbox branch 3 times, most recently from 3b3b6c3 to 61c3e4b Compare January 10, 2021 22:31
@kzc
Copy link
Contributor

kzc commented Jan 11, 2021

exec won't be fast enough for ufuzz and test/reduce without some form of child process reuse - perhaps even a pool of child processes. Might this dual vm/exec sandbox be used for test/compress only for the time being, with ufuzz and test/reduce only using vm?

@alexlamsl
Copy link
Collaborator Author

Any idea how to make a pool of child processes and perform synchronous send/receive on Node.js? 😅

I'm in no hurry to merge this PR − actually amused that all the tests passed so soon, but will just keep it tracking master for now, get more ufuzz exposure in parallel to shake out other potential compatibility issues.

@kzc
Copy link
Contributor

kzc commented Jan 11, 2021

Any idea how to make a pool of child processes and perform synchronous send/receive on Node.js?

I guess it wouldn't be. Would have to shim Promise support into the compress test runner.

@alexlamsl
Copy link
Collaborator Author

I guess it wouldn't be. Would have to shim Promise support into the compress test runner.

I've found a performant solution reusing a child process without turning sandbox.run_code() asynchronous − but that reveals the next roadblock:

  • Without the luxury of child process exiting to signal completion, how do you know within the child process that all of asynchronous test case has finished?

@alexlamsl
Copy link
Collaborator Author

An order of magnitude slower than typical test/reduce vm cases?

That's the best case − for some reason the variation between runs is huge as well.

The sandbox ought to have an option to force the use of exec (or vm for that matter), and in turn it could be enabled by the CLI with --test-reduce=exec or --test-reduce=vm.

Could do, but then these failures are so esoteric, and strictly applies to a code base which showed little intention of tackling them, I'd prefer the existing behaviour of watching v8 die than getting bugged by another false positive TBH.

@alexlamsl
Copy link
Collaborator Author

The (currently discovered) showstopper for using later version of Node.js for ufuzz is the bypassing of timeout during vm.runInThisContext() which effectively halts the job until it is killed.

run_code_exec() would have workaround that, however I don't see a way to effectively detect these cases, and the frequency of occurrence would take an unacceptable toil on the fuzzing rate as well.

@kzc
Copy link
Contributor

kzc commented Jan 16, 2021

these failures are so esoteric, and strictly applies to a code base which showed little intention of tackling them, I'd prefer the existing behaviour of watching v8 die than getting bugged by another false positive TBH.

That's fair, I guess it's strictly a V8 issue, not something that can be remedied within uglify minify.

The (currently discovered) showstopper for using later version of Node.js for ufuzz is the bypassing of timeout during vm.runInThisContext() which effectively halts the job until it is killed.

vm is so limited in NodeJS, and its utility is decreasing with each new release. Makes you wonder what use case it was designed for.

@kzc
Copy link
Contributor

kzc commented Jan 16, 2021

That's the best case − for some reason the variation between runs is huge as well.

In this PR, is run_code_exec presently used more than it should due to ufuzz generating synchronous programs with variables and functions named async? i.e., what is the rough percentage of ufuzz generated programs with an async keyword or symbol name?

@alexlamsl
Copy link
Collaborator Author

vm is so limited in NodeJS, and its utility is decreasing with each new release. Makes you wonder what use case it was designed for.

The kindest thing I can say is "hindsight's 20/20" 😏

In this PR, is run_code_exec presently used more than it should due to ufuzz generating synchronous programs with variables and functions named async?

Oh that issue is mostly resolved now − ufuzz timings for this PR is now within acceptable limits. There are occasional jobs getting stuck for some mysterious reasons, but they are both rare and shouldn't stay stuck for the scheduled jobs as they are determined and killed by the parent process instead.

My plan is to merge this after the next release − it's just that I've been getting a trickling feed of ufuzz failures on master which is why this is still hanging around for further studies.

Assuming an uneventful evening I'll make that release tomorrow and we can turn our full attention to watching this PR does wonders 😎

@alexlamsl alexlamsl force-pushed the sandbox branch 4 times, most recently from 82e13a7 to fbfd852 Compare January 19, 2021 02:42
@alexlamsl alexlamsl merged commit d37ee4d into mishoo:master Jan 19, 2021
@alexlamsl alexlamsl deleted the sandbox branch January 19, 2021 23:27
@kzc
Copy link
Contributor

kzc commented Jan 20, 2021

Is fuzzing slower now with the hybrid vm/exec sandbox?

@alexlamsl
Copy link
Collaborator Author

Nothing measurable beyond the usual variations between runs yet.

Not helped by the fact that GitHub seems to be having issues with scheduling workflows, or marking one as "Completed" regardless of whether its jobs are running or even started.

Whatever they thought to be "resolved" clearly hasn't 🤔
https://www.githubstatus.com/incidents/rk5rhrklkdk3

@alexlamsl
Copy link
Collaborator Author

Let's hope these don't pop up too frequently 👻
https://github.com/mishoo/UglifyJS/runs/1732658088?check_suite_focus=true#step:4:1603

@kzc
Copy link
Contributor

kzc commented Jan 20, 2021

I'm trying to grok the transform that avoids a core dump in NodeJS v8.x...

$ echo '(async (foo = [ ([] = 0), ...0 ]) => 0)();' | uglify-js -bc
(async (foo = ([] = 0, [ ...0 ])) => 0)();

Is the default value [ ([] = 0), ...0 ] equivalent to ([] = 0, [ ...0 ]) ?

@alexlamsl
Copy link
Collaborator Author

No, they are not equivalent − but that's alright because foo is unused:

UglifyJS/lib/compress.js

Lines 6206 to 6210 in 7793c6c

function trim_default(trimmer, node) {
node.value = node.value.transform(tt);
var name = node.name.transform(trimmer);
if (!name) {
var value = node.value.drop_side_effect_free(compressor);

$ echo '(async (foo = [ ([] = 0), ...0 ]) => 0)();' | uglify-js -bc unused=0
(async (foo = [ [] = 0, ...0 ]) => 0)();

@kzc
Copy link
Contributor

kzc commented Jan 20, 2021

that's alright because foo is unused

Ah, yes of course. The side effects are retained. Thanks.

Side note... test/reduce could drop the async designation of the function and still produce a reduced test case with a core dump in NodeJS v8.x. But it's trickier with concrete classes for all the various Async function combinations instead of an async property - a new non-async function node would have to be created and the attributes copied.

@alexlamsl
Copy link
Collaborator Author

Indeed − taking the async off the arrow function and using run_code_exec() unconditionally yields:

((foo = [ [] = 0, ...0 ]) => 0)();

... which works on Node.js 6 👻

@alexlamsl
Copy link
Collaborator Author

This is the same v8 (non-crashing) bug, but this time uglify-js triggers rather than suppresses it:

// reduced test case (output will differ)

// (beautified)
(async (NaN_2 = 0(...[ ...0, "object" ])) => {})();
// output: 
// minify: SyntaxError: Rest parameter must be last formal parameter
// options: {
//   "mangle": false,
//   "output": {
//     "v8": true
//   },
//   "validate": true
// }

@kzc
Copy link
Contributor

kzc commented Jan 21, 2021

You can't win 'em all.

@kzc
Copy link
Contributor

kzc commented Jan 26, 2021

when async is in use, anything relying on Unicode will break (at least on Windows)

Can you force a certain Windows unicode code page or something like that?

I tried to no avail − it's a Node.js bug, as I can see and print Unicode in Command Prompt without issues.

Not sure if you're still experiencing this issue. If so, here's a windows unicode bug workaround:

evanw/esbuild@93423e5

@alexlamsl
Copy link
Collaborator Author

Indeed − thanks for the pointer 👍

Node's fs.write API is broken when writing Unicode to stdout and stderr on Windows, and this will never be fixed:

... I see we share the same enthusiasm 😏

@alexlamsl
Copy link
Collaborator Author

Unfortunately we are running into a different Unicode bug than their workaround has provided (and no it's not because Command Prompt doesn't support Unicode by default − the only program that has b0rk3n output is Node.js)

@alexlamsl
Copy link
Collaborator Author

Specifically, it's the unpaired surrogate characters which are being mistranslated.

@kzc
Copy link
Contributor

kzc commented Jan 26, 2021

The unicode code page hack chcp 65001 mentioned in that NodeJS issue doesn't resolve it either?

@alexlamsl
Copy link
Collaborator Author

The unicode code page hack chcp 65001 mentioned in that NodeJS issue doesn't resolve it either?

First thing I tried after Google-fu − does not change any behaviour on later versions of Windows 😏

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

Successfully merging this pull request may close these issues.

2 participants