From 49b4719e0ab377862d075ce5225198de4f5243a7 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Mon, 6 Mar 2023 17:12:24 -0800 Subject: [PATCH 1/2] Checkpoint: Only emit wasm imports if they are used. Generate functions into a temporary buffer before generating all the sections, so we know which imports are used Fix estimateHeat --- src/mono/mono/mini/interp/interp.c | 8 +- .../wasm/runtime/jiterpreter-interp-entry.ts | 5 +- src/mono/wasm/runtime/jiterpreter-jit-call.ts | 5 +- src/mono/wasm/runtime/jiterpreter-support.ts | 183 ++++++++++++++---- .../runtime/jiterpreter-trace-generator.ts | 1 + src/mono/wasm/runtime/jiterpreter.ts | 112 +++++------ 6 files changed, 217 insertions(+), 97 deletions(-) diff --git a/src/mono/mono/mini/interp/interp.c b/src/mono/mono/mini/interp/interp.c index fcb463cd34112a..cf33661b2d8eb4 100644 --- a/src/mono/mono/mini/interp/interp.c +++ b/src/mono/mono/mini/interp/interp.c @@ -7733,9 +7733,11 @@ MINT_IN_CASE(MINT_BRTRUE_I8_SP) ZEROP_SP(gint64, !=); MINT_IN_BREAK; break; case JITERPRETER_NOT_JITTED: // Patch opcode to disable it because this trace failed to JIT. - mono_memory_barrier (); - *mutable_ip = MINT_TIER_NOP_JITERPRETER; - mono_memory_barrier (); + if (!mono_opt_jiterpreter_estimate_heat) { + mono_memory_barrier (); + *mutable_ip = MINT_TIER_NOP_JITERPRETER; + mono_memory_barrier (); + } ip += 3; break; default: diff --git a/src/mono/wasm/runtime/jiterpreter-interp-entry.ts b/src/mono/wasm/runtime/jiterpreter-interp-entry.ts index bc649e63ada189..cf4ce72da75e2d 100644 --- a/src/mono/wasm/runtime/jiterpreter-interp-entry.ts +++ b/src/mono/wasm/runtime/jiterpreter-interp-entry.ts @@ -283,10 +283,10 @@ function flush_wasm_entry_trampoline_jit_queue () { for (let i = 0; i < trampImports.length; i++) { mono_assert(trampImports[i], () => `trace #${i} missing`); const wasmName = compress ? i.toString(shortNameBase) : undefined; - builder.defineImportedFunction("i", trampImports[i][0], trampImports[i][1], wasmName); + builder.defineImportedFunction("i", trampImports[i][0], trampImports[i][1], true, wasmName); } - builder.generateImportSection(); + builder._generateImportSection(); // Function section builder.beginSection(3); @@ -326,6 +326,7 @@ function flush_wasm_entry_trampoline_jit_queue () { throw new Error(`Failed to generate ${info.traceName}`); builder.appendU8(WasmOpcode.end); + builder.endFunction(true); } builder.endSection(); diff --git a/src/mono/wasm/runtime/jiterpreter-jit-call.ts b/src/mono/wasm/runtime/jiterpreter-jit-call.ts index 0a75f642c05b29..6dd43b0a7dec96 100644 --- a/src/mono/wasm/runtime/jiterpreter-jit-call.ts +++ b/src/mono/wasm/runtime/jiterpreter-jit-call.ts @@ -394,9 +394,9 @@ export function mono_interp_flush_jitcall_queue () : void { // Emit function imports for (let i = 0; i < trampImports.length; i++) { const wasmName = compress ? i.toString(shortNameBase) : undefined; - builder.defineImportedFunction("i", trampImports[i][0], trampImports[i][1], wasmName); + builder.defineImportedFunction("i", trampImports[i][0], trampImports[i][1], true, wasmName); } - builder.generateImportSection(); + builder._generateImportSection(); // Function section builder.beginSection(3); @@ -432,6 +432,7 @@ export function mono_interp_flush_jitcall_queue () : void { if (!ok) throw new Error(`Failed to generate ${info.name}`); builder.appendU8(WasmOpcode.end); + builder.endFunction(true); } builder.endSection(); diff --git a/src/mono/wasm/runtime/jiterpreter-support.ts b/src/mono/wasm/runtime/jiterpreter-support.ts index 4badaebe14cd3e..a9ddfa8e0d2877 100644 --- a/src/mono/wasm/runtime/jiterpreter-support.ts +++ b/src/mono/wasm/runtime/jiterpreter-support.ts @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { mono_assert } from "./types"; import { NativePointer, ManagedPointer, VoidPtr } from "./types/emscripten"; import { Module } from "./imports"; import { WasmOpcode } from "./jiterpreter-opcodes"; @@ -30,6 +31,26 @@ type FunctionTypeByIndex = [ returnType: WasmValtype, ]; +type FunctionInfo = { + index: number; + name: string; + typeName: string; + typeIndex: number; + locals: { [name: string]: WasmValtype }; + export: boolean; + generator: Function; + error: Error | null; + blob: Uint8Array | null; +} + +type ImportedFunctionInfo = { + index?: number; + typeIndex: number; + module: string; + name: string; + friendlyName: string; +} + export class WasmBuilder { stack: Array; stackSize!: number; @@ -49,10 +70,11 @@ export class WasmBuilder { functionTypesByIndex: { [index: number] : FunctionTypeByIndex } = {}; importedFunctionCount!: number; - importedFunctions!: { [name: string] : [ - index: number, typeIndex: number, unknown: string - ] }; - importsToEmit!: Array<[string, string, number, number]>; + importedFunctions!: { [name: string] : ImportedFunctionInfo }; + nextImportIndex = 0; + + functions: Array = []; + argumentCount!: number; activeBlocks!: number; base!: MintOpcodePtr; @@ -80,9 +102,12 @@ export class WasmBuilder { this.functionTypesByShape = Object.create(this.permanentFunctionTypesByShape); this.functionTypesByIndex = Object.create(this.permanentFunctionTypesByIndex); + this.nextImportIndex = 0; this.importedFunctionCount = 0; this.importedFunctions = {}; - this.importsToEmit = []; + + this.functions.length = 0; + this.argumentCount = 0; this.current.clear(); this.traceBuf.length = 0; @@ -97,23 +122,27 @@ export class WasmBuilder { this.allowNullCheckOptimization = this.options.eliminateNullChecks; } - push () { + _push () { this.stackSize++; if (this.stackSize >= this.stack.length) this.stack.push(new BlobBuilder()); this.current.clear(); } - pop () { + _pop (writeToOutput: boolean) { if (this.stackSize <= 1) throw new Error("Stack empty"); const current = this.current; this.stackSize--; - this.appendULeb(current.size); const av = current.getArrayView(); - this.appendBytes(av); + if (writeToOutput) { + this.appendULeb(current.size); + this.appendBytes(av); + return null; + } else + return av.slice(); } get bytesGeneratedSoFar () { @@ -274,17 +303,23 @@ export class WasmBuilder { this.endSection(); } - generateImportSection () { + _generateImportSection () { + const allImports = Object.values(this.importedFunctions); + const importsToEmit = allImports.filter(f => f.index !== undefined); + importsToEmit.sort((lhs, rhs) => lhs.index! - rhs.index!); + // Import section this.beginSection(2); - this.appendULeb(1 + this.importsToEmit.length + this.constantSlots.length); - - for (let i = 0; i < this.importsToEmit.length; i++) { - const tup = this.importsToEmit[i]; - this.appendName(tup[0]); - this.appendName(tup[1]); - this.appendU8(tup[2]); - this.appendULeb(tup[3]); + this.appendULeb(1 + importsToEmit.length + this.constantSlots.length); + + // console.log(`referenced ${importsToEmit.length}/${allImports.length} import(s)`); + for (let i = 0; i < importsToEmit.length; i++) { + const ifi = importsToEmit[i]; + // console.log(` #${ifi.index} ${ifi.module}.${ifi.name} = ${ifi.friendlyName}`); + this.appendName(ifi.module); + this.appendName(ifi.name); + this.appendU8(0x0); // function + this.appendU8(ifi.typeIndex); } for (let i = 0; i < this.constantSlots.length; i++) { @@ -306,33 +341,110 @@ export class WasmBuilder { defineImportedFunction ( module: string, name: string, functionTypeName: string, - wasmName?: string - ) { - const index = this.importedFunctionCount++; + assumeUsed: boolean, wasmName?: string + ) : ImportedFunctionInfo { const type = this.functionTypes[functionTypeName]; if (!type) throw new Error("No function type named " + functionTypeName); const typeIndex = type[0]; - this.importedFunctions[name] = [ - index, typeIndex, type[3] - ]; - this.importsToEmit.push([module, wasmName || name, 0, typeIndex]); - return index; + const result = this.importedFunctions[name] = { + index: assumeUsed ? this.importedFunctionCount++ : undefined, + typeIndex, + module, + name: wasmName || name, + friendlyName: name, + }; + return result; + } + + defineFunction ( + options: { + type: string, + name: string, + export: boolean, + locals: { [name: string]: WasmValtype } + }, generator: Function + ) { + const rec : FunctionInfo = { + index: this.functions.length, + name: options.name, + typeName: options.type, + typeIndex: this.functionTypes[options.type][0], + export: options.export, + locals: options.locals, + generator, + error: null, + blob: null + }; + this.functions.push(rec); + } + + emitImportsAndFunctions () { + let exportCount = 0; + for (let i = 0; i < this.functions.length; i++) { + const func = this.functions[i]; + if (func.export) + exportCount++; + + this.beginFunction(func.typeName, func.locals); + try { + func.generator(); + /* + } catch (exc) { + func.error = exc; + */ + } finally { + func.blob = this.endFunction(false); + } + } + + this._generateImportSection(); + + // Function section + this.beginSection(3); + this.appendULeb(this.functions.length); + for (let i = 0; i < this.functions.length; i++) + this.appendULeb(this.functions[i].typeIndex); + + // Export section + this.beginSection(7); + this.appendULeb(exportCount); + for (let i = 0; i < this.functions.length; i++) { + const func = this.functions[i]; + if (!func.export) + continue; + this.appendName(func.name); + this.appendU8(0); // func export + this.appendULeb(this.importedFunctionCount + i); + } + + // Code section + this.beginSection(10); + this.appendULeb(this.functions.length); + for (let i = 0; i < this.functions.length; i++) { + const func = this.functions[i]; + mono_assert(func.blob, () => `expected function ${func.name} to have a body`); + this.appendULeb(func.blob.length); + this.appendBytes(func.blob); + } + this.endSection(); } callImport (name: string) { const func = this.importedFunctions[name]; if (!func) throw new Error("No imported function named " + name); + if (func.index === undefined) + func.index = this.importedFunctionCount++; this.appendU8(WasmOpcode.call); - this.appendULeb(func[0]); + this.appendULeb(func.index); } beginSection (type: number) { if (this.inSection) - this.pop(); + this._pop(true); this.appendU8(type); - this.push(); + this._push(); this.inSection = true; } @@ -340,8 +452,8 @@ export class WasmBuilder { if (!this.inSection) throw new Error("Not in section"); if (this.inFunction) - this.endFunction(); - this.pop(); + this.endFunction(true); + this._pop(true); this.inSection = false; } @@ -350,8 +462,8 @@ export class WasmBuilder { locals?: {[name: string]: WasmValtype} ) { if (this.inFunction) - this.endFunction(); - this.push(); + throw new Error("Already in function"); + this._push(); const signature = this.functionTypes[type]; this.locals.clear(); @@ -449,13 +561,14 @@ export class WasmBuilder { this.inFunction = true; } - endFunction () { + endFunction (writeToOutput: boolean) { if (!this.inFunction) throw new Error("Not in function"); if (this.activeBlocks > 0) throw new Error(`${this.activeBlocks} unclosed block(s) at end of function`); - this.pop(); + const result = this._pop(writeToOutput); this.inFunction = false; + return result; } block (type?: WasmValtype, opcode?: WasmOpcode) { diff --git a/src/mono/wasm/runtime/jiterpreter-trace-generator.ts b/src/mono/wasm/runtime/jiterpreter-trace-generator.ts index a5abe6f8e152b0..93be204e53bd85 100644 --- a/src/mono/wasm/runtime/jiterpreter-trace-generator.ts +++ b/src/mono/wasm/runtime/jiterpreter-trace-generator.ts @@ -1238,6 +1238,7 @@ export function generate_wasm_body ( // from there. builder.local("eip"); builder.appendU8(WasmOpcode.return_); + builder.appendU8(WasmOpcode.end); return result; } diff --git a/src/mono/wasm/runtime/jiterpreter.ts b/src/mono/wasm/runtime/jiterpreter.ts index 1ba27cfb1e7653..419cd11ec705ab 100644 --- a/src/mono/wasm/runtime/jiterpreter.ts +++ b/src/mono/wasm/runtime/jiterpreter.ts @@ -670,74 +670,72 @@ function generate_wasm ( for (let i = 0; i < traceImports.length; i++) { mono_assert(traceImports[i], () => `trace #${i} missing`); const wasmName = compress ? i.toString(shortNameBase) : undefined; - builder.defineImportedFunction("i", traceImports[i][0], traceImports[i][1], wasmName); + builder.defineImportedFunction("i", traceImports[i][0], traceImports[i][1], false, wasmName); } - builder.generateImportSection(); - - // Function section - builder.beginSection(3); - builder.appendULeb(1); - // Function type for our compiled trace - mono_assert(builder.functionTypes["trace"], "func type missing"); - builder.appendULeb(builder.functionTypes["trace"][0]); - - // Export section - builder.beginSection(7); - builder.appendULeb(1); - builder.appendName(traceName); - builder.appendU8(0); - // Imports get added to the function index space, so we need to add - // the count of imported functions to get the index of our compiled trace - builder.appendULeb(builder.importedFunctionCount + 0); - - // Code section - builder.beginSection(10); - builder.appendULeb(1); - builder.beginFunction("trace", { - "eip": WasmValtype.i32, - "temp_ptr": WasmValtype.i32, - "cknull_ptr": WasmValtype.i32, - "math_lhs32": WasmValtype.i32, - "math_rhs32": WasmValtype.i32, - "math_lhs64": WasmValtype.i64, - "math_rhs64": WasmValtype.i64, - "temp_f32": WasmValtype.f32, - "temp_f64": WasmValtype.f64, - }); - - if (emitPadding) { - builder.appendU8(WasmOpcode.nop); - builder.appendU8(WasmOpcode.nop); - } + let keep = true, + opcodesProcessed = 0; + builder.defineFunction( + { + type: "trace", + name: traceName, + export: true, + locals: { + "eip": WasmValtype.i32, + "temp_ptr": WasmValtype.i32, + "cknull_ptr": WasmValtype.i32, + "math_lhs32": WasmValtype.i32, + "math_rhs32": WasmValtype.i32, + "math_lhs64": WasmValtype.i64, + "math_rhs64": WasmValtype.i64, + "temp_f32": WasmValtype.f32, + "temp_f64": WasmValtype.f64, + } + }, () => { + if (emitPadding) { + builder.appendU8(WasmOpcode.nop); + builder.appendU8(WasmOpcode.nop); + } - builder.base = ip; - if (getU16(ip) !== MintOpcode.MINT_TIER_PREPARE_JITERPRETER) - throw new Error(`Expected *ip to be MINT_TIER_PREPARE_JITERPRETER but was ${getU16(ip)}`); - - // TODO: Call generate_wasm_body before generating any of the sections and headers. - // This will allow us to do things like dynamically vary the number of locals, in addition - // to using global constants and figuring out how many constant slots we need in advance - // since a long trace might need many slots and that bloats the header. - const opcodes_processed = generate_wasm_body( - frame, traceName, ip, startOfBody, endOfBody, - builder, instrumentedTraceId, backwardBranchTable + builder.base = ip; + if (getU16(ip) !== MintOpcode.MINT_TIER_PREPARE_JITERPRETER) + throw new Error(`Expected *ip to be MINT_TIER_PREPARE_JITERPRETER but was ${getU16(ip)}`); + + // TODO: Call generate_wasm_body before generating any of the sections and headers. + // This will allow us to do things like dynamically vary the number of locals, in addition + // to using global constants and figuring out how many constant slots we need in advance + // since a long trace might need many slots and that bloats the header. + opcodesProcessed = generate_wasm_body( + frame, traceName, ip, startOfBody, endOfBody, + builder, instrumentedTraceId, backwardBranchTable + ); + keep = (opcodesProcessed >= mostRecentOptions!.minimumTraceLength); + } ); - const keep = (opcodes_processed >= mostRecentOptions.minimumTraceLength); + + builder.emitImportsAndFunctions(); + + /* + + builder.beginFunction("trace", ); + + ... + + builder.appendU8(WasmOpcode.end); + builder.endSection(); + + */ if (!keep) { const ti = traceInfo[ip]; if (ti && (ti.abortReason === "end-of-body")) ti.abortReason = "trace-too-small"; - if (traceTooSmall && (opcodes_processed > 1)) - console.log(`${traceName} too small: ${opcodes_processed} opcodes, ${builder.current.size} wasm bytes`); + if (traceTooSmall && (opcodesProcessed > 1)) + console.log(`${traceName} too small: ${opcodesProcessed} opcodes, ${builder.current.size} wasm bytes`); return 0; } - builder.appendU8(WasmOpcode.end); - builder.endSection(); - compileStarted = _now(); const buffer = builder.getArrayView(); if (trace > 0) @@ -1020,6 +1018,9 @@ export function jiterpreter_dump_stats (b?: boolean, concise?: boolean) { // logging it. if (!traces[i].name) continue; + // This means the trace did compile and just aborted later on + if (traces[i].fnPtr) + continue; // Filter out noisy methods that we don't care about optimizing if (traces[i].name!.indexOf("Xunit.") >= 0) continue; @@ -1037,6 +1038,7 @@ export function jiterpreter_dump_stats (b?: boolean, concise?: boolean) { switch (traces[i].abortReason) { // not feasible to fix case "trace-too-small": + case "trace-too-big": case "call": case "callvirt.fast": case "calli.nat.fast": From 620d83f17c3e62fd704e675246a1cfd40793f64b Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Mon, 6 Mar 2023 17:48:14 -0800 Subject: [PATCH 2/2] Improve size calculation --- src/mono/wasm/runtime/jiterpreter-support.ts | 18 +++++++++++++++++- .../runtime/jiterpreter-trace-generator.ts | 8 +++++++- src/mono/wasm/runtime/jiterpreter.ts | 17 ----------------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/mono/wasm/runtime/jiterpreter-support.ts b/src/mono/wasm/runtime/jiterpreter-support.ts index a9ddfa8e0d2877..665757385ed802 100644 --- a/src/mono/wasm/runtime/jiterpreter-support.ts +++ b/src/mono/wasm/runtime/jiterpreter-support.ts @@ -74,6 +74,7 @@ export class WasmBuilder { nextImportIndex = 0; functions: Array = []; + estimatedExportBytes = 0; argumentCount!: number; activeBlocks!: number; @@ -107,6 +108,7 @@ export class WasmBuilder { this.importedFunctions = {}; this.functions.length = 0; + this.estimatedExportBytes = 0; this.argumentCount = 0; this.current.clear(); @@ -145,8 +147,19 @@ export class WasmBuilder { return av.slice(); } + // HACK: Approximate amount of space we need to generate the full module at present + // FIXME: This does not take into account any other functions already generated if they weren't + // emitted into the module immediately get bytesGeneratedSoFar () { - return this.stack[0].size; + return this.stack[0].size + + // HACK: A random constant for section headers and padding + 32 + + // mod (2 bytes) name (2-3 bytes) type (1 byte) typeidx (1-2 bytes) + (this.importedFunctionCount * 8) + + // type index for each function + (this.functions.length * 2) + + // export entry for each export + this.estimatedExportBytes; } get current() { @@ -377,6 +390,9 @@ export class WasmBuilder { blob: null }; this.functions.push(rec); + if (rec.export) + this.estimatedExportBytes += rec.name.length + 8; + return rec; } emitImportsAndFunctions () { diff --git a/src/mono/wasm/runtime/jiterpreter-trace-generator.ts b/src/mono/wasm/runtime/jiterpreter-trace-generator.ts index 93be204e53bd85..0b8ff2e4cc9bfa 100644 --- a/src/mono/wasm/runtime/jiterpreter-trace-generator.ts +++ b/src/mono/wasm/runtime/jiterpreter-trace-generator.ts @@ -18,7 +18,7 @@ import { getMemberOffset, JiterpMember } from "./jiterpreter-support"; import { - sizeOfDataItem, maxModuleSize, + sizeOfDataItem, disabledOpcodes, countCallTargets, callTargetCounts, trapTraceErrors, @@ -178,7 +178,13 @@ export function generate_wasm_body ( record_abort(traceIp, ip, traceName, "end-of-body"); break; } + + // HACK: Browsers set a limit of 4KB, we lower it slightly since a single opcode + // might generate a ton of code and we generate a bit of an epilogue after + // we finish + const maxModuleSize = 3850; if (builder.size >= maxModuleSize - builder.bytesGeneratedSoFar) { + // console.log(`trace too big, estimated size is ${builder.size + builder.bytesGeneratedSoFar}`); record_abort(traceIp, ip, traceName, "trace-too-big"); break; } diff --git a/src/mono/wasm/runtime/jiterpreter.ts b/src/mono/wasm/runtime/jiterpreter.ts index 419cd11ec705ab..3bce90e22bd2d0 100644 --- a/src/mono/wasm/runtime/jiterpreter.ts +++ b/src/mono/wasm/runtime/jiterpreter.ts @@ -126,12 +126,6 @@ export const traceInfo : { [key: string] : TraceInfo } = {}; export const sizeOfDataItem = 4, sizeOfObjectHeader = 8, - - // HACK: Typically we generate ~12 bytes of extra gunk after the function body so we are - // subtracting 20 from the maximum size to make sure we don't produce too much - // Also subtract some more size since the wasm we generate for one opcode could be big - // WASM implementations only allow compiling 4KB of code at once :-) - maxModuleSize = 4000 - 160, // While stats are enabled, dump concise stats every N traces so that it's clear a long-running // task isn't frozen if it's jitting lots of traces autoDumpInterval = 500; @@ -715,17 +709,6 @@ function generate_wasm ( builder.emitImportsAndFunctions(); - /* - - builder.beginFunction("trace", ); - - ... - - builder.appendU8(WasmOpcode.end); - builder.endSection(); - - */ - if (!keep) { const ti = traceInfo[ip]; if (ti && (ti.abortReason === "end-of-body"))