Skip to content

Commit 1912f63

Browse files
WIP: Add support for caching prepared WASM
1 parent ed045c7 commit 1912f63

File tree

10 files changed

+130
-34
lines changed

10 files changed

+130
-34
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"name": "@fluxprotocol/oracle-vm",
33
"description": "Oracle VM for Flux Protocol",
44
"version": "1.1.0",
5-
"main": "dist/src/main.js",
6-
"types": "dist/src/main.d.ts",
5+
"main": "dist/Process.js",
6+
"types": "dist/Process.d.ts",
77
"devDependencies": {
88
"@types/big.js": "^6.1.2",
99
"@types/bn.js": "^5.1.0",

src/Process.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,24 @@ import { extractMessageFromEvent } from './services/WorkerService';
66
import { MessageType, RequestMessage } from './models/WorkerMessage';
77
import ProcessMethods from './ProcessMethods';
88
import { ExecuteResult } from './models/ExecuteResult';
9+
import ICache from './models/Cache';
10+
import InMemoryCache from './models/InMemoryCache';
911

1012
export default class Process extends EventEmitter {
11-
public context: Context;
1213
private processMethods: ProcessMethods;
1314
private notifierBuffer = new SharedArrayBuffer(16);
1415

15-
constructor(context: Context) {
16+
constructor(
17+
public context: Context,
18+
private cache: ICache = new InMemoryCache()
19+
) {
1620
super();
17-
18-
this.context = context;
1921
this.processMethods = new ProcessMethods(this.notifierBuffer);
2022
}
2123

22-
spawn() {
24+
async spawn() {
2325
const worker = new Worker(new URL('./Process.worker.ts', import.meta.url) as NodeURL);
26+
const binaryId = this.context.args[0];
2427

2528
worker.addListener('message', async (event: MessageEvent) => {
2629
const data = extractMessageFromEvent(event);
@@ -30,14 +33,20 @@ export default class Process extends EventEmitter {
3033
this.emit('exit', data.value);
3134
} else if (data.type === MessageType.MethodCall) {
3235
this.processMethods.executeMethodCallMessage(data);
36+
} else if (data.type === MessageType.Cache) {
37+
this.cache.set(binaryId, data.value.binary);
3338
}
3439
});
3540

41+
const cachedBinary = await this.cache.get(binaryId);
42+
3643
const spawnMessage: RequestMessage = {
3744
type: MessageType.Spawn,
3845
value: {
3946
context: {
4047
...this.context,
48+
isFromCache: cachedBinary ? true : false,
49+
binary: cachedBinary ?? this.context.binary,
4150
notifierBuffer: this.notifierBuffer,
4251
},
4352
},
@@ -47,14 +56,16 @@ export default class Process extends EventEmitter {
4756
}
4857
}
4958

50-
export function execute(context: Context): Promise<ExecuteResult> {
59+
export function execute(context: Context, cache: ICache = new InMemoryCache()): Promise<ExecuteResult> {
5160
return new Promise((resolve) => {
52-
const process = new Process(context);
61+
const process = new Process(context, cache);
5362

5463
process.on('exit', (result: ExecuteResult) => {
5564
resolve(result);
5665
});
5766

5867
process.spawn();
5968
});
60-
}
69+
}
70+
71+
export { ICache, InMemoryCache, Context };

src/Process.worker.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { VmContext } from "./models/Context";
1+
import { VmContext, WorkerContext } from "./models/Context";
22
import { MessageType } from "./models/WorkerMessage";
33
import { executeWasm } from "./services/VirtualMachineService";
4+
import { prepareWasmBinary } from "./services/WasmService";
45
import { extractMessageFromEvent, workerAddEventListener, selfPostMessage } from "./services/WorkerService";
56

67
workerAddEventListener('message', async (event: MessageEvent) => {
@@ -10,7 +11,22 @@ workerAddEventListener('message', async (event: MessageEvent) => {
1011
return;
1112
}
1213

13-
const vmContext = new VmContext(data.value.context);
14+
const workerContext: WorkerContext = {
15+
...data.value.context,
16+
};
17+
18+
if (!workerContext.isFromCache) {
19+
workerContext.binary = prepareWasmBinary(workerContext.binary);
20+
21+
selfPostMessage({
22+
type: MessageType.Cache,
23+
value: {
24+
binary: workerContext.binary,
25+
},
26+
});
27+
}
28+
29+
const vmContext = new VmContext(workerContext);
1430
const executeResult = await executeWasm(vmContext);
1531

1632
selfPostMessage({

src/models/Cache.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default interface ICache {
2+
set(id: string, binary: Uint8Array): Promise<void>;
3+
get(id: string): Promise<Uint8Array | undefined>;
4+
delete(id: string): Promise<void>;
5+
clear(): Promise<void>;
6+
}

src/models/Context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface Context {
1212
}
1313

1414
export interface WorkerContext extends Context {
15+
isFromCache: boolean;
1516
notifierBuffer: SharedArrayBuffer;
1617
}
1718

src/models/InMemoryCache.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import ICache from './Cache';
2+
3+
export default class InMemoryCache implements ICache {
4+
private internalCache: Map<string, Uint8Array> = new Map();
5+
6+
async set(id: string, binary: Uint8Array) {
7+
this.internalCache.set(id, binary);
8+
}
9+
10+
async delete(id: string) {
11+
this.internalCache.delete(id);
12+
}
13+
14+
async get(id: string) {
15+
return this.internalCache.get(id);
16+
}
17+
18+
async clear() {
19+
this.internalCache.clear();
20+
}
21+
}

src/models/WorkerMessage.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ExecuteResult } from "./ExecuteResult";
44
export enum MessageType {
55
Event = 'EVENT',
66
Spawn = 'SPAWN',
7+
Cache = 'CACHE',
78
Provider = 'PROVIDER',
89
Message = 'MESSAGE',
910
MethodCall = 'METHOD_CALL',
@@ -35,6 +36,13 @@ export interface MethodCallMessage extends RequestMessageBase {
3536
};
3637
}
3738

38-
export type RequestMessage = SpawnMessage | ExitMessage | MethodCallMessage;
39+
export interface CacheMessage extends RequestMessageBase {
40+
type: MessageType.Cache,
41+
value: {
42+
binary: Uint8Array;
43+
}
44+
}
45+
46+
export type RequestMessage = SpawnMessage | ExitMessage | MethodCallMessage | CacheMessage;
3947

4048

src/services/VirtualMachineService.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@ import { generateFixedBuffer } from './CryptoService';
88
import VirtualFs from './VirtualFsService';
99
import WasmImports from './WasmImports';
1010

11-
const metering = require('wasm-metering');
12-
13-
export function prepareWasmBinary(binary: Uint8Array): Uint8Array {
14-
const meteredWasm = metering.meterWASM(binary, {
15-
meterType: 'i32',
16-
});
17-
18-
return meteredWasm;
19-
}
20-
2111
export function executeWasm(context: VmContext): Promise<ExecuteResult> {
2212
return new Promise(async (resolve) => {
2313
const wasmFs = new WasmFs();
@@ -47,8 +37,7 @@ export function executeWasm(context: VmContext): Promise<ExecuteResult> {
4737
},
4838
});
4939

50-
const preparedBinary = prepareWasmBinary(context.binary);
51-
const module = await WebAssembly.compile(preparedBinary.buffer);
40+
const module = await WebAssembly.compile(context.binary);
5241
const wasiImports = wasi.getImports(module);
5342
const wasmImports = new WasmImports(context, wasiImports);
5443
const instance = await WebAssembly.instantiate(module, wasmImports.getImports());

src/services/WasmService.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const metering = require('wasm-metering');
2+
3+
export function prepareWasmBinary(binary: Uint8Array): Uint8Array {
4+
const meteredWasm = metering.meterWASM(binary, {
5+
meterType: 'i32',
6+
});
7+
8+
return meteredWasm;
9+
}

test/vm.test.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { execute } from '../dist/Process';
1+
import { Context, execute, InMemoryCache } from '../dist/Process';
22
import { readFileSync } from 'fs';
33

44
jest.setTimeout(10_000);
55

66
describe('Process', () => {
7+
const memoryCache = new InMemoryCache();
8+
79
it('should be able to run a wasm file', async () => {
810
const wasm = readFileSync(__dirname + '/wasm/cowsay.wasm');
911
const result = await execute({
@@ -13,22 +15,23 @@ describe('Process', () => {
1315
gasLimit: (300_000_000_000_000).toString(),
1416
randomSeed: '0x012',
1517
timestamp: 1,
16-
});
18+
}, memoryCache);
1719

1820
expect(result.gasUsed).toBe('26844206');
1921
expect(result.code).toBe(0);
2022
});
2123

2224
it('should throw a gas error when the gas limit has been exceeded', async () => {
2325
const wasm = readFileSync(__dirname + '/wasm/cowsay.wasm');
26+
2427
const result = await execute({
2528
args: ['cowsay', 'blah'],
2629
binary: new Uint8Array(wasm),
2730
env: {},
2831
gasLimit: (21_000_000).toString(),
2932
randomSeed: '0x012',
3033
timestamp: 1,
31-
});
34+
}, memoryCache);
3235

3336
expect(result.code).toBe(1);
3437
expect(result.logs[0]).toBe('ERR_OUT_OF_GAS');
@@ -53,7 +56,7 @@ describe('Process', () => {
5356
gasLimit: (300_000_000_000_000).toString(),
5457
randomSeed: '0x012',
5558
timestamp: new Date().getTime(),
56-
});
59+
}, memoryCache);
5760

5861
const outcome = JSON.parse(result.logs[result.logs.length - 1]);
5962
expect(outcome.value).toBe('imposter');
@@ -82,11 +85,11 @@ describe('Process', () => {
8285
gasLimit: (300_000_000_000_000).toString(),
8386
randomSeed: '0x012',
8487
timestamp: new Date().getTime(),
85-
});
88+
}, memoryCache);
8689

8790
const outcome = JSON.parse(result.logs[result.logs.length - 1]);
8891

89-
expect(result.gasUsed).toBe('627358915');
92+
expect(result.gasUsed).toBe('633190654');
9093
expect(outcome.value).toBe('70500');
9194
});
9295

@@ -113,11 +116,11 @@ describe('Process', () => {
113116
gasLimit: (300_000_000_000_000).toString(),
114117
randomSeed: '0x012',
115118
timestamp: new Date().getTime(),
116-
});
119+
}, memoryCache);
117120

118121
const outcome = JSON.parse(result.logs[result.logs.length - 1]);
119122

120-
expect(result.gasUsed).toBe('627070273');
123+
expect(result.gasUsed).toBe('632902012');
121124
expect(outcome.value).toBe('705000000000000000000000000');
122125
});
123126

@@ -140,10 +143,42 @@ describe('Process', () => {
140143
gasLimit: (300_000_000_000_000).toString(),
141144
randomSeed: '0x012',
142145
timestamp: new Date().getTime(),
143-
});
146+
}, memoryCache);
144147

145148
const outcome = JSON.parse(result.logs[result.logs.length - 1]);
146149
expect(result.gasUsed).toBe('23223018');
147150
expect(outcome.value).toBe('493625367592069900000000000000');
148151
});
152+
153+
it('should always use cache when available', async () => {
154+
const internalMemoryCache = new InMemoryCache();
155+
const wasm = readFileSync(__dirname + '/wasm/basic-fetch.wasm');
156+
const setSpy = jest.spyOn(internalMemoryCache, 'set');
157+
const getSpy = jest.spyOn(internalMemoryCache, 'get');
158+
159+
const context: Context = {
160+
args: [
161+
'0xdeadbeef',
162+
JSON.stringify([
163+
{
164+
end_point: 'https://api.coinpaprika.com/v1/coins/btc-bitcoin/ohlcv/historical?start=1630612481&end=1630612781',
165+
source_path: '$[0].close',
166+
},
167+
]),
168+
'number',
169+
(10e24).toString(),
170+
],
171+
binary: new Uint8Array(wasm),
172+
env: {},
173+
gasLimit: (300_000_000_000_000).toString(),
174+
randomSeed: '0x012',
175+
timestamp: new Date().getTime()
176+
};
177+
178+
await execute(context, internalMemoryCache);
179+
await execute(context, internalMemoryCache);
180+
181+
expect(getSpy).toHaveBeenCalledTimes(2);
182+
expect(setSpy).toHaveBeenCalledTimes(1);
183+
});
149184
});

0 commit comments

Comments
 (0)