Skip to content
26 changes: 18 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
24 changes: 19 additions & 5 deletions assembly/api-debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <u32>pc;
int.gas.set(initialGas);
Expand All @@ -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);
Expand All @@ -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);

Expand Down
5 changes: 2 additions & 3 deletions assembly/api-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,16 @@ 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;
}

/** 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);
}
Expand Down
1 change: 0 additions & 1 deletion assembly/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export class InitialChunk {
}

export class VmRunOptions {
useSbrkGas: boolean = false;
logs: boolean = false;
dumpMemory: boolean = false;
}
Expand Down
38 changes: 25 additions & 13 deletions assembly/api-utils.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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<BlockGasCost>(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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -110,7 +123,6 @@ export function runProgram(

const vmOptions = new VmRunOptions();
vmOptions.logs = logs;
vmOptions.useSbrkGas = useSbrkGas;
vmOptions.dumpMemory = dumpMemory;

return vmRunOnce(vmInput, vmOptions);
Expand All @@ -126,11 +138,11 @@ const pvms = new Map<u32, Interpreter>();
*
* 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;
}

Expand Down
20 changes: 10 additions & 10 deletions assembly/arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export enum Arguments {
}

/** How many numbers in `Args` is relevant for given `Arguments`. */
export const RELEVANT_ARGS = [<i32>0, 1, 2, 1, 2, 3, 3, 3, 2, 3, 3, 4, 3];
export const RELEVANT_ARGS: StaticArray<i32> = StaticArray.fromArray<i32>([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 = [<i32>0, 0, 1, 0, 1, 9, 1, 1, 1, 1, 1, 2, 2];
export const REQUIRED_BYTES: StaticArray<i32> = StaticArray.fromArray<i32>([0, 0, 1, 0, 1, 9, 1, 1, 1, 1, 1, 2, 2]);

// @unmanaged
export class Args {
Expand Down Expand Up @@ -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<u8>, 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<u8>, 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<ArgsDecoder> = StaticArray.fromArray<ArgsDecoder>([
// DECODERS[Arguments.Zero] =
(args, _d, _o, _l) => {
return args.fill(0, 0, 0, 0);
Expand Down Expand Up @@ -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 {
Expand All @@ -146,7 +146,7 @@ export function higNibble(byte: u8): u8 {
}

//@inline
function decodeI32(input: u8[], start: u32, end: u32): u32 {
function decodeI32(input: StaticArray<u8>, start: u32, end: u32): u32 {
if (end <= start) {
return 0;
}
Expand All @@ -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<u8>, offset: u32): u32 {
let num = u32(data[offset + 0]);
num |= u32(data[offset + 1]) << 8;
num |= u32(data[offset + 2]) << 16;
Expand Down
47 changes: 0 additions & 47 deletions assembly/gas-costs.ts

This file was deleted.

7 changes: 4 additions & 3 deletions assembly/gas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
4 changes: 3 additions & 1 deletion assembly/index-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = <u32>result.result.length;
Expand Down
Loading