Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/engine/EngineOptions.v3
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ component EngineOptions {
.onSet(fun v => void(StackTuning.defaultStackSize = v));
def MAX_STACKS = group.newIntOption("max-stacks", StackTuning.maxStacks, "Maximum number of Wasm execution stacks.")
.onSet(fun v => void(StackTuning.maxStacks = v));
def LAZY_ENTRY_STACKS = group.newIntOption("lazy-entry-stacks", StackTuning.lazyEntryStacks,
"Give new continuations a stack segment when first resumed: 0 never, 1 near capacity, 2 always.")
.onSet(fun v => void(StackTuning.lazyEntryStacks = v));
def STACK_COMPRESSION = group.newBoolOption("stack-compression", StackTuning.enableCompression, "Enable stack compression.")
.onSet(fun v => void(StackTuning.enableCompression = v));
def FAST_FUNCTIONS = group.newBoolOption("fast-functions", false, "Treat functions exported with `fast:` in the name as fast functions.")
Expand Down
28 changes: 28 additions & 0 deletions src/engine/Metrics.v3
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ component Metrics {
def whamm_bind_time_us = t("whamm:bind_time_us", "Time taken to bind Whamm monitors, including static predicates.");
def whamm_report_time_us = t("whamm:report_time_us", "Time taken to report Whamm monitors.");

// Metrics from native stacks and stack compression.
def stack_segments = m("stack:segments", "segments", "Number of native stack segments mapped");
def stack_forced_gcs = m("stack:forced_gcs", "GCs", "Number of GCs forced to reclaim stack segments");
def stack_lazy_entries = m("stack:lazy_entries", "stacks", "Number of new continuations given a stack without a segment");
def stack_compressed = m("stack:compressed", "stacks", "Number of stacks compressed");
def stack_compressed_bytes = m("stack:compressed_bytes", bytes, "Total estimated size of compressed stacks");
def stack_compress_cycles = c("stack:compress_cycles", "Cycles taken to compress stacks");
def stack_decompressed = m("stack:decompressed", "stacks", "Number of stacks decompressed");
def stack_decompress_cycles = c("stack:decompress_cycles", "Cycles taken to decompress stacks");

// Metrics from the garbage collector of the runtime, if any.
def gc_count = register(GcMetric.new("gc:count", "GCs", "Number of garbage collections", Target.gcStat(_, 0)));
def gc_time_us = register(GcMetric.new("gc:time_us", us, "Total time spent in garbage collection", Target.gcStat(_, 1)));
def gc_max_time_us = register(GcMetric.new("gc:max_time_us", us, "Longest garbage collection", Target.gcStat(_, 2)));
def gc_max_live_bytes = register(GcMetric.new("gc:max_live_bytes", bytes, "Most bytes surviving a garbage collection", Target.gcStat(_, 3)));
def gc_heap_bytes = register(GcMetric.new("gc:heap_bytes", bytes, "Usable heap size", Target.gcStat(_, 4)));

// Keep the metrics in the above-declared order.
private def unused__ = all = Lists.reverse(all);

Expand Down Expand Up @@ -166,6 +183,17 @@ class CyclesMetric extends Metric {
}
}

// A metric whose value is read from the runtime when reported.
class GcMetric extends Metric {
def get: void -> u64;

new(name: string, units: string, help: string, get) super(name, units, help) { }

def report(out: StringBuilder) -> StringBuilder {
return out.putd(get());
}
}

// A metric which is the ratio of two other metrics.
class RatioMetric extends Metric {
def n: Metric;
Expand Down
3 changes: 1 addition & 2 deletions src/engine/Runtime.v3
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,8 @@ component Runtime {
var func = Function.!(Value.Ref.!(func_ref).val);
if (func == null) return stack.trap(TrapReason.NULL_DEREF);

var new_stack = Target.newWasmStack();
var new_stack = Target.newWasmStackFor(func);
if (new_stack == null) return stack.trap(TrapReason.OUT_OF_STACKS);
new_stack.reset(func);
new_stack.bottom = new_stack;
var cont = Continuations.makeContinuation(new_stack);
stack.push(Value.Cont(cont));
Expand Down
1 change: 1 addition & 0 deletions src/engine/Tuning.v3
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,5 @@ component StackTuning {
var defaultStackSize = 512u * 1024u;
var maxStacks = 2048;
var enableCompression = false;
var lazyEntryStacks = 1; // 0: never, 1: near capacity, 2: always give new continuations a segment lazily
}
1 change: 1 addition & 0 deletions src/engine/WasmStack.v3
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ enum StackState {
SUSPENDED, // contains one or more frames, waiting for inputs
CALL_CHILD, // waiting for child stack to return
RUNNING, // currently running Wasm or host code
COMPRESSED, // suspended, with frames compressed into the heap
}

// Provides access to the state of an executing frame.
Expand Down
1 change: 1 addition & 0 deletions src/engine/compiler/MacroAssembler.v3
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ class MacroAssembler(valuerep: Tagging, regConfig: RegConfig) {
def emit_call_runtime_SUSPEND();
def emit_call_runtime_SWITCH();
def emit_call_runtime_RESUME_THROW();
def emit_call_runtime_DECOMPRESS_CONT();
def emit_call_runtime_RESUME_THROW_REF();
def emit_jump_to_trap_at(reason: TrapReason);
def emit_call_runtime_op(op: Opcode);
Expand Down
23 changes: 23 additions & 0 deletions src/engine/compiler/SinglePassCompiler.v3
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,7 @@ class SinglePassCompiler(xenv: SpcExecEnv, masm: MacroAssembler, regAlloc: RegAl
var stub_resume = masm.newLabel(it.pc), end = masm.newLabel(it.pc);
var cont_decl = ContDecl.!(module.heaptypes[cont_id]);
if (checkForConstNull(state.peek())) return;
emit_decompress_top_cont();

var vsp = allocTmpFixed(ValueKind.REF, regs.vsp);
var top = popReg();
Expand Down Expand Up @@ -2360,6 +2361,28 @@ class SinglePassCompiler(xenv: SpcExecEnv, masm: MacroAssembler, regAlloc: RegAl
masm.emit_mov_r_r(ValueKind.REF, dst, regs.vfp); // XXX: use 3-addr adjustment of VSP (i.e. lea)
if (slots > 0) masm.emit_addw_r_i(dst, int.view(slots) * masm.valuerep.slot_size);
}
// Decompresses the stack of the continuation on top of the value stack, if compressed.
// All values are spilled, so that GC can update them during the runtime call.
def emit_decompress_top_cont() {
state.emitSaveAll(resolver, SpillMode.SAVE_AND_FREE_REGS);
emit_compute_vsp(regs.vsp, state.sp);
emit_spill_vsp(regs.vsp);
masm.emit_store_curstack_vsp(regs.vsp);
var done = masm.newLabel(it.pc), tmp = regs.scratch;
var valuerep = masm.valuerep;
masm.emit_mov_r_m(ValueKind.REF, tmp, MasmAddr(regs.vsp, valuerep.tag_size - valuerep.slot_size));
masm.emit_br_r(tmp, MasmBrCond.REF_NULL, done); // null continuation traps later
masm.emit_mov_r_m(ValueKind.I32, tmp, MasmAddr(tmp, masm.getOffsets().NativeStack_state));
masm.emit_brne_r_i(tmp, StackState.COMPRESSED.tag, done);
masm.emit_get_curstack(regs.runtime_arg0);
masm.emit_v3_set_NativeStack_rsp_r_r(regs.runtime_arg0, regs.sp);
masm.emit_push_NativeStack_rsp_r_r(regs.runtime_arg0);
masm.emit_call_runtime_DECOMPRESS_CONT();
masm.emit_get_curstack(regs.scratch);
masm.emit_pop_NativeStack_rsp_r_r(regs.scratch);
emit_reload_regs();
masm.bindLabel(done);
}
def emit_spill_vsp(reg: Reg) {
masm.emit_mov_m_r(ValueKind.REF, frame.vsp_slot, reg);
}
Expand Down
5 changes: 5 additions & 0 deletions src/engine/compression/NaiveCompressionStrategy.v3
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ class NaiveCompressedStack extends CompressedStack {
var frames: Array<RelocatableFrame>;

def name() -> string { return "Naive Compressed Stack"; }
def size() -> u64 {
var s = 32uL + 16uL * u64.view(frames.length);
for (f in frames) s += 64uL + 24uL * u64.view(f.data.vals.length);
return s;
}
}
4 changes: 4 additions & 0 deletions src/engine/compression/PackedCompressionStrategy.v3
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,8 @@ class PackedCompressedStack extends CompressedStack {
var refs: Array<Object>;

def name() -> string { return "Packed Compressed Stack"; }
// Estimates the size of this object and its arrays in bytes.
def size() -> u64 {
return 32uL + 16uL * 3 + 40uL * u64.view(frames.length) + u64.view(packed.length) + 8uL * u64.view(refs.length);
}
}
38 changes: 35 additions & 3 deletions src/engine/native/NativeStack.v3
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ class NativeStack extends WasmStack {
var return_results: Array<ValueType>;
var state_: StackState = StackState.EMPTY;
var next_stack: NativeStack;
var compressed: CompressedStack; // frames or bound values, if {COMPRESSED}; see {NativeStackCompression}
var scanned: bool; // GC scanner is registered

new(range) {
if (valuerep.tagged) RiGc.registerScanner(this, NativeStack.scan);
if (range == null) return; // allocated without a mapping, no need to initialize further
if (range == null) return; // allocated without a segment, no need to initialize further
registerScanner();
clear();
if (Trace.stack && Debug.stack) Trace.OUT.put2("newStack start=0x%x, end=0x%x", range.start - Pointer.NULL, range.end - Pointer.NULL).ln();
}
Expand All @@ -35,25 +37,52 @@ class NativeStack extends WasmStack {
pushRspPointer(STACK_RETURN_PARENT_STUB.getEntry() + NOP_ENTRY);
}
// Requires {state == EMPTY}.
// A stack without a segment becomes {COMPRESSED}, getting a segment when resumed.
def reset(func: Function) -> this {
if (range == null) {
checkState("reset()", StackState.EMPTY);
this.func = func;
return_results = func.sig.results;
parent = null;
bottom = this;
state_ = StackState.COMPRESSED;
return;
}
reset0(func, func.sig.params.length);
pushRspPointer(STACK_ENTER_FUNC_STUB.getEntry() + NOP_ENTRY);
}
// Requires {state == EMPTY}.
def resetForFrameWrites(func: Function) -> this {
reset0(func, 0);
}
// Attaches {segment} to this stack, which has none.
def attach(segment: MemoryRange) {
range = segment;
registerScanner();
}
private def registerScanner() {
if (scanned || !valuerep.tagged) return;
RiGc.registerScanner(this, NativeStack.scan);
scanned = true;
}
// Returns {true} if this stack has no frames yet and will enter {func} when resumed.
def isAtFuncEntry() -> bool {
return range != null && rsp < range.end && rsp.load<Pointer>() == STACK_ENTER_FUNC_STUB.getEntry() + NOP_ENTRY;
}
// Requires {state == SUSPENDED}.
// Pushes {args} incrementally onto the value stack.
def bind(args: Range<Value>) -> this {
if (args.length == 0) return;
if (NativeStackCompression.bindEntry(this, args)) return;
NativeStackCompression.decompress(this);
checkState("bind()", StackState.SUSPENDED);
for (v in args) push(v);
}
// Requires {state == SUSPENDED}.
// Resumes running the Wasm or host code in this stack until that code either returns, throws,
// or suspends itself.
def resume() -> Result {
NativeStackCompression.decompress(this);
checkState("resume()", StackState.SUSPENDED);
if (Trace.stack) traceFrames("stack.resume", rsp + RETADDR_SIZE);
var bottom = NativeStack.!(this.bottom);
Expand Down Expand Up @@ -464,6 +493,7 @@ class NativeStack extends WasmStack {
}
// GC callback to scan (tagged) values on this stack
def scan() {
if (range == null) return; // compressed, or allocated without a mapping
if (Trace.gc) {
Trace.OUT.put1("scanStack obj=0x%x", u64.view(Pointer.atObject(this) - Pointer.NULL)).ln();
}
Expand Down Expand Up @@ -505,14 +535,16 @@ class NativeStack extends WasmStack {
BpTypeCode.ARRAYREF.code,
BpTypeCode.FUNCREF.code,
BpTypeCode.EXNREF.code,
BpTypeCode.NULLEXNREF.code,
BpTypeCode.NULLFUNCREF.code => return Value.Ref(vp.load<Object>());

BpTypeCode.I32.code => return Value.I32(vp.load<u32>());
BpTypeCode.I64.code => return Value.I64(vp.load<u64>());
BpTypeCode.F32.code => return Value.F32(vp.load<u32>());
BpTypeCode.F64.code => return Value.F64(vp.load<u64>());
BpTypeCode.V128.code => return Value.V128(vp.load<u64>(), (vp + 8).load<u64>());
BpTypeCode.CONTREF.code => {
BpTypeCode.CONTREF.code,
BpTypeCode.NULLCONTREF.code => {
var stack = vp.load<WasmStack>();
var version = (vp + 8).load<u64>();
return Value.Cont(Continuation(stack, version));
Expand Down
120 changes: 120 additions & 0 deletions src/engine/native/NativeStackCompression.v3
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2026 Wizard authors. All rights reserved.
// See LICENSE for details of Apache 2.0 license.

// Values bound to the function of a {COMPRESSED} stack that has not been entered.
class EntryValues(vals: Array<Value>) extends CompressedStack {
def name() -> string { return "Entry Values"; }
def size() -> u64 { return 32uL + 24uL * u64.view(vals.length); }
}

// Compresses suspended stacks to free their segments, and decompresses them before use.
// A {COMPRESSED} stack has no segment; its {compressed} field holds its frames, the values bound
// to its function ({EntryValues}), or {null} if neither, and its {parent_rsp_ptr} field holds
// the saved contents of the parent rsp slot.
component NativeStackCompression {
var numCompressed: int;
var numDecompressed: int;

// Returns {true} if {stack} is suspended at a function entry or with only interpreter and SPC frames.
def canCompress(stack: NativeStack) -> bool {
if (stack.state_ != StackState.SUSPENDED) return false;
var range = stack.range;
if (range == null) return false;
var end = range.end;
// [end - 8] holds the parent's rsp, [end - 16] the return-parent stub.
if (stack.parent_rsp_ptr != end + -Pointer.SIZE) return false;
if (stack.isAtFuncEntry()) return stack.rsp == end + -3 * Pointer.SIZE;
var srp = stack.rsp, count = 0, bottom_sfp = Pointer.NULL;
while (srp >= range.start && srp < end) {
var retip = srp.load<Pointer>();
var code = RiRuntime.findUserCode(retip);
match (code) {
x: X86_64InterpreterCode, x: X86_64SpcModuleCode => {
count++;
bottom_sfp = srp + Pointer.SIZE;
srp = code.nextFrame(retip, srp).1;
}
x: X86_64ReturnParentStub => {
if (count == 0 || srp != end + -2 * Pointer.SIZE) return false;
// Values below the bottom frame would be lost.
return NativeFrameHandle.Interpreter(bottom_sfp).vfp() == range.start;
}
_ => return false; // inlined frames, stubs, or host frames
}
}
return false;
}
// Compresses {stack}, which must satisfy {canCompress}, and returns its now unowned segment.
def compress(stack: NativeStack) -> NativeStackSegment {
var before = Metrics.stack_compress_cycles.ticks();
var parent_rsp = stack.parent_rsp_ptr.load<Pointer>();
var c: CompressedStack;
if (stack.isAtFuncEntry()) {
var n = int.!((stack.vsp - stack.range.start) / stack.valuerep.slot_size);
if (n > 0) {
var vals = Array<Value>.new(n);
for (i < n) vals[i] = stack.readValue(stack.range.start, i);
c = EntryValues.new(vals);
}
} else {
c = StackCompression.compressStack(stack);
}
var segment = NativeStackSegment.!(stack.range);
if (Trace.stack) Trace.OUT.put2("compress stack @ 0x%x, freeing segment 0x%x",
Pointer.atObject(stack) - Pointer.NULL, segment.start - Pointer.NULL).ln();
segment.stack.set(null);
stack.range = null;
stack.vsp = stack.rsp = Pointer.NULL;
stack.parent_rsp_ptr = parent_rsp;
stack.compressed = c;
stack.state_ = StackState.COMPRESSED;
numCompressed++;
Metrics.stack_compressed.val++;
if (c != null) Metrics.stack_compressed_bytes.val += c.size();
Metrics.stack_compress_cycles.updateDiff(before);
return segment;
}
// Binds {args} to a {COMPRESSED} stack that has not been entered, without decompressing it.
def bindEntry(stack: NativeStack, args: Range<Value>) -> bool {
if (stack.state_ != StackState.COMPRESSED) return false;
var vals: Array<Value>;
match (stack.compressed) {
null => vals = Ranges.dup(args);
x: EntryValues => vals = Arrays.concat(x.vals, Ranges.dup(args));
_ => return false;
}
stack.compressed = EntryValues.new(vals);
return true;
}
// Decompresses {stack}, if compressed, onto a fresh segment.
def decompress(stack: NativeStack) {
if (stack.state_ != StackState.COMPRESSED) return;
var before = Metrics.stack_decompress_cycles.ticks();
var segment = NativeStackManager.allocSegment();
var c = stack.compressed, parent_rsp = stack.parent_rsp_ptr;
var func = stack.func, results = stack.return_results, parent = stack.parent, bottom = stack.bottom;
if (Trace.stack) Trace.OUT.put2("decompress stack @ 0x%x onto segment 0x%x",
Pointer.atObject(stack) - Pointer.NULL, segment.start - Pointer.NULL).ln();
stack.attach(segment);
segment.stack.set(stack);
stack.clear();
match (c) {
null => stack.reset(func);
x: EntryValues => {
stack.reset(func);
for (v in x.vals) stack.push(v);
}
_ => Target.writeFramesToStack(stack, StackCompression.decompressFrames(c));
}
stack.func = func;
stack.return_results = results;
stack.parent = parent;
stack.bottom = bottom;
stack.parent_rsp_ptr.store<Pointer>(parent_rsp);
stack.compressed = null;
stack.state_ = StackState.SUSPENDED;
numDecompressed++;
Metrics.stack_decompressed.val++;
Metrics.stack_decompress_cycles.updateDiff(before);
}
}
Loading
Loading