summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/tools/wasm-split.cpp199
-rw-r--r--test/lit/wasm-split/export-name-already-exists.wast8
-rw-r--r--test/lit/wasm-split/instrument-funcs.wast75
-rw-r--r--test/lit/wasm-split/instrument-memory-too-small.wast10
4 files changed, 290 insertions, 2 deletions
diff --git a/src/tools/wasm-split.cpp b/src/tools/wasm-split.cpp
index 5f6207e9a..61f52d5fa 100644
--- a/src/tools/wasm-split.cpp
+++ b/src/tools/wasm-split.cpp
@@ -19,10 +19,13 @@
#include "ir/module-splitting.h"
#include "ir/module-utils.h"
+#include "ir/names.h"
#include "support/name.h"
#include "support/utilities.h"
#include "tool-options.h"
+#include "wasm-builder.h"
#include "wasm-io.h"
+#include "wasm-type.h"
#include "wasm-validator.h"
#include <sstream>
@@ -43,6 +46,7 @@ std::set<Name> parseNameList(const std::string& list) {
struct WasmSplitOptions : ToolOptions {
bool verbose = false;
+ bool emitBinary = true;
bool instrument = false;
@@ -162,6 +166,18 @@ WasmSplitOptions::WasmSplitOptions()
verbose = true;
quiet = false;
})
+ .add("--emit-text",
+ "-S",
+ "Emit text instead of binary for the output file or files.",
+ Options::Arguments::Zero,
+ [&](Options* o, const std::string& argument) { emitBinary = false; })
+ .add("--debuginfo",
+ "-g",
+ "Emit names section in wasm binary (or full debuginfo in wast)",
+ Options::Arguments::Zero,
+ [&](Options* o, const std::string& arguments) {
+ passOptions.debugInfo = true;
+ })
.add_positional(
"INFILE",
Options::Arguments::One,
@@ -245,8 +261,186 @@ void parseInput(Module& wasm, const WasmSplitOptions& options) {
options.applyFeatures(wasm);
}
+// Add a global monotonic counter and a timestamp global for each function, code
+// at the beginning of each function to set its timestamp, and a new exported
+// function for dumping the profile data.
+struct Instrumenter : public Pass {
+ PassRunner* runner = nullptr;
+ Module* wasm = nullptr;
+
+ const std::string& profileExport;
+ uint64_t moduleHash;
+
+ Name counterGlobal;
+ std::vector<Name> functionGlobals;
+
+ Instrumenter(const std::string& profileExport, uint64_t moduleHash);
+
+ void run(PassRunner* runner, Module* wasm) override;
+ void addGlobals();
+ void instrumentFuncs();
+ void addProfileExport();
+};
+
+Instrumenter::Instrumenter(const std::string& profileExport,
+ uint64_t moduleHash)
+ : profileExport(profileExport), moduleHash(moduleHash) {}
+
+void Instrumenter::run(PassRunner* runner, Module* wasm) {
+ this->runner = runner;
+ this->wasm = wasm;
+ addGlobals();
+ instrumentFuncs();
+ addProfileExport();
+}
+
+void Instrumenter::addGlobals() {
+ // Create fresh global names (over-reserves, but that's ok)
+ counterGlobal = Names::getValidGlobalName(*wasm, "monotonic_counter");
+ functionGlobals.reserve(wasm->functions.size());
+ ModuleUtils::iterDefinedFunctions(*wasm, [&](Function* func) {
+ functionGlobals.push_back(Names::getValidGlobalName(
+ *wasm, std::string(func->name.c_str()) + "_timestamp"));
+ });
+
+ // Create and add new globals
+ auto addGlobal = [&](Name name) {
+ auto global = Builder::makeGlobal(
+ name,
+ Type::i32,
+ Builder(*wasm).makeConst(Literal::makeZero(Type::i32)),
+ Builder::Mutable);
+ global->hasExplicitName = true;
+ wasm->addGlobal(std::move(global));
+ };
+ addGlobal(counterGlobal);
+ for (auto& name : functionGlobals) {
+ addGlobal(name);
+ }
+}
+
+void Instrumenter::instrumentFuncs() {
+ // Inject the following code at the beginning of each function to advance the
+ // monotonic counter and set the function's timestamp if it hasn't already
+ // been set.
+ //
+ // (if (i32.eqz (global.get $timestamp))
+ // (block
+ // (global.set $monotonic_counter
+ // (i32.add
+ // (global.get $monotonic_counter)
+ // (i32.const 1)
+ // )
+ // )
+ // (global.set $timestamp
+ // (global.get $monotonic_counter)
+ // )
+ // )
+ // )
+ Builder builder(*wasm);
+ auto globalIt = functionGlobals.begin();
+ ModuleUtils::iterDefinedFunctions(*wasm, [&](Function* func) {
+ func->body = builder.makeSequence(
+ builder.makeIf(
+ builder.makeUnary(EqZInt32,
+ builder.makeGlobalGet(*globalIt, Type::i32)),
+ builder.makeSequence(
+ builder.makeGlobalSet(
+ counterGlobal,
+ builder.makeBinary(AddInt32,
+ builder.makeGlobalGet(counterGlobal, Type::i32),
+ builder.makeConst(Literal::makeOne(Type::i32)))),
+ builder.makeGlobalSet(
+ *globalIt, builder.makeGlobalGet(counterGlobal, Type::i32)))),
+ func->body,
+ func->body->type);
+ ++globalIt;
+ });
+}
+
+void Instrumenter::addProfileExport() {
+ // Create and export a function to dump the profile into a given memory
+ // buffer. The function takes the available address and buffer size as
+ // arguments and returns the total size of the profile. It only actually
+ // writes the profile if the given space is sufficient to hold it.
+ auto name = Names::getValidFunctionName(*wasm, profileExport);
+ auto writeProfile = Builder::makeFunction(
+ name, Signature({Type::i32, Type::i32}, Type::i32), {});
+ writeProfile->hasExplicitName = true;
+ writeProfile->setLocalName(0, "addr");
+ writeProfile->setLocalName(1, "size");
+
+ // Calculate the size of the profile:
+ // 8 bytes module hash +
+ // 4 bytes for the timestamp for each function
+ const size_t profileSize = 8 + 4 * functionGlobals.size();
+
+ // Create the function body
+ Builder builder(*wasm);
+ auto getAddr = [&]() { return builder.makeLocalGet(0, Type::i32); };
+ auto getSize = [&]() { return builder.makeLocalGet(1, Type::i32); };
+ auto hashConst = [&]() { return builder.makeConst(int64_t(moduleHash)); };
+ auto profileSizeConst = [&]() {
+ return builder.makeConst(int32_t(profileSize));
+ };
+
+ // Write the hash followed by all the time stamps
+ Expression* writeData =
+ builder.makeStore(8, 0, 1, getAddr(), hashConst(), Type::i64);
+
+ uint32_t offset = 8;
+ for (const auto& global : functionGlobals) {
+ writeData = builder.blockify(
+ writeData,
+ builder.makeStore(4,
+ offset,
+ 1,
+ getAddr(),
+ builder.makeGlobalGet(global, Type::i32),
+ Type::i32));
+ offset += 4;
+ }
+
+ writeProfile->body = builder.makeSequence(
+ builder.makeIf(builder.makeBinary(GeUInt32, getSize(), profileSizeConst()),
+ writeData),
+ profileSizeConst());
+
+ // Create an export for the function
+ wasm->addFunction(std::move(writeProfile));
+ wasm->addExport(
+ Builder::makeExport(profileExport, name, ExternalKind::Function));
+
+ // Also make sure there is a memory with enough pages to write into
+ size_t pages = (profileSize + Memory::kPageSize - 1) / Memory::kPageSize;
+ if (!wasm->memory.exists) {
+ wasm->memory.exists = true;
+ wasm->memory.initial = pages;
+ wasm->memory.max = pages;
+ } else if (wasm->memory.initial < pages) {
+ wasm->memory.initial = pages;
+ if (wasm->memory.max < pages) {
+ wasm->memory.max = pages;
+ }
+ }
+}
+
void instrumentModule(Module& wasm, const WasmSplitOptions& options) {
- Fatal() << "TODO: implement instrumentation\n";
+ // Check that the profile export name is not already taken
+ if (wasm.getExportOrNull(options.profileExport) != nullptr) {
+ Fatal() << "error: Export " << options.profileExport << " already exists.";
+ }
+
+ // TODO: calculate module hash.
+ uint64_t moduleHash = 0;
+ PassRunner runner(&wasm, options.passOptions);
+ Instrumenter(options.profileExport, moduleHash).run(&runner, &wasm);
+
+ // Write the output modules
+ ModuleWriter writer;
+ writer.setBinary(options.emitBinary);
+ writer.setDebugInfo(options.passOptions.debugInfo);
+ writer.write(wasm, options.output);
}
void splitModule(Module& wasm, const WasmSplitOptions& options) {
@@ -337,7 +531,8 @@ void splitModule(Module& wasm, const WasmSplitOptions& options) {
// Write the output modules
ModuleWriter writer;
- writer.setBinary(true);
+ writer.setBinary(options.emitBinary);
+ writer.setDebugInfo(options.passOptions.debugInfo);
writer.write(wasm, options.primaryOutput);
writer.write(*secondary, options.secondaryOutput);
}
diff --git a/test/lit/wasm-split/export-name-already-exists.wast b/test/lit/wasm-split/export-name-already-exists.wast
new file mode 100644
index 000000000..708929e98
--- /dev/null
+++ b/test/lit/wasm-split/export-name-already-exists.wast
@@ -0,0 +1,8 @@
+;; RUN: not wasm-split %s --instrument --profile-export=foo 2>&1 \
+;; RUN: | filecheck %s
+
+;; CHECK: error: Export foo already exists.
+
+(module
+ (export "foo" (memory 0 0))
+)
diff --git a/test/lit/wasm-split/instrument-funcs.wast b/test/lit/wasm-split/instrument-funcs.wast
new file mode 100644
index 000000000..bdd222d82
--- /dev/null
+++ b/test/lit/wasm-split/instrument-funcs.wast
@@ -0,0 +1,75 @@
+;; RUN: wasm-split %s --instrument -S -o - | filecheck %s
+
+;; Check that the output round trips and validates as well
+;; RUN: wasm-split %s --instrument -g -o %t
+;; RUN: wasm-opt %t --print | filecheck %s
+
+(module
+ (import "env" "foo" (func $foo))
+ (export "bar" (func $bar))
+ (func $bar
+ (call $foo)
+ )
+ (func $baz (param i32) (result i32)
+ (local.get 0)
+ )
+)
+
+;; Check that a memory has been added
+;; CHECK: (memory $0 1 1)
+
+;; Check that the counter and timestamps have been added
+;; CHECK: (global $monotonic_counter (mut i32) (i32.const 0))
+;; CHECK: (global $bar_timestamp (mut i32) (i32.const 0))
+;; CHECK: (global $baz_timestamp (mut i32) (i32.const 0))
+
+;; And the profiling function exported
+;; CHECK: (export "__write_profile" (func $__write_profile))
+
+;; Check that the function instrumentation is correct
+
+;; CHECK: (func $baz (param $0 i32) (result i32)
+;; CHECK-NEXT: (if
+;; CHECK-NEXT: (i32.eqz
+;; CHECK-NEXT: (global.get $baz_timestamp)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (block
+;; CHECK-NEXT: (global.set $monotonic_counter
+;; CHECK-NEXT: (i32.add
+;; CHECK-NEXT: (global.get $monotonic_counter)
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (global.set $baz_timestamp
+;; CHECK-NEXT: (global.get $monotonic_counter)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (local.get $0)
+;; CHECK-NEXT: )
+
+;; Check that the profiling function is correct.
+
+;; CHECK: (func $__write_profile (param $addr i32) (param $size i32) (result i32)
+;; CHECK-NEXT: (if
+;; CHECK-NEXT: (i32.ge_u
+;; CHECK-NEXT: (local.get $size)
+;; CHECK-NEXT: (i32.const 16)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (block
+;; CHECK-NEXT: (i64.store align=1
+;; CHECK-NEXT: (local.get $addr)
+;; CHECK-NEXT: (i64.const 0)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (i32.store offset=8 align=1
+;; CHECK-NEXT: (local.get $addr)
+;; CHECK-NEXT: (global.get $bar_timestamp)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (i32.store offset=12 align=1
+;; CHECK-NEXT: (local.get $addr)
+;; CHECK-NEXT: (global.get $baz_timestamp)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (i32.const 16)
+;; CHECK-NEXT: )
diff --git a/test/lit/wasm-split/instrument-memory-too-small.wast b/test/lit/wasm-split/instrument-memory-too-small.wast
new file mode 100644
index 000000000..3c5017390
--- /dev/null
+++ b/test/lit/wasm-split/instrument-memory-too-small.wast
@@ -0,0 +1,10 @@
+;; Test that the instrumentation increases the memory bounds if necessary
+
+;; RUN: wasm-split %s --instrument -S -o - | filecheck %s
+
+;; CHECK: (memory $0 1 1)
+;; CHECK: (export "__write_profile" (func $__write_profile))
+
+(module
+ (memory $0 0 0)
+)