diff --git a/cli/cli.js b/cli/cli.js index 3ff16b9a..c75daa43 100755 --- a/cli/cli.js +++ b/cli/cli.js @@ -110,6 +110,7 @@ function withCommonRunOptions (cmd) { .env("W4_NO_QR") .default(false) ) + .option("--metering", "Inject gas metering into WASM", false) .option("--hot", "Enable hot swapping. When the cart is reloaded, the console memory will be preserved, allowing code changes to the cart without resetting.", false); } diff --git a/cli/lib/server.js b/cli/lib/server.js index 35751e94..7c1838de 100644 --- a/cli/lib/server.js +++ b/cli/lib/server.js @@ -7,6 +7,7 @@ const { Server: WebSocketServer } = require("ws"); const open = require("open"); const process = require("process"); const { buffer } = require('node:stream/consumers'); +const metering = require("wasm-metering"); async function start (cartFile, opts) { @@ -16,23 +17,27 @@ async function start (cartFile, opts) { if (cartFile === "-") { // Filename "-" means read from standard in. // This can only be read once, so must be cached. - let cart_data = await buffer(process.stdin); + const cartRawWasm = await buffer(process.stdin); + const cartWasm = opts.metering ? metering.meterWASM(cartRawWasm) : cartRawWasm; app.get("/cart.wasm", (req, res) => { - res.send(cart_data); + res.send(cartWasm); }); } else if (!(await fs.stat(cartFile)).isFile()) { // If the file is not a regular file, such as a fifo, input stream etc. // we must also cache the data. - let cart_data = await fs.readFile(cartFile); + const cartRawWasm = await fs.readFile(cartFile); + const cartWasm = opts.metering ? metering.meterWASM(cartRawWasm) : cartRawWasm; app.get("/cart.wasm", (req, res) => { - res.send(cart_data); + res.send(cartWasm); }); } else { // otherwise it's a regular file, and can be read from disk every time. - app.get("/cart.wasm", (req, res) => { - res.sendFile(path.resolve(cartFile)); + app.get("/cart.wasm", async (req, res) => { + const cartRawWasm = await fs.readFile(cartFile); + const cartWasm = opts.metering ? metering.meterWASM(cartRawWasm) : cartRawWasm; + res.send(cartWasm); }); app.get("/cart.wasm.map", (req, res) => { res.sendFile(path.resolve(cartFile+".map")); diff --git a/cli/package-lock.json b/cli/package-lock.json index 1d3589eb..fea72f67 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -17,6 +17,7 @@ "pngjs": "^6.0.0", "qrcode": "^1.4.4", "recursive-copy": "^2.0.13", + "wasm-metering": "^0.2.1", "ws": "^7.5.3" }, "bin": { @@ -110,6 +111,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "node_modules/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -139,6 +145,14 @@ "concat-map": "0.0.1" } }, + "node_modules/buffer-pipe": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/buffer-pipe/-/buffer-pipe-0.0.0.tgz", + "integrity": "sha512-PvKbsvQOH4dcUyUEvQQSs3CIkkuPcOHt3gKnXwf4HsPKFDxSN7bkmICVIWgOmW/jx/fAEGGn4mIayIJPLs7G8g==", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, "node_modules/bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -589,6 +603,15 @@ "node": ">=0.10.0" } }, + "node_modules/leb128": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/leb128/-/leb128-0.0.4.tgz", + "integrity": "sha512-2zejk0fCIgY8RVcc/KzvyfpDio5Oo8HgPZmkrOmdwmbk0KpKpgD+JKwikxKk8cZYkANIhwHK50SNukkCm3XkCQ==", + "dependencies": { + "bn.js": "^4.11.6", + "buffer-pipe": "0.0.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -1132,6 +1155,41 @@ "node": ">= 0.8" } }, + "node_modules/wasm-json-toolkit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/wasm-json-toolkit/-/wasm-json-toolkit-0.2.3.tgz", + "integrity": "sha512-W0pESOST9hHFEmHq9kzMxAEhcPYuASdYCDw4FavKSyQKh3uOmH2slRXR/MhTKJY+gp1AauUDNd9DeE0cS4bV4A==", + "dependencies": { + "bn.js": "^4.11.8", + "buffer-pipe": "0.0.2", + "leb128": "0.0.4", + "safe-buffer": "^5.1.1" + }, + "bin": { + "json2wasm": "bin/json2wasm", + "wasm2json": "bin/wasm2json" + } + }, + "node_modules/wasm-json-toolkit/node_modules/buffer-pipe": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/buffer-pipe/-/buffer-pipe-0.0.2.tgz", + "integrity": "sha512-YlqzbWVqMv+xEeRyg0OXAJym3zAFTAIuku9l7okwxOXNDxbmSlL5o3QaF5k6IQ2iHO9o1OCo6tT4UkrQkI5VbQ==", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, + "node_modules/wasm-metering": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/wasm-metering/-/wasm-metering-0.2.1.tgz", + "integrity": "sha512-YDlTPY4jspknNyDaVBQhLTuTYBh+39qI0P9F0grmR88NR4oh7qfgpTwZ2ly4oX2hHCj9KlIwhy2Yyez+3/wY2Q==", + "dependencies": { + "leb128": "^0.0.4", + "wasm-json-toolkit": "0.2.3" + }, + "bin": { + "wasm-meter": "bin/wasm-meter" + } + }, "node_modules/which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", @@ -1275,6 +1333,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -1301,6 +1364,14 @@ "concat-map": "0.0.1" } }, + "buffer-pipe": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/buffer-pipe/-/buffer-pipe-0.0.0.tgz", + "integrity": "sha512-PvKbsvQOH4dcUyUEvQQSs3CIkkuPcOHt3gKnXwf4HsPKFDxSN7bkmICVIWgOmW/jx/fAEGGn4mIayIJPLs7G8g==", + "requires": { + "safe-buffer": "^5.1.1" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1646,6 +1717,15 @@ "resolved": "https://registry.npmjs.org/junk/-/junk-1.0.3.tgz", "integrity": "sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI=" }, + "leb128": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/leb128/-/leb128-0.0.4.tgz", + "integrity": "sha512-2zejk0fCIgY8RVcc/KzvyfpDio5Oo8HgPZmkrOmdwmbk0KpKpgD+JKwikxKk8cZYkANIhwHK50SNukkCm3XkCQ==", + "requires": { + "bn.js": "^4.11.6", + "buffer-pipe": "0.0.0" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -2049,6 +2129,36 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "wasm-json-toolkit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/wasm-json-toolkit/-/wasm-json-toolkit-0.2.3.tgz", + "integrity": "sha512-W0pESOST9hHFEmHq9kzMxAEhcPYuASdYCDw4FavKSyQKh3uOmH2slRXR/MhTKJY+gp1AauUDNd9DeE0cS4bV4A==", + "requires": { + "bn.js": "^4.11.8", + "buffer-pipe": "0.0.2", + "leb128": "0.0.4", + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "buffer-pipe": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/buffer-pipe/-/buffer-pipe-0.0.2.tgz", + "integrity": "sha512-YlqzbWVqMv+xEeRyg0OXAJym3zAFTAIuku9l7okwxOXNDxbmSlL5o3QaF5k6IQ2iHO9o1OCo6tT4UkrQkI5VbQ==", + "requires": { + "safe-buffer": "^5.1.1" + } + } + } + }, + "wasm-metering": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/wasm-metering/-/wasm-metering-0.2.1.tgz", + "integrity": "sha512-YDlTPY4jspknNyDaVBQhLTuTYBh+39qI0P9F0grmR88NR4oh7qfgpTwZ2ly4oX2hHCj9KlIwhy2Yyez+3/wY2Q==", + "requires": { + "leb128": "^0.0.4", + "wasm-json-toolkit": "0.2.3" + } + }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", diff --git a/cli/package.json b/cli/package.json index d0e3ef20..037ac946 100644 --- a/cli/package.json +++ b/cli/package.json @@ -42,6 +42,7 @@ "pngjs": "^6.0.0", "qrcode": "^1.4.4", "recursive-copy": "^2.0.13", + "wasm-metering": "^0.2.1", "ws": "^7.5.3" }, "engines": { diff --git a/devtools/web/src/components/devtools/devtools.ts b/devtools/web/src/components/devtools/devtools.ts index 6fa11b50..fe104b85 100644 --- a/devtools/web/src/components/devtools/devtools.ts +++ b/devtools/web/src/components/devtools/devtools.ts @@ -70,6 +70,7 @@ export class Wasm4Devtools extends LitElement { private _renderGeneralView = ({ memoryView, fps, + gasUsed, wasmBufferByteLen, }: UpdateControllerState) => { const drawColors = memoryView.drawColors ?? 0; @@ -110,6 +111,10 @@ export class Wasm4Devtools extends LitElement {

fps

${fps} +
+

gas

+ ${gasUsed.toLocaleString('en-US')} +

cartridge size ${wasmBufferByteLen > MAX_CART_SIZE ? `⚠️` : ''}

diff --git a/devtools/web/src/controllers/UpdateController.ts b/devtools/web/src/controllers/UpdateController.ts index 14d2faf3..4d9fd541 100644 --- a/devtools/web/src/controllers/UpdateController.ts +++ b/devtools/web/src/controllers/UpdateController.ts @@ -9,6 +9,7 @@ export interface UpdateControllerState { memoryView: MemoryView; storedValue: string | null; fps: number; + gasUsed: number; wasmBufferByteLen: number; } @@ -31,6 +32,7 @@ export class UpdateController implements ReactiveController { memoryView: detail.memory, storedValue: detail.storedValue ?? null, fps: detail.fps, + gasUsed: detail.gasUsed, wasmBufferByteLen: detail.wasmBufferByteLen, }; diff --git a/devtools/web/src/devtools-manager.ts b/devtools/web/src/devtools-manager.ts index ebb22ba8..6d351c63 100644 --- a/devtools/web/src/devtools-manager.ts +++ b/devtools/web/src/devtools-manager.ts @@ -45,6 +45,7 @@ class BufferedRuntimeData implements BufferedData { interface RuntimeInfo { data: DataView; wasmBufferByteLen: number; + gasUsed: number; } export class DevtoolsManager { @@ -85,7 +86,8 @@ export class DevtoolsManager { this._notifyUpdateCompleted( runtimeInfo.data, runtimeInfo.wasmBufferByteLen, - this._calcAvgFPS() + this._calcAvgFPS(), + runtimeInfo.gasUsed, ); } }; @@ -113,12 +115,13 @@ export class DevtoolsManager { }; private _notifyUpdateCompleted = throttle( - (dataView: DataView, wasmBufferByteLen: number, fps: number) => { + (dataView: DataView, wasmBufferByteLen: number, fps: number, gasUsed: number) => { window.dispatchEvent( createUpdateCompletedEvent({ dataView, wasmBufferByteLen, fps, + gasUsed, bufferedData: this._bufferedData.flush(), }) ); diff --git a/devtools/web/src/events/update-completed.ts b/devtools/web/src/events/update-completed.ts index 6be80593..8fbaad3c 100644 --- a/devtools/web/src/events/update-completed.ts +++ b/devtools/web/src/events/update-completed.ts @@ -5,6 +5,7 @@ export const updateCompletedEventType = 'wasm4-update-completed'; export interface UpdateCompletedDetails { memory: MemoryView; fps: number; + gasUsed: number; wasmBufferByteLen: number; storedValue: string | null; } @@ -28,6 +29,7 @@ function getStoredValue(): string | null { export interface UpdateCompletedData { dataView: DataView; fps: number; + gasUsed: number; bufferedData: BufferedMemoryData; wasmBufferByteLen: number; } @@ -42,7 +44,7 @@ export interface UpdateCompletedData { * @returns */ export function createUpdateCompletedEvent( - { dataView, fps, bufferedData, wasmBufferByteLen }: UpdateCompletedData, + { dataView, fps, gasUsed, bufferedData, wasmBufferByteLen }: UpdateCompletedData, eventInit: EventInit = { bubbles: true } ): Wasm4UpdateCompletedEvent { return new CustomEvent(updateCompletedEventType, { @@ -50,6 +52,7 @@ export function createUpdateCompletedEvent( detail: { memory: new MemoryView(dataView, bufferedData), fps, + gasUsed, wasmBufferByteLen, storedValue: getStoredValue(), }, diff --git a/runtimes/web/src/runtime.ts b/runtimes/web/src/runtime.ts index bf83d762..54e519b7 100644 --- a/runtimes/web/src/runtime.ts +++ b/runtimes/web/src/runtime.ts @@ -21,6 +21,7 @@ export class Runtime { diskName: string; diskBuffer: ArrayBuffer; diskSize: number; + gasUsed: number; constructor (diskName: string) { const canvas = document.createElement("canvas"); @@ -39,7 +40,7 @@ export class Runtime { } this.compositor = new WebGLCompositor(gl); - + this.apu = new APU(); this.diskName = diskName; @@ -67,6 +68,7 @@ export class Runtime { this.pauseState = 0; this.wasmBufferByteLen = 0; + this.gasUsed = 0; } async init () { @@ -159,8 +161,14 @@ export class Runtime { tracef: this.tracef.bind(this), }; + const metering = { + usegas: (gas: number) => { + this.gasUsed += gas; + }, + }; + await this.bluescreenOnError(async () => { - const module = await WebAssembly.instantiate(wasmBuffer, { env }); + const module = await WebAssembly.instantiate(wasmBuffer, { env, metering }); this.wasm = module.instance; // Call the WASI _start/_initialize function (different from WASM-4's start callback!) @@ -335,6 +343,7 @@ export class Runtime { this.framebuffer.clear(); } + this.gasUsed = 0; let update_function = this.wasm!.exports["update"]; if (typeof update_function === "function") { this.bluescreenOnError(update_function);