Skip to content

Commit

Permalink
Merge pull request #919 from thefrontside/benchmark
Browse files Browse the repository at this point in the history
✨Add benchmarks
  • Loading branch information
cowboyd authored Nov 20, 2024
2 parents c8a5eec + 5b74218 commit 0c1bdb6
Show file tree
Hide file tree
Showing 13 changed files with 536 additions and 1 deletion.
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"tasks": {
"test": "deno test --allow-run=deno",
"test:node": "deno task build:npm 0.0.0 && node test/main/node.mjs hello world",
"build:npm": "deno run -A tasks/build-npm.ts"
"build:npm": "deno run -A tasks/build-npm.ts",
"bench": "deno run -A tasks/bench.ts"
},
"lint": {
"rules": {
Expand Down
158 changes: 158 additions & 0 deletions tasks/bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { parser } from "npm:zod-opts";
import { z } from "npm:zod";
import {
all,
createQueue,
each,
main,
Operation,
spawn,
Task,
withResolvers,
} from "../mod.ts";
import { useWorker } from "./bench/worker.ts";
import scenarios from "./bench/scenarios.ts";
import {
BenchmarkDoneEvent,
BenchmarkOptions,
BenchmarkWorkerEvent,
WorkerCommand,
} from "./bench/types.ts";
import { Ok } from "../lib/result.ts";

import { Cell, Row, Table } from "jsr:@cliffy/[email protected]";

await main(function* (args) {
let options = parser()
.name("bench")
.description(
"Run Effection benchmarks",
)
.version("0.0.0")
.options({
include: {
type: z.string().optional(),
description: "include only scenarios matching REGEXP",
},
exclude: {
type: z.string().optional(),
description: "exclude all scenanios matching REGEXP",
},
repeat: {
type: z.number().positive().default(10),
description: "number of times to repeat",
alias: "n",
},
depth: {
type: z.number().positive().default(100),
description: "number of levels of recursion to run",
alias: "d",
},
})
.parse(args);

let { include, exclude } = options;

let tasks: Task<BenchmarkDoneEvent>[] = [];

for (let scenario of filter(scenarios, { include, exclude })) {
tasks.push(
yield* spawn(() =>
runBenchmark(scenario, { ...options, type: "benchmark" })
),
);
}

let results = yield* all(tasks);

let events = results.filter((result) => result.name.match("events"));
let recursion = results.filter((result) => result.name.match("recursion"));

if (events.length == 0 && recursion.length === 0) {
console.log("no benchmarks run");
return;
}

let rows = [];

if (recursion.length > 0) {
rows.push(Row.from([new Cell("Basic Recursion").colSpan(2).border()]));
rows.push(Row.from<Cell | string>(["Library", "Avg (ms)"]).border());
rows.push(...recursion.map((event) => Row.from(toTableRow(event))));
}

if (events.length > 0) {
rows.push(Row.from([new Cell("Recursive Events").colSpan(2).border()]));
rows.push(Row.from<Cell | string>(["Library", "Avg (ms)"]).border());
rows.push(...events.map((event) => Row.from(toTableRow(event))));
}

Table.from(rows).render();
});

function* runBenchmark(
scenario: string,
options: BenchmarkOptions,
): Operation<BenchmarkDoneEvent> {
let results = createQueue<BenchmarkDoneEvent, never>();
let closed = withResolvers<void>();
let worker = yield* useWorker<WorkerCommand, BenchmarkWorkerEvent>(scenario);

yield* spawn(function* () {
for (let event of yield* each(worker.errors)) {
event.preventDefault();
throw event.error;
}
});

yield* spawn(function* () {
for (let event of yield* each(worker.messages)) {
if (event.data.type === "done") {
results.add(event.data);
} else if (event.data.type === "close") {
console.log(event.data);
if (!event.data.result.ok) {
throw event.data.result.error;
} else {
closed.resolve();
}
}
yield* each.next();
}
});

try {
yield* worker.postMessage(options);
let value = (yield* results.next()).value;
return value;
} finally {
yield* worker.postMessage({ type: "close", result: Ok() });
}
}

import { basename } from "jsr:@std/path";

function filter(
strings: string[],
options: { include?: string; exclude?: string },
): string[] {
let { include, exclude } = options;
let result = strings;
if (include) {
result = result.filter((s) => basename(s).match(new RegExp(include)));
}
if (exclude) {
result = result.filter((s) => !basename(s).match(new RegExp(exclude)));
}
return result;
}

function toTableRow(event: BenchmarkDoneEvent): string[] {
let [name = event.name] = event.name.split(".");
if (event.result.ok) {
let { avgTime } = event.result.value;
return [name, String(avgTime)];
} else {
return [name, "❌"];
}
}
9 changes: 9 additions & 0 deletions tasks/bench/scenarios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default [
"./scenarios/effection.recursion.ts",
"./scenarios/rxjs.recursion.ts",
"./scenarios/co.recursion.ts",
"./scenarios/async+await.recursion.ts",
"./scenarios/effection.events.ts",
"./scenarios/rxjs.events.ts",
"./scenarios/add-event-listener.events.ts",
].map((mod) => import.meta.resolve(mod));
52 changes: 52 additions & 0 deletions tasks/bench/scenarios/add-event-listener.events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { call } from "../../../mod.ts";
import { scenario } from "./scenario.ts";

await scenario("add-event-listener.events", (depth) => call(() => run(depth)));

export async function run(depth: number): Promise<void> {
const target = new EventTarget();
const c = new AbortController();
const promised = recurse(target, c.signal, depth);
for (let i = 0; i < 100; i++) {
await Promise.resolve();
target.dispatchEvent(new Event("foo"));
}
await Promise.resolve();
c.abort();
await promised;
}

async function recurse(
target: EventTarget,
signal: AbortSignal,
depth: number,
): Promise<void> {
let abort: (() => void) | undefined;
let handler: (() => void) | undefined;
let resolve: (() => void) | undefined;
let promise: Promise<void> | undefined;
function finalize() {
abort && (signal.removeEventListener("abort", abort), abort = undefined);
handler &&
(target.removeEventListener("foo", handler), handler = undefined);
resolve && (resolve(), resolve = undefined);
}
if (depth > 0) {
const subTarget = new EventTarget();
const subPromise = recurse(subTarget, signal, depth - 1);
handler = function handler() {
subTarget.dispatchEvent(new Event("foo"));
};
target.addEventListener("foo", handler);
await subPromise;
} else {
promise = new Promise<void>((r) => resolve = r);
abort = finalize;
handler = function handler() {
//probeMemory("bottom");
};
target.addEventListener("foo", handler);
signal.addEventListener("abort", abort);
await promise;
}
}
14 changes: 14 additions & 0 deletions tasks/bench/scenarios/async+await.recursion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { call } from "../../../mod.ts";
import { scenario } from "./scenario.ts";

await scenario("async+await.recursion", (depth) => call(() => recurse(depth)));

async function recurse(depth: number): Promise<void> {
if (depth > 1) {
await recurse(depth - 1);
} else {
for (let i = 0; i < 100; i++) {
await Promise.resolve();
}
}
}
15 changes: 15 additions & 0 deletions tasks/bench/scenarios/co.recursion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { call } from "../../../mod.ts";
import { scenario } from "./scenario.ts";
import co from "npm:co";

await scenario("co.recursion", (depth) => call(() => co(recurse, depth)));

function* recurse(depth: number): Generator<unknown, void> {
if (depth > 1) {
yield recurse(depth - 1);
} else {
for (let i = 0; i < 100; i++) {
yield Promise.resolve();
}
}
}
31 changes: 31 additions & 0 deletions tasks/bench/scenarios/effection.events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { scenario } from "./scenario.ts";
import { each, on, Operation, sleep, spawn } from "../../../mod.ts";

await scenario("effection.events", start);

export function* start(depth: number): Operation<void> {
const target = new EventTarget();
const task = yield* spawn(() => recurse(target, depth));
for (let i = 0; i < 100; i++) {
yield* sleep(0);
target.dispatchEvent(new Event("foo"));
}
yield* sleep(0);
yield* task.halt();
}

function* recurse(target: EventTarget, depth: number): Operation<void> {
const eventStream = on(target, "foo");
if (depth > 1) {
const subTarget = new EventTarget();
yield* spawn(() => recurse(subTarget, depth - 1));
for (const _ of yield* each(eventStream)) {
subTarget.dispatchEvent(new Event("foo"));
yield* each.next();
}
} else {
for (const _ of yield* each(eventStream)) {
yield* each.next();
}
}
}
14 changes: 14 additions & 0 deletions tasks/bench/scenarios/effection.recursion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { call, Operation } from "../../../mod.ts";
import { scenario } from "./scenario.ts";

await scenario("effection.recursion", recurse);

function* recurse(depth: number): Operation<void> {
if (depth > 1) {
yield* recurse(depth - 1);
} else {
for (let i = 0; i < 100; i++) {
yield* call(() => Promise.resolve());
}
}
}
50 changes: 50 additions & 0 deletions tasks/bench/scenarios/rxjs.events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { fromEvent, Observable, Subject, takeUntil } from "npm:rxjs";
import { scenario } from "./scenario.ts";
import { action, Operation, sleep, spawn } from "../../../mod.ts";

await scenario("rxjs.events", run);

function* run(depth: number): Operation<void> {
const target = new EventTarget();
const abort = new Subject<void>();
const promised = yield* spawn(() =>
action<void>((resolve) => {
let observable = recurse(target, depth)
.pipe(takeUntil(abort))
.subscribe({
complete() {
resolve();
},
});
return () => observable.unsubscribe();
})
);
for (let i = 0; i < 100; i++) {
yield* sleep(0);
target.dispatchEvent(new Event("foo"));
}
yield* sleep(0);
abort.next();
yield* promised;
}

function recurse(target: EventTarget, depth: number): Observable<void> {
return new Observable<void>((subscriber) => {
const o = fromEvent(target, "foo");
if (depth > 1) {
const subTarget = new EventTarget();
subscriber.add(
o.subscribe(() => {
subTarget.dispatchEvent(new Event("foo"));
}),
);
subscriber.add(recurse(subTarget, depth - 1).subscribe());
} else {
subscriber.add(
o.subscribe(() => {
// probeMemory("bottom");
}),
);
}
});
}
Loading

0 comments on commit 0c1bdb6

Please sign in to comment.