Skip to content

Instantly share code, notes, and snippets.

@surma

surma/.gitignore Secret

Last active May 8, 2021 00:08
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save surma/0eb306fa9acc8bdf2f58150b2f1e82b4 to your computer and use it in GitHub Desktop.
Save surma/0eb306fa9acc8bdf2f58150b2f1e82b4 to your computer and use it in GitHub Desktop.
node_modules
*.wasm
c.js
*.wat
package-lock.json

Build

npm install
npm run build
npm run serve

Then browse to the URL shown and open the console.

<!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"
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment