diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62fd7d8..36ac2d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,8 @@ jobs: runs-on: ubuntu-latest # Only run benchmarks on PRs, not on main branch pushes if: github.event_name == 'pull_request' + permissions: + pull-requests: write steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ env.NODE_VERSION }} @@ -75,18 +77,26 @@ jobs: cd ./baseline npm ci npm run build - if npm run bench:baseline 2>/dev/null; then - cp ./bench/baseline.json ../baseline.json - else - echo "bench:baseline script not found on base branch, skipping comparison" - echo '{}' > ../baseline.json - fi + npm run bench:baseline + cp ./bench/baseline.json ../../baseline.json # Run benchmarks on current PR - name: Run PR benchmark run: | npm run bench:ci cp ./bench/results.json ../results.json - # Compare results + # Compare results and post PR comment - name: Compare benchmarks run: | - npm run bench:compare ../baseline.json ../results.json -- --threshold 5 + npm run bench:compare ../baseline.json ../results.json -- --threshold 100 --markdown ./bench-comment.md + - name: Post benchmark comment + if: always() && hashFiles('bench-comment.md') != '' + env: + GH_TOKEN: ${{ github.token }} + run: | + # Delete previous benchmark comment if it exists + COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \ + --jq '.[] | select(.body | startswith("## Benchmark Results")) | .id' | head -1) + if [ -n "$COMMENT_ID" ]; then + gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID -X DELETE + fi + gh pr comment ${{ github.event.pull_request.number }} --body-file ./bench-comment.md diff --git a/assembly/api-debugger.ts b/assembly/api-debugger.ts index d63705a..b1326ec 100644 --- a/assembly/api-debugger.ts +++ b/assembly/api-debugger.ts @@ -11,10 +11,17 @@ import { decodeSpi } from "./spi"; let interpreter: Interpreter | null = null; -export function resetJAM(program: u8[], pc: u32, initialGas: Gas, args: u8[], hasMetadata: boolean = false): void { +export function resetJAM( + program: u8[], + pc: u32, + initialGas: Gas, + args: u8[], + hasMetadata: boolean = false, + useBlockGas: boolean = false, +): void { const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); - const p = decodeSpi(code, liftBytes(args), 128); + const p = decodeSpi(code, liftBytes(args), 128, useBlockGas); const int = new Interpreter(p.program, p.registers, p.memory); int.nextPc = pc; int.gas.set(initialGas); @@ -26,10 +33,16 @@ export function resetJAM(program: u8[], pc: u32, initialGas: Gas, args: u8[], ha interpreter = int; } -export function resetGeneric(program: u8[], flatRegisters: u8[], initialGas: Gas, hasMetadata: boolean = false): void { +export function resetGeneric( + program: u8[], + flatRegisters: u8[], + initialGas: Gas, + hasMetadata: boolean = false, + useBlockGas: boolean = false, +): void { const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); - const p = deblob(code); + const p = deblob(code, useBlockGas); const registers: Registers = newRegisters(); fillRegisters(registers, flatRegisters); const int = new Interpreter(p, registers); @@ -49,10 +62,11 @@ export function resetGenericWithMemory( chunks: Uint8Array, initialGas: Gas, hasMetadata: boolean = false, + useBlockGas: boolean = false, ): void { const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); - const p = deblob(code); + const p = deblob(code, useBlockGas); const registers: Registers = newRegisters(); fillRegisters(registers, flatRegisters); diff --git a/assembly/api-internal.ts b/assembly/api-internal.ts index d53bb6a..f6ea831 100644 --- a/assembly/api-internal.ts +++ b/assembly/api-internal.ts @@ -71,9 +71,8 @@ export function buildMemory(builder: MemoryBuilder, pages: InitialPage[], chunks } /** Initialize new VM for execution. */ -export function vmInit(input: VmInput, useSbrkGas: boolean = false): Interpreter { +export function vmInit(input: VmInput): Interpreter { const int = new Interpreter(input.program, input.registers, input.memory); - int.useSbrkGas = useSbrkGas; int.nextPc = input.pc; int.gas.set(input.gas); return int; @@ -81,7 +80,7 @@ export function vmInit(input: VmInput, useSbrkGas: boolean = false): Interpreter /** Initialize & run & destroy a VM in a single go. */ export function vmRunOnce(input: VmInput, options: VmRunOptions): VmOutput { - const int = vmInit(input, options.useSbrkGas); + const int = vmInit(input); vmExecute(int, options.logs); return vmDestroy(int, options.dumpMemory); } diff --git a/assembly/api-types.ts b/assembly/api-types.ts index db23745..4b30aba 100644 --- a/assembly/api-types.ts +++ b/assembly/api-types.ts @@ -18,7 +18,6 @@ export class InitialChunk { } export class VmRunOptions { - useSbrkGas: boolean = false; logs: boolean = false; dumpMemory: boolean = false; } diff --git a/assembly/api-utils.ts b/assembly/api-utils.ts index 32b5eae..6f62390 100644 --- a/assembly/api-utils.ts +++ b/assembly/api-utils.ts @@ -1,11 +1,9 @@ import { buildMemory, getAssembly, vmDestroy, vmExecute, vmInit, vmRunOnce } from "./api-internal"; import { InitialChunk, InitialPage, VmInput, VmOutput, VmPause, VmRunOptions } from "./api-types"; import { Gas } from "./gas"; -import { BlockGasCost, computeGasCosts } from "./gas-costs"; import { Interpreter } from "./interpreter"; import { MaybePageFault, MemoryBuilder } from "./memory"; -import { portable } from "./portable"; -import { deblob, extractCodeAndMetadata, liftBytes } from "./program"; +import { deblob, extractCodeAndMetadata, liftBytes, ProgramCounter } from "./program"; import { NO_OF_REGISTERS, newRegisters, Registers } from "./registers"; import { decodeSpi, StandardProgram } from "./spi"; @@ -19,15 +17,29 @@ export enum HasMetadata { No = 1, } -export function getGasCosts(input: u8[], kind: InputKind, withMetadata: HasMetadata): BlockGasCost[] { - const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0); +class BlockGasCost { + pc: ProgramCounter = 0; + gas: Gas = 0; +} - // @ts-ignore: AS returns T[], JS returns iterator - asArray handles both - return portable.asArray(computeGasCosts(program.program).values()); +export function getBlockGasCosts(input: u8[], kind: InputKind, withMetadata: HasMetadata): BlockGasCost[] { + const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0, true); + const blockCosts: BlockGasCost[] = []; + const costs = program.program.gasCosts.codeAndGas; + for (let n: i32 = 0; n < costs.length; n += 1) { + const gas = costs[n] >> 8; + if (gas !== 0) { + const x = new BlockGasCost(); + x.pc = n; + x.gas = costs[n]; + blockCosts.push(x); + } + } + return blockCosts; } export function disassemble(input: u8[], kind: InputKind, withMetadata: HasMetadata): string { - const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0); + const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0, false); let output = ""; if (withMetadata === HasMetadata.Yes) { @@ -56,6 +68,8 @@ export function prepareProgram( args: u8[], /** Preallocate a bunch of memory pages for faster execution. */ preallocateMemoryPages: u32, + /** Compute gas per-block instead of per-instruction. */ + useBlockGas: boolean, ): StandardProgram { let code = liftBytes(program); let metadata = new Uint8Array(0); @@ -69,7 +83,7 @@ export function prepareProgram( } if (kind === InputKind.Generic) { - const program = deblob(code); + const program = deblob(code, useBlockGas); const builder = new MemoryBuilder(preallocateMemoryPages); const memory = buildMemory(builder, initialPageMap, initialMemory); @@ -101,7 +115,6 @@ export function runProgram( initialGas: i64 = 0, programCounter: u32 = 0, logs: boolean = false, - useSbrkGas: boolean = false, dumpMemory: boolean = false, ): VmOutput { const vmInput = new VmInput(program.program, program.memory, program.registers); @@ -110,7 +123,6 @@ export function runProgram( const vmOptions = new VmRunOptions(); vmOptions.logs = logs; - vmOptions.useSbrkGas = useSbrkGas; vmOptions.dumpMemory = dumpMemory; return vmRunOnce(vmInput, vmOptions); @@ -126,11 +138,11 @@ const pvms = new Map(); * * NOTE: the PVM MUST be de-allocated using `pvmDestroy`. */ -export function pvmStart(program: StandardProgram, useSbrkGas: boolean = false): u32 { +export function pvmStart(program: StandardProgram): u32 { const vmInput = new VmInput(program.program, program.memory, program.registers); nextPvmId += 1; - pvms.set(nextPvmId, vmInit(vmInput, useSbrkGas)); + pvms.set(nextPvmId, vmInit(vmInput)); return nextPvmId; } diff --git a/assembly/arguments.ts b/assembly/arguments.ts index b371546..a0463b4 100644 --- a/assembly/arguments.ts +++ b/assembly/arguments.ts @@ -18,9 +18,9 @@ export enum Arguments { } /** How many numbers in `Args` is relevant for given `Arguments`. */ -export const RELEVANT_ARGS = [0, 1, 2, 1, 2, 3, 3, 3, 2, 3, 3, 4, 3]; +export const RELEVANT_ARGS: StaticArray = StaticArray.fromArray([0, 1, 2, 1, 2, 3, 3, 3, 2, 3, 3, 4, 3]); /** How many bytes is required by given `Arguments`. */ -export const REQUIRED_BYTES = [0, 0, 1, 0, 1, 9, 1, 1, 1, 1, 1, 2, 2]; +export const REQUIRED_BYTES: StaticArray = StaticArray.fromArray([0, 0, 1, 0, 1, 9, 1, 1, 1, 1, 1, 2, 2]); // @unmanaged export class Args { @@ -51,17 +51,17 @@ export class Args { d: u32 = 0; } -type ArgsDecoder = (args: Args, code: u8[], offset: u32, end: u32) => Args; +type ArgsDecoder = (args: Args, code: StaticArray, offset: u32, end: u32) => Args; -function twoImm(args: Args, code: u8[], offset: u32, end: u32): Args { - const low = lowNibble(portable.arrayAt(code, offset)); +function twoImm(args: Args, code: StaticArray, offset: u32, end: u32): Args { + const low = lowNibble(portable.staticArrayAt(code, offset)); const split = minI32(4, low) + 1; const first = decodeI32(code, offset + 1, offset + split); const second = decodeI32(code, offset + split, end); return args.fill(first, second, 0, 0); } -export const DECODERS: ArgsDecoder[] = [ +export const DECODERS: StaticArray = StaticArray.fromArray([ // DECODERS[Arguments.Zero] = (args, _d, _o, _l) => { return args.fill(0, 0, 0, 0); @@ -135,7 +135,7 @@ export const DECODERS: ArgsDecoder[] = [ const b = lowNibble(data[o + 1]); return args.fill(hig, low, b, 0); }, -]; +]); // @inline export function lowNibble(byte: u8): u8 { @@ -146,7 +146,7 @@ export function higNibble(byte: u8): u8 { } //@inline -function decodeI32(input: u8[], start: u32, end: u32): u32 { +function decodeI32(input: StaticArray, start: u32, end: u32): u32 { if (end <= start) { return 0; } @@ -156,14 +156,14 @@ function decodeI32(input: u8[], start: u32, end: u32): u32 { for (let i: u32 = 0; i < len; i++) { num |= u32(input[start + i]) << (i * 8); } - const msb = portable.arrayAt(input, start + len - 1) & 0x80; + const msb = portable.staticArrayAt(input, start + len - 1) & 0x80; if (len < 4 && msb > 0) { num |= 0xffff_ffff << (len * 8); } return num; } -function decodeU32(data: u8[], offset: u32): u32 { +function decodeU32(data: StaticArray, offset: u32): u32 { let num = u32(data[offset + 0]); num |= u32(data[offset + 1]) << 8; num |= u32(data[offset + 2]) << 16; diff --git a/assembly/gas-costs.ts b/assembly/gas-costs.ts deleted file mode 100644 index 0ab9298..0000000 --- a/assembly/gas-costs.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { INSTRUCTIONS, MISSING_INSTRUCTION } from "./instructions"; -import { portable } from "./portable"; -import { Program } from "./program"; - -export class BlockGasCost { - pc: u32 = 0; - gas: u64 = u64(0); -} - -export function computeGasCosts(p: Program): Map { - const len = p.code.length; - const blocks: Map = new Map(); - let currentBlock: BlockGasCost | null = null; - - for (let i = 0; i < len; i++) { - if (!p.mask.isInstruction(i)) { - throw new Error("We should iterate only over instructions!"); - } - - const instruction = p.code[i]; - const iData = instruction >= INSTRUCTIONS.length ? MISSING_INSTRUCTION : INSTRUCTIONS[instruction]; - - if (p.basicBlocks.isStart(i)) { - if (currentBlock !== null) { - blocks.set(currentBlock.pc, currentBlock); - } - currentBlock = new BlockGasCost(); - currentBlock.pc = i; - } - - if (currentBlock !== null) { - // add gas for current instruction - currentBlock.gas = portable.u64_add(currentBlock.gas, iData.gas); - } - - // move forward - const skipBytes = p.mask.skipBytesToNextInstruction(i); - i += skipBytes; - } - - // add the final block - if (currentBlock !== null) { - blocks.set(currentBlock.pc, currentBlock); - } - - return blocks; -} diff --git a/assembly/gas.ts b/assembly/gas.ts index fcc771c..8b9981d 100644 --- a/assembly/gas.ts +++ b/assembly/gas.ts @@ -18,12 +18,13 @@ export class GasCounter { } @inline - sub(g: Gas): boolean { - if (g > this.gas) { + sub(g: u32): boolean { + const cost = u64(g); + if (cost > this.gas) { this.gas = u64(0); return true; } - this.gas = this.gas - g; + this.gas = this.gas - cost; return false; } } diff --git a/assembly/index-compiler.ts b/assembly/index-compiler.ts index 47dfa85..c5ace5a 100644 --- a/assembly/index-compiler.ts +++ b/assembly/index-compiler.ts @@ -81,6 +81,7 @@ export function main(argsPtr: u32, argsLen: u32): void { // Parse SPI program and prepare memory layout const preallocateMemoryPages: u32 = 0; + const useBlockGas = true; const program = prepareProgram( InputKind.SPI, HasMetadata.Yes, @@ -90,10 +91,11 @@ export function main(argsPtr: u32, argsLen: u32): void { [], innerArgs, preallocateMemoryPages, + useBlockGas, ); // Run the program - const result = runProgram(program, gas, pc, false, false, false); + const result = runProgram(program, gas, pc, false, false); // Calculate exact result size: 1 (status) + 4 (exitCode) + 8 (gas) + 4 (pc) + ? (result data) const dataLen: u32 = result.result.length; diff --git a/assembly/instructions.ts b/assembly/instructions.ts index 8aa52c6..122e814 100644 --- a/assembly/instructions.ts +++ b/assembly/instructions.ts @@ -4,7 +4,7 @@ import { Gas } from "./gas"; export class Instruction { name: string = ""; kind: Arguments = Arguments.Zero; - gas: Gas = u64(0); + gas: u32 = u32(0); isTerminating: boolean = false; } @@ -12,7 +12,7 @@ function instruction(name: string, kind: Arguments, gas: Gas, isTerminating: boo const i = new Instruction(); i.name = name; i.kind = kind; - i.gas = u64(gas); + i.gas = u32(gas); i.isTerminating = isTerminating; return i; } @@ -21,7 +21,7 @@ export const MISSING_INSTRUCTION = instruction("INVALID", Arguments.Zero, 1, fal export const SBRK = instruction("SBRK", Arguments.TwoReg, 1); -export const INSTRUCTIONS: Instruction[] = [ +export const INSTRUCTIONS: StaticArray = StaticArray.fromArray([ /* 000 */ instruction("TRAP", Arguments.Zero, 1, true), /* 001 */ instruction("FALLTHROUGH", Arguments.Zero, 1, true), MISSING_INSTRUCTION, @@ -276,4 +276,4 @@ export const INSTRUCTIONS: Instruction[] = [ /* 229 */ instruction("MIN", Arguments.ThreeReg, 1), /* 230 */ instruction("MIN_U", Arguments.ThreeReg, 1), -]; +]); diff --git a/assembly/interpreter.ts b/assembly/interpreter.ts index dad1e41..a95de8b 100644 --- a/assembly/interpreter.ts +++ b/assembly/interpreter.ts @@ -1,11 +1,10 @@ import { Args } from "./arguments"; import { GasCounter, gasCounter } from "./gas"; -import { INSTRUCTIONS, MISSING_INSTRUCTION, SBRK } from "./instructions"; +import { INSTRUCTIONS, MISSING_INSTRUCTION } from "./instructions"; import { Outcome, OutcomeData, Result } from "./instructions/outcome"; -import { reg } from "./instructions/utils"; import { RUN } from "./instructions-exe"; import { Memory, MemoryBuilder } from "./memory"; -import { PAGE_SIZE, PAGE_SIZE_SHIFT, RESERVED_MEMORY } from "./memory-page"; +import { RESERVED_MEMORY } from "./memory-page"; import { portable } from "./portable"; import { BasicBlocks, decodeArguments, JumpTable, Program, ProgramCounter } from "./program"; import { Registers } from "./registers"; @@ -47,7 +46,6 @@ export class Interpreter { public status: Status; public exitCode: u32; public nextPc: u32; - public useSbrkGas: boolean; private djumpRes: DjumpResult = new DjumpResult(); private argsRes: Args = new Args(); @@ -63,7 +61,6 @@ export class Interpreter { this.status = Status.OK; this.exitCode = 0; this.nextPc = 0; - this.useSbrkGas = false; } nextSteps(nSteps: u32 = 1): boolean { @@ -88,9 +85,12 @@ export class Interpreter { return true; } - const program = this.program; - const code = program.code; - const mask = program.mask; + const code = this.program.code; + const mask = this.program.mask; + const gasCosts = this.program.gasCosts.codeAndGas; + const basicBlocks = this.program.basicBlocks; + const jumpTable = this.program.jumpTable; + const argsRes = this.argsRes; const outcomeRes = this.outcomeRes; @@ -112,11 +112,13 @@ export class Interpreter { return false; } - const instruction = unchecked(code[pc]); + // check gas via pre-computed cost table (per-instruction or per-block) + const codeAndGas = portable.staticArrayAt(gasCosts, pc); + const instruction = codeAndGas & 0xff; + const gasCost = codeAndGas >> 8; const iData = instruction < INSTRUCTIONS.length ? unchecked(INSTRUCTIONS[instruction]) : MISSING_INSTRUCTION; - // check gas (might be done for each block instead). - if (this.gas.sub(iData.gas)) { + if (gasCost > 0 && this.gas.sub(gasCost)) { this.status = Status.OOG; return false; } @@ -130,19 +132,6 @@ export class Interpreter { const skipBytes = mask.skipBytesToNextInstruction(pc); const args = decodeArguments(argsRes, iData.kind, code, pc + 1, skipBytes); - // additional gas cost of sbrk - if (iData === SBRK && this.useSbrkGas) { - const alloc = u64(u32(this.registers[reg(u64(args.a))])); - const gas = portable.u64_mul( - portable.u64_sub(portable.u64_add(alloc, u64(PAGE_SIZE)), u64(1)) >> u64(PAGE_SIZE_SHIFT), - u64(16), - ); - if (this.gas.sub(gas)) { - this.status = Status.OOG; - return false; - } - } - const exe = unchecked(RUN[instruction]); const outcome = exe(outcomeRes, args, this.registers, this.memory); @@ -154,7 +143,7 @@ export class Interpreter { switch (outcome.outcome) { case Outcome.StaticJump: { - const branchResult = branch(this.branchRes, program.basicBlocks, pc, outcome.staticJump); + const branchResult = branch(this.branchRes, basicBlocks, pc, outcome.staticJump); if (!branchResult.isOkay) { this.status = Status.PANIC; return false; @@ -164,7 +153,7 @@ export class Interpreter { continue; } case Outcome.DynamicJump: { - const res = dJump(this.djumpRes, program.jumpTable, outcome.dJump); + const res = dJump(this.djumpRes, jumpTable, outcome.dJump); if (res.status === DjumpStatus.HALT) { this.status = Status.HALT; return false; @@ -173,7 +162,7 @@ export class Interpreter { this.status = Status.PANIC; return false; } - const branchResult = branch(this.branchRes, program.basicBlocks, res.newPc, 0); + const branchResult = branch(this.branchRes, basicBlocks, res.newPc, 0); if (!branchResult.isOkay) { this.status = Status.PANIC; return false; diff --git a/assembly/portable.ts b/assembly/portable.ts index 00d09de..0d0b68b 100644 --- a/assembly/portable.ts +++ b/assembly/portable.ts @@ -22,6 +22,7 @@ export class portable { arr.fill(0); return arr; }; + g.StaticArray.fromArray = (x: any[]) => x; g.ASC_TARGET = 0; diff --git a/assembly/program.test.ts b/assembly/program.test.ts index 8523115..e35ad6f 100644 --- a/assembly/program.test.ts +++ b/assembly/program.test.ts @@ -46,7 +46,7 @@ export const TESTS: Test[] = [ test("should decode arguments correctly", () => { const r = new Args(); - const data: u8[] = [0xff, 0xff, 0xff, 0xff]; + const data = StaticArray.fromArray([0xff, 0xff, 0xff, 0xff]); const args = decodeArguments(r, Arguments.OneImm, data, 0, 4); const assert = new Assert(); @@ -59,7 +59,7 @@ export const TESTS: Test[] = [ test("should decode positive bounded by skip", () => { const r = new Args(); - const data: u8[] = [0x05, 0x05]; + const data = StaticArray.fromArray([0x05, 0x05]); const args = decodeArguments(r, Arguments.OneImm, data, 0, 1); const assert = new Assert(); @@ -75,7 +75,7 @@ export const TESTS: Test[] = [ 0, 0, 33, 51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138, 200, 152, 8, 100, 169, 40, 243, 100, 135, 51, 8, 51, 9, 1, 50, 0, 73, 147, 82, 213, 0, ]); - const program = deblob(raw); + const program = deblob(raw, false); const assert = new Assert(); assert.isEqual( program.mask.toString(), @@ -86,11 +86,39 @@ export const TESTS: Test[] = [ program.basicBlocks.toString(), "BasicBlocks[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", ); + assert.isEqual( + program.basicBlocks.toString(), + "BasicBlocks[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", + ); + assert.isEqual( + program.gasCosts.toString(), + "GasCosts[0 -> 1, 3 -> 1, 6 -> 1, 8 -> 1, 9 -> 1, 12 -> 1, 15 -> 1, 17 -> 1, 20 -> 1, 22 -> 1, 24 -> 1, 26 -> 1, 28 -> 1, 30 -> 1, 31 -> 1, ]", + ); + return assert; + }), + + test("should deblob with block gas costs", () => { + const raw = u8arr([ + 0, 0, 33, 51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138, 200, 152, 8, 100, 169, 40, 243, 100, + 135, 51, 8, 51, 9, 1, 50, 0, 73, 147, 82, 213, 0, + ]); + const program = deblob(raw, true); + const assert = new Assert(); + assert.isEqual( + program.mask.toString(), + "Mask[0, 2, 1, 0, 2, 1, 0, 1, 0, 0, 2, 1, 0, 2, 1, 0, 1, 0, 2, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, ]", + ); + assert.isEqual(program.jumpTable.toString(), "JumpTable[]"); + assert.isEqual( + program.basicBlocks.toString(), + "BasicBlocks[0 -> start, 6 -> end, 8 -> startend, 9 -> start, 12 -> end, 15 -> start, 22 -> end, 24 -> start, 30 -> end, 31 -> startend, ]", + ); + assert.isEqual(program.gasCosts.toString(), "GasCosts[0 -> 3, 8 -> 1, 9 -> 2, 15 -> 4, 24 -> 4, 31 -> 1, ]"); return assert; }), test("should construct basic blocks correctly based on skip", () => { - const code: u8[] = [ + const code = StaticArray.fromArray([ opcode(trap), 0, 0, @@ -123,7 +151,7 @@ export const TESTS: Test[] = [ 0, 0, opcode(jump_ind), - ]; + ]); const mask = new Mask(u8arr([0b0000_0001, 0b0000_0000, 0b0000_0000, 0b1000_0000]), 32); const basicBlocks = new BasicBlocks(code, mask); const assert = new Assert(); diff --git a/assembly/program.ts b/assembly/program.ts index 5f47f56..0aad11f 100644 --- a/assembly/program.ts +++ b/assembly/program.ts @@ -6,6 +6,7 @@ import { portable } from "./portable"; import { Registers } from "./registers"; export type ProgramCounter = u32; +export type Code = StaticArray; const MAX_SKIP: u32 = 24; @@ -32,9 +33,9 @@ export function liftBytes(data: u8[]): Uint8Array { return p; } -/** Convert `Uint8Array` to `u8` */ -export function lowerBytes(data: Uint8Array): u8[] { - const r = new Array(data.length); +/** Convert `Uint8Array` to `Code` (StaticArray) */ +export function lowerBytes(data: Uint8Array): Code { + const r = new StaticArray(data.length); for (let i = 0; i < data.length; i++) { r[i] = data[i]; } @@ -42,7 +43,7 @@ export function lowerBytes(data: Uint8Array): u8[] { } /** https://graypaper.fluffylabs.dev/#/cc517d7/234f01234f01?v=0.6.5 */ -export function deblob(program: Uint8Array): Program { +export function deblob(program: Uint8Array, useBlockGas: boolean): Program { const decoder = new Decoder(program); // number of items in the jump table @@ -64,8 +65,9 @@ export function deblob(program: Uint8Array): Program { const mask = new Mask(rawMask, codeLength); const jumpTable = new JumpTable(jumpTableItemLength, rawJumpTable); const basicBlocks = new BasicBlocks(rawCode, mask); + const gasCosts = new GasCosts(rawCode, mask, basicBlocks, useBlockGas); - return new Program(rawCode, mask, jumpTable, basicBlocks); + return new Program(rawCode, mask, jumpTable, basicBlocks, gasCosts); } /** @@ -131,6 +133,62 @@ export class Mask { } } +export class GasCosts { + // Since code is just u8, we use the other 24 bytes to store the gas cost. + readonly codeAndGas: StaticArray; + + constructor(code: Code, mask: Mask, blocks: BasicBlocks, useBlockGasCost: boolean) { + const len = code.length; + const costs = new StaticArray(len); + for (let n: i32 = 0; n < len; n += 1) { + const isInstructionInMask = mask.isInstruction(n); + if (!isInstructionInMask) { + costs[n] = code[n]; + continue; + } + + const skipArgs = mask.skipBytesToNextInstruction(n); + const iData = code[n] >= INSTRUCTIONS.length ? MISSING_INSTRUCTION : INSTRUCTIONS[code[n]]; + costs[n] = code[n] | (iData.gas << 8); + n += skipArgs; + } + + // sum up costs per block + if (useBlockGasCost) { + let previousStart: u32 = 0; + let previousSum: u32 = 0; + for (let n: i32 = 0; n < len; n += 1) { + const currentGas = costs[n] >> 8; + costs[n] = code[n]; // reset to just opcode (gas=0) + if (blocks.isStart(n)) { + costs[previousStart] = code[previousStart] | (previousSum << 8); + previousSum = currentGas; + previousStart = n; + } else { + previousSum += currentGas; + } + + n += mask.skipBytesToNextInstruction(n); + } + // final assignment + costs[previousStart] = code[previousStart] | (previousSum << 8); + } + + this.codeAndGas = costs; + } + + toString(): string { + let v = "GasCosts["; + for (let i = 0; i < this.codeAndGas.length; i += 1) { + const gas = this.codeAndGas[i] >> 8; + if (gas !== 0) { + v += `${i} -> ${gas}, `; + } + } + return `${v}]`; + } +} + export enum BasicBlock { NONE = 0, START = 2, @@ -143,7 +201,7 @@ export enum BasicBlock { export class BasicBlocks { readonly isStartOrEnd: StaticArray; - constructor(code: u8[], mask: Mask) { + constructor(code: Code, mask: Mask) { const len = code.length; const isStartOrEnd = new StaticArray(len); if (len > 0) { @@ -230,34 +288,35 @@ export class JumpTable { export class Program { constructor( - public readonly code: u8[], + public readonly code: Code, public readonly mask: Mask, public readonly jumpTable: JumpTable, public readonly basicBlocks: BasicBlocks, + public readonly gasCosts: GasCosts, ) {} toString(): string { - return `Program { code: ${this.code}, mask: ${this.mask}, jumpTable: ${this.jumpTable}, basicBlocks: ${this.basicBlocks} }`; + return `Program { code: ${this.code}, mask: ${this.mask}, jumpTable: ${this.jumpTable}, basicBlocks: ${this.basicBlocks}, gasCosts: ${this.gasCosts} }`; } } // Pre-allocated buffer for the rare case when code is shorter than needed. // Max REQUIRED_BYTES is 9 (OneRegOneExtImm). We allocate 16 for safety. -const EXTENDED_BUF: u8[] = new Array(16); +const EXTENDED_BUF: Code = new StaticArray(16); -export function decodeArguments(args: Args, kind: Arguments, code: u8[], offset: i32, lim: u32): Args { +export function decodeArguments(args: Args, kind: Arguments, code: Code, offset: i32, lim: u32): Args { if (code.length < offset + REQUIRED_BYTES[kind]) { // in case we have less data than needed we extend the data with zeros. - const reqBytes = REQUIRED_BYTES[kind]; + const reqBytes = unchecked(REQUIRED_BYTES[kind]); for (let i = 0; i < reqBytes; i++) { EXTENDED_BUF[i] = 0; } for (let i = offset; i < code.length; i++) { - EXTENDED_BUF[i - offset] = code[i]; + EXTENDED_BUF[i - offset] = unchecked(code[i]); } - return DECODERS[kind](args, EXTENDED_BUF, 0, lim); + return unchecked(DECODERS[kind])(args, EXTENDED_BUF, 0, lim); } - return DECODERS[kind](args, code, offset, offset + lim); + return unchecked(DECODERS[kind])(args, code, offset, offset + lim); } class ResolvedArguments { @@ -271,7 +330,7 @@ class ResolvedArguments { export function resolveArguments( argsRes: Args, kind: Arguments, - code: u8[], + code: Code, offset: u32, lim: u32, registers: Registers, diff --git a/assembly/spi.test.ts b/assembly/spi.test.ts index 950bf09..d72e6e0 100644 --- a/assembly/spi.test.ts +++ b/assembly/spi.test.ts @@ -15,7 +15,7 @@ export const TESTS: Test[] = [ const assert = new Assert(); assert.isEqual( spi.toString(), - "StandardProgram { program: Program { code: 200,135,9, mask: Mask[0, 2, 1, ], jumpTable: JumpTable[], basicBlocks: BasicBlocks[0 -> start, ] }, memory_pages: 7, registers: 4294901760,4278059008,0,0,0,0,0,4278124544,3,0,0,0,0 }", + "StandardProgram { program: Program { code: 200,135,9, mask: Mask[0, 2, 1, ], jumpTable: JumpTable[], basicBlocks: BasicBlocks[0 -> start, ], gasCosts: GasCosts[0 -> 1, ] }, memory_pages: 7, registers: 4294901760,4278059008,0,0,0,0,0,4278124544,3,0,0,0,0 }", ); return assert; }), diff --git a/assembly/spi.ts b/assembly/spi.ts index 955a5cc..dc5ac54 100644 --- a/assembly/spi.ts +++ b/assembly/spi.ts @@ -12,7 +12,12 @@ export const ARGS_SEGMENT_START: u32 = 2 ** 32 - SEGMENT_SIZE - MAX_ARGS_LEN; export const STACK_SEGMENT_END: u32 = ARGS_SEGMENT_START - SEGMENT_SIZE; /** https://graypaper.fluffylabs.dev/#/ab2cdbd/2da3002da300?v=0.7.2 */ -export function decodeSpi(data: Uint8Array, args: Uint8Array, preallocateMemoryPages: u32 = 0): StandardProgram { +export function decodeSpi( + data: Uint8Array, + args: Uint8Array, + preallocateMemoryPages: u32 = 0, + useBlockGas: boolean = false, +): StandardProgram { const argsLength = args.length; if (argsLength > MAX_ARGS_LEN) { throw new Error(`Arguments length is too big. Got: ${argsLength}, max: ${MAX_ARGS_LEN}`); @@ -32,7 +37,7 @@ export function decodeSpi(data: Uint8Array, args: Uint8Array, preallocateMemoryP const code = decoder.bytes(codeLength); decoder.finish(); - const program = deblob(code); + const program = deblob(code, useBlockGas); // building memory const builder = new MemoryBuilder(preallocateMemoryPages); diff --git a/bench/compare.ts b/bench/compare.ts index 11a4a1d..56e8ec5 100644 --- a/bench/compare.ts +++ b/bench/compare.ts @@ -1,18 +1,18 @@ #!/usr/bin/env node /** * Benchmark Comparison Tool - * + * * Compares two benchmark result JSON files and reports regressions/improvements. - * + * * Usage: * tsx bench/compare.ts [--threshold ] - * + * * Options: * --threshold Regression threshold as percentage (default: 5%) * --verbose Show detailed per-trace comparison */ -import { readFileSync, existsSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { parseArgs } from "node:util"; @@ -20,6 +20,7 @@ const { values, positionals } = parseArgs({ options: { threshold: { type: "string", short: "t", default: "5" }, verbose: { type: "boolean", short: "v", default: false }, + markdown: { type: "string", short: "m", default: "" }, help: { type: "boolean", short: "h", default: false }, }, allowPositionals: true, @@ -36,7 +37,7 @@ Options: } const [baselinePath, resultsPath] = positionals; -const threshold = parseFloat(values.threshold!); +const threshold = parseFloat(values.threshold ?? "5"); // Validate threshold if (!Number.isFinite(threshold) || threshold < 0) { @@ -81,12 +82,8 @@ const baselineSuite = baseline as SuiteResult; const resultsSuite = results as SuiteResult; // Build maps for easy lookup -const baselineTraces = new Map( - baselineSuite.traces.map((t) => [t.name, t]) -); -const resultsTraces = new Map( - resultsSuite.traces.map((t) => [t.name, t]) -); +const baselineTraces = new Map(baselineSuite.traces.map((t) => [t.name, t])); +const resultsTraces = new Map(resultsSuite.traces.map((t) => [t.name, t])); interface Comparison { name: string; @@ -98,8 +95,8 @@ interface Comparison { } const comparisons: Comparison[] = []; -let regressions: Comparison[] = []; -let improvements: Comparison[] = []; +const regressions: Comparison[] = []; +const improvements: Comparison[] = []; // Compare traces for (const [name, baselineTrace] of baselineTraces) { @@ -110,11 +107,11 @@ for (const [name, baselineTrace] of baselineTraces) { } const diffMs = currentTrace.medianMs - baselineTrace.medianMs; - + // Guard against division by zero let diffPercent: number; if (baselineTrace.medianMs === 0) { - diffPercent = diffMs === 0 ? 0 : (diffMs > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY); + diffPercent = diffMs === 0 ? 0 : diffMs > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; } else { diffPercent = (diffMs / baselineTrace.medianMs) * 100; } @@ -163,38 +160,33 @@ console.log(`Threshold: ${threshold}%\n`); console.log("--- Summary ---\n"); console.log( - `Total trace time: ${baselineSuite.summary.totalTraceMedianMs.toFixed(1)}ms -> ${resultsSuite.summary.totalTraceMedianMs.toFixed(1)}ms` + `Total trace time: ${baselineSuite.summary.totalTraceMedianMs.toFixed(1)}ms -> ${resultsSuite.summary.totalTraceMedianMs.toFixed(1)}ms`, ); -const totalDiff = - resultsSuite.summary.totalTraceMedianMs - - baselineSuite.summary.totalTraceMedianMs; +const totalDiff = resultsSuite.summary.totalTraceMedianMs - baselineSuite.summary.totalTraceMedianMs; // Guard against division by zero let totalDiffPercent: number; if (baselineSuite.summary.totalTraceMedianMs === 0) { - totalDiffPercent = totalDiff === 0 ? 0 : (totalDiff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY); + totalDiffPercent = totalDiff === 0 ? 0 : totalDiff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; } else { totalDiffPercent = (totalDiff / baselineSuite.summary.totalTraceMedianMs) * 100; } - `Difference: ${totalDiff >= 0 ? "+" : ""}${totalDiff.toFixed(1)}ms (${totalDiffPercent.toFixed(2)}%)` -); +console.log(`Difference: ${totalDiff >= 0 ? "+" : ""}${totalDiff.toFixed(1)}ms (${totalDiffPercent.toFixed(2)}%)`); if (baselineSuite.w3f && resultsSuite.w3f) { - const w3fDiff = - resultsSuite.w3f.medianMs - baselineSuite.w3f.medianMs; - + const w3fDiff = resultsSuite.w3f.medianMs - baselineSuite.w3f.medianMs; + // Guard against division by zero let w3fDiffPercent: number; if (baselineSuite.w3f.medianMs === 0) { - w3fDiffPercent = w3fDiff === 0 ? 0 : (w3fDiff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY); + w3fDiffPercent = w3fDiff === 0 ? 0 : w3fDiff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; } else { w3fDiffPercent = (w3fDiff / baselineSuite.w3f.medianMs) * 100; } - `\nW3F suite: ${baselineSuite.w3f.medianMs.toFixed(1)}ms -> ${resultsSuite.w3f.medianMs.toFixed(1)}ms` - ); console.log( - `Difference: ${w3fDiff >= 0 ? "+" : ""}${w3fDiff.toFixed(1)}ms (${w3fDiffPercent.toFixed(2)}%)` + `\nW3F suite: ${baselineSuite.w3f.medianMs.toFixed(1)}ms -> ${resultsSuite.w3f.medianMs.toFixed(1)}ms`, ); + console.log(`Difference: ${w3fDiff >= 0 ? "+" : ""}${w3fDiff.toFixed(1)}ms (${w3fDiffPercent.toFixed(2)}%)`); } console.log(`\nRegressions: ${regressions.length}`); @@ -203,18 +195,14 @@ console.log(`Improvements: ${improvements.length}`); if (regressions.length > 0) { console.log("\n--- Regressions (worst first) ---\n"); for (const r of regressions) { - console.log( - ` ${r.name.padEnd(40)} ${r.currentMs.toFixed(1).padStart(8)}ms (+${r.diffPercent.toFixed(1)}%)` - ); + console.log(` ${r.name.padEnd(40)} ${r.currentMs.toFixed(1).padStart(8)}ms (+${r.diffPercent.toFixed(1)}%)`); } } if (improvements.length > 0) { console.log("\n--- Improvements ---\n"); for (const i of improvements) { - console.log( - ` ${i.name.padEnd(40)} ${i.currentMs.toFixed(1).padStart(8)}ms (${i.diffPercent.toFixed(1)}%)` - ); + console.log(` ${i.name.padEnd(40)} ${i.currentMs.toFixed(1).padStart(8)}ms (${i.diffPercent.toFixed(1)}%)`); } } @@ -225,11 +213,27 @@ if (values.verbose && comparisons.length > 0) { const sign = c.diffMs >= 0 ? "+" : ""; const marker = c.status === "regression" ? "⚠️" : c.status === "improvement" ? "✓" : " "; console.log( - ` ${marker} ${c.name.padEnd(40)} ${c.baselineMs.toFixed(1).padStart(7)}ms -> ${c.currentMs.toFixed(1).padStart(7)}ms (${sign}${c.diffPercent.toFixed(1)}%)` + ` ${marker} ${c.name.padEnd(40)} ${c.baselineMs.toFixed(1).padStart(7)}ms -> ${c.currentMs.toFixed(1).padStart(7)}ms (${sign}${c.diffPercent.toFixed(1)}%)`, ); } } +// Write markdown report +if (values.markdown) { + const md = formatMarkdown( + baselineSuite, + resultsSuite, + comparisons, + regressions, + improvements, + totalDiff, + totalDiffPercent, + threshold, + ); + writeFileSync(resolve(values.markdown), md); + console.log(`\nMarkdown report written to ${values.markdown}`); +} + // Exit code if (regressions.length > 0) { console.log("\n❌ FAILED: Regressions detected above threshold\n"); @@ -241,3 +245,73 @@ if (regressions.length > 0) { console.log("\n✅ PASSED: No regressions or improvements beyond threshold\n"); process.exit(0); } + +function formatMarkdown( + base: SuiteResult, + current: SuiteResult, + all: Comparison[], + regs: Comparison[], + imps: Comparison[], + totalDiffMs: number, + totalDiffPct: number, + thresh: number, +): string { + const sign = (n: number) => (n >= 0 ? "+" : ""); + const status = + regs.length > 0 + ? `### :warning: ${regs.length} regression(s) detected (>${thresh}% threshold)` + : imps.length > 0 + ? "### :white_check_mark: No regressions (improvements detected)" + : "### :white_check_mark: No significant changes"; + + let md = "## Benchmark Results\n\n"; + md += `${status}\n\n`; + + // Summary table + md += "| Metric | Baseline | Current | Change |\n"; + md += "|--------|----------|---------|--------|\n"; + md += `| **Trace total** | ${base.summary.totalTraceMedianMs.toFixed(1)}ms | ${current.summary.totalTraceMedianMs.toFixed(1)}ms | ${sign(totalDiffMs)}${totalDiffMs.toFixed(1)}ms (${sign(totalDiffPct)}${totalDiffPct.toFixed(1)}%) |\n`; + + if (base.w3f && current.w3f) { + const w3fDiff = current.w3f.medianMs - base.w3f.medianMs; + const w3fPct = base.w3f.medianMs === 0 ? 0 : (w3fDiff / base.w3f.medianMs) * 100; + md += `| **W3F suite** | ${base.w3f.medianMs.toFixed(1)}ms | ${current.w3f.medianMs.toFixed(1)}ms | ${sign(w3fDiff)}${w3fDiff.toFixed(1)}ms (${sign(w3fPct)}${w3fPct.toFixed(1)}%) |\n`; + } + + // Regressions + if (regs.length > 0) { + md += "\n
Regressions (worst first)\n\n"; + md += "| Trace | Baseline | Current | Change |\n"; + md += "|-------|----------|---------|--------|\n"; + for (const r of regs) { + md += `| ${r.name} | ${r.baselineMs.toFixed(1)}ms | ${r.currentMs.toFixed(1)}ms | +${r.diffPercent.toFixed(1)}% |\n`; + } + md += "\n
\n"; + } + + // Improvements + if (imps.length > 0) { + md += "\n
Improvements\n\n"; + md += "| Trace | Baseline | Current | Change |\n"; + md += "|-------|----------|---------|--------|\n"; + for (const i of imps) { + md += `| ${i.name} | ${i.baselineMs.toFixed(1)}ms | ${i.currentMs.toFixed(1)}ms | ${i.diffPercent.toFixed(1)}% |\n`; + } + md += "\n
\n"; + } + + // All traces + if (all.length > 0) { + const sorted = [...all].sort((a, b) => Math.abs(b.diffPercent) - Math.abs(a.diffPercent)); + md += "\n
All traces\n\n"; + md += "| Trace | Baseline | Current | Change |\n"; + md += "|-------|----------|---------|--------|\n"; + for (const c of sorted) { + const icon = c.status === "regression" ? ":warning:" : c.status === "improvement" ? ":white_check_mark:" : ""; + md += `| ${icon} ${c.name} | ${c.baselineMs.toFixed(1)}ms | ${c.currentMs.toFixed(1)}ms | ${sign(c.diffPercent)}${c.diffPercent.toFixed(1)}% |\n`; + } + md += "\n
\n"; + } + + return md; +} diff --git a/bench/run.ts b/bench/run.ts index 925fb0b..5a606b7 100644 --- a/bench/run.ts +++ b/bench/run.ts @@ -46,8 +46,8 @@ Options: process.exit(0); } -const ITERATIONS = parseInt(values.iterations!, 10); -const WARMUP = parseInt(values.warmup!, 10); +const ITERATIONS = parseInt(values.iterations ?? "5", 10); +const WARMUP = parseInt(values.warmup ?? "1", 10); // Validate iterations and warmup if (!Number.isInteger(ITERATIONS) || ITERATIONS < 1) { @@ -151,6 +151,7 @@ function benchTraces(dir: string): BenchResult[] { hasMetadata: HasMetadata.Yes, verify: false, tracer: new NoOpTracer(), + useBlockGas: values["block-gas"], }); }); @@ -212,8 +213,18 @@ function benchW3f(dir: string): BenchResult | null { const gas = BigInt(data["initial-gas"] || 10000); const pc = data["initial-pc"] || 0; - const exe = prepareProgram(InputKind.Generic, HasMetadata.No, data.program, registers, pageMap, memory, [], 16); - runProgram(exe, gas, pc, false, false, false); + const exe = prepareProgram( + InputKind.Generic, + HasMetadata.No, + data.program, + registers, + pageMap, + memory, + [], + 16, + values["block-gas"], + ); + runProgram(exe, gas, pc, false, false); } }); @@ -237,11 +248,11 @@ function main() { }; // Trace benchmarks - const traceDirs = [values.traces, "../anan-as2/bench/traces", "./bench/traces"].filter(Boolean); + const traceDirs = [values.traces, "./bench/traces"].filter(Boolean); let traceDir: string | null = null; for (const d of traceDirs) { - const resolved = resolve(d!); + const resolved = resolve(d); if (existsSync(resolved)) { traceDir = resolved; break; @@ -262,7 +273,7 @@ function main() { let w3fDir: string | null = null; for (const d of w3fDirs) { - const resolved = resolve(d!); + const resolved = resolve(d); if (existsSync(resolved)) { w3fDir = resolved; break; diff --git a/bin/index.ts b/bin/index.ts index d7d434d..1bb35c2 100755 --- a/bin/index.ts +++ b/bin/index.ts @@ -192,8 +192,19 @@ function handleRun(args: string[]) { try { const preallocateMemoryPages = 128; - const program = prepareProgram(kind, hasMetadata, programCode, [], [], [], spiArgs, preallocateMemoryPages); - const id = pvmStart(program, false); + const useBlockGas = true; + const program = prepareProgram( + kind, + hasMetadata, + programCode, + [], + [], + [], + spiArgs, + preallocateMemoryPages, + useBlockGas, + ); + const id = pvmStart(program); let gas = initialGas; let pc = initialPc; diff --git a/bin/src/fuzz.ts b/bin/src/fuzz.ts index 5806aff..5ed6ed6 100755 --- a/bin/src/fuzz.ts +++ b/bin/src/fuzz.ts @@ -26,7 +26,7 @@ export function fuzz(data: Uint8Array | number[]) { .join(",") .split(",") .map(() => BigInt(0)); - const exe = prepareProgram(InputKind.Generic, HasMetadata.No, Array.from(program), registers, [], [], [], 0); + const exe = prepareProgram(InputKind.Generic, HasMetadata.No, Array.from(program), registers, [], [], [], 0, false); const output = runProgram(exe, gas, pc, printDebugInfo); const vmRegisters = decodeRegistersFromTypeberry(vm); diff --git a/bin/src/test-json.ts b/bin/src/test-json.ts index 9eadf63..e5d6020 100644 --- a/bin/src/test-json.ts +++ b/bin/src/test-json.ts @@ -16,8 +16,6 @@ export interface TestOptions { isDebug: boolean; /** don't print anything (jsonin-jsonout mode) */ isSilent: boolean; - /** enable sbrk gas */ - useSbrkGas: boolean; } type ProcessJsonFn = (data: T, options: TestOptions, filePath: string) => T; @@ -40,9 +38,6 @@ export function run(processJson: ProcessJsonFn, op if (args[0] === "--debug") { args.shift(); options.isDebug = true; - } else if (args[0] === "--sbrk-gas") { - args.shift(); - options.useSbrkGas = true; } else { break; } @@ -50,8 +45,8 @@ export function run(processJson: ProcessJsonFn, op if (args.length === 0) { console.error("Error: No JSON files provided."); - console.error("Usage: index.js [--debug] [--sbrk-gas] [file2.json ...]"); - console.error("read from stdin: index.js [--debug] [--sbrk-gas] -"); + console.error("Usage: index.js [--debug] [file2.json ...]"); + console.error("read from stdin: index.js [--debug] -"); process.exit(1); } diff --git a/bin/src/trace-replay.ts b/bin/src/trace-replay.ts index 4150209..0640d46 100644 --- a/bin/src/trace-replay.ts +++ b/bin/src/trace-replay.ts @@ -32,6 +32,7 @@ type ReplayOptions = { verify: boolean; logHostCall?: boolean; tracer?: Tracer; + useBlockGas?: boolean; }; export function replayTraceFile(filePath: string, options: ReplayOptions): TraceSummary { @@ -45,8 +46,10 @@ export function replayTraceFile(filePath: string, options: ReplayOptions): Trace const programInput = Array.from(program); const spiArgs = Array.from(extractSpiArgs(start, initialMemWrites)); + const preallocateMemoryPages = 128; + const useBlockGas = options.useBlockGas ?? false; const preparedProgram = useSpi - ? prepareProgram(InputKind.SPI, hasMetadata, programInput, [], [], [], spiArgs, 128) + ? prepareProgram(InputKind.SPI, hasMetadata, programInput, [], [], [], spiArgs, preallocateMemoryPages, useBlockGas) : prepareProgram( InputKind.Generic, hasMetadata, @@ -55,10 +58,11 @@ export function replayTraceFile(filePath: string, options: ReplayOptions): Trace buildInitialPages(initialMemWrites), buildInitialChunks(initialMemWrites), [], - 128, + preallocateMemoryPages, + useBlockGas, ); - const id = pvmStart(preparedProgram, true); + const id = pvmStart(preparedProgram); const initialEcalliCount = ecalliEntries.length; const tracer = options.tracer ?? new ConsoleTracer(); diff --git a/biome.jsonc b/biome.jsonc index 180f233..815cda5 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -43,7 +43,7 @@ } }, "files": { - "includes": ["**/assembly/**/*", "**/bin/**/*", "**/test/**/*", "!dist", "!web/build"] + "includes": ["**/assembly/**/*", "**/bin/**/*", "**/test/**/*", "**/bench/**/*", "!dist", "!web/build"] }, "overrides": [ { diff --git a/portable/types/assemblyscript.d.ts b/portable/types/assemblyscript.d.ts index daba955..16eeb6d 100644 --- a/portable/types/assemblyscript.d.ts +++ b/portable/types/assemblyscript.d.ts @@ -76,6 +76,7 @@ declare function bool(value: ASPrimitive): bool; interface StaticArray extends Array {} interface StaticArrayConstructor { new (length: number): StaticArray; + fromArray(arr: T[]): StaticArray; } declare const StaticArray: StaticArrayConstructor; diff --git a/test/test-gas-cost.ts b/test/test-gas-cost.ts index dc985f3..a063c79 100755 --- a/test/test-gas-cost.ts +++ b/test/test-gas-cost.ts @@ -3,7 +3,7 @@ import "json-bigint-patch"; import * as assert from "node:assert"; import { ERR, OK, ProcessableData, read, run, TestOptions } from "../bin/src/test-json.js"; -import { disassemble, getGasCosts, HasMetadata, InputKind } from "../build/release.js"; +import { disassemble, getBlockGasCosts, HasMetadata, InputKind } from "../build/release.js"; // Run the CLI application main(); @@ -18,7 +18,6 @@ function main() { const options: TestOptions = { isDebug: false, isSilent: false, - useSbrkGas: false, }; run(processGasCost, options); @@ -41,7 +40,7 @@ function processGasCost(data: GasCostTest, options: TestOptions, filePath: strin console.info("\n^^^^^^^^^^^\n"); } - const result = asMap(getGasCosts(input.program, InputKind.Generic, HasMetadata.No)); + const result = asMap(getBlockGasCosts(input.program, InputKind.Generic, HasMetadata.No)); // silent mode - just put our vals into expected (comparison done externally) if (options.isSilent) { diff --git a/test/test-w3f-common.ts b/test/test-w3f-common.ts index 08476f9..ff7c911 100644 --- a/test/test-w3f-common.ts +++ b/test/test-w3f-common.ts @@ -62,7 +62,6 @@ export function runW3fTests(pvm: PvmModule) { const options: TestOptions = { isDebug: false, isSilent: false, - useSbrkGas: false, }; run((data, opts, filePath) => processW3f(pvm, data, opts, filePath), options); @@ -106,7 +105,7 @@ function processW3f(pvm: PvmModule, data: PvmTest, options: TestOptions, _filePa [], 16, ); - const result = pvm.runProgram(exe, input.gas, input.pc, options.isDebug, options.useSbrkGas, true); + const result = pvm.runProgram(exe, input.gas, input.pc, options.isDebug, true); const status = statusAsString(result.status); // Normalize registers to plain unsigned BigInt array for comparison.