diff --git a/tasks/bench.ts b/tasks/bench.ts index 4bc1cb689..8c743d172 100644 --- a/tasks/bench.ts +++ b/tasks/bench.ts @@ -25,9 +25,7 @@ import { Cell, Row, Table } from "jsr:@cliffy/table@1.0.0-rc.7"; await main(function* (args) { let options = parser() .name("bench") - .description( - "Run Effection benchmarks", - ) + .description("Run Effection benchmarks") .version("0.0.0") .options({ include: { @@ -58,7 +56,7 @@ await main(function* (args) { for (let scenario of filter(scenarios, { include, exclude })) { tasks.push( yield* spawn(() => - runBenchmark(scenario, { ...options, type: "benchmark" }) + runBenchmark(scenario, { ...options, type: "benchmark" }), ), ); } @@ -66,6 +64,7 @@ await main(function* (args) { let results = yield* all(tasks); let events = results.filter((result) => result.name.match("events")); + let startups = results.filter((result) => result.name.match("startup")); let recursion = results.filter((result) => result.name.match("recursion")); if (events.length == 0 && recursion.length === 0) { @@ -73,20 +72,29 @@ await main(function* (args) { return; } - let rows = []; + let rows: Row[] = []; + + let cells = ["Library", "Avg (ms)", "Avg Startup Time (ms)"]; + let cols = cells.length; if (recursion.length > 0) { - rows.push(Row.from([new Cell("Basic Recursion").colSpan(2).border()])); - rows.push(Row.from(["Library", "Avg (ms)"]).border()); + rows.push(Row.from([new Cell("Basic Recursion").colSpan(cols).border()])); + rows.push(Row.from(cells).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(["Library", "Avg (ms)"]).border()); + rows.push(Row.from([new Cell("Recursive Events").colSpan(cols).border()])); + rows.push(Row.from(cells).border()); rows.push(...events.map((event) => Row.from(toTableRow(event)))); } + if (startups.length > 0) { + rows.push(Row.from([new Cell("Start-up Time").colSpan(cols).border()])); + rows.push(Row.from(cells).border()); + rows.push(...startups.map((event) => Row.from(toTableRow(event)))); + } + Table.from(rows).render(); }); @@ -150,8 +158,8 @@ function filter( 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)]; + let { avgTime, avgStartupTime } = event.result.value; + return [name, String(avgTime), String(avgStartupTime)]; } else { return [name, "❌"]; } diff --git a/tasks/bench/scenarios.ts b/tasks/bench/scenarios.ts index 799c9e09e..fd57bb7af 100644 --- a/tasks/bench/scenarios.ts +++ b/tasks/bench/scenarios.ts @@ -8,4 +8,6 @@ export default [ "./scenarios/rxjs.events.ts", "./scenarios/add-event-listener.events.ts", "./scenarios/effect.events.ts", + "./scenarios/effection.startup.ts", + "./scenarios/effect.startup.ts", ].map((mod) => import.meta.resolve(mod)); diff --git a/tasks/bench/scenarios/effect.recursion.ts b/tasks/bench/scenarios/effect.recursion.ts index 0acff9ee2..4b38a6f8c 100644 --- a/tasks/bench/scenarios/effect.recursion.ts +++ b/tasks/bench/scenarios/effect.recursion.ts @@ -2,9 +2,8 @@ import { Effect } from "npm:effect"; import { call } from "../../../mod.ts"; import { scenario } from "./scenario.ts"; -await scenario( - "effect.recursion", - (depth) => call(() => Effect.runPromise(recurse(depth))), +await scenario("effect.recursion", (depth) => + call(() => Effect.runPromise(recurse(depth))) ); function recurse(depth: number): Effect.Effect { diff --git a/tasks/bench/scenarios/effect.startup.ts b/tasks/bench/scenarios/effect.startup.ts new file mode 100644 index 000000000..99d154395 --- /dev/null +++ b/tasks/bench/scenarios/effect.startup.ts @@ -0,0 +1,20 @@ +import { Effect, Fiber } from "npm:effect"; +import { call, ensure } from "../../../mod.ts"; +import { scenario } from "./scenario.ts"; + +await scenario("effect.startup", function* (_, exit) { + let start = performance.now(); + + const startup = Effect.gen(function* () { + exit(performance.now() - start); + yield* Effect.promise(() => Promise.resolve()); + }); + + const fiber = Effect.runFork(startup); + + yield* ensure(function* () { + yield* call(() => Effect.runPromise(Fiber.interrupt(fiber))); + }); + + return yield* call(() => Effect.runPromise(Fiber.join(fiber))); +}); diff --git a/tasks/bench/scenarios/effection.startup.ts b/tasks/bench/scenarios/effection.startup.ts new file mode 100644 index 000000000..eb7e0d913 --- /dev/null +++ b/tasks/bench/scenarios/effection.startup.ts @@ -0,0 +1,13 @@ +import { call, type Operation } from "../../../mod.ts"; +import { scenario } from "./scenario.ts"; + +await scenario("effection.startup", function* (_, exit) { + let start = performance.now(); + + function* startup(): Operation { + exit(performance.now() - start); + yield* call(() => Promise.resolve()); + } + + return yield* startup(); +}); diff --git a/tasks/bench/scenarios/scenario.ts b/tasks/bench/scenarios/scenario.ts index ebfff2644..7e7cad349 100644 --- a/tasks/bench/scenarios/scenario.ts +++ b/tasks/bench/scenarios/scenario.ts @@ -2,11 +2,14 @@ import { callcc } from "../../../lib/callcc.ts"; import { Err, Ok } from "../../../lib/result.ts"; import { encapsulate } from "../../../lib/task.ts"; import { + call, createChannel, each, main, type Operation, + race, spawn, + withResolvers, } from "../../../mod.ts"; import type { BenchmarkOptions, @@ -21,7 +24,7 @@ const send = (event: BenchmarkWorkerEvent) => self.postMessage(event); export function scenario( name: string, - perform: (depth: number) => Operation, + perform: (depth: number, exit: (time: number) => void) => Operation, ) { return main(function* () { try { @@ -41,25 +44,52 @@ export function scenario( for (let options of yield* each(work)) { let times: number[] = []; + let entryTimes: number[] = []; + for (let i = 0; i < options.repeat; i++) { let start = performance.now(); - yield* encapsulate(() => perform(options.depth)); + const exit = withResolvers(); + + const task = yield* spawn(function* () { + yield* encapsulate(() => perform(options.depth, exit.resolve)); + }); + + // Avoid blocking indefintely because not all benchmarks need the exit function + yield* race([ + exit.operation, + call(function* () { + yield* task; + exit.resolve(null); + }), + ]); + + const entryTime = yield* exit.operation; let time = performance.now() - start; send({ type: "repeat", name, time, rep: i + 1 }); times.push(time); + + if (entryTime) { + entryTimes.push(entryTime); + } } let total = times.reduce((sum, time) => sum + time, 0); let avgTime = total / times.length; - let result = Ok({ avgTime, reps: options.repeat }); + + let startupTime = entryTimes.reduce((sum, time) => sum + time, 0); + + let avgStartupTime = + entryTimes.length > 0 ? startupTime / entryTimes.length : 0; + + let result = Ok({ avgTime, avgStartupTime, reps: options.repeat }); send({ type: "done", name, result }); yield* each.next(); } - }) + }), ); } catch (error) { send({ type: "done", name, result: Err(error as Error) }); diff --git a/tasks/bench/types.ts b/tasks/bench/types.ts index 3bb530175..b0038beea 100644 --- a/tasks/bench/types.ts +++ b/tasks/bench/types.ts @@ -31,5 +31,6 @@ export interface BenchmarkDoneEvent { result: Result<{ reps: number; avgTime: number; + avgStartupTime: number; }>; }