diff --git a/src/benchmark.cts b/src/benchmark.cts index b1035443..52ff0e46 100644 --- a/src/benchmark.cts +++ b/src/benchmark.cts @@ -7,13 +7,13 @@ import { Trace } from "./tracer.cjs" //@ts-ignore import { instrument_wasm } from '../wasabi/wasabi_js.js' import { createMeasure } from './performance.cjs' +import { generateJavascript } from './js-generator.cjs' export type Record = { binary: number[], trace: Trace }[] type WasabiRuntime = string[] export default class Benchmark { - private record: Record private constructor() { } @@ -23,19 +23,19 @@ export default class Benchmark { await Promise.all(this.record.map(async ({ binary, trace }, i) => { const binPath = path.join(benchmarkPath, `bin_${i}`) await fs.mkdir(binPath) - // if (options.trace === true) { + if (options.trace === true) { await fs.writeFile(path.join(binPath, 'trace.r3'), trace.toString()) - // } + } const diskSave = `temp-trace-${i}.r3` await fs.writeFile(diskSave, trace.toString()) - // console.log('wrote temp trace to disk and start stream code generation') const p_measureCodeGen = createMeasure('ir-gen', { phase: 'replay-generation', description: `The time it takes to generate the IR code for subbenchmark ${i}` }) const code = await new Generator().generateReplayFromStream(fss.createReadStream(diskSave)) p_measureCodeGen() const p_measureJSWrite = createMeasure('string-gen', { phase: 'replay-generation', description: `The time it takes to stream the replay code to the file for subbenchmark ${i}` }) - await code.toWriteStream(fss.createWriteStream(path.join(binPath, 'replay.js'))) + await generateJavascript(fss.createWriteStream(path.join(binPath, 'replay.js')), code) p_measureJSWrite() await fs.writeFile(path.join(binPath, 'index.wasm'), Buffer.from(binary)) + await fs.rm(diskSave) })) p_measureSave() } diff --git a/src/instrumenter.cts b/src/instrumenter.cts index 0365663f..25f1141d 100644 --- a/src/instrumenter.cts +++ b/src/instrumenter.cts @@ -5,12 +5,13 @@ import { askQuestion } from './util.cjs'; import { Options } from './cli/options.cjs' import Generator from './replay-generator.cjs'; import { initPerformance } from './performance.cjs'; +import { generateJavascript } from './js-generator.cjs'; export default async function run(url: string, options: Options) { await initPerformance(url, 'manual-run', 'performance.ndjson') if (options.file !== undefined) { const code = await new Generator().generateReplayFromStream(fss.createReadStream(options.file)) - code.toWriteStream(fss.createWriteStream(options.file + '.js')) + generateJavascript(fss.createWriteStream(options.file + '.js'), code) return } const analyser = new Analyser('./dist/src/tracer.cjs', { extended: options.extended, noRecord: options.noRecord }) diff --git a/src/js-generator.cts b/src/js-generator.cts new file mode 100644 index 00000000..a7d20b5f --- /dev/null +++ b/src/js-generator.cts @@ -0,0 +1,243 @@ +import { WriteStream } from "fs"; +import { ReplayToWriteStream, Result, Store, Call, TableCall, MemGrow, TableSet, TableGrow, GlobalSet, Context } from "./replay-generator.cjs"; +import { unreachable } from "./util.cjs"; + +export const generateJavascript: ReplayToWriteStream = async (stream, code) => { + stream.write(`import fs from 'fs'\n`) + stream.write(`import path from 'path'\n`) + stream.write(`let instance\n`) + stream.write('let imports = {}\n') + + // Init modules + for (let module of code.modules) { + stream.write(`${writeModule(module)}\n`) + } + // Init memories + for (let memidx in code.memImports) { + let mem = code.memImports[memidx] + stream.write(`const ${mem.name} = new WebAssembly.Memory({ initial: ${mem.initial}, maximum: ${mem.maximum} })\n`) + stream.write(`${writeImport(mem.module, mem.name)}${mem.name}\n`) + } + // Init globals + for (let globalIdx in code.globalImports) { + let global = code.globalImports[globalIdx] + // There is a special case here: + // you can also import the values NaN and Infinity as globals in WebAssembly + // Thats why whe need this if else + if (Number.isNaN(global.initial) || !Number.isFinite(global.initial)) { + if (global.name.toLocaleLowerCase() === 'infinity') { + stream.write(`${writeImport(global.module, global.name)}Infinity\n`) + } else if (global.name.toLocaleLowerCase() === 'nan') { + stream.write(`${writeImport(global.module, global.name)}NaN\n`) + } else { + throw new Error(`Could not generate javascript code for the global initialisation, the initial value is NaN. The website you where recording did some weired stuff that I was not considering during the implementation of Wasm-R3. Tried to genereate global ${global}`) + } + } else { + stream.write(`const ${global.name} = new WebAssembly.Global({ value: '${global.value}', mutable: ${global.mutable}}, ${global.initial})\n`) + stream.write(`${writeImport(global.module, global.name)}${global.name}\n`) + } + } + // Init tables + for (let tableidx in code.tableImports) { + let table = code.tableImports[tableidx] + stream.write(`const ${table.name} = new WebAssembly.Table({ initial: ${table.initial}, maximum: ${table.maximum}, element: '${table.element}'})\n`) + stream.write(`${writeImport(table.module, table.name)}${table.name}\n`) + } + // Imported functions + for (let funcidx in code.funcImports) { + stream.write(`let ${writeFuncGlobal(funcidx)} = -1\n`) + let func = code.funcImports[funcidx] + stream.write(`${writeImport(func.module, func.name)}() => {\n`) + stream.write(`${writeFuncGlobal(funcidx)}++\n`) + if (func.bodys.length !== 0) { + stream.write(`switch (${writeFuncGlobal(funcidx)}) {\n`) + for (let i = 0; i < func.bodys.length; i++) { + await writeBody(stream, func.bodys[i], i) + } + stream.write('}\n') + } + await writeResults(stream, func.results, writeFuncGlobal(funcidx)) + stream.write('}\n') + } + stream.write(`export function replay(wasm) {`) + stream.write(`instance = wasm.instance\n`) + for (let exp of code.calls) { + stream.write(`instance.exports.${exp.name}(${writeParamsString(exp.params)}) \n`) + } + // Init entity states + for (let event of code.initialization) { + switch (event.type) { + case 'Store': + await write(stream, storeEvent(event)) + break + case 'MemGrow': + await write(stream, memGrowEvent(event)) + break + case 'TableSet': + await write(stream, tableSetEvent(event)) + break + case 'TableGrow': + await write(stream, tableGrowEvent(event)) + break + case 'Call': + await write(stream, callEvent(event)) + break + case 'TableCall': + await write(stream, tableCallEvent(event)) + break + case 'GlobalSet': + await write(stream, globalSet(event)) + break + default: unreachable(event) + } + } + stream.write(`}\n`) + stream.write(`export function instantiate(wasmBinary) {\n`) + stream.write(`return WebAssembly.instantiate(wasmBinary, imports)\n`) + stream.write(`}\n`) + stream.write(`if (process.argv[2] === 'run') {\n`) + stream.write(`const p = path.join(path.dirname(import.meta.url).replace(/^file:/, ''), 'index.wasm')\n`) + stream.write(`const wasmBinary = fs.readFileSync(p)\n`) + stream.write(`instantiate(wasmBinary).then((wasm) => replay(wasm))\n`) + stream.write(`}\n`) + stream.close() +} + + +async function writeBody(stream: WriteStream, b: Context, i: number) { + if (b.length !== 0) { + await write(stream, `case ${i}:\n`) + for (let event of b) { + switch (event.type) { + case 'Call': + await write(stream, callEvent(event)) + break + case 'TableCall': + await write(stream, tableCallEvent(event)) + break + case 'Store': + await write(stream, storeEvent(event)) + break + case 'MemGrow': + await write(stream, memGrowEvent(event)) + break + case 'TableSet': + await write(stream, tableSetEvent(event)) + break + case 'TableGrow': + await write(stream, tableGrowEvent(event)) + break + case 'GlobalSet': + await write(stream, globalSet(event)) + break + default: unreachable(event) + } + } + stream.write(`break\n`) + } +} + +async function writeResults(stream: WriteStream, results: Result[], funcGlobal: string) { + let current = 0 + for (let r of results) { + const newC = current + r.reps + stream.write(`if ((${funcGlobal} >= ${current}) && ${funcGlobal} < ${newC}) {\n`) + stream.write(`return ${r.results[0]} }\n`) + current = newC + } +} + +async function write(stream: WriteStream, s: string) { + if (stream.write(s) === false) { + await new Promise((resolve) => stream.once('drain', resolve)) + } +} + +function storeEvent(event: Store) { + let jsString = '' + event.data.forEach((byte, j) => { + event = event as Store + if (event.import) { + jsString += `new Uint8Array(${event.name}.buffer)[${event.addr + j}] = ${byte}\n` + } else { + jsString += `new Uint8Array(instance.exports.${event.name}.buffer)[${event.addr + j}] = ${byte}\n` + } + }) + return jsString +} + +function callEvent(event: Call) { + return `instance.exports.${event.name}(${writeParamsString(event.params)})\n` +} + +function tableCallEvent(event: TableCall) { + return `instance.exports.${event.tableName}.get(${event.funcidx})(${writeParamsString(event.params)})\n` +} + +function memGrowEvent(event: MemGrow) { + let jsString = '' + if (event.import) { + jsString += `${event.name}.grow(${event.amount})\n` + } else { + jsString += `instance.exports.${event.name}.grow(${event.amount})\n` + } + return jsString +} + +function tableSetEvent(event: TableSet) { + let jsString = '' + if (event.import) { + jsString += `${event.name}.set(${event.idx}, ` + } else { + jsString += `instance.exports.${event.name}.set(${event.idx}, ` + } + if (event.funcImport) { + jsString += `${event.funcName}` + } else { + jsString += `instance.exports.${event.funcName}` + } + jsString += `)\n` + return jsString +} + +function tableGrowEvent(event: TableGrow) { + let jsString = '' + if (event.import) { + jsString += `${event.name}.grow(${event.amount})\n` + } else { + jsString += `instance.exports.${event.name}.grow(${event.amount})\n` + } + return jsString +} + +function globalSet(event: GlobalSet) { + let jsString = '' + if (event.import) { + jsString += `${event.name}.value = ${event.value}\n` + } else { + jsString += `instance.exports.${event.name}.value = ${event.bigInt ? `BigInt(${event.value})` : event.value}\n` + } + return jsString +} + + +function writeFuncGlobal(funcidx: string) { + return `global_${funcidx}` +} + +function writeModule(module: string) { + return `imports['${module}'] = {}` +} + +function writeImport(module: string, name: string) { + return `imports['${module}']['${name}'] = ` +} + +function writeParamsString(params: number[]) { + let paramsString = '' + for (let p of params) { + paramsString += p + ',' + } + paramsString = paramsString.slice(0, -1) + return paramsString +} \ No newline at end of file diff --git a/src/replay-generator.cts b/src/replay-generator.cts index b913ceb5..7b877c98 100644 --- a/src/replay-generator.cts +++ b/src/replay-generator.cts @@ -4,43 +4,70 @@ import fs, { WriteStream } from "fs" import readline from 'readline' import { WasmEvent } from "../trace.cjs" -type ImpExp = { import: boolean, name: string } -type Call = { type: "Call", name: string, params: number[] } -type TableCall = { type: 'TableCall', tableName: string, funcidx: number, params: number[] } -type Store = { type: "Store", addr: number, data: number[] } & ImpExp -type MemGrow = { type: "MemGrow", amount: number } & ImpExp -type TableSet = { type: "TableSet", idx: number, funcImport: boolean, funcName: string } & ImpExp -type TableGrow = { type: "TableGrow", idx: number, amount: number } & ImpExp -type GlobalSet = { type: "GlobalSet", value: number, bigInt: boolean } & ImpExp -type Event = Call | TableCall | Store | MemGrow | TableSet | TableGrow | GlobalSet -type Import = { module: string, name: string } -type Result = { results: number[], reps: number } -type Context = Event[] -type Function = Import & { bodys: Context[], results: Result[] } -type Memory = Import & WebAssembly.MemoryDescriptor -type Table = Import & WebAssembly.TableDescriptor -type Global = Import & WebAssembly.GlobalDescriptor & { initial: number } + +export type ImpExp = { import: boolean, name: string } +export type Call = { type: "Call", name: string, params: number[] } +export type TableCall = { type: 'TableCall', tableName: string, funcidx: number, params: number[] } +export type Store = { type: "Store", addr: number, data: number[] } & ImpExp +export type MemGrow = { type: "MemGrow", amount: number } & ImpExp +export type TableSet = { type: "TableSet", idx: number, funcImport: boolean, funcName: string } & ImpExp +export type TableGrow = { type: "TableGrow", idx: number, amount: number } & ImpExp +export type GlobalSet = { type: "GlobalSet", value: number, bigInt: boolean } & ImpExp +export type Event = Call | TableCall | Store | MemGrow | TableSet | TableGrow | GlobalSet +export type Import = { module: string, name: string } +export type Result = { results: number[], reps: number } +export type Context = Event[] +export type Function = Import & { bodys: Context[], results: Result[] } +export type Memory = Import & WebAssembly.MemoryDescriptor +export type Table = Import & WebAssembly.TableDescriptor +export type Global = Import & WebAssembly.GlobalDescriptor & { initial: number } +export type Replay = { + funcImports: { [key: string]: Function } + memImports: { [key: string]: Memory } + tableImports: { [key: string]: Table } + globalImports: { [key: string]: Global } + calls: { name: string, params: number[] }[] + initialization: Event[] + modules: string[] +} + +/** + * Replay Generator backend + */ +export type ReplayToWriteStream = (stream: WriteStream, code: Replay) => Promise + +/** + * Replay optimiser + */ +export type ReplayOptimise = (code: Replay) => Replay + type State = { importCallStack: CallStack, lastFunc?: Function, lastFuncReturn: boolean, globalScope: boolean, importCallStackFunction: Function[] } type CallStack = Context[] - export default class Generator { - private code: Code + private code: Replay = { + funcImports: {}, + memImports: {}, + tableImports: {}, + globalImports: {}, + calls: [], + initialization: [], + modules: [], + } private state: State constructor() { - this.code = new Code() this.state = { importCallStack: [this.code.initialization], globalScope: true, lastFuncReturn: false, importCallStackFunction: [] } } generateReplay(trace: Trace) { - if (trace.into().length === 0) { + if (trace.getLength() === 0) { throw new Error('No trace has been provided. Are you sure the app that you are using instantiates WebAssembly.') } - trace.into().forEach((event) => this.consumeEvent(event)) + trace.forEach((event) => this.consumeEvent(event)) return this.code } - generateReplayFromStream(traceStream: fs.ReadStream): Promise { + generateReplayFromStream(traceStream: fs.ReadStream): Promise { return new Promise((resolve, reject) => { const rl = readline.createInterface({ input: traceStream, @@ -48,8 +75,8 @@ export default class Generator { }); const trace = new Trace() rl.on('line', (line) => { - trace.push(Trace.parseEvent(line)) - this.consumeEvent(Trace.parseEventToObj(trace.resolve(trace.getTop()))) + trace.push(line) + this.consumeEvent(trace.parseEventToObj(trace.resolve(trace.getTop()))) }); rl.on('close', () => { @@ -179,8 +206,6 @@ export default class Generator { case 'FuncReturn': case 'LoadExt': case 'TableGetExt': - case 'Call': - case 'StoreExt': break default: unreachable(event) @@ -213,265 +238,4 @@ export default class Generator { this.code.modules.push(event.module) } } -} - -class Code { - funcImports: { [key: string]: Function } - memImports: { [key: string]: Memory } - tableImports: { [key: string]: Table } - globalImports: { [key: string]: Global } - calls: { name: string, params: number[] }[] - initialization: Event[] - modules: string[] - - constructor() { - this.funcImports = {} - this.memImports = {} - this.tableImports = {} - this.globalImports = {} - this.calls = [] - this.initialization = [] - this.modules = [] - } - - async toWriteStream(stream: WriteStream) { - stream.write(`import fs from 'fs'\n`) - stream.write(`import path from 'path'\n`) - stream.write(`let instance\n`) - // stream.write(`function getImports(wasmBinary) {\n`) - stream.write('let imports = {}\n') - - // Init modules - for (let module of this.modules) { - stream.write(`${writeModule(module)}\n`) - } - // Init memories - for (let memidx in this.memImports) { - let mem = this.memImports[memidx] - stream.write(`const ${mem.name} = new WebAssembly.Memory({ initial: ${mem.initial}, maximum: ${mem.maximum} })\n`) - stream.write(`${writeImport(mem.module, mem.name)}${mem.name}\n`) - } - // Init globals - for (let globalIdx in this.globalImports) { - let global = this.globalImports[globalIdx] - // There is a special case here: - // you can also import the values NaN and Infinity as globals in WebAssembly - // Thats why whe need this if else - if (Number.isNaN(global.initial) || !Number.isFinite(global.initial)) { - if (global.name.toLocaleLowerCase() === 'infinity') { - stream.write(`${writeImport(global.module, global.name)}Infinity\n`) - } else if (global.name.toLocaleLowerCase() === 'nan') { - stream.write(`${writeImport(global.module, global.name)}NaN\n`) - } else { - throw new Error(`Could not generate javascript code for the global initialisation, the initial value is NaN. The website you where recording did some weired stuff that I was not considering during the implementation of Wasm-R3. Tried to genereate global ${global}`) - } - } else { - stream.write(`const ${global.name} = new WebAssembly.Global({ value: '${global.value}', mutable: ${global.mutable}}, ${global.initial})\n`) - stream.write(`${writeImport(global.module, global.name)}${global.name}\n`) - } - } - // Init tables - for (let tableidx in this.tableImports) { - let table = this.tableImports[tableidx] - stream.write(`const ${table.name} = new WebAssembly.Table({ initial: ${table.initial}, maximum: ${table.maximum}, element: '${table.element}'})\n`) - stream.write(`${writeImport(table.module, table.name)}${table.name}\n`) - } - // Imported functions - for (let funcidx in this.funcImports) { - stream.write(`let ${writeFuncGlobal(funcidx)} = -1\n`) - let func = this.funcImports[funcidx] - stream.write(`${writeImport(func.module, func.name)}() => {\n`) - stream.write(`${writeFuncGlobal(funcidx)}++\n`) - if (func.bodys.length !== 0) { - stream.write(`switch (${writeFuncGlobal(funcidx)}) {\n`) - for (let i = 0; i < func.bodys.length; i++) { - await this.writeBody(stream, func.bodys[i], i) - } - stream.write('}\n') - } - await this.writeResults(stream, func.results, writeFuncGlobal(funcidx)) - stream.write('}\n') - } - // stream.write(`return imports\n`) - // stream.write('}\n') - stream.write(`export function replay(wasm) {`) - stream.write(`instance = wasm.instance\n`) - for (let exp of this.calls) { - stream.write(`instance.exports.${exp.name}(${writeParamsString(exp.params)}) \n`) - } - // Init entity states - for (let event of this.initialization) { - switch (event.type) { - case 'Store': - await this.write(stream, this.storeEvent(event)) - break - case 'MemGrow': - await this.write(stream, this.memGrowEvent(event)) - break - case 'TableSet': - await this.write(stream, this.tableSetEvent(event)) - break - case 'TableGrow': - await this.write(stream, this.tableGrowEvent(event)) - break - case 'Call': - await this.write(stream, this.callEvent(event)) - break - case 'TableCall': - await this.write(stream, this.tableCallEvent(event)) - break - case 'GlobalSet': - await this.write(stream, this.globalSet(event)) - break - default: unreachable(event) - } - } - stream.write(`}\n`) - stream.write(`export function instantiate(wasmBinary) {\n`) - stream.write(`return WebAssembly.instantiate(wasmBinary, imports)\n`) - stream.write(`}\n`) - stream.write(`if (process.argv[2] === 'run') {\n`) - stream.write(`const p = path.join(path.dirname(import.meta.url).replace(/^file:/, ''), 'index.wasm')\n`) - stream.write(`const wasmBinary = fs.readFileSync(p)\n`) - stream.write(`instantiate(wasmBinary).then((wasm) => replay(wasm))\n`) - stream.write(`}\n`) - stream.close() - } - - private async writeBody(stream: WriteStream, b: Event[], i: number) { - if (b.length !== 0) { - await this.write(stream, `case ${i}:\n`) - for (let event of b) { - switch (event.type) { - case 'Call': - await this.write(stream, this.callEvent(event)) - break - case 'TableCall': - await this.write(stream, this.tableCallEvent(event)) - break - case 'Store': - await this.write(stream, this.storeEvent(event)) - break - case 'MemGrow': - await this.write(stream, this.memGrowEvent(event)) - break - case 'TableSet': - await this.write(stream, this.tableSetEvent(event)) - break - case 'TableGrow': - await this.write(stream, this.tableGrowEvent(event)) - break - case 'GlobalSet': - await this.write(stream, this.globalSet(event)) - break - default: unreachable(event) - } - } - stream.write(`break\n`) - } - } - - private async writeResults(stream: WriteStream, results: Result[], funcGlobal: string) { - let current = 0 - for (let r of results) { - const newC = current + r.reps - stream.write(`if ((${funcGlobal} >= ${current}) && ${funcGlobal} < ${newC}) {\n`) - stream.write(`return ${r.results[0]} }\n`) - current = newC - } - } - - private async write(stream: WriteStream, s: string) { - if (stream.write(s) === false) { - await new Promise((resolve) => stream.once('drain', resolve)) - } - } - - private storeEvent(event: Store) { - let jsString = '' - event.data.forEach((byte, j) => { - event = event as Store - if (event.import) { - jsString += `new Uint8Array(${event.name}.buffer)[${event.addr + j}] = ${byte}\n` - } else { - jsString += `new Uint8Array(instance.exports.${event.name}.buffer)[${event.addr + j}] = ${byte}\n` - } - }) - return jsString - } - - private callEvent(event: Call) { - return `instance.exports.${event.name}(${writeParamsString(event.params)})\n` - } - - private tableCallEvent(event: TableCall) { - return `instance.exports.${event.tableName}.get(${event.funcidx})(${writeParamsString(event.params)})\n` - } - - private memGrowEvent(event: MemGrow) { - let jsString = '' - if (event.import) { - jsString += `${event.name}.grow(${event.amount})\n` - } else { - jsString += `instance.exports.${event.name}.grow(${event.amount})\n` - } - return jsString - } - - private tableSetEvent(event: TableSet) { - let jsString = '' - if (event.import) { - jsString += `${event.name}.set(${event.idx}, ` - } else { - jsString += `instance.exports.${event.name}.set(${event.idx}, ` - } - if (event.funcImport) { - jsString += `${event.funcName}` - } else { - jsString += `instance.exports.${event.funcName}` - } - jsString += `)\n` - return jsString - } - - private tableGrowEvent(event: TableGrow) { - let jsString = '' - if (event.import) { - jsString += `${event.name}.grow(${event.amount})\n` - } else { - jsString += `instance.exports.${event.name}.grow(${event.amount})\n` - } - return jsString - } - - private globalSet(event: GlobalSet) { - let jsString = '' - if (event.import) { - jsString += `${event.name}.value = ${event.value}\n` - } else { - jsString += `instance.exports.${event.name}.value = ${event.bigInt ? `BigInt(${event.value})` : event.value}\n` - } - return jsString - } -} - -function writeFuncGlobal(funcidx: string) { - return `global_${funcidx}` -} - -function writeModule(module: string) { - return `imports['${module}'] = {}` -} - -function writeImport(module: string, name: string) { - return `imports['${module}']['${name}'] = ` -} - -function writeParamsString(params: number[]) { - let paramsString = '' - for (let p of params) { - paramsString += p + ',' - } - paramsString = paramsString.slice(0, -1) - return paramsString } \ No newline at end of file diff --git a/tests/run-tests.cts b/tests/run-tests.cts index 5a0bb1ac..9cdb9fdd 100644 --- a/tests/run-tests.cts +++ b/tests/run-tests.cts @@ -13,6 +13,7 @@ import { Server } from 'http' import Analyser, { AnalysisResult } from '../src/analyser.cjs' import commandLineArgs from 'command-line-args' import { initPerformance } from '../src/performance.cjs' +import { generateJavascript } from '../src/js-generator.cjs' let extended = false @@ -97,7 +98,8 @@ async function runNodeTest(name: string): Promise { } let replayCode try { - replayCode = await new Generator().generateReplay(trace).toWriteStream(fss.createWriteStream(replayPath)) + replayCode = await new Generator().generateReplay(trace) + await generateJavascript(fss.createWriteStream(replayPath), replayCode) await delay(0) // WTF why do I need this WHAT THE FUCK } catch (e: any) { return { testPath, success: false, reason: e.stack }