summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/passes/CMakeLists.txt1
-rw-r--r--src/passes/TraceCalls.cpp218
-rw-r--r--src/passes/pass.cpp4
-rw-r--r--src/passes/passes.h1
-rw-r--r--test/lit/help/wasm-opt.test4
-rw-r--r--test/lit/help/wasm2js.test4
-rw-r--r--test/lit/passes/trace-calls.wast156
-rw-r--r--test/lit/passes/trace-calls_multi-value-result.wast9
8 files changed, 397 insertions, 0 deletions
diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt
index b17730279..703b27e05 100644
--- a/src/passes/CMakeLists.txt
+++ b/src/passes/CMakeLists.txt
@@ -93,6 +93,7 @@ set(passes_SOURCES
StringLowering.cpp
Strip.cpp
StripTargetFeatures.cpp
+ TraceCalls.cpp
RedundantSetElimination.cpp
RemoveImports.cpp
RemoveMemory.cpp
diff --git a/src/passes/TraceCalls.cpp b/src/passes/TraceCalls.cpp
new file mode 100644
index 000000000..01278c2e9
--- /dev/null
+++ b/src/passes/TraceCalls.cpp
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2024 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.
+ */
+
+//
+// Instruments the build with code to intercept selected function calls.
+// This can be e.g. used to trace allocations (malloc, free, calloc, realloc)
+// and build tools for memory usage analysis.
+// The pass supports SIMD but the multi-value feature is not supported yet.
+//
+// Instrumenting void free(void*):
+
+// Instrumenting function `void* malloc(int32_t)` with a user-defined
+// name of the tracer `trace_alloc` and function `void free(void*)`
+// with the default name of the tracer `trace_free` (`trace_` prefix
+// is added by default):
+// wasm-opt --trace-calls=malloc:trace_alloc,free -o test-opt.wasm test.wasm
+//
+// Before:
+// (call $malloc
+// (local.const 32))
+// (call $free (i32.const 64))
+//
+// After:
+// (local $0 i32)
+// (local $1 i32)
+// (local $2 i32)
+// (block (result i32)
+// (call $trace_alloc
+// (local.get $0)
+// (local.tee $1
+// (call $malloc
+// (local.tee $0 (i32.const 2))
+// )
+// )
+// )
+// )
+// (block
+// (call $free
+// (local.tee $3
+// (i32.const 64)
+// )
+// )
+// (call $trace_free
+// (local.get $3)
+// )
+// )
+
+#include <map>
+
+#include "asmjs/shared-constants.h"
+#include "ir/import-utils.h"
+#include "pass.h"
+#include "support/string.h"
+#include "wasm-builder.h"
+
+namespace wasm {
+
+using TracedFunctions = std::map<Name /* originName */, Name /* tracerName */>;
+
+struct AddTraceWrappers : public WalkerPass<PostWalker<AddTraceWrappers>> {
+ AddTraceWrappers(TracedFunctions tracedFunctions)
+ : tracedFunctions(std::move(tracedFunctions)) {}
+ void visitCall(Call* curr) {
+ auto* target = getModule()->getFunction(curr->target);
+
+ auto iter = tracedFunctions.find(target->name);
+ if (iter != tracedFunctions.end()) {
+ addInstrumentation(curr, target, iter->second);
+ }
+ }
+
+private:
+ void addInstrumentation(Call* curr,
+ const wasm::Function* target,
+ const Name& wrapperName) {
+ Builder builder(*getModule());
+ std::vector<wasm::Expression*> realCallParams, trackerCallParams;
+
+ for (const auto& op : curr->operands) {
+ auto localVar = builder.addVar(getFunction(), op->type);
+ realCallParams.push_back(builder.makeLocalTee(localVar, op, op->type));
+ trackerCallParams.push_back(builder.makeLocalGet(localVar, op->type));
+ }
+
+ auto resultType = target->type.getSignature().results;
+ auto realCall = builder.makeCall(target->name, realCallParams, resultType);
+
+ if (resultType.isConcrete()) {
+ auto resultLocal = builder.addVar(getFunction(), resultType);
+ trackerCallParams.insert(
+ trackerCallParams.begin(),
+ builder.makeLocalTee(resultLocal, realCall, resultType));
+
+ replaceCurrent(builder.makeBlock(
+ {builder.makeCall(
+ wrapperName, trackerCallParams, Type::BasicType::none),
+ builder.makeLocalGet(resultLocal, resultType)}));
+ } else {
+ replaceCurrent(builder.makeBlock(
+ {realCall,
+ builder.makeCall(
+ wrapperName, trackerCallParams, Type::BasicType::none)}));
+ }
+ }
+
+ TracedFunctions tracedFunctions;
+};
+
+struct TraceCalls : public Pass {
+ // Adds calls to new imports.
+ bool addsEffects() override { return true; }
+
+ void run(Module* module) override {
+ auto functionsDefinitions = getPassOptions().getArgument(
+ "trace-calls",
+ "TraceCalls usage: wasm-opt "
+ "--trace-calls=FUNCTION_TO_TRACE[:TRACER_NAME][,...]");
+
+ auto tracedFunctions = parseArgument(functionsDefinitions);
+
+ for (const auto& tracedFunction : tracedFunctions) {
+ auto func = module->getFunctionOrNull(tracedFunction.first);
+ if (!func) {
+ std::cerr << "[TraceCalls] Function '" << tracedFunction.first
+ << "' not found" << std::endl;
+ } else {
+ addImport(module, *func, tracedFunction.second);
+ }
+ }
+
+ AddTraceWrappers(std::move(tracedFunctions)).run(getPassRunner(), module);
+ }
+
+private:
+ Type getTracerParamsType(ImportInfo& info, const Function& func) {
+ auto resultsType = func.type.getSignature().results;
+ if (resultsType.isTuple()) {
+ Fatal() << "Failed to instrument function '" << func.name
+ << "': Multi-value result type is not supported";
+ }
+
+ std::vector<Type> tracerParamTypes;
+ if (resultsType.isConcrete()) {
+ tracerParamTypes.push_back(resultsType);
+ }
+ for (auto& op : func.type.getSignature().params) {
+ tracerParamTypes.push_back(op);
+ }
+
+ return Type(tracerParamTypes);
+ }
+
+ TracedFunctions parseArgument(const std::string& arg) {
+ TracedFunctions tracedFunctions;
+
+ for (const auto& definition : String::Split(arg, ",")) {
+ if (definition.empty()) {
+ // Empty definition, ignore.
+ continue;
+ }
+
+ std::string originName, traceName;
+ parseFunctionName(definition, originName, traceName);
+
+ tracedFunctions[Name(originName)] = Name(traceName);
+ }
+
+ return tracedFunctions;
+ }
+
+ void parseFunctionName(const std::string& str,
+ std::string& originName,
+ std::string& traceName) {
+ auto parts = String::Split(str, ":");
+ switch (parts.size()) {
+ case 1:
+ originName = parts[0];
+ traceName = "trace_" + originName;
+ break;
+ case 2:
+ originName = parts[0];
+ traceName = parts[1];
+ break;
+ default:
+ Fatal() << "Failed to parse function name ('" << str
+ << "'): expected format FUNCTION_TO_TRACE[:TRACER_NAME]";
+ }
+ }
+
+ void addImport(Module* wasm, const Function& f, const Name& tracerName) {
+ ImportInfo info(*wasm);
+
+ if (!info.getImportedFunction(ENV, tracerName)) {
+ auto import = Builder::makeFunction(
+ tracerName, Signature(getTracerParamsType(info, f), Type::none), {});
+ import->module = ENV;
+ import->base = tracerName;
+ wasm->addFunction(std::move(import));
+ }
+ }
+};
+
+Pass* createTraceCallsPass() { return new TraceCalls(); }
+
+} // namespace wasm
diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp
index ad0cc448c..fd9106092 100644
--- a/src/passes/pass.cpp
+++ b/src/passes/pass.cpp
@@ -235,6 +235,10 @@ void PassRegistry::registerPasses() {
"lower all uses of i64s to use i32s instead",
createI64ToI32LoweringPass);
registerPass(
+ "trace-calls",
+ "instrument the build with code to intercept specific function calls",
+ createTraceCallsPass);
+ registerPass(
"instrument-locals",
"instrument the build with code to intercept all loads and stores",
createInstrumentLocalsPass);
diff --git a/src/passes/passes.h b/src/passes/passes.h
index 337a681ab..02b164279 100644
--- a/src/passes/passes.h
+++ b/src/passes/passes.h
@@ -74,6 +74,7 @@ Pass* createLocalCSEPass();
Pass* createLocalSubtypingPass();
Pass* createLogExecutionPass();
Pass* createIntrinsicLoweringPass();
+Pass* createTraceCallsPass();
Pass* createInstrumentLocalsPass();
Pass* createInstrumentMemoryPass();
Pass* createLoopInvariantCodeMotionPass();
diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test
index 9dd38e03e..59409dcff 100644
--- a/test/lit/help/wasm-opt.test
+++ b/test/lit/help/wasm-opt.test
@@ -500,6 +500,10 @@
;; CHECK-NEXT:
;; CHECK-NEXT: --table64-lowering lower 64-bit tables 32-bit ones
;; CHECK-NEXT:
+;; CHECK-NEXT: --trace-calls instrument the build with code
+;; CHECK-NEXT: to intercept specific function
+;; CHECK-NEXT: calls
+;; CHECK-NEXT:
;; CHECK-NEXT: --translate-to-exnref translate old Phase 3 EH
;; CHECK-NEXT: instructions to new ones with
;; CHECK-NEXT: exnref
diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test
index 72efcd223..3c1da7ff2 100644
--- a/test/lit/help/wasm2js.test
+++ b/test/lit/help/wasm2js.test
@@ -454,6 +454,10 @@
;; CHECK-NEXT:
;; CHECK-NEXT: --table64-lowering lower 64-bit tables 32-bit ones
;; CHECK-NEXT:
+;; CHECK-NEXT: --trace-calls instrument the build with code
+;; CHECK-NEXT: to intercept specific function
+;; CHECK-NEXT: calls
+;; CHECK-NEXT:
;; CHECK-NEXT: --translate-to-exnref translate old Phase 3 EH
;; CHECK-NEXT: instructions to new ones with
;; CHECK-NEXT: exnref
diff --git a/test/lit/passes/trace-calls.wast b/test/lit/passes/trace-calls.wast
new file mode 100644
index 000000000..d12e959c6
--- /dev/null
+++ b/test/lit/passes/trace-calls.wast
@@ -0,0 +1,156 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+
+;; RUN: wasm-opt --enable-simd --trace-calls="noparamsnoresults,singleparamnoresults,multiparamsnoresults:tracempnr,noparamssingleresult,multiparamssingleresult" %s -S -o - | filecheck %s
+
+(module
+
+ (import "env" "no_params_no_results"
+ (func $noparamsnoresults))
+ (import "env" "single_param_no_results"
+ (func $singleparamnoresults (param f64)))
+ (import "env" "multi_params_no_results"
+ (func $multiparamsnoresults (param i32 i64 f32)))
+ (import "env" "no_params_single_result"
+ (func $noparamssingleresult (result v128)))
+ (import "env" "multi_params_single_result"
+ (func $multiparamssingleresult (param i32 v128)(result v128)))
+ (import "env" "dont_trace_me"
+ (func $donttraceme))
+
+
+ ;; CHECK: (type $0 (func))
+
+ ;; CHECK: (type $1 (func (result v128)))
+
+ ;; CHECK: (type $2 (func (param f64)))
+
+ ;; CHECK: (type $3 (func (param i32 i64 f32)))
+
+ ;; CHECK: (type $4 (func (param i32 v128) (result v128)))
+
+ ;; CHECK: (type $5 (func (param v128 i32 v128)))
+
+ ;; CHECK: (type $6 (func (param v128)))
+
+ ;; CHECK: (import "env" "no_params_no_results" (func $noparamsnoresults))
+
+ ;; CHECK: (import "env" "single_param_no_results" (func $singleparamnoresults (param f64)))
+
+ ;; CHECK: (import "env" "multi_params_no_results" (func $multiparamsnoresults (param i32 i64 f32)))
+
+ ;; CHECK: (import "env" "no_params_single_result" (func $noparamssingleresult (result v128)))
+
+ ;; CHECK: (import "env" "multi_params_single_result" (func $multiparamssingleresult (param i32 v128) (result v128)))
+
+ ;; CHECK: (import "env" "dont_trace_me" (func $donttraceme))
+
+ ;; CHECK: (import "env" "tracempnr" (func $tracempnr (param i32 i64 f32)))
+
+ ;; CHECK: (import "env" "trace_multiparamssingleresult" (func $trace_multiparamssingleresult (param v128 i32 v128)))
+
+ ;; CHECK: (import "env" "trace_noparamsnoresults" (func $trace_noparamsnoresults))
+
+ ;; CHECK: (import "env" "trace_noparamssingleresult" (func $trace_noparamssingleresult (param v128)))
+
+ ;; CHECK: (import "env" "trace_singleparamnoresults" (func $trace_singleparamnoresults (param f64)))
+
+ ;; CHECK: (func $test_no_params_no_results
+ ;; CHECK-NEXT: (call $noparamsnoresults)
+ ;; CHECK-NEXT: (call $trace_noparamsnoresults)
+ ;; CHECK-NEXT: )
+ (func $test_no_params_no_results
+ (call $noparamsnoresults)
+ )
+
+ ;; CHECK: (func $test_single_param_no_results
+ ;; CHECK-NEXT: (local $0 f64)
+ ;; CHECK-NEXT: (call $singleparamnoresults
+ ;; CHECK-NEXT: (local.tee $0
+ ;; CHECK-NEXT: (f64.const 4.5)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $trace_singleparamnoresults
+ ;; CHECK-NEXT: (local.get $0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $test_single_param_no_results
+ (call $singleparamnoresults (f64.const 4.5))
+ )
+
+ ;; we specify a custom name (tracempnr) for the tracer function
+ ;; CHECK: (func $test_multi_params_no_results
+ ;; CHECK-NEXT: (local $0 i32)
+ ;; CHECK-NEXT: (local $1 i64)
+ ;; CHECK-NEXT: (local $2 f32)
+ ;; CHECK-NEXT: (call $multiparamsnoresults
+ ;; CHECK-NEXT: (local.tee $0
+ ;; CHECK-NEXT: (i32.const 5)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (i64.const 6)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (local.tee $2
+ ;; CHECK-NEXT: (f32.const 1.5)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $tracempnr
+ ;; CHECK-NEXT: (local.get $0)
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: (local.get $2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $test_multi_params_no_results
+ (call $multiparamsnoresults
+ (i32.const 5)
+ (i64.const 6)
+ (f32.const 1.5)
+ )
+ )
+
+ ;; CHECK: (func $test_no_params_single_result (result v128)
+ ;; CHECK-NEXT: (local $0 v128)
+ ;; CHECK-NEXT: (call $trace_noparamssingleresult
+ ;; CHECK-NEXT: (local.tee $0
+ ;; CHECK-NEXT: (call $noparamssingleresult)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (local.get $0)
+ ;; CHECK-NEXT: )
+ (func $test_no_params_single_result (result v128)
+ call $noparamssingleresult
+ )
+
+ ;; CHECK: (func $test_multi_params_single_result (result v128)
+ ;; CHECK-NEXT: (local $0 i32)
+ ;; CHECK-NEXT: (local $1 v128)
+ ;; CHECK-NEXT: (local $2 v128)
+ ;; CHECK-NEXT: (call $trace_multiparamssingleresult
+ ;; CHECK-NEXT: (local.tee $2
+ ;; CHECK-NEXT: (call $multiparamssingleresult
+ ;; CHECK-NEXT: (local.tee $0
+ ;; CHECK-NEXT: (i32.const 3)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (v128.const i32x4 0x00000001 0x00000002 0x00000003 0x00000004)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (local.get $0)
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (local.get $2)
+ ;; CHECK-NEXT: )
+ (func $test_multi_params_single_result (result v128)
+ (call $multiparamssingleresult
+ (i32.const 3)
+ (v128.const i32x4 1 2 3 4))
+ )
+
+ ;; this function should not be traced
+ ;; CHECK: (func $test_dont_trace_me
+ ;; CHECK-NEXT: (call $donttraceme)
+ ;; CHECK-NEXT: )
+ (func $test_dont_trace_me
+ (call $donttraceme)
+ )
+)
diff --git a/test/lit/passes/trace-calls_multi-value-result.wast b/test/lit/passes/trace-calls_multi-value-result.wast
new file mode 100644
index 000000000..ee4445dbd
--- /dev/null
+++ b/test/lit/passes/trace-calls_multi-value-result.wast
@@ -0,0 +1,9 @@
+;; Test that a traced function with a multi-value result type
+;; results in a useful error message
+
+;; RUN: not wasm-opt --enable-simd --enable-multivalue --trace-calls=multi_param_result %s 2>&1 | filecheck %s
+
+;; CHECK: Fatal: Failed to instrument function 'multi_param_result': Multi-value result type is not supported
+(module
+ (import "env" "multi_param_result" (func $multi_param_result (result i32 i32)))
+)