198 lines
5.3 KiB
TypeScript
198 lines
5.3 KiB
TypeScript
/**
|
|
* Compile-time (AOT) vs runtime codegen benchmark.
|
|
*
|
|
* The AOT codecs are produced by running our transformer on a sample TS file
|
|
* at bench startup, writing the result to a temp file, and importing it. The
|
|
* runtime codecs come from the regular `type(...)` runtime path.
|
|
*/
|
|
|
|
import { bench, beforeAll, afterAll, describe } from 'vitest';
|
|
import { writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { transform } from '../../plugin/compile/transformer.ts';
|
|
import {
|
|
type as runtimeType,
|
|
type TypeCodec,
|
|
u53,
|
|
f64,
|
|
str,
|
|
list,
|
|
enumOf,
|
|
flags,
|
|
clearRegistry,
|
|
Reader,
|
|
Writer,
|
|
} from '../../plugin/index.ts';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const GEN_FILE = join(HERE, '__aot_codecs.ts');
|
|
|
|
const AOT_SOURCE = `
|
|
import { type, u53, f64, str, list, enumOf, flags } from '../../plugin/index.ts';
|
|
|
|
export const Ticker = type('AotTicker', {
|
|
symbol: str,
|
|
last: f64,
|
|
bid: f64,
|
|
ask: f64,
|
|
volume: f64,
|
|
});
|
|
|
|
export const Order = type('AotOrder', {
|
|
id: u53,
|
|
account: u53,
|
|
symbol: str,
|
|
side: enumOf(['buy', 'sell'] as const),
|
|
price: f64,
|
|
qty: f64,
|
|
filledQty: f64,
|
|
ts: u53,
|
|
flags: flags(['ioc', 'post_only', 'reduce_only'] as const),
|
|
});
|
|
|
|
export const Level = type('AotLevel', { p: f64, q: f64 });
|
|
|
|
export const Book = type('AotBook', {
|
|
symbol: str,
|
|
ts: u53,
|
|
bids: list(Level),
|
|
asks: list(Level),
|
|
});
|
|
`;
|
|
|
|
interface AotCodec {
|
|
encode: (v: unknown, w?: Writer) => Uint8Array;
|
|
decode: (b: Uint8Array) => unknown;
|
|
encodeInto: (v: unknown, w: Writer) => void;
|
|
decodeFrom: (r: Reader) => unknown;
|
|
id: number;
|
|
}
|
|
|
|
let aot: Record<string, AotCodec>;
|
|
let rtTicker: TypeCodec<unknown>;
|
|
let rtOrder: TypeCodec<unknown>;
|
|
let rtLevel: TypeCodec<unknown>;
|
|
let rtBook: TypeCodec<unknown>;
|
|
|
|
const ticker = {
|
|
symbol: 'BTC-USD', last: 67891.23, bid: 67890.5, ask: 67892.0, volume: 1234567.89,
|
|
};
|
|
const order = {
|
|
id: 9876543210, account: 12345678, symbol: 'BTC-USD',
|
|
side: 'buy' as const, price: 67500.5, qty: 0.125, filledQty: 0,
|
|
ts: 1716100000123,
|
|
flags: { ioc: false, post_only: true, reduce_only: false },
|
|
};
|
|
const book = {
|
|
symbol: 'BTC-USD',
|
|
ts: 1716100000123,
|
|
bids: Array.from({ length: 1000 }, (_, i) => ({ p: 67890 - i * 0.5, q: 0.1 + (i % 100) * 0.01 })),
|
|
asks: Array.from({ length: 1000 }, (_, i) => ({ p: 67891 + i * 0.5, q: 0.1 + (i % 100) * 0.01 })),
|
|
};
|
|
|
|
const wT = new Writer(256);
|
|
const wO = new Writer(256);
|
|
const wB = new Writer(64 * 1024);
|
|
|
|
let tickerAot: Uint8Array;
|
|
let tickerRt: Uint8Array;
|
|
let orderAot: Uint8Array;
|
|
let orderRt: Uint8Array;
|
|
let bookAot: Uint8Array;
|
|
let bookRt: Uint8Array;
|
|
|
|
beforeAll(async () => {
|
|
// Build the AOT module on the fly.
|
|
const transformed = transform(AOT_SOURCE, GEN_FILE, {
|
|
importPath: '../../plugin/index.ts',
|
|
packageAliases: ['../../plugin/index.ts'],
|
|
});
|
|
writeFileSync(GEN_FILE, transformed.code, 'utf8');
|
|
const url = `${pathToFileURL(GEN_FILE).href}?t=${Date.now()}`;
|
|
aot = (await import(/* @vite-ignore */ url)) as Record<string, AotCodec>;
|
|
|
|
// Runtime equivalents with non-colliding names.
|
|
clearRegistry();
|
|
rtTicker = runtimeType('RtTicker', {
|
|
symbol: str, last: f64, bid: f64, ask: f64, volume: f64,
|
|
});
|
|
rtOrder = runtimeType('RtOrder', {
|
|
id: u53, account: u53, symbol: str,
|
|
side: enumOf(['buy', 'sell'] as const),
|
|
price: f64, qty: f64, filledQty: f64, ts: u53,
|
|
flags: flags(['ioc', 'post_only', 'reduce_only'] as const),
|
|
});
|
|
rtLevel = runtimeType('RtLevel', { p: f64, q: f64 });
|
|
rtBook = runtimeType('RtBook', {
|
|
symbol: str, ts: u53, bids: list(rtLevel), asks: list(rtLevel),
|
|
});
|
|
|
|
// Pre-encode for decode benches
|
|
tickerAot = aot.Ticker!.encode(ticker);
|
|
wT.reset(); rtTicker.encodeInto(ticker, wT); tickerRt = wT.bytes().slice();
|
|
orderAot = aot.Order!.encode(order);
|
|
wO.reset(); rtOrder.encodeInto(order, wO); orderRt = wO.bytes().slice();
|
|
bookAot = aot.Book!.encode(book);
|
|
wB.reset(); rtBook.encodeInto(book, wB); bookRt = wB.bytes().slice();
|
|
});
|
|
|
|
afterAll(() => {
|
|
if (existsSync(GEN_FILE)) rmSync(GEN_FILE, { force: true });
|
|
});
|
|
|
|
describe('encode ticker (AOT vs runtime)', () => {
|
|
bench('AOT (compiled)', () => {
|
|
wT.reset();
|
|
aot.Ticker!.encodeInto(ticker, wT);
|
|
});
|
|
bench('runtime (new Function)', () => {
|
|
wT.reset();
|
|
rtTicker.encodeInto(ticker, wT);
|
|
});
|
|
});
|
|
|
|
describe('encode order (AOT vs runtime)', () => {
|
|
bench('AOT (compiled)', () => {
|
|
wO.reset();
|
|
aot.Order!.encodeInto(order, wO);
|
|
});
|
|
bench('runtime', () => {
|
|
wO.reset();
|
|
rtOrder.encodeInto(order, wO);
|
|
});
|
|
});
|
|
|
|
describe('encode book 1000 levels (AOT vs runtime)', () => {
|
|
bench('AOT (compiled)', () => {
|
|
wB.reset();
|
|
aot.Book!.encodeInto(book, wB);
|
|
});
|
|
bench('runtime', () => {
|
|
wB.reset();
|
|
rtBook.encodeInto(book, wB);
|
|
});
|
|
});
|
|
|
|
describe('decode ticker (AOT vs runtime)', () => {
|
|
bench('AOT (compiled)', () => {
|
|
const r = new Reader(tickerAot);
|
|
aot.Ticker!.decodeFrom(r);
|
|
});
|
|
bench('runtime', () => {
|
|
const r = new Reader(tickerRt);
|
|
rtTicker.decodeFrom(r);
|
|
});
|
|
});
|
|
|
|
describe('decode book 1000 levels (AOT vs runtime)', () => {
|
|
bench('AOT (compiled)', () => {
|
|
const r = new Reader(bookAot);
|
|
aot.Book!.decodeFrom(r);
|
|
});
|
|
bench('runtime', () => {
|
|
const r = new Reader(bookRt);
|
|
rtBook.decodeFrom(r);
|
|
});
|
|
});
|