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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 73 additions & 28 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,80 @@ jobs:
jamtestvectors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Checkout JAM test vectors
uses: actions/checkout@v4
with:
repository: FluffyLabs/jamtestvectors
path: "./jamtestvectors"
ref: ba76542dbf7a0c72d414a87ad2e30ce4da380448 # New test vectors.
- name: Run W3F tests (WASM)
run: npm run test:w3f ./jamtestvectors/pvm/programs/*.json
- name: Run W3F tests (portable JS)
run: npm run test:w3f-portable ./jamtestvectors/pvm/programs/*.json
- uses: actions/checkout@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Checkout JAM test vectors
uses: actions/checkout@v4
with:
repository: FluffyLabs/jamtestvectors
path: "./jamtestvectors"
ref: ba76542dbf7a0c72d414a87ad2e30ce4da380448 # New test vectors.
Comment thread
tomusdrw marked this conversation as resolved.
- name: Run W3F tests (WASM)
run: npm run test:w3f ./jamtestvectors/pvm/programs/*.json
- name: Run W3F tests (portable JS)
run: npm run test:w3f-portable ./jamtestvectors/pvm/programs/*.json

build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run qa
- run: npm run build
- run: npm test --if-present
- uses: actions/checkout@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run qa
- run: npm run build
- run: npm test --if-present

benchmark:
runs-on: ubuntu-latest
# Only run benchmarks on PRs, not on main branch pushes
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Get baseline branch
run: |
# Get the base branch (main) to compare against
echo "BASE_BRANCH=${{ github.base_ref }}" >> $GITHUB_ENV
- run: npm ci
- run: npm run build
# Checkout base branch for baseline
- name: Checkout base branch for baseline
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
path: ./baseline
Comment thread
tomusdrw marked this conversation as resolved.
# Run benchmarks on baseline
- name: Run baseline benchmark
run: |
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
# Run benchmarks on current PR
- name: Run PR benchmark
run: |
npm run bench:ci
cp ./bench/results.json ../results.json
# Compare results
- name: Compare benchmarks
run: |
npm run bench:compare ../baseline.json ../results.json -- --threshold 5
Comment thread
tomusdrw marked this conversation as resolved.
2 changes: 1 addition & 1 deletion assembly/api-debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export function getGasLeft(): i64 {
return int.gas.get();
}

export function setGasLeft(gas: i64): void {
export function setGasLeft(gas: Gas): void {
if (interpreter !== null) {
const int = <Interpreter>interpreter;
int.gas.set(gas);
Expand Down
8 changes: 4 additions & 4 deletions assembly/api-types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/** We split out type definitions, because they can't be exported from WASM. */

import { Gas } from "./gas";
import { Status } from "./interpreter";
import { Memory } from "./memory";
import { Access } from "./memory-page";
import { Program } from "./program";
import { Registers } from "./registers";

export class InitialPage {
address: u32 = 0;
length: u32 = 0;
Expand All @@ -25,7 +25,7 @@ export class VmRunOptions {

export class VmInput {
pc: u32 = 0;
gas: i64 = i64(0);
gas: Gas = u64(0);

constructor(
public readonly program: Program,
Expand All @@ -39,15 +39,15 @@ export class VmPause {
exitCode: u32 = 0;
pc: u32 = 0;
nextPc: u32 = 0;
gas: i64 = i64(0);
gas: Gas = u64(0);
registers: u64[] = [];
}

export class VmOutput {
status: Status = Status.OK;
exitCode: u32 = 0;
pc: u32 = 0;
gas: i64 = i64(0);
gas: Gas = u64(0);
result: u8[] = [];
registers: u64[] = [];
memory: InitialChunk[] = [];
Expand Down
3 changes: 2 additions & 1 deletion assembly/api-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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";
Expand Down Expand Up @@ -228,7 +229,7 @@ export function pvmWriteMemory(pvmId: u32, address: u32, data: Uint8Array): bool
}

/** Resume execution of paused VM. */
export function pvmResume(pvmId: u32, gas: i64, pc: u32, logs: boolean = false): VmPause | null {
export function pvmResume(pvmId: u32, gas: Gas, pc: u32, logs: boolean = false): VmPause | null {
if (pvms.has(pvmId)) {
const int = pvms.get(pvmId);
int.nextPc = pc;
Expand Down
34 changes: 10 additions & 24 deletions assembly/gas.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,29 @@
/** Gas type. */
export type Gas = i64;
export type Gas = u64;

/** Create a new gas counter instance depending on the gas value. */
export function gasCounter(gas: i64): GasCounter {
return new GasCounterU64(gas);
/** Create a new gas counter instance. */
export function gasCounter(gas: Gas): GasCounter {
return new GasCounter(gas);
}

/** An abstraction over gas counter.
*
* It can be optimized to use numbers instead of bigint in case of small gas.
*/
export interface GasCounter {
/** Return remaining gas. */
get(): Gas;

/** Overwite remaining gas. Prefer sub method instead. */
set(g: Gas): void;

/** Returns true if there was an underflow. */
sub(g: Gas): boolean;
}

class GasCounterU64 implements GasCounter {
export class GasCounter {
constructor(private gas: Gas) {}

set(g: Gas): void {
this.gas = i64(g);
this.gas = g;
}

get(): Gas {
return this.gas;
}

@inline
sub(g: Gas): boolean {
this.gas = this.gas - i64(g);
if (this.gas < i64(0)) {
this.gas = i64(0);
if (g > this.gas) {
this.gas = u64(0);
return true;
}
this.gas = this.gas - g;
return false;
}
}
4 changes: 2 additions & 2 deletions assembly/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { Gas } from "./gas";
export class Instruction {
name: string = "";
kind: Arguments = Arguments.Zero;
gas: Gas = i64(0);
gas: Gas = u64(0);
isTerminating: boolean = false;
}

function instruction(name: string, kind: Arguments, gas: Gas, isTerminating: boolean = false): Instruction {
const i = new Instruction();
i.name = name;
i.kind = kind;
i.gas = i64(gas);
i.gas = u64(gas);
i.isTerminating = isTerminating;
return i;
}
Expand Down
4 changes: 1 addition & 3 deletions assembly/instructions/outcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export enum Outcome {
Result = 3,
}

// @unmanaged
export class OutcomeData {
outcome: Outcome = Outcome.Ok;
staticJump: i32 = 0;
Expand Down Expand Up @@ -46,8 +45,7 @@ export function dJump(r: OutcomeData, address: u32): OutcomeData {
}

export function ok(r: OutcomeData): OutcomeData {
r.outcome = Outcome.Ok;
r.dJump = 0;
// outcome is already pre-set to Ok by the interpreter loop
return r;
}

Expand Down
38 changes: 19 additions & 19 deletions assembly/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ export class Interpreter {
return false;
}

const instruction = code[pc];
const iData = <i32>instruction < INSTRUCTIONS.length ? INSTRUCTIONS[instruction] : MISSING_INSTRUCTION;
const instruction = unchecked(code[pc]);
const iData = <i32>instruction < INSTRUCTIONS.length ? unchecked(INSTRUCTIONS[instruction]) : MISSING_INSTRUCTION;

// check gas (might be done for each block instead).
if (this.gas.sub(iData.gas)) {
Expand Down Expand Up @@ -143,11 +143,26 @@ export class Interpreter {
}
}

const exe = RUN[instruction];
const exe = unchecked(RUN[instruction]);
const outcome = exe(outcomeRes, args, this.registers, this.memory);

// TODO [ToDr] Spaghetti
// Fast path: Ok is the most common outcome (~70%+ of instructions)
if (outcome.outcome === Outcome.Ok) {
this.pc += 1 + skipBytes;
continue;
}

switch (outcome.outcome) {
case Outcome.StaticJump: {
const branchResult = branch(this.branchRes, program.basicBlocks, pc, outcome.staticJump);
if (!branchResult.isOkay) {
this.status = Status.PANIC;
return false;
}

this.pc = branchResult.newPc;
continue;
}
case Outcome.DynamicJump: {
const res = dJump(this.djumpRes, program.jumpTable, outcome.dJump);
if (res.status === DjumpStatus.HALT) {
Expand All @@ -166,16 +181,6 @@ export class Interpreter {
this.pc = branchResult.newPc;
continue;
}
case Outcome.StaticJump: {
const branchResult = branch(this.branchRes, program.basicBlocks, pc, outcome.staticJump);
if (!branchResult.isOkay) {
this.status = Status.PANIC;
return false;
}

this.pc = branchResult.newPc;
continue;
}
case Outcome.Result: {
if (outcome.result === Result.HOST) {
this.status = Status.HOST;
Expand Down Expand Up @@ -207,11 +212,6 @@ export class Interpreter {

throw new Error("Unknown result");
}
case Outcome.Ok: {
// by default move to next instruction.
this.pc += 1 + skipBytes;
continue;
}
}
}

Expand Down
15 changes: 5 additions & 10 deletions assembly/memory-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ export const SEGMENT_SIZE_SHIFT: u32 = 16;
export const RESERVED_MEMORY: u32 = 2 ** 16;
export const RESERVED_PAGES: u32 = RESERVED_MEMORY / PAGE_SIZE; // 16

/** Amount of memory to allocate eagerly */
export const ALLOCATE_EAGERLY: u32 = 2 ** 21; // 2MB

export enum Access {
None = 0,
Read = 1,
Expand All @@ -30,6 +27,7 @@ export class Page {
public readonly raw: RawPage,
) {}

@inline
can(access: Access): boolean {
return this.access === Access.Write || this.access === access;
}
Expand All @@ -38,14 +36,12 @@ export class Page {
export class RawPage {
constructor(
public readonly id: ArenaId,
public page: Uint8Array | null,
public page: Uint8Array,
) {}

@inline
get data(): Uint8Array {
if (this.page === null) {
this.page = new Uint8Array(PAGE_SIZE).fill(0);
}
return this.page as Uint8Array;
return this.page;
}
}

Expand Down Expand Up @@ -75,8 +71,7 @@ export class Arena {
console.log("Warning: Run out of pages! Allocating.");
}

// actually allocate for some time, but later do it lazily
const data = allocatedMemory < ALLOCATE_EAGERLY ? new Uint8Array(PAGE_SIZE) : null;
const data = new Uint8Array(PAGE_SIZE);
this.extraPageIndex += 1;
return new RawPage(this.extraPageIndex, data);
}
Expand Down
2 changes: 1 addition & 1 deletion assembly/memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ export const TESTS: Test[] = [
return assert;
}),
test("sbrk maxHeapPointer prevents heap from growing into stack region", (assert) => {
const heapStart: u32 = RESERVED_MEMORY;
const stackStart: u32 = 0xfe000000;
const heapStart: u32 = stackStart - 300;
const mem = new MemoryBuilder().build(heapStart, stackStart);
const fault = new MaybePageFault();
const maxHeapSize: u32 = stackStart - heapStart;
Expand Down
Loading
Loading