| /* |
| * Copyright 2017 WebAssembly Community Group participants |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // |
| // Tries to reduce the input wasm into the smallest possible wasm |
| // that still generates the same result on a given command. This is |
| // useful to reduce bug testcases, for example, if a file crashes |
| // the optimizer, you can reduce it to find the smallest file that |
| // also crashes it (which generally will show the same bug, in a |
| // much more debuggable manner). |
| // |
| |
| #include <memory> |
| #include <cstdio> |
| #include <cstdlib> |
| |
| #include "pass.h" |
| #include "support/command-line.h" |
| #include "support/file.h" |
| #include "support/path.h" |
| #include "support/timing.h" |
| #include "wasm-io.h" |
| #include "wasm-builder.h" |
| #include "ir/literal-utils.h" |
| #include "wasm-validator.h" |
| #ifdef _WIN32 |
| #ifndef NOMINMAX |
| #define NOMINMAX |
| #endif |
| #include <Windows.h> |
| // Create a string with last error message |
| std::string GetLastErrorStdStr() { |
| DWORD error = GetLastError(); |
| if (error) { |
| LPVOID lpMsgBuf; |
| DWORD bufLen = FormatMessage( |
| FORMAT_MESSAGE_ALLOCATE_BUFFER | |
| FORMAT_MESSAGE_FROM_SYSTEM | |
| FORMAT_MESSAGE_IGNORE_INSERTS, |
| NULL, |
| error, |
| MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), |
| (LPTSTR) &lpMsgBuf, |
| 0, NULL ); |
| if (bufLen) { |
| LPCSTR lpMsgStr = (LPCSTR)lpMsgBuf; |
| std::string result(lpMsgStr, lpMsgStr+bufLen); |
| LocalFree(lpMsgBuf); |
| return result; |
| } |
| } |
| return std::string(); |
| } |
| #endif |
| using namespace wasm; |
| |
| // a timeout on every execution of the command |
| size_t timeout = 2; |
| |
| struct ProgramResult { |
| int code; |
| std::string output; |
| double time; |
| |
| ProgramResult() {} |
| ProgramResult(std::string command) { |
| getFromExecution(command); |
| } |
| |
| #ifdef _WIN32 |
| void getFromExecution(std::string command) { |
| Timer timer; |
| timer.start(); |
| SECURITY_ATTRIBUTES saAttr; |
| saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); |
| saAttr.bInheritHandle = TRUE; |
| saAttr.lpSecurityDescriptor = NULL; |
| |
| HANDLE hChildStd_OUT_Rd; |
| HANDLE hChildStd_OUT_Wr; |
| |
| if ( |
| // Create a pipe for the child process's STDOUT. |
| !CreatePipe(&hChildStd_OUT_Rd, &hChildStd_OUT_Wr, &saAttr, 0) || |
| // Ensure the read handle to the pipe for STDOUT is not inherited. |
| !SetHandleInformation(hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0) |
| ) { |
| Fatal() << "CreatePipe \"" << command << "\" failed: " << GetLastErrorStdStr() << ".\n"; |
| } |
| |
| STARTUPINFO si; |
| PROCESS_INFORMATION pi; |
| |
| ZeroMemory(&si, sizeof(si)); |
| si.cb = sizeof(si); |
| si.hStdError = hChildStd_OUT_Wr; |
| si.hStdOutput = hChildStd_OUT_Wr; |
| si.dwFlags |= STARTF_USESTDHANDLES; |
| ZeroMemory(&pi, sizeof(pi)); |
| |
| // Start the child process. |
| if (!CreateProcess(NULL, // No module name (use command line) |
| (LPSTR)command.c_str(),// Command line |
| NULL, // Process handle not inheritable |
| NULL, // Thread handle not inheritable |
| TRUE, // Set handle inheritance to TRUE |
| 0, // No creation flags |
| NULL, // Use parent's environment block |
| NULL, // Use parent's starting directory |
| &si, // Pointer to STARTUPINFO structure |
| &pi ) // Pointer to PROCESS_INFORMATION structure |
| ) { |
| Fatal() << "CreateProcess \"" << command << "\" failed: " << GetLastErrorStdStr() << ".\n"; |
| } |
| |
| // Wait until child process exits. |
| DWORD retVal = WaitForSingleObject(pi.hProcess, timeout * 1000); |
| if (retVal == WAIT_TIMEOUT) { |
| printf("Command timeout: %s", command.c_str()); |
| TerminateProcess(pi.hProcess, -1); |
| } |
| DWORD dwordExitCode; |
| if (!GetExitCodeProcess(pi.hProcess, &dwordExitCode)) { |
| Fatal() << "GetExitCodeProcess failed: " << GetLastErrorStdStr() << ".\n"; |
| } |
| code = (int)dwordExitCode; |
| |
| // Close process and thread handles. |
| CloseHandle(pi.hProcess); |
| CloseHandle(pi.hThread); |
| |
| // Read output from the child process's pipe for STDOUT |
| // Stop when there is no more data. |
| { |
| const int BUFSIZE = 4096; |
| DWORD dwRead, dwTotal, dwTotalRead = 0; |
| CHAR chBuf[BUFSIZE]; |
| BOOL bSuccess = FALSE; |
| |
| PeekNamedPipe(hChildStd_OUT_Rd, NULL, 0, NULL, &dwTotal, NULL); |
| while (dwTotalRead < dwTotal) { |
| bSuccess = ReadFile(hChildStd_OUT_Rd, chBuf, BUFSIZE - 1, &dwRead, NULL); |
| if (!bSuccess || dwRead == 0) break; |
| chBuf[dwRead] = 0; |
| dwTotalRead += dwRead; |
| output.append(chBuf); |
| } |
| } |
| timer.stop(); |
| time = timer.getTotal(); |
| } |
| #else // POSIX |
| // runs the command and notes the output |
| // TODO: also stderr, not just stdout? |
| void getFromExecution(std::string command) { |
| Timer timer; |
| timer.start(); |
| // do this using just core stdio.h and stdlib.h, for portability |
| // sadly this requires two invokes |
| code = system(("timeout " + std::to_string(timeout) + "s " + command + " > /dev/null 2> /dev/null").c_str()); |
| const int MAX_BUFFER = 1024; |
| char buffer[MAX_BUFFER]; |
| FILE *stream = popen(("timeout " + std::to_string(timeout) + "s " + command + " 2> /dev/null").c_str(), "r"); |
| while (fgets(buffer, MAX_BUFFER, stream) != NULL) { |
| output.append(buffer); |
| } |
| pclose(stream); |
| timer.stop(); |
| time = timer.getTotal() / 2; |
| } |
| #endif // _WIN32 |
| |
| bool operator==(ProgramResult& other) { |
| return code == other.code && output == other.output; |
| } |
| bool operator!=(ProgramResult& other) { |
| return !(*this == other); |
| } |
| |
| bool failed() { |
| return code != 0; |
| } |
| |
| void dump(std::ostream& o) { |
| o << "[ProgramResult] code: " << code << " stdout: \n" << output << "[====]\nin " << time << " seconds\n[/ProgramResult]\n"; |
| } |
| }; |
| |
| namespace std { |
| |
| inline std::ostream& operator<<(std::ostream& o, ProgramResult& result) { |
| result.dump(o); |
| return o; |
| } |
| |
| } |
| |
| ProgramResult expected; |
| |
| // Removing functions is extremely beneficial and efficient. We aggressively |
| // try to remove functions, unless we've seen they can't be removed, in which |
| // case we may try again but much later. |
| static std::unordered_set<Name> functionsWeTriedToRemove; |
| |
| struct Reducer : public WalkerPass<PostWalker<Reducer, UnifiedExpressionVisitor<Reducer>>> { |
| std::string command, test, working; |
| bool verbose, debugInfo; |
| |
| // test is the file we write to that the command will operate on |
| // working is the current temporary state, the reduction so far |
| Reducer(std::string command, std::string test, std::string working, bool verbose, bool debugInfo) : command(command), test(test), working(working), verbose(verbose), debugInfo(debugInfo) {} |
| |
| // runs passes in order to reduce, until we can't reduce any more |
| // the criterion here is wasm binary size |
| void reduceUsingPasses() { |
| // run optimization passes until we can't shrink it any more |
| std::vector<std::string> passes = { |
| "-Oz", |
| "-Os", |
| "-O1", |
| "-O2", |
| "-O3", |
| "--coalesce-locals --vacuum", |
| "--dce", |
| "--duplicate-function-elimination", |
| "--inlining", |
| "--inlining-optimizing", |
| "--optimize-level=3 --inlining-optimizing", |
| "--local-cse --vacuum", |
| "--memory-packing", |
| "--remove-unused-names --merge-blocks --vacuum", |
| "--optimize-instructions", |
| "--precompute", |
| "--remove-imports", |
| "--remove-memory", |
| "--remove-unused-names --remove-unused-brs", |
| "--remove-unused-module-elements", |
| "--reorder-functions", |
| "--reorder-locals", |
| "--simplify-locals --vacuum", |
| "--vacuum" |
| }; |
| auto oldSize = file_size(working); |
| bool more = true; |
| while (more) { |
| //std::cerr << "| starting passes loop iteration\n"; |
| more = false; |
| // try both combining with a generic shrink (so minor pass overhead is compensated for), and without |
| for (auto shrinking : { false, true }) { |
| for (auto pass : passes) { |
| std::string currCommand = Path::getBinaryenBinaryTool("wasm-opt") + " "; |
| if (shrinking) currCommand += " --dce --vacuum "; |
| currCommand += working + " -o " + test + " " + pass; |
| if (debugInfo) currCommand += " -g "; |
| if (verbose) std::cerr << "| trying pass command: " << currCommand << "\n"; |
| if (!ProgramResult(currCommand).failed()) { |
| auto newSize = file_size(test); |
| if (newSize < oldSize) { |
| // the pass didn't fail, and the size looks smaller, so promising |
| // see if it is still has the property we are preserving |
| if (ProgramResult(command) == expected) { |
| std::cerr << "| command \"" << currCommand << "\" succeeded, reduced size to " << newSize << ", and preserved the property\n"; |
| copy_file(test, working); |
| more = true; |
| oldSize = newSize; |
| } |
| } |
| } |
| } |
| } |
| } |
| if (verbose) std::cerr << "| done with passes for now\n"; |
| } |
| |
| // does one pass of slow and destructive reduction. returns whether it |
| // succeeded or not |
| // the criterion here is a logical change in the program. this may actually |
| // increase wasm size in some cases, but it should allow more reduction later. |
| // @param factor how much to ignore. starting with a high factor skips through |
| // most of the file, which is often faster than going one by one |
| // from the start |
| size_t reduceDestructively(int factor_) { |
| factor = factor_; |
| // prepare |
| loadWorking(); |
| reduced = 0; |
| funcsSeen = 0; |
| // before we do any changes, it should be valid to write out the module: |
| // size should be as expected, and output should be as expected |
| ProgramResult result; |
| if (!writeAndTestReduction(result)) { |
| std::cerr << "\n|! WARNING: writing before destructive reduction fails, very unlikely reduction can work\n" << result << '\n'; |
| } |
| // destroy! |
| walkModule(getModule()); |
| return reduced; |
| } |
| |
| void loadWorking() { |
| module = make_unique<Module>(); |
| Module wasm; |
| ModuleReader reader; |
| reader.read(working, *module); |
| builder = make_unique<Builder>(*module); |
| setModule(module.get()); |
| } |
| |
| // destructive reduction state |
| |
| size_t reduced; |
| Expression* beforeReduction; |
| std::unique_ptr<Module> module; |
| std::unique_ptr<Builder> builder; |
| Index funcsSeen; |
| int factor; |
| |
| // write the module and see if the command still fails on it as expected |
| bool writeAndTestReduction() { |
| ProgramResult result; |
| return writeAndTestReduction(result); |
| } |
| |
| bool writeAndTestReduction(ProgramResult& out) { |
| // write the module out |
| ModuleWriter writer; |
| writer.setBinary(true); |
| writer.setDebugInfo(debugInfo); |
| writer.write(*getModule(), test); |
| // note that it is ok for the destructively-reduced module to be bigger |
| // than the previous - each destructive reduction removes logical code, |
| // and so is strictly better, even if the wasm binary format happens to |
| // encode things slightly less efficiently. |
| // test it |
| out.getFromExecution(command); |
| return out == expected; |
| } |
| |
| bool shouldTryToReduce(size_t bonus = 1) { |
| static size_t counter = 0; |
| counter += bonus; |
| return (counter % factor) <= bonus; |
| } |
| |
| // tests a reduction on the current traversal node, and undos if it failed |
| bool tryToReplaceCurrent(Expression* with) { |
| auto* curr = getCurrent(); |
| //std::cerr << "try " << curr << " => " << with << '\n'; |
| if (curr->type != with->type) return false; |
| if (!shouldTryToReduce()) return false; |
| replaceCurrent(with); |
| if (!writeAndTestReduction()) { |
| replaceCurrent(curr); |
| return false; |
| } |
| std::cerr << "| tryToReplaceCurrent succeeded (in " << getLocation() << ")\n"; |
| noteReduction(); |
| return true; |
| } |
| |
| void noteReduction(size_t amount = 1) { |
| reduced += amount; |
| copy_file(test, working); |
| } |
| |
| // tests a reduction on an arbitrary child |
| bool tryToReplaceChild(Expression*& child, Expression* with) { |
| if (child->type != with->type) return false; |
| if (!shouldTryToReduce()) return false; |
| auto* before = child; |
| child = with; |
| if (!writeAndTestReduction()) { |
| child = before; |
| return false; |
| } |
| std::cerr << "| tryToReplaceChild succeeded (in " << getLocation() << ")\n"; |
| //std::cerr << "| " << before << " => " << with << '\n'; |
| noteReduction(); |
| return true; |
| } |
| |
| std::string getLocation() { |
| if (getFunction()) return getFunction()->name.str; |
| return "(non-function context)"; |
| } |
| |
| // visitors. in each we try to remove code in a destructive and nontrivial way. |
| // "nontrivial" means something that optimization passes can't achieve, since we |
| // don't need to duplicate work that they do |
| |
| void visitExpression(Expression* curr) { |
| if (curr->type == none) { |
| if (tryToReduceCurrentToNone()) return; |
| } else if (isConcreteType(curr->type)) { |
| if (tryToReduceCurrentToConst()) return; |
| } else { |
| assert(curr->type == unreachable); |
| if (tryToReduceCurrentToUnreachable()) return; |
| } |
| // specific reductions |
| if (auto* iff = curr->dynCast<If>()) { |
| if (iff->type == none) { |
| // perhaps we need just the condition? |
| if (tryToReplaceCurrent(builder->makeDrop(iff->condition))) { |
| return; |
| } |
| } |
| handleCondition(iff->condition); |
| } else if (auto* br = curr->dynCast<Break>()) { |
| handleCondition(br->condition); |
| } else if (auto* select = curr->dynCast<Select>()) { |
| handleCondition(select->condition); |
| } else if (auto* sw = curr->dynCast<Switch>()) { |
| handleCondition(sw->condition); |
| } else if (auto* set = curr->dynCast<SetLocal>()) { |
| if (set->isTee()) { |
| // maybe we don't need the set |
| tryToReplaceCurrent(set->value); |
| } |
| } else if (auto* unary = curr->dynCast<Unary>()) { |
| // maybe we can pass through |
| tryToReplaceCurrent(unary->value); |
| } else if (auto* binary = curr->dynCast<Binary>()) { |
| // maybe we can pass through |
| if (!tryToReplaceCurrent(binary->left)) { |
| tryToReplaceCurrent(binary->right); |
| } |
| } else if (auto* call = curr->dynCast<Call>()) { |
| handleCall(call); |
| } else if (auto* call = curr->dynCast<CallImport>()) { |
| handleCall(call); |
| } else if (auto* call = curr->dynCast<CallIndirect>()) { |
| if (tryToReplaceCurrent(call->target)) return; |
| handleCall(call); |
| } |
| } |
| |
| void visitFunction(Function* curr) { |
| // extra chance to work on the function toplevel element, as if it can |
| // be reduced it's great |
| visitExpression(curr->body); |
| // finish function |
| funcsSeen++; |
| static int last = 0; |
| int percentage = (100 * funcsSeen) / getModule()->functions.size(); |
| if (std::abs(percentage - last) >= 5) { |
| std::cerr << "| " << percentage << "% of funcs complete\n"; |
| last = percentage; |
| } |
| } |
| |
| // TODO: bisection on segment shrinking? |
| |
| void visitTable(Table* curr) { |
| std::cerr << "| try to simplify table\n"; |
| Name first; |
| for (auto& segment : curr->segments) { |
| for (auto item : segment.data) { |
| first = item; |
| break; |
| } |
| if (!first.isNull()) break; |
| } |
| visitSegmented(curr, first, 100); |
| } |
| |
| void visitMemory(Memory* curr) { |
| std::cerr << "| try to simplify memory\n"; |
| visitSegmented(curr, 0, 2); |
| } |
| |
| template<typename T, typename U> |
| void visitSegmented(T* curr, U zero, size_t bonus) { |
| // try to reduce to first function. first, shrink segment elements. |
| // while we are shrinking successfully, keep going exponentially. |
| bool justShrank = false; |
| bool shrank = false; |
| for (auto& segment : curr->segments) { |
| auto& data = segment.data; |
| size_t skip = 1; // when we succeed, try to shrink by more and more, similar to bisection |
| for (size_t i = 0; i < data.size() && !data.empty(); i++) { |
| if (!justShrank && !shouldTryToReduce(bonus)) continue; |
| auto save = data; |
| for (size_t j = 0; j < skip; j++) { |
| if (!data.empty()) data.pop_back(); |
| } |
| auto justShrank = writeAndTestReduction(); |
| if (justShrank) { |
| std::cerr << "| shrank segment (skip: " << skip << ")\n"; |
| shrank = true; |
| noteReduction(); |
| skip = std::min(size_t(factor), 2 * skip); |
| } else { |
| data = save; |
| break; |
| } |
| } |
| } |
| // the "opposite" of shrinking: copy a 'zero' element |
| for (auto& segment : curr->segments) { |
| if (segment.data.empty()) continue; |
| for (auto& item : segment.data) { |
| if (!shouldTryToReduce(bonus)) continue; |
| if (item == zero) continue; |
| auto save = item; |
| item = zero; |
| if (writeAndTestReduction()) { |
| std::cerr << "| zeroed segment\n"; |
| noteReduction(); |
| } else { |
| item = save; |
| } |
| if (shrank) { |
| // zeroing is fairly inefficient. if we are managing to shrink |
| // (which we do exponentially), just zero one per segment at most |
| break; |
| } |
| } |
| } |
| } |
| |
| void visitModule(Module* curr) { |
| assert(curr == module.get()); |
| // try to remove functions |
| std::cerr << "| try to remove functions\n"; |
| std::vector<Name> functionNames; |
| for (auto& func : module->functions) { |
| functionNames.push_back(func->name); |
| } |
| size_t skip = 1; |
| // If we just removed some functions in the previous iteration, keep trying to remove more |
| // as this is one of the most efficient ways to reduce. |
| bool justRemoved = false; |
| for (size_t i = 0; i < functionNames.size(); i++) { |
| if (!justRemoved && |
| functionsWeTriedToRemove.count(functionNames[i]) == 1 && |
| !shouldTryToReduce(std::max((factor / 100) + 1, 1000))) continue; |
| std::vector<Name> names; |
| for (size_t j = 0; names.size() < skip && i + j < functionNames.size(); j++) { |
| auto name = functionNames[i + j]; |
| if (module->getFunctionOrNull(name)) { |
| names.push_back(name); |
| functionsWeTriedToRemove.insert(name); |
| } |
| } |
| if (names.size() == 0) continue; |
| std::cout << "| try to remove " << names.size() << " functions (skip: " << skip << ")\n"; |
| justRemoved = tryToRemoveFunctions(names); |
| if (justRemoved) { |
| noteReduction(names.size()); |
| i += skip; |
| skip = std::min(size_t(factor), 2 * skip); |
| } else { |
| skip = std::max(skip / 2, size_t(1)); // or 1? |
| } |
| } |
| // try to remove exports |
| std::cerr << "| try to remove exports (with factor " << factor << ")\n"; |
| std::vector<Export> exports; |
| for (auto& exp : module->exports) { |
| exports.push_back(*exp); |
| } |
| skip = 1; |
| for (size_t i = 0; i < exports.size(); i++) { |
| if (!shouldTryToReduce(std::max((factor / 100) + 1, 1000))) continue; |
| std::vector<Export> currExports; |
| for (size_t j = 0; currExports.size() < skip && i + j < exports.size(); j++) { |
| auto exp = exports[i + j]; |
| if (module->getExportOrNull(exp.name)) { |
| currExports.push_back(exp); |
| module->removeExport(exp.name); |
| } |
| } |
| ProgramResult result; |
| if (!writeAndTestReduction(result)) { |
| for (auto exp : currExports) { |
| module->addExport(new Export(exp)); |
| } |
| skip = std::max(skip / 2, size_t(1)); // or 1? |
| } else { |
| std::cerr << "| removed " << currExports.size() << " exports\n"; |
| noteReduction(currExports.size()); |
| i += skip; |
| skip = std::min(size_t(factor), 2 * skip); |
| } |
| } |
| } |
| |
| bool tryToRemoveFunctions(std::vector<Name> names) { |
| for (auto name : names) { |
| module->removeFunction(name); |
| } |
| |
| // remove all references to them |
| struct FunctionReferenceRemover : public PostWalker<FunctionReferenceRemover> { |
| std::unordered_set<Name> names; |
| std::vector<Name> exportsToRemove; |
| |
| FunctionReferenceRemover(std::vector<Name>& vec) { |
| for (auto name : vec) { |
| names.insert(name); |
| } |
| } |
| void visitCall(Call* curr) { |
| if (names.count(curr->target)) { |
| replaceCurrent(Builder(*getModule()).replaceWithIdenticalType(curr)); |
| } |
| } |
| void visitExport(Export* curr) { |
| if (names.count(curr->value)) { |
| exportsToRemove.push_back(curr->name); |
| } |
| } |
| void visitTable(Table* curr) { |
| Name other; |
| for (auto& segment : curr->segments) { |
| for (auto name : segment.data) { |
| if (!names.count(name)) { |
| other = name; |
| break; |
| } |
| } |
| if (!other.isNull()) break; |
| } |
| if (other.isNull()) return; // we failed to find a replacement |
| for (auto& segment : curr->segments) { |
| for (auto& name : segment.data) { |
| if (names.count(name)) { |
| name = other; |
| } |
| } |
| } |
| } |
| void doWalkModule(Module* module) { |
| PostWalker<FunctionReferenceRemover>::doWalkModule(module); |
| for (auto name : exportsToRemove) { |
| module->removeExport(name); |
| } |
| } |
| }; |
| FunctionReferenceRemover referenceRemover(names); |
| referenceRemover.walkModule(module.get()); |
| |
| if (WasmValidator().validate(*module, Feature::All, WasmValidator::Globally | WasmValidator::Quiet) && |
| writeAndTestReduction()) { |
| std::cerr << "| removed " << names.size() << " functions\n"; |
| return true; |
| } else { |
| loadWorking(); // restore it from orbit |
| return false; |
| } |
| } |
| |
| // helpers |
| |
| // try to replace condition with always true and always false |
| void handleCondition(Expression*& condition) { |
| if (!condition) return; |
| if (condition->is<Const>()) return; |
| auto* c = builder->makeConst(Literal(int32_t(0))); |
| if (!tryToReplaceChild(condition, c)) { |
| c->value = Literal(int32_t(1)); |
| tryToReplaceChild(condition, c); |
| } |
| } |
| |
| template<typename T> |
| void handleCall(T* call) { |
| for (auto* op : call->operands) { |
| if (tryToReplaceCurrent(op)) return; |
| } |
| } |
| |
| bool tryToReduceCurrentToNone() { |
| auto* curr = getCurrent(); |
| if (curr->is<Nop>()) return false; |
| // try to replace with a trivial value |
| Nop nop; |
| if (tryToReplaceCurrent(&nop)) { |
| replaceCurrent(builder->makeNop()); |
| return true; |
| } |
| return false; |
| } |
| |
| // try to replace a concrete value with a trivial constant |
| bool tryToReduceCurrentToConst() { |
| auto* curr = getCurrent(); |
| if (curr->is<Const>()) return false; |
| // try to replace with a trivial value |
| Const* c = builder->makeConst(Literal(int32_t(0))); |
| if (tryToReplaceCurrent(c)) return true; |
| c->value = LiteralUtils::makeLiteralFromInt32(1, curr->type); |
| c->type = curr->type; |
| return tryToReplaceCurrent(c); |
| } |
| |
| bool tryToReduceCurrentToUnreachable() { |
| auto* curr = getCurrent(); |
| if (curr->is<Unreachable>()) return false; |
| // try to replace with a trivial value |
| Unreachable un; |
| if (tryToReplaceCurrent(&un)) { |
| replaceCurrent(builder->makeUnreachable()); |
| return true; |
| } |
| // maybe a return? TODO |
| return false; |
| } |
| }; |
| |
| // |
| // main |
| // |
| |
| int main(int argc, const char* argv[]) { |
| std::string input, test, working, command; |
| bool verbose = false, |
| debugInfo = false, |
| force = false; |
| Options options("wasm-reduce", "Reduce a wasm file to a smaller one that has the same behavior on a given command"); |
| options |
| .add("--command", "-cmd", "The command to run on the test, that we want to reduce while keeping the command's output identical. " |
| "We look at the command's return code and stdout here (TODO: stderr), " |
| "and we reduce while keeping those unchanged.", |
| Options::Arguments::One, |
| [&](Options* o, const std::string& argument) { |
| command = argument; |
| }) |
| .add("--test", "-t", "Test file (this will be written to to test, the given command should read it when we call it)", |
| Options::Arguments::One, |
| [&](Options* o, const std::string& argument) { |
| test = argument; |
| }) |
| .add("--working", "-w", "Working file (this will contain the current good state while doing temporary computations, " |
| "and will contain the final best result at the end)", |
| Options::Arguments::One, |
| [&](Options* o, const std::string& argument) { |
| working = argument; |
| }) |
| .add("--binaries", "-b", "binaryen binaries location (bin/ directory)", |
| Options::Arguments::One, |
| [&](Options* o, const std::string& argument) { |
| // Add separator just in case |
| Path::setBinaryenBinDir(argument + Path::getPathSeparator()); |
| }) |
| .add("--verbose", "-v", "Verbose output mode", |
| Options::Arguments::Zero, |
| [&](Options* o, const std::string& argument) { |
| verbose = true; |
| }) |
| .add("--debugInfo", "-g", "Keep debug info in binaries", |
| Options::Arguments::Zero, |
| [&](Options* o, const std::string& argument) { |
| debugInfo = true; |
| }) |
| .add("--force", "-f", "Force the reduction attempt, ignoring problems that imply it is unlikely to succeed", |
| Options::Arguments::Zero, |
| [&](Options* o, const std::string& argument) { |
| force = true; |
| }) |
| .add("--timeout", "-to", "A timeout to apply to each execution of the command, in seconds (default: 2)", |
| Options::Arguments::One, |
| [&](Options* o, const std::string& argument) { |
| timeout = atoi(argument.c_str()); |
| std::cout << "|applying timeout: " << timeout << "\n"; |
| }) |
| .add_positional("INFILE", Options::Arguments::One, |
| [&](Options* o, const std::string& argument) { |
| input = argument; |
| }); |
| options.parse(argc, argv); |
| |
| if (test.size() == 0) Fatal() << "test file not provided\n"; |
| if (working.size() == 0) Fatal() << "working file not provided\n"; |
| |
| std::cerr << "|wasm-reduce\n"; |
| std::cerr << "|input: " << input << '\n'; |
| std::cerr << "|test: " << test << '\n'; |
| std::cerr << "|working: " << working << '\n'; |
| |
| // get the expected output |
| copy_file(input, test); |
| expected.getFromExecution(command); |
| |
| std::cerr << "|expected result:\n" << expected << '\n'; |
| std::cerr << "|!! Make sure the above is what you expect! !!\n\n"; |
| |
| auto stopIfNotForced = [&](std::string message, ProgramResult& result) { |
| std::cerr << "|! " << message << '\n' << result << '\n'; |
| if (!force) { |
| Fatal() << "|! stopping, as it is very unlikely reduction can succeed (use -f to ignore this check)"; |
| } |
| }; |
| |
| if (expected.time + 1 >= timeout) { |
| stopIfNotForced("execution time is dangerously close to the timeout - you should probably increase the timeout", expected); |
| } |
| |
| std::cerr << "|checking that command has different behavior on invalid binary (this verifies that the test file is used by the command)\n"; |
| { |
| { |
| std::ofstream dst(test, std::ios::binary); |
| dst << "waka waka\n"; |
| } |
| ProgramResult result(command); |
| if (result == expected) { |
| stopIfNotForced("running command on an invalid module should give different results", result); |
| } |
| } |
| |
| std::cerr << "|checking that command has expected behavior on canonicalized (read-written) binary\n"; |
| { |
| // read and write it |
| ProgramResult readWrite(Path::getBinaryenBinaryTool("wasm-opt") + " " + input + " -o " + test); |
| if (readWrite.failed()) { |
| stopIfNotForced("failed to read and write the binary", readWrite); |
| } else { |
| ProgramResult result(command); |
| if (result != expected) { |
| stopIfNotForced("running command on the canonicalized module should give the same results", result); |
| } |
| } |
| } |
| |
| copy_file(input, working); |
| auto workingSize = file_size(working); |
| std::cerr << "|input size: " << workingSize << "\n"; |
| |
| std::cerr << "|starting reduction!\n"; |
| |
| int factor = workingSize * 2; |
| size_t lastDestructiveReductions = 0; |
| size_t lastPostPassesSize = 0; |
| |
| bool stopping = false; |
| |
| while (1) { |
| Reducer reducer(command, test, working, verbose, debugInfo); |
| |
| // run binaryen optimization passes to reduce. passes are fast to run |
| // and can often reduce large amounts of code efficiently, as opposed |
| // to detructive reduction (i.e., that doesn't preserve correctness as |
| // passes do) since destrucive must operate one change at a time |
| std::cerr << "| reduce using passes...\n"; |
| auto oldSize = file_size(working); |
| reducer.reduceUsingPasses(); |
| auto newSize = file_size(working); |
| auto passProgress = oldSize - newSize; |
| std::cerr << "| after pass reduction: " << newSize << "\n"; |
| |
| // always stop after a pass reduction attempt, for final cleanup |
| if (stopping) break; |
| |
| // check if the full cycle (destructive/passes) has helped or not |
| if (lastPostPassesSize && newSize >= lastPostPassesSize) { |
| std::cerr << "| progress has stopped, skipping to the end\n"; |
| if (factor == 1) { |
| // this is after doing work with factor 1, so after the remaining work, stop |
| stopping = true; |
| } else { |
| // just try to remove all we can and finish up |
| factor = 1; |
| } |
| } |
| lastPostPassesSize = newSize; |
| |
| // if destructive reductions lead to useful proportionate pass reductions, keep |
| // going at the same factor, as pass reductions are far faster |
| std::cerr << "| pass progress: " << passProgress << ", last destructive: " << lastDestructiveReductions << '\n'; |
| if (passProgress >= 4 * lastDestructiveReductions) { |
| // don't change |
| std::cerr << "| progress is good, do not quickly decrease factor\n"; |
| } else { |
| if (factor > 10) { |
| factor = (factor / 3) + 1; |
| } else { |
| factor = (factor + 1) / 2; // stable on 1 |
| } |
| } |
| |
| // no point in a factor lorger than the size |
| assert(newSize > 4); // wasm modules are >4 bytes anyhow |
| factor = std::min(factor, int(newSize) / 4); |
| |
| // try to reduce destructively. if a high factor fails to find anything, |
| // quickly try a lower one (no point in doing passes until we reduce |
| // destructively at least a little) |
| while (1) { |
| std::cerr << "| reduce destructively... (factor: " << factor << ")\n"; |
| lastDestructiveReductions = reducer.reduceDestructively(factor); |
| if (lastDestructiveReductions > 0) break; |
| // we failed to reduce destructively |
| if (factor == 1) { |
| stopping = true; |
| break; |
| } |
| factor = std::max(1, factor / 4); // quickly now, try to find *something* we can reduce |
| } |
| |
| std::cerr << "| destructive reduction led to size: " << file_size(working) << '\n'; |
| } |
| std::cerr << "|finished, final size: " << file_size(working) << "\n"; |
| copy_file(working, test); // just to avoid confusion |
| } |
| |