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" | |
] | |
} |