blob: 773b52ae66714b04528c0225775798a21a39fbdb [file] [log] [blame] [edit]
/*
* 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.
*/
#include "tools/wasm-reduce/reducer.h"
#include "destructive-reducer.h"
#include "support/file.h"
#include "support/path.h"
#include "support/timing.h"
#include "wasm-io.h"
namespace {
#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
} // anonymous namespace
namespace wasm {
void ProgramResult::getFromExecution(std::string command) {
#ifdef _WIN32
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
// TODO: also stderr, not just stdout?
Timer timer;
timer.start();
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);
}
code = pclose(stream);
timer.stop();
time = timer.getTotal();
#endif // _WIN32
}
// runs passes in order to reduce, until we can't reduce any more
// the criterion here is wasm binary size
void Reducer::reduceUsingPasses() {
// run optimization passes until we can't shrink it any more
std::vector<std::string> passes = {
// Optimization modes.
"-Oz",
"-Os",
"-O1",
"-O2",
"-O3",
"-O4",
// Optimization modes + passes that work well with them.
"--flatten -Os",
"--flatten -O3",
"--flatten --simplify-locals-notee-nostructure --local-cse -Os",
"--type-ssa -Os --type-merging",
"--gufa -O1",
// Individual passes or combinations of them.
"--coalesce-locals --vacuum",
"--dae",
"--dae-optimizing",
"--dce",
"--duplicate-function-elimination",
"--enclose-world",
"--gto",
"--inlining",
"--inlining-optimizing",
"--optimize-level=3 --inlining-optimizing",
"--local-cse",
"--memory-packing",
"--remove-unused-names --merge-blocks --vacuum",
"--optimize-instructions",
"--precompute",
"--remove-imports",
"--remove-memory-init",
"--remove-unused-names --remove-unused-brs",
"--remove-unused-module-elements",
"--remove-unused-nonfunction-module-elements",
"--reorder-functions",
"--reorder-locals",
// TODO: signature* passes
"--simplify-globals",
"--simplify-locals --vacuum",
"--strip",
"--remove-unused-types --closed-world",
"--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 pass : passes) {
std::string currCommand = Path::getBinaryenBinaryTool("wasm-opt") + " ";
currCommand += working + " -o " + test + " " + pass + " " + extraFlags;
if (!binary) {
currCommand += " -S ";
}
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 << '\n';
applyTestToWorking();
more = true;
oldSize = newSize;
}
}
}
}
}
if (verbose) {
std::cerr << "| done with passes for now\n";
}
}
// Apply the test file to the working file, after we saw that it successfully
// reduced the testcase.
void Reducer::applyTestToWorking() {
copy_file(test, working);
if (saveAllWorkingFiles) {
copy_file(working, working + '.' + std::to_string(workingFileIndex++));
}
}
size_t Reducer::reduceDestructively(int factor) {
this->factor = factor;
loadWorking();
// 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!
DestructiveReducer destructiveReducer(*this);
destructiveReducer.walkModule(&wasm);
return destructiveReducer.reduced;
}
bool Reducer::writeAndTestReduction() {
ProgramResult result;
return writeAndTestReduction(result);
}
bool Reducer::writeAndTestReduction(ProgramResult& out) {
// write the module out
ModuleWriter writer(toolOptions.passOptions);
writer.setBinary(binary);
writer.setDebugInfo(debugInfo);
writer.write(wasm, 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;
}
void Reducer::loadWorking() {
ModuleUtils::clearModule(wasm);
toolOptions.applyOptionsBeforeParse(wasm);
// Assume we may need all features.
wasm.features = FeatureSet::All;
ModuleReader reader;
try {
reader.read(working, wasm);
} catch (ParseException& p) {
p.dump(std::cerr);
std::cerr << '\n';
Fatal() << "error in parsing working wasm binary";
}
toolOptions.applyOptionsAfterParse(wasm);
}
// Returns a random number in the range [0, max). This is deterministic given
// all the previous work done in the reducer.
size_t Reducer::deterministicRandom(size_t max) {
assert(max > 0);
hash_combine(decisionCounter, max);
return decisionCounter % max;
}
bool Reducer::shouldTryToReduce(size_t bonus) {
assert(bonus > 0);
// Increment to avoid returning the same result each time.
decisionCounter += bonus;
return (decisionCounter % factor) <= bonus;
}
} // namespace wasm