planck-js
Version:
2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development
224 lines (189 loc) • 6.88 kB
text/typescript
export interface TestInterface {
name: string;
createBoxShape: (hx: number, hy: number) => any;
createBoxBody: (shape: any, x: number, y: number, density: number) => any;
step: (timeStep: number, velocityIterations: number, positionIterations: number) => void;
}
export interface XY {
x: number;
y: number;
}
export type TestFactory = (gravity: XY, edgeV1: XY, edgeV2: XY, edgeDensity: number) => TestInterface;
const PYRAMID_SIZE = 40;
const GRAVITY = { x: 0, y: -10 };
const DELTA_X = { x: 0.5625, y: 1 };
const DELTA_Y = { x: 1.125, y: 0 };
const BOX_DENSITY = 5;
const GROUND_DENSITY = 0;
const TIME_STEP = 1 / 60;
const VELOCITY_ITERATIONS = 3;
const POSITION_ITERATIONS = 3;
const EDGE_V1 = { x: -40, y: 0 };
const EDGE_V2 = { x: 40, y: 0 };
const START_X = { x: -7, y: 0.75 };
const BOX_SIZE = { x: 0.5, y: 0.5 };
const WARMUP_ITERATIONS = 64;
const BENCH_ITERATIONS = 256;
function setupWorld(test: TestInterface) {
// Setup world
const shape = test.createBoxShape(BOX_SIZE.x, BOX_SIZE.y);
const x = { ...START_X };
for (let i = 0; i < PYRAMID_SIZE; ++i) {
const y = { ...x };
for (let j = i; j < PYRAMID_SIZE; ++j) {
test.createBoxBody(shape, y.x, y.y, BOX_DENSITY);
y.x += DELTA_Y.x;
y.y += DELTA_Y.y;
}
x.x += DELTA_X.x;
x.y += DELTA_X.y;
}
}
export type ProgressFunc = (value: number, max: number) => void;
async function warmupAsync(test: TestInterface, progress: ProgressFunc): Promise<void> {
return new Promise<void>((resolve: (value: void) => void) => {
let step = 0;
progress(0, WARMUP_ITERATIONS + BENCH_ITERATIONS);
const runNext = () => {
if (step < WARMUP_ITERATIONS) {
test.step(TIME_STEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS);
progress(step, WARMUP_ITERATIONS + BENCH_ITERATIONS);
step++;
window.requestAnimationFrame(runNext);
} else {
resolve();
}
};
window.requestAnimationFrame(runNext);
});
}
async function benchAsync(test: TestInterface, progress: ProgressFunc): Promise<number[]> {
return new Promise((resolve: (value: number[]) => void) => {
let step = 0;
progress(0, WARMUP_ITERATIONS + BENCH_ITERATIONS);
const times: number[] = [];
const runNext = () => {
if (step < BENCH_ITERATIONS) {
const begin = performance.now();
test.step(TIME_STEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS);
times.push(performance.now() - begin);
progress(WARMUP_ITERATIONS + step, WARMUP_ITERATIONS + BENCH_ITERATIONS);
step++;
window.requestAnimationFrame(runNext);
} else {
resolve(times);
}
};
window.requestAnimationFrame(runNext);
});
}
function warmup(test: TestInterface) {
for (let i = 0; i < WARMUP_ITERATIONS; i++) {
test.step(TIME_STEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS);
}
}
function bench(test: TestInterface) {
const times: number[] = [];
for (let i = 0; i < BENCH_ITERATIONS; i++) {
const begin = performance.now();
test.step(TIME_STEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS);
times.push(performance.now() - begin);
}
return times.sort();
}
function mean(values: number[]) {
let total = 0;
for (const value of values) total += value;
return total / BENCH_ITERATIONS;
}
// Simple nearest-rank %ile (on sorted array). We should have enough samples to make this reasonable.
function percentile(values: number[], pc: number) {
const rank = Math.floor((pc * values.length) / 100);
return values[rank];
}
export function prepareTests(factories: TestFactory[]) {
return factories.map((factory: TestFactory) => factory(GRAVITY, EDGE_V1, EDGE_V2, GROUND_DENSITY));
}
export function runTest(test: TestInterface) {
setupWorld(test);
warmup(test);
const times = bench(test);
return {
name: test.name,
avg: mean(times),
p5: percentile(times, 5),
p95: percentile(times, 95),
};
}
export async function runTestAsync(test: TestInterface, progress: ProgressFunc) {
setupWorld(test);
await warmupAsync(test, progress);
const times = await benchAsync(test, progress);
return {
name: test.name,
avg: mean(times),
p5: percentile(times, 5),
p95: percentile(times, 95),
};
}
export type TestResult = ReturnType<typeof runTest>;
export function runAllTests(factories: TestFactory[]) {
const tests = prepareTests(factories);
const results: TestResult[] = [];
console.log("Running Benchmarks:");
for (const test of tests) {
const result = runTest(test);
results.push(result);
console.log(` ✓ ${result.name}`);
}
return results.sort((a: TestResult, b: TestResult) => a.avg - b.avg);
}
const noop = () => undefined;
export async function runAllTestsAsync(factories: TestFactory[]) {
const tests = prepareTests(factories);
console.log("Running Benchmarks:");
const results: TestResult[] = [];
for (const test of tests) {
// eslint-disable-next-line no-await-in-loop
const result = await runTestAsync(test, noop);
console.log(` ✓ ${result.name}`);
results.push(result);
}
return results.sort((a: TestResult, b: TestResult) => a.avg - b.avg);
}
export function resultsToMarkdown(results: TestResult[]) {
const header = ["Name", "avg ms/frame", "5th %ile", "95th %ile", "Ratio"];
const rows = results.map((r: TestResult) => [
r.name,
r.avg.toFixed(2),
r.p5.toString(),
r.p95.toString(),
(r.avg / results[0].avg).toFixed(2),
]);
const lengths = header.map((label: string, index: number) =>
Math.max(label.length, ...rows.map((columns: string[]) => columns[index].length)),
);
function getDashes(length: number) {
return "-".repeat(length);
}
const dashes = `| ${lengths.map(getDashes).join(" | ")} |`;
const lines: string[] = [];
lines.push(`| ${header.map(pad).join(" | ")} |`);
lines.push(dashes);
function pad(value: string, index: number) {
const length = lengths[index];
if (value.length < length) {
if (index > 0) return " ".repeat(length - value.length) + value;
return value + " ".repeat(length - value.length);
}
return value;
}
for (const row of rows) {
lines.push(`| ${row.map(pad).join(" | ")} |`);
}
return lines.join("\n");
}
export function logResults(results: TestResult[]) {
console.log("\nResults:");
console.log(resultsToMarkdown(results));
}