npm install
npm run build
npm run serve
Then browse to the URL shown and open the console.
| node_modules | |
| *.wasm | |
| c.js | |
| *.wat | |
| package-lock.json |
| <!DOCTYPE html> | |
| <style> | |
| canvas, | |
| img { | |
| width: 128px; | |
| height: 128px; | |
| } | |
| </style> | |
| <h1>Performance test</h1> | |
| <button id="go">Go</button> | |
| <script type="module"> | |
| import { rotate } from "./rotate.js"; | |
| let NUM_ITERS = 10000; | |
| let IMAGE_SIZE = 512; | |
| if (location.search.includes("single")) { | |
| NUM_ITERS = 1; | |
| IMAGE_SIZE = 4096; | |
| } | |
| function showResult(buffer, width, height, textContent) { | |
| const div = document.createElement("div"); | |
| div.appendChild( | |
| Object.assign(document.createElement("h1"), { textContent }) | |
| ); | |
| const canvas = document.createElement("canvas"); | |
| Object.assign(canvas, { width, height }); | |
| const resultData = new Uint8ClampedArray( | |
| buffer, | |
| width * height * 4, | |
| width * height * 4 | |
| ); | |
| const ctx = canvas.getContext("2d"); | |
| ctx.putImageData(new ImageData(resultData, width, height), 0, 0); | |
| div.appendChild(canvas); | |
| document.body.appendChild(div); | |
| } | |
| async function measure(cb) { | |
| const timings = []; | |
| for (let i = 0; i < NUM_ITERS; i++) { | |
| const start = performance.now(); | |
| cb(); | |
| timings.push(performance.now() - start); | |
| } | |
| timings.sort((a, b) => a - b); | |
| const sum = timings.reduce((sum, v) => sum + v); | |
| const avg = sum / timings.length; | |
| const stddev = | |
| timings.map(v => Math.pow(v - avg, 2)).reduce((sum, v) => sum + v) / | |
| (timings.length - 1); | |
| const p90 = timings[Math.floor(timings.length * 0.9)]; | |
| const p95 = timings[Math.floor(timings.length * 0.95)]; | |
| const p99 = timings[Math.floor(timings.length * 0.99)]; | |
| return { avg, stddev, p90, p95, p99 }; | |
| } | |
| async function init() { | |
| const img = new ImageData( | |
| new Uint8ClampedArray(IMAGE_SIZE * IMAGE_SIZE * 4), | |
| IMAGE_SIZE, | |
| IMAGE_SIZE | |
| ); | |
| const pixelview = new Uint32Array(img.data.buffer); | |
| pixelview.fill(0xFF000000); | |
| for (let x = 0; x < IMAGE_SIZE; x++) { | |
| pixelview[x * IMAGE_SIZE + x] = 0xFF0000FF; | |
| pixelview[x * IMAGE_SIZE + (IMAGE_SIZE - x)] = 0xFF00FF00; | |
| } | |
| const bytesPerImage = img.width * img.height * 4; | |
| const minimumMemorySize = bytesPerImage * 2 + 4; | |
| const pagesNeeded = Math.ceil(minimumMemorySize / (64 * 1024)); | |
| console.log(`Operating on a ${IMAGE_SIZE}px x ${IMAGE_SIZE}px image`); | |
| console.log(`Running rotate() ${NUM_ITERS} times each`); | |
| // Benchmark JS | |
| { | |
| const buffer = new ArrayBuffer(bytesPerImage * 2); | |
| new Uint8ClampedArray(buffer).set(img.data); | |
| console.log("%c JavaScript", "font-size: 2em"); | |
| console.log( | |
| await measure(() => { | |
| rotate(buffer, img.width, img.height, 90); | |
| }) | |
| ); | |
| showResult(buffer, img.width, img.height, "JavaScript"); | |
| } | |
| for (const language of ["c", "assemblyscript", "rust"]) { | |
| let memory = new WebAssembly.Memory({ initial: 256 }); | |
| const { instance } = await WebAssembly.instantiate( | |
| await fetch(`/${language}.wasm`).then(r => r.arrayBuffer()), | |
| { | |
| env: { memory } | |
| } | |
| ); | |
| if (instance.exports.memory) { | |
| memory = instance.exports.memory; | |
| } | |
| memory.grow(pagesNeeded); | |
| new Uint8ClampedArray(memory.buffer, 4).set(img.data); | |
| console.log(`%c ${language}`, "font-size: 2em"); | |
| console.log( | |
| await measure(() => { | |
| (instance.exports.a || instance.exports.rotate)( | |
| img.width, | |
| img.height, | |
| 90 | |
| ); | |
| }) | |
| ); | |
| showResult(memory.buffer, img.width, img.height, language); | |
| } | |
| } | |
| document.all.go.onclick = init; | |
| </script> |
| { | |
| "name": "cruft", | |
| "main": "index.js", | |
| "scripts": { | |
| "build": "npm run build:asc && npm run build:rust && npm run build:c", | |
| "build:asc": "asc rotate.ts -b assemblyscript.wasm --validate -O3", | |
| "build:rust": "rustup run nightly rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs && wasm-strip rust.wasm", | |
| "build:c": "docker run -t -i --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS=['_rotate'] -o c.js rotate.c", | |
| "serve": "http-server -c0" | |
| }, | |
| "devDependencies": { | |
| "assemblyscript": "github:AssemblyScript/assemblyscript", | |
| "http-server": "^0.11.1" | |
| } | |
| } |
| #include <inttypes.h> | |
| #include <emscripten.h> | |
| EMSCRIPTEN_KEEPALIVE | |
| void rotate(int inputWidth, int inputHeight, int rotate) { | |
| // In the straight-copy case, d1 is x, d2 is y. | |
| // x starts at 0 and increases. | |
| // y starts at 0 and increases. | |
| int d1Start = 0; | |
| int d1Limit = inputWidth; | |
| int d1Advance = 1; | |
| int d1Multiplier = 1; | |
| int d2Start = 0; | |
| int d2Limit = inputHeight; | |
| int d2Advance = 1; | |
| int d2Multiplier = inputWidth; | |
| if (rotate == 90) { | |
| // d1 is y, d2 is x. | |
| // y starts at its max value and decreases. | |
| // x starts at 0 and increases. | |
| d1Start = inputHeight - 1; | |
| d1Limit = inputHeight; | |
| d1Advance = -1; | |
| d1Multiplier = inputWidth; | |
| d2Start = 0; | |
| d2Limit = inputWidth; | |
| d2Advance = 1; | |
| d2Multiplier = 1; | |
| } else if (rotate == 180) { | |
| // d1 is x, d2 is y. | |
| // x starts at its max and decreases. | |
| // y starts at its max and decreases. | |
| d1Start = inputWidth - 1; | |
| d1Limit = inputWidth; | |
| d1Advance = -1; | |
| d1Multiplier = 1; | |
| d2Start = inputHeight - 1; | |
| d2Limit = inputHeight; | |
| d2Advance = -1; | |
| d2Multiplier = inputWidth; | |
| } else if (rotate == 270) { | |
| // d1 is y, d2 is x. | |
| // y starts at 0 and increases. | |
| // x starts at its max and decreases. | |
| d1Start = 0; | |
| d1Limit = inputHeight; | |
| d1Advance = 1; | |
| d1Multiplier = inputWidth; | |
| d2Start = inputWidth - 1; | |
| d2Limit = inputWidth; | |
| d2Advance = -1; | |
| d2Multiplier = 1; | |
| } | |
| int bpp = 4; | |
| int imageSize = inputWidth * inputHeight * bpp; | |
| uint32_t* inBuffer = (uint32_t*) 4; | |
| uint32_t* outBuffer = (uint32_t*) (imageSize + 4); | |
| int i = 0; | |
| for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) { | |
| for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) { | |
| int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier)); | |
| outBuffer[i] = inBuffer[in_idx]; | |
| i += 1; | |
| } | |
| } | |
| } | |
| export function rotate(memory, inputWidth, inputHeight, rotate) { | |
| let i = 0; | |
| // In the straight-copy case, d1 is x, d2 is y. | |
| // x starts at 0 and increases. | |
| // y starts at 0 and increases. | |
| let d1Start = 0; | |
| let d1Limit = inputWidth; | |
| let d1Advance = 1; | |
| let d1Multiplier = 1; | |
| let d2Start = 0; | |
| let d2Limit = inputHeight; | |
| let d2Advance = 1; | |
| let d2Multiplier = inputWidth; | |
| if (rotate === 90) { | |
| // d1 is y, d2 is x. | |
| // y starts at its max value and decreases. | |
| // x starts at 0 and increases. | |
| d1Start = inputHeight - 1; | |
| d1Limit = inputHeight; | |
| d1Advance = -1; | |
| d1Multiplier = inputWidth; | |
| d2Start = 0; | |
| d2Limit = inputWidth; | |
| d2Advance = 1; | |
| d2Multiplier = 1; | |
| } else if (rotate === 180) { | |
| // d1 is x, d2 is y. | |
| // x starts at its max and decreases. | |
| // y starts at its max and decreases. | |
| d1Start = inputWidth - 1; | |
| d1Limit = inputWidth; | |
| d1Advance = -1; | |
| d1Multiplier = 1; | |
| d2Start = inputHeight - 1; | |
| d2Limit = inputHeight; | |
| d2Advance = -1; | |
| d2Multiplier = inputWidth; | |
| } else if (rotate === 270) { | |
| // d1 is y, d2 is x. | |
| // y starts at 0 and increases. | |
| // x starts at its max and decreases. | |
| d1Start = 0; | |
| d1Limit = inputHeight; | |
| d1Advance = 1; | |
| d1Multiplier = inputWidth; | |
| d2Start = inputWidth - 1; | |
| d2Limit = inputWidth; | |
| d2Advance = -1; | |
| d2Multiplier = 1; | |
| } | |
| const inB = new Uint32Array(memory); | |
| const outB = new Uint32Array(memory, inputWidth * inputHeight * 4); | |
| for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) { | |
| for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) { | |
| const start = ((d1 * d1Multiplier) + (d2 * d2Multiplier)); | |
| outB[i] = inB[start]; | |
| i += 1; | |
| } | |
| } | |
| } |
| #![no_std] | |
| #![no_main] | |
| use core::panic::PanicInfo; | |
| use core::slice::from_raw_parts_mut; | |
| #[no_mangle] | |
| fn rotate(inputWidth: isize, inputHeight: isize, rotate: isize) { | |
| let mut i = 0isize; | |
| // In the straight-copy case, d1 is x, d2 is y. | |
| // x starts at 0 and increases. | |
| // y starts at 0 and increases. | |
| let mut d1Start: isize = 0; | |
| let mut d1Limit: isize = inputWidth; | |
| let mut d1Advance: isize = 1; | |
| let mut d1Multiplier: isize = 1; | |
| let mut d2Start: isize = 0; | |
| let mut d2Limit: isize = inputHeight; | |
| let mut d2Advance: isize = 1; | |
| let mut d2Multiplier: isize = inputWidth; | |
| if rotate == 90 { | |
| // d1 is y, d2 is x. | |
| // y starts at its max value and decreases. | |
| // x starts at 0 and increases. | |
| d1Start = inputHeight - 1; | |
| d1Limit = inputHeight; | |
| d1Advance = -1; | |
| d1Multiplier = inputWidth; | |
| d2Start = 0; | |
| d2Limit = inputWidth; | |
| d2Advance = 1; | |
| d2Multiplier = 1; | |
| } else if rotate == 180 { | |
| // d1 is x, d2 is y. | |
| // x starts at its max and decreases. | |
| // y starts at its max and decreases. | |
| d1Start = inputWidth - 1; | |
| d1Limit = inputWidth; | |
| d1Advance = -1; | |
| d1Multiplier = 1; | |
| d2Start = inputHeight - 1; | |
| d2Limit = inputHeight; | |
| d2Advance = -1; | |
| d2Multiplier = inputWidth; | |
| } else if rotate == 270 { | |
| // d1 is y, d2 is x. | |
| // y starts at 0 and increases. | |
| // x starts at its max and decreases. | |
| d1Start = 0; | |
| d1Limit = inputHeight; | |
| d1Advance = 1; | |
| d1Multiplier = inputWidth; | |
| d2Start = inputWidth - 1; | |
| d2Limit = inputWidth; | |
| d2Advance = -1; | |
| d2Multiplier = 1; | |
| } | |
| let imageSize = (inputWidth * inputHeight) as usize; | |
| let inBuffer: &mut [u32]; | |
| let outBuffer: &mut [u32]; | |
| unsafe { | |
| inBuffer = from_raw_parts_mut::<u32>(4 as *mut u32, imageSize); | |
| outBuffer = from_raw_parts_mut::<u32>((inputWidth * inputHeight * 4 + 4) as *mut u32, imageSize); | |
| } | |
| for d2 in 0..d2Limit { | |
| for d1 in 0..d1Limit { | |
| let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier; | |
| outBuffer[i as usize] = inBuffer[in_idx as usize]; | |
| i += 1; | |
| } | |
| } | |
| } | |
| #[panic_handler] | |
| fn panic(_info: &PanicInfo) -> ! { | |
| loop {} | |
| } |
| export function rotate(inputWidth: i32, inputHeight: i32, rotate: i32): void { | |
| const bpp = 4; | |
| let offset = inputWidth * inputHeight * bpp; | |
| let i = 0; | |
| // In the straight-copy case, d1 is x, d2 is y. | |
| // x starts at 0 and increases. | |
| // y starts at 0 and increases. | |
| let d1Start = 0; | |
| let d1Limit = inputWidth; | |
| let d1Advance = 1; | |
| let d1Multiplier = 1; | |
| let d2Start = 0; | |
| let d2Limit = inputHeight; | |
| let d2Advance = 1; | |
| let d2Multiplier = inputWidth; | |
| if (rotate === 90) { | |
| // d1 is y, d2 is x. | |
| // y starts at its max value and decreases. | |
| // x starts at 0 and increases. | |
| d1Start = inputHeight - 1; | |
| d1Limit = inputHeight; | |
| d1Advance = -1; | |
| d1Multiplier = inputWidth; | |
| d2Start = 0; | |
| d2Limit = inputWidth; | |
| d2Advance = 1; | |
| d2Multiplier = 1; | |
| } else if (rotate === 180) { | |
| // d1 is x, d2 is y. | |
| // x starts at its max and decreases. | |
| // y starts at its max and decreases. | |
| d1Start = inputWidth - 1; | |
| d1Limit = inputWidth; | |
| d1Advance = -1; | |
| d1Multiplier = 1; | |
| d2Start = inputHeight - 1; | |
| d2Limit = inputHeight; | |
| d2Advance = -1; | |
| d2Multiplier = inputWidth; | |
| } else if (rotate === 270) { | |
| // d1 is y, d2 is x. | |
| // y starts at 0 and increases. | |
| // x starts at its max and decreases. | |
| d1Start = 0; | |
| d1Limit = inputHeight; | |
| d1Advance = 1; | |
| d1Multiplier = inputWidth; | |
| d2Start = inputWidth - 1; | |
| d2Limit = inputWidth; | |
| d2Advance = -1; | |
| d2Multiplier = 1; | |
| } | |
| for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) { | |
| for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) { | |
| let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier)); | |
| store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4)); | |
| i += 1; | |
| } | |
| } | |
| } |
| { | |
| "extends": "./node_modules/assemblyscript/std/assembly.json", | |
| "include": [ | |
| "./**/*.ts" | |
| ] | |
| } |