diff options
-rw-r--r-- | src/passes/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/passes/TraceCalls.cpp | 218 | ||||
-rw-r--r-- | src/passes/pass.cpp | 4 | ||||
-rw-r--r-- | src/passes/passes.h | 1 | ||||
-rw-r--r-- | test/lit/help/wasm-opt.test | 4 | ||||
-rw-r--r-- | test/lit/help/wasm2js.test | 4 | ||||
-rw-r--r-- | test/lit/passes/trace-calls.wast | 156 | ||||
-rw-r--r-- | test/lit/passes/trace-calls_multi-value-result.wast | 9 |
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))) +) |