From: Alex Lam S.L Date: Mon, 17 Feb 2020 15:35:07 +0000 (+0000) Subject: speed up `--reduce-test` (#3726) X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=53517db3e40e9a2b2a6cc64b0dd7dcef0305ec1e;p=UglifyJS.git speed up `--reduce-test` (#3726) - avoid pathological test case branches via adaptive time-out - use initial test case elapsed time to adjust maximum time-out - index output cache using hash instead of raw source --- diff --git a/test/mocha/reduce.js b/test/mocha/reduce.js index f55c278d..c7d5a7de 100644 --- a/test/mocha/reduce.js +++ b/test/mocha/reduce.js @@ -2,6 +2,7 @@ var assert = require("assert"); var exec = require("child_process").exec; var fs = require("fs"); var reduce_test = require("../reduce"); +var semver = require("semver"); function read(path) { return fs.readFileSync(path, "utf8"); @@ -43,7 +44,6 @@ describe("test/reduce.js", function() { "// {", '// "toplevel": true', "// }", - "", ].join("\n")); }); it("Should handle test result of NaN", function() { @@ -55,7 +55,6 @@ describe("test/reduce.js", function() { '// "compress": {},', '// "mangle": false', "// }", - "", ].join("\n")); }); it("Should print correct output for irreducible test case", function() { @@ -136,4 +135,33 @@ describe("test/reduce.js", function() { "// }", ].join("\n")); }); + it("Should reduce infinite loops with reasonable performance", function() { + if (semver.satisfies(process.version, "0.10")) return; + this.timeout(120000); + var code = [ + "var a = 9007199254740992, b = 1;", + "", + "while (a++ + (1 - b) < a) {", + " 0;", + "}", + ].join("\n"); + var result = reduce_test(code, { + compress: { + unsafe_math: true, + }, + mangle: false, + }); + if (result.error) throw result.error; + assert.strictEqual(result.code.replace(/ timed out after [0-9]+ms/, " timed out."), [ + code, + "// output: ", + "// minify: Error: Script execution timed out.", + "// options: {", + '// "compress": {', + '// "unsafe_math": true', + "// },", + '// "mangle": false', + "// }", + ].join("\n")); + }); }); diff --git a/test/reduce.js b/test/reduce.js index 0b31ea8e..0d803aad 100644 --- a/test/reduce.js +++ b/test/reduce.js @@ -1,3 +1,4 @@ +var crypto = require("crypto"); var U = require("./node"); var List = U.List; var sandbox = require("./sandbox"); @@ -17,21 +18,32 @@ var sandbox = require("./sandbox"); // Returns a `minify` result object with an additonal boolean property `reduced`. module.exports = function reduce_test(testcase, minify_options, reduce_options) { + if (testcase instanceof U.AST_Node) testcase = testcase.print_to_string(); minify_options = minify_options || { compress: {}, mangle: false }; reduce_options = reduce_options || {}; var max_iterations = reduce_options.max_iterations || 1000; - var max_timeout = reduce_options.max_timeout || 15000; + var max_timeout = reduce_options.max_timeout || 10000; var verbose = reduce_options.verbose; var minify_options_json = JSON.stringify(minify_options, null, 2); - var timeout = 1000; // start with a low timeout var result_cache = Object.create(null); - var differs; - - if (testcase instanceof U.AST_Node) testcase = testcase.print_to_string(); - // the initial timeout to assess the viability of the test case must be large - if (differs = producesDifferentResultWhenMinified(result_cache, testcase, minify_options, max_timeout)) { - if (differs.error) return differs; + var differs = producesDifferentResultWhenMinified(result_cache, testcase, minify_options, max_timeout); + + if (!differs) { + // same stdout result produced when minified + return { + code: "// Can't reproduce test failure with minify options provided:" + + "\n// " + to_comment(minify_options_json) + }; + } else if (differs.timed_out) { + return { + code: "// Can't reproduce test failure within " + max_timeout + "ms:" + + "\n// " + to_comment(minify_options_json) + }; + } else if (differs.error) { + return differs; + } else { + max_timeout = Math.min(100 * differs.elapsed, max_timeout); // Replace expressions with constants that will be parsed into // AST_Nodes as required. Each AST_Node has its own permutation count, // so these replacements can't be shared. @@ -400,15 +412,11 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options) console.error("*** Discarding permutation and continuing."); continue; } - var diff = producesDifferentResultWhenMinified(result_cache, code, minify_options, timeout); + var diff = producesDifferentResultWhenMinified(result_cache, code, minify_options, max_timeout); if (diff) { if (diff.timed_out) { // can't trust the validity of `code_ast` and `code` when timed out. // no harm done - just ignore latest change and continue iterating. - if (timeout < max_timeout) { - timeout += 250; - result_cache = Object.create(null); - } } else if (diff.error) { // something went wrong during minify() - could be malformed AST or genuine bug. // no harm done - just log code & error, ignore latest change and continue iterating. @@ -433,23 +441,23 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options) console.error("// reduce test pass " + pass + ": " + testcase.length + " bytes"); } } - testcase += "\n// output: " + to_comment(differs.unminified_result) - + "\n// minify: " + to_comment(differs.minified_result) - + "\n// options: " + to_comment(minify_options_json); - } else { - // same stdout result produced when minified - testcase = "// Can't reproduce test failure with minify options provided:" - + "\n// " + to_comment(minify_options_json); + testcase = U.minify(testcase, { + compress: false, + mangle: false, + output: { + beautify: true, + braces: true, + comments: true, + }, + }); + testcase.code += [ + "", + "// output: " + to_comment(differs.unminified_result), + "// minify: " + to_comment(differs.minified_result), + "// options: " + to_comment(minify_options_json), + ].join("\n").replace(/\u001b\[\d+m/g, ""); + return testcase; } - return U.minify(testcase.replace(/\u001b\[\d+m/g, ""), { - compress: false, - mangle: false, - output: { - beautify: true, - braces: true, - comments: true, - } - }); }; function to_comment(value) { @@ -489,6 +497,10 @@ function is_error(result) { return typeof result == "object" && typeof result.name == "string" && typeof result.message == "string"; } +function is_timed_out(result) { + return is_error(result) && /timed out/.test(result); +} + function is_statement(node) { return node instanceof U.AST_Statement && !(node instanceof U.AST_Function); } @@ -519,23 +531,30 @@ function to_statement(node) { } function run_code(result_cache, code, toplevel, timeout) { - return result_cache[code] || (result_cache[code] = sandbox.run_code(code, toplevel, timeout)); + var key = crypto.createHash("sha1").update(code).digest("base64"); + return result_cache[key] || (result_cache[key] = sandbox.run_code(code, toplevel, timeout)); } -function producesDifferentResultWhenMinified(result_cache, code, minify_options, timeout) { +function producesDifferentResultWhenMinified(result_cache, code, minify_options, max_timeout) { var minified = U.minify(code, minify_options); if (minified.error) return minified; var toplevel = minify_options.toplevel; - var unminified_result = run_code(result_cache, code, toplevel, timeout); - if (/timed out/i.test(unminified_result)) return false; - + var elapsed = Date.now(); + var unminified_result = run_code(result_cache, code, toplevel, max_timeout); + elapsed = Date.now() - elapsed; + var timeout = Math.min(100 * elapsed, max_timeout); var minified_result = run_code(result_cache, minified.code, toplevel, timeout); - if (/timed out/i.test(minified_result)) return { timed_out: true }; - return !sandbox.same_stdout(unminified_result, minified_result) ? { + if (sandbox.same_stdout(unminified_result, minified_result)) { + return is_timed_out(unminified_result) && is_timed_out(minified_result) && { + timed_out: true, + }; + } + return { unminified_result: unminified_result, minified_result: minified_result, - } : false; + elapsed: elapsed, + }; } Error.stackTraceLimit = Infinity;