diff --git a/src/engine/Instrumentation.v3 b/src/engine/Instrumentation.v3 index 44a72b72..bbf9f83e 100644 --- a/src/engine/Instrumentation.v3 +++ b/src/engine/Instrumentation.v3 @@ -4,17 +4,12 @@ // Provides a common entrypoint to the engine's instrumentation capabilities, including // the insertion/removal of probes, as well as firing probes during execution. component Instrumentation { - def TRACE = TraceProbe.new(); def probes = ProbeList.new(); // probes on the global interpreter loop // Reset the instrumentation to the default, i.e. only tracing probes. - def reset() { + def reset() { // TODO: only used in testing. probes.clear(); - if (Trace.interpreter) probes.add(TRACE); - } - // Special mode to enable interpreter tracing. - def enableTraceInt() { - probes.add(TRACE); + BasicTracing.reset(); // TODO: engine depends on tracing in util } // Insert a probe into the global interpreter loop. def insertGlobalProbe(p: Probe) { @@ -92,7 +87,6 @@ component Instrumentation { if (map == null) return null; return map[offset]; } - // Called by the engine to fire probes on the global interpreter loop. def fireGlobalProbes(dynamicLoc: DynamicLoc) -> Throwable { Metrics.probe_fires.val++; diff --git a/src/engine/Trace.v3 b/src/engine/Trace.v3 index a612f146..367e0069 100644 --- a/src/engine/Trace.v3 +++ b/src/engine/Trace.v3 @@ -21,33 +21,12 @@ component Trace { var stack = false; var whamm = false; - var moduleFilter: DeclFilter; - var callsFilter: DeclFilter; - def OUT = TraceBuilder.new(); def STDOUT = System.write(1, _); def STDOUT_void(r: Range) { System.write(1, r); } - def CALL_TRACE_PROBE = CallTraceProbe.new(); - - def instrumentModule(module: Module) { - if (callsFilter == null) return; - // Add a call tracer if the global option is turned on. - for (i < module.functions.length) { - var func = module.functions[i]; - if (callsFilter.matches(module, func)) { - Instrumentation.insertFuncEntryProbe(module, func.func_index, CALL_TRACE_PROBE); - } - } - } - def instrumentFunc(module: Module, func: FuncDecl) { - if (callsFilter == null) return; - if (callsFilter.matches(module, func)) { - Instrumentation.insertFuncEntryProbe(module, func.func_index, CALL_TRACE_PROBE); - } - } def renderCspRange(buf: StringBuilder, r: Range, render: (T, StringBuilder) -> StringBuilder) -> StringBuilder { for (i < r.length) { if (i > 0) buf.csp(); diff --git a/src/engine/TraceOptions.v3 b/src/engine/TraceOptions.v3 index 544ccdf4..86df260c 100644 --- a/src/engine/TraceOptions.v3 +++ b/src/engine/TraceOptions.v3 @@ -46,7 +46,7 @@ component TraceOptions { if (arg.length >= (traceModule.length + 1) && arg[traceModule.length] == '=') { pat = Arrays.range(arg, traceModule.length + 1, arg.length); } - Trace.moduleFilter = DeclFilters.parseString(pat); + BasicTracing.moduleFilter = DeclFilters.parseString(pat); return true; } var traceCalls = "--trace-calls"; @@ -55,7 +55,7 @@ component TraceOptions { if (arg.length >= (traceCalls.length + 1) && arg[traceCalls.length] == '=') { pat = Arrays.range(arg, traceCalls.length + 1, arg.length); } - Trace.callsFilter = DeclFilters.parseString(pat); + BasicTracing.callsFilter = DeclFilters.parseString(pat); return true; } } else if (Strings.startsWith(arg, "-t")) { @@ -82,13 +82,13 @@ component TraceOptions { } private def setOption(opt: TraceOption) { match (opt) { - int => { Trace.interpreter = true; Instrumentation.enableTraceInt(); } + int => { Trace.interpreter = true; BasicTracing.enableTraceInt(); } binparse => Trace.binparse = true; validation => Trace.validation = true; test => Trace.test = true; spectest => Trace.spectest = true; operands => Trace.operands = true; - memory => Trace.memory = true; + memory => { Trace.memory = true; BasicTracing.memoryFilter = DeclFilters.parseString(null); } canon => { Trace.canon = true; Trace.uid = true; } uid => Trace.uid = true; compiler => Trace.compiler = true; diff --git a/src/monitors/ModuleInstrumenter.v3 b/src/monitors/ModuleInstrumenter.v3 index b05f2ff1..01ddf8e9 100644 --- a/src/monitors/ModuleInstrumenter.v3 +++ b/src/monitors/ModuleInstrumenter.v3 @@ -358,8 +358,8 @@ class MemoryVisitor( } def visit_MEMORY_GROW(memory_index: u31) { - if (growFn != null) - Instrumentation.insertLocalProbe(module, bi.func.func_index, bi.pc, MemoryGrowProbe.new(memory_index, growFn)); + if (growFn == null) return; + Instrumentation.insertLocalProbe(module, bi.func.func_index, bi.pc, MemoryGrowProbe.new(memory_index, growFn)); } } private class MemoryAccessProbe(imm: MemArg, operand: int, size: u64, f: (DynamicLoc, Memory, u64, u64) -> Resumption) extends Probe { diff --git a/src/util/BasicTracing.v3 b/src/util/BasicTracing.v3 new file mode 100644 index 00000000..34f0b264 --- /dev/null +++ b/src/util/BasicTracing.v3 @@ -0,0 +1,202 @@ +// Copyright 2024 Wizard authors. All rights reserved. +// See LICENSE for details of Apache 2.0 license. + +def OUT = Trace.OUT; +// Implements basic tracing functionality such as --trace-interpreter, --trace-calls, --trace-memory. +component BasicTracing { + var moduleFilter: DeclFilter; + var callsFilter: DeclFilter; + var memoryFilter: DeclFilter; + + def reset() { + if (Trace.interpreter) Instrumentation.probes.add(TRACE_INTERP_PROBE); + } + // Special mode to enable interpreter tracing. + def enableTraceInt() { + Instrumentation.probes.add(TRACE_INTERP_PROBE); + } + def instrumentFunc(module: Module, func: FuncDecl) { + // Add a call and memory tracing if the global options are turned on. + if (callsFilter != null && callsFilter.matches(module, func)) addTraceCallsProbes(module, func); + if (memoryFilter != null && memoryFilter.matches(module, func)) addTraceMemoryProbes(module, func); + } + def instrumentModule(module: Module) { + if (callsFilter == null && memoryFilter == null) return; + for (i < module.functions.length) instrumentFunc(module, module.functions[i]); + } + def addTraceCallsProbes(module: Module, func: FuncDecl) { + Instrumentation.insertFuncEntryProbe(module, func.func_index, CALL_TRACE_PROBE); + } + def addTraceMemoryProbes(module: Module, func: FuncDecl) { + TraceMemoryInstrumenter.new(module).runOne(func); + } +} + +// Basic tracing support for the interpreter. +def TRACE_INTERP_PROBE = TraceInstrProbe.new(); +class TraceInstrProbe extends Probe { + def tracer = InstrTracer.new(); + def codeptr = DataReader.new([]); + + def fire(dynamicLoc: DynamicLoc) -> Resumption { + var func = dynamicLoc.func; + var offset = dynamicLoc.pc; + codeptr.reset(func.decl.cur_bytecode, offset, func.decl.cur_bytecode.length); + var module = if(func.instance != null, func.instance.module); + var len = OUT.length; + var accessor = dynamicLoc.frame.getFrameAccessor(); + OUT.pad(' ', len + 2 * accessor.depth()); + OUT.mark(); + OUT.putc('+').putd(offset).puts(": "); + OUT.ljustify_mark(6); + + var opcode = codeptr.data[codeptr.pos]; + OUT.mark(); + if (opcode == InternalOpcode.PROBE.code) { + OUT.puts(" "); + var prev = (codeptr.data, codeptr.pos, codeptr.limit); + codeptr.reset(func.decl.orig_bytecode, prev.1, prev.2); + tracer.putInstr(OUT, module, codeptr); + codeptr.reset(prev.0, prev.1, prev.2); + } else { + tracer.putInstr(OUT, module, codeptr); + } + if (Trace.operands) { + OUT.sp(); + OUT.ljustify_mark(24); + OUT.puts("|"); + for (i < accessor.numLocals()) { + OUT.sp().putv(accessor.getLocal(i)); + } + var count = accessor.numOperands(); + OUT.puts(" |"); + for (j = 1 - count; j <= 0; j++) { + OUT.sp().putv(accessor.getOperand(j)); + } + } + OUT.ln(); + return Resumption.Continue; + } +} + +// Basic tracing support for calls. +def CALL_TRACE_PROBE = CallTraceProbe.new(); +class CallTraceProbe extends Probe { + def fire(dynamicLoc: DynamicLoc) -> Resumption { + var func = dynamicLoc.func; + var accessor = dynamicLoc.frame.getFrameAccessor(); + OUT.indent(accessor.depth()); + func.render(OUT); + OUT.puts("("); + for (i < func.sig.params.length) { + var val = accessor.getLocal(i); + if (i > 0) OUT.csp(); + Values.render(val, OUT); + } + OUT.puts(")").ln(); + return Resumption.Continue; + } +} + +// Basic tracing support for memory loads. +class TraceMemoryInstrumenter extends BytecodeInstrumenter { + new(module: Module) super(module) { } + + def visitLoad(op: Opcode, imm: MemArg, size: u8) { + insertProbeHere(MemoryTraceLoadProbe.new(op, imm, size)); + } + def visitStore(op: Opcode, imm: MemArg, size: u8) { + insertProbeHere(MemoryTraceStoreProbe.new(op, imm, size)); + } + def visit_MEMORY_SIZE(memory_index: u31) { // TODO + } + def visit_MEMORY_GROW(memory_index: u31) { // TODO + } + def visit_MEMORY_INIT(data_index: u31, memory_index: u31) { // TODO + } + def visit_MEMORY_COPY(dst_memory_index: u31, src_memory_index: u31) { // TODO + } + def visit_MEMORY_FILL(memory_index: u31) { // TODO + } + def visit_MEMORY_ATOMIC_NOTIFY(imm: MemArg) { // TODO + } + def visit_MEMORY_ATOMIC_WAIT32(imm: MemArg) { // TODO + } + def visit_MEMORY_ATOMIC_WAIT64(imm: MemArg) { // TODO + } + def visit_ATOMIC_FENCE(flags: u8) { // TODO + } +} + +class MemoryTraceLoadProbe(op: Opcode, imm: MemArg, size: u8) extends Probe { + def fire(dynamicLoc: DynamicLoc) -> Resumption { + var func = dynamicLoc.func; + var accessor = dynamicLoc.frame.getFrameAccessor(); + OUT.indent(accessor.depth()); + var index = outputOpAndAddr(op, imm.memory_index, imm.offset, accessor.getOperand(0)); + OUT.puts(" -> "); + var mem = func.instance.memories[imm.memory_index]; + var t = mem.range_oil_64(imm.offset, index, size); + if (t.ok()) { + for (i < t.result.length) OUT.putx_8(t.result[i]); + } else { + OUT.puts("!out-of-bounds"); + } + OUT.ln(); + return Resumption.Continue; + } +} + +class MemoryTraceStoreProbe(op: Opcode, imm: MemArg, size: u8) extends Probe { + def fire(dynamicLoc: DynamicLoc) -> Resumption { + var func = dynamicLoc.func; + var accessor = dynamicLoc.frame.getFrameAccessor(); + OUT.indent(accessor.depth()); + var index = outputOpAndAddr(op, imm.memory_index, imm.offset, accessor.getOperand(-1)); + OUT.puts(" <- "); + var mem = func.instance.memories[imm.memory_index]; + var val = accessor.getOperand(0); + + match (size) { + 1 => match (val) { + I32(v) => OUT.putx_8(byte.view(v)); + I64(v) => OUT.putx_8(byte.view(v)); + _ => ; // TODO: error + } + 2 => match (val) { + I32(v) => OUT.putx_16(u16.view(v)); + I64(v) => OUT.putx_16(u16.view(v)); + _ => ; // TODO: error + } + 4 => match (val) { + I32(v) => OUT.putx_32(u32.view(v)); + I64(v) => OUT.putx_32(u32.view(v)); + F32(v) => OUT.putx_32(u32.view(v)); + _ => ; // TODO: error + } + 8 => match (val) { + I64(v) => OUT.putx_64(u64.view(v)); + F64(v) => OUT.putx_64(u64.view(v)); + _ => ; // TODO: error + } + _ => ; // TODO: other sizes + } + OUT.ln(); + return Resumption.Continue; + } +} + +def outputOpAndAddr(op: Opcode, memory_index: u31, offset: u64, index: Value) -> u64 { + OUT.mark() + .puts(op.mnemonic).sp() + .ljustify_mark(20) + .put1("mem%d[", memory_index); + var result: u64; + match (index) { + I32(v) => OUT.putx_32(u32.view(offset + (result = v))); + I64(v) => OUT.putx_64(offset + (result = v)); + _ => ; // TODO: dynamic type error + } + OUT.putc(']'); + return result; +} \ No newline at end of file diff --git a/src/util/BytecodeInstrumenter.v3 b/src/util/BytecodeInstrumenter.v3 index 6ec37fe4..7ed3fe8b 100644 --- a/src/util/BytecodeInstrumenter.v3 +++ b/src/util/BytecodeInstrumenter.v3 @@ -8,7 +8,7 @@ class BytecodeInstrumenter(module: Module) extends BytecodeVisitor { def bi = BytecodeIterator.new(); var filter: DeclFilter; // client can set to filter functions instrumented - // Runs instrumentation on all functions in this module that match + // Runs instrumentation on all functions in this module that match any {filter} that's been installed. def run() { for (i < module.functions.length) { var func = module.functions[i]; @@ -17,7 +17,7 @@ class BytecodeInstrumenter(module: Module) extends BytecodeVisitor { bi.iterate(func, this); } } - + // Runs instrumentation on all functions in this module that match {matcher}. def runMatching(matcher: (Module, FuncDecl) -> bool) { for (i < module.functions.length) { var func = module.functions[i]; @@ -26,7 +26,11 @@ class BytecodeInstrumenter(module: Module) extends BytecodeVisitor { bi.iterate(func, this); } } - + // Runs instrumentation on the given function {func}, if it is not imported. + def runOne(func: FuncDecl) { + if (func.imp == null) bi.iterate(func, this); + } + // Utility that can be used by subclasses to insert a probe on the current bytecode while iterating. def insertProbeHere(probe: Probe) { Instrumentation.insertLocalProbe(module, bi.func.func_index, bi.pc, probe); } diff --git a/src/util/OptionsRegistry.v3 b/src/util/OptionsRegistry.v3 index 1ea917ca..40be063a 100644 --- a/src/util/OptionsRegistry.v3 +++ b/src/util/OptionsRegistry.v3 @@ -44,6 +44,13 @@ component OptionsRegistry { if (parse(a)) args[i] = null; } } + def matchesPrefixWithOptionalEquals(arg: string, prefix: string) -> (bool, Range) { + if (!Strings.startsWith(arg, prefix)) return (false, null); + var sub = arg[prefix.length ...]; + if (sub.length == 0) return (true, null); + if (sub.length > 0 && sub[0] == '=') return (true, sub[1 ...]); + return (false, null); + } } // Collects related options and stores a help string for each. diff --git a/src/util/ProbeUtil.v3 b/src/util/ProbeUtil.v3 index 00351bcc..e766b064 100644 --- a/src/util/ProbeUtil.v3 +++ b/src/util/ProbeUtil.v3 @@ -36,52 +36,6 @@ class CallbackProbe(f: DynamicLoc -> Resumption) extends Probe { return f(dynamicLoc); } } -// Traces an instruction, including operands, when fired. -class TraceProbe extends Probe { - def tracer = InstrTracer.new(); - def codeptr = DataReader.new([]); - - def fire(dynamicLoc: DynamicLoc) -> Resumption { - var func = dynamicLoc.func; - var offset = dynamicLoc.pc; - codeptr.reset(func.decl.cur_bytecode, offset, func.decl.cur_bytecode.length); - var module = if(func.instance != null, func.instance.module); - var out = Trace.OUT; - var len = out.length; - var accessor = dynamicLoc.frame.getFrameAccessor(); - out.pad(' ', len + 2 * accessor.depth()); - out.mark(); - out.putc('+').putd(offset).puts(": "); - out.ljustify_mark(6); - - var opcode = codeptr.data[codeptr.pos]; - out.mark(); - if (opcode == InternalOpcode.PROBE.code) { - out.puts(" "); - var prev = (codeptr.data, codeptr.pos, codeptr.limit); - codeptr.reset(func.decl.orig_bytecode, prev.1, prev.2); - tracer.putInstr(out, module, codeptr); - codeptr.reset(prev.0, prev.1, prev.2); - } else { - tracer.putInstr(out, module, codeptr); - } - if (Trace.operands) { - out.sp(); - out.ljustify_mark(24); - out.puts("|"); - for (i < accessor.numLocals()) { - out.sp().putv(accessor.getLocal(i)); - } - var count = accessor.numOperands(); - out.puts(" |"); - for (j = 1 - count; j <= 0; j++) { - out.sp().putv(accessor.getOperand(j)); - } - } - out.ln(); - return Resumption.Continue; - } -} // Probes that use the top of the stack, optimized by SPC. class OperandProbe extends Probe { } class OperandProbe_i_v extends OperandProbe { @@ -143,23 +97,6 @@ class SamplingProbe(probe: Probe) extends Probe { // } } -// A utility class to trace calls into functions. -class CallTraceProbe extends Probe { - def fire(dynamicLoc: DynamicLoc) -> Resumption { - var func = dynamicLoc.func; - var accessor = dynamicLoc.frame.getFrameAccessor(); - Trace.OUT.indent(accessor.depth()); - func.render(Trace.OUT); - Trace.OUT.puts("("); - for (i < func.sig.params.length) { - var val = accessor.getLocal(i); - if (i > 0) Trace.OUT.csp(); - Values.render(val, Trace.OUT); - } - Trace.OUT.puts(")").ln(); - return Resumption.Continue; - } -} // A utility class that triggers an external debugger breakpoint when fired. class ExternalDebuggerBreakpointProbe extends Probe { def fire(dynamicLoc: DynamicLoc) -> Resumption { diff --git a/src/wizeng.main.v3 b/src/wizeng.main.v3 index 50852f33..0b2efde0 100644 --- a/src/wizeng.main.v3 +++ b/src/wizeng.main.v3 @@ -52,7 +52,7 @@ def main(args: Array) -> int { match (result) { Ok(m) => { module = m; - Trace.instrumentModule(m); + BasicTracing.instrumentModule(m); } FileNotFound => return ErrorBuilder.new() .puts("wizeng: could not load file ") @@ -81,7 +81,7 @@ def main(args: Array) -> int { .puts(path) .put2(": import[%d] from unknown module \"%s\"\n", j, name) .exit(7); - var trace = Trace.moduleFilter != null && Trace.moduleFilter.matchesStr(name, null); + var trace = BasicTracing.moduleFilter != null && BasicTracing.moduleFilter.matchesStr(name, null); p.init(programArgs, trace, err); map[name] = p; i.processors.put(p); @@ -92,7 +92,7 @@ def main(args: Array) -> int { for (i < monitors.length) monitors[i].onReset(); // Add tracing to import calls as specified by filters, if any. - if (Trace.moduleFilter != null) i.binder = ImportTracer.new(Trace.moduleFilter).wrap; + if (BasicTracing.moduleFilter != null) i.binder = ImportTracer.new(BasicTracing.moduleFilter).wrap; // Run the instantiatior. var instance = i.run(); diff --git a/test/unittest/ModuleBuilder.v3 b/test/unittest/ModuleBuilder.v3 index 97ab6113..91b98fb9 100644 --- a/test/unittest/ModuleBuilder.v3 +++ b/test/unittest/ModuleBuilder.v3 @@ -76,7 +76,7 @@ class ModuleBuilder { func.setOrigCode(body); // XXX: using u16.view because of internal test exceeds maximum func.num_locals = u16.view(func.sig.params.length + num_locals); - Trace.instrumentFunc(module, func); + BasicTracing.instrumentFunc(module, func); } def code_op(opcode: Opcode) -> this { sig(opcode.sig); @@ -147,7 +147,7 @@ class ModuleBuilder { var func = newFunc(addSig(sig)); func.setOrigCode(makeBody(raw, Vector.new())); func.num_locals = u16.!(sig.params.length); - Trace.instrumentFunc(module, func); + BasicTracing.instrumentFunc(module, func); return func; } def addTable(length: u32, offset: int, indices: Array) -> this { @@ -195,7 +195,7 @@ class ModuleBuilder { def addCodeNV(func: FuncDecl, raw: Array) { var body = makeBody(raw, Vector.new()); func.setOrigCode(body); - Trace.instrumentFunc(module, func); + BasicTracing.instrumentFunc(module, func); func.num_locals = u16.!(func.sig.params.length); } def addCodeV(func: FuncDecl, raw: Array) { @@ -203,7 +203,7 @@ class ModuleBuilder { } def addCodeV_extra_raw(func: FuncDecl, body: Array) { func.setOrigCode(body); - Trace.instrumentFunc(module, func); + BasicTracing.instrumentFunc(module, func); var limits = Limits.new().set(extensions); var err = ErrorGen.new("ModuleBuilder.v3"); var cv = CodeValidator.new(extensions, limits, module, err); diff --git a/test/wasm-spec/SpecTestInterpreter.v3 b/test/wasm-spec/SpecTestInterpreter.v3 index 09f5b82b..ce9ac38d 100644 --- a/test/wasm-spec/SpecTestInterpreter.v3 +++ b/test/wasm-spec/SpecTestInterpreter.v3 @@ -261,7 +261,7 @@ class SpecTestInterpreter(engine: Engine, filename: string) { var r = mp.finish(); match (r) { Ok(module) => { - Trace.instrumentModule(module); + BasicTracing.instrumentModule(module); return doInstantiate(module); } Error => return StModuleResult.ParseError(r);