diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..3588a6f --- /dev/null +++ b/bench/README.md @@ -0,0 +1,37 @@ +## Benchmarks + +We are still trying to find a fair way to compare the performance of various +Nostr libraries. You should NOT consider the current results reliable. + +### Method + +WIP + +### Results + +```sh +cpu: Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz +runtime: deno 1.41.3 (x86_64-unknown-linux-gnu) + +file:///home/hasundue/lophus/bench/bench.ts +benchmark time (avg) iter/s (min … max) p75 p99 p995 +----------------------------------------------------------------- ----------------------------- + +# Time to deliver a request to a relay +group subscribe +lophus 52.32 µs/iter 19,111.7 (18.74 µs … 6.17 ms) 52.78 µs 132.19 µs 165.39 µs +nostr_tools 49.33 µs/iter 20,270.0 (38.33 µs … 182.21 µs) 49.32 µs 111.07 µs 133.75 µs + +summary + lophus + 1.06x slower than nostr_tools + +# Time to get an event from a relay +group get an event +lophus 56.3 µs/iter 17,761.7 (27.8 µs … 8.69 ms) 44.75 µs 119.7 µs 166.07 µs +nostr_tools 2.44 ms/iter 409.6 (2.11 ms … 4.93 ms) 2.34 ms 4.4 ms 4.93 ms + +summary + lophus + 43.36x faster than nostr_tools +``` diff --git a/bench/bench.ts b/bench/bench.ts new file mode 100644 index 0000000..d07c733 --- /dev/null +++ b/bench/bench.ts @@ -0,0 +1,61 @@ +import { MockWebSocket } from "@lophus/lib/testing"; +import { TextNoteComposer } from "@lophus/std/notes"; +import { generatePrivateKey, Signer } from "@lophus/std/signs"; + +const LIBS = ["lophus", "nostr_tools"] as const; + +const nsec = generatePrivateKey(); +const note = new TextNoteComposer().compose({ + content: "bench", +}); +const event = new Signer(nsec).sign(note); + +for (const lib of LIBS) { + Deno.bench({ + name: lib, + baseline: lib === "lophus", + group: "subscribe", + async fn({ start, end }) { + const { setup, subscribe } = await import(`./${lib}.ts`); + const done = (async () => { + const { value: ws } = await MockWebSocket.instances().next(); + return new Promise((resolve) => + ws!.remote.addEventListener("message", resolve) + ); + })(); + setup({ WebSocket: MockWebSocket }); + start(); + subscribe(); + await done; + end(); + }, + }); + + Deno.bench({ + name: lib, + baseline: lib === "lophus", + group: "get an event", + async fn({ start, end }) { + const { setup, subscribe, receive } = await import(`./${lib}.ts`); + setup({ WebSocket: MockWebSocket }); + const sent = (async () => { + const { value: ws } = await MockWebSocket.instances().next(); + return new Promise((resolve) => { + ws!.remote.addEventListener("message", (msg) => { + const [, id] = JSON.parse(msg.data); + ws!.remote.send( + JSON.stringify(["EVENT", id, event]), + ); + resolve(); + }); + }); + })(); + start(); + const sub = await subscribe(); + const received = receive(sub); + await sent; + await received; + end(); + }, + }); +} diff --git a/bench/lophus.ts b/bench/lophus.ts new file mode 100644 index 0000000..1a0f1fb --- /dev/null +++ b/bench/lophus.ts @@ -0,0 +1,15 @@ +import { NostrEvent, Relay } from "@lophus/nips"; +import { BenchContext } from "./types.ts"; + +export function setup(c: BenchContext) { + globalThis.WebSocket = c.WebSocket; +} + +export function subscribe() { + const relay = new Relay("ws://localhost:80"); + return relay.subscribe({ kinds: [1] }, { id: "bench" }); +} + +export async function receive(sub: ReadableStream>) { + await sub.getReader().read(); +} diff --git a/bench/nostr_tools.ts b/bench/nostr_tools.ts new file mode 100644 index 0000000..cd9d4ef --- /dev/null +++ b/bench/nostr_tools.ts @@ -0,0 +1,19 @@ +import { Relay, useWebSocketImplementation } from "nostr-tools/relay"; +import { BenchContext } from "./types.ts"; + +let received: (() => void) | undefined; + +export function setup(c: BenchContext) { + useWebSocketImplementation(c.WebSocket); +} + +export async function subscribe() { + const relay = await Relay.connect("ws://localhost:80"); + return relay.subscribe([{ kinds: [1] }], { onevent: () => received?.() }); +} + +export async function receive() { + await new Promise((resolve) => { + received = resolve; + }); +} diff --git a/bench/types.ts b/bench/types.ts new file mode 100644 index 0000000..51e0aea --- /dev/null +++ b/bench/types.ts @@ -0,0 +1,3 @@ +export interface BenchContext { + WebSocket: typeof WebSocket; +} diff --git a/deno.json b/deno.json index 523d2e8..4023b43 100644 --- a/deno.json +++ b/deno.json @@ -31,7 +31,8 @@ "@lophus/std/watch": "./std/watch.ts", "@std/assert": "jsr:@std/assert@^0.219.1", "@std/streams": "jsr:@std/streams@^0.219.1", - "@std/testing": "jsr:@std/testing@^0.219.1" + "@std/testing": "jsr:@std/testing@^0.219.1", + "nostr-tools": "npm:nostr-tools@2.3.2" }, "tasks": { "build": "mkdir -p ./dist && deno run -A ./bin/bundle.ts", diff --git a/deno.lock b/deno.lock index fc1b47e..99a26fd 100644 --- a/deno.lock +++ b/deno.lock @@ -9,7 +9,8 @@ "jsr:@std/streams@^0.219.1": "jsr:@std/streams@0.219.1", "jsr:@std/testing@^0.219.1": "jsr:@std/testing@0.219.1", "npm:@noble/curves@^1.3.0": "npm:@noble/curves@1.3.0", - "npm:@noble/hashes@^1.3.3": "npm:@noble/hashes@1.3.3" + "npm:@noble/hashes@^1.3.3": "npm:@noble/hashes@1.3.3", + "npm:nostr-tools@2.3.2": "npm:nostr-tools@2.3.2" }, "jsr": { "@std/assert@0.219.1": { @@ -40,15 +41,74 @@ } }, "npm": { + "@noble/ciphers@0.5.1": { + "integrity": "sha512-aNE06lbe36ifvMbbWvmmF/8jx6EQPu2HVg70V95T+iGjOuYwPpAccwAQc2HlXO2D0aiQ3zavbMga4jjWnrpiPA==", + "dependencies": {} + }, + "@noble/curves@1.1.0": { + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.3.1" + } + }, + "@noble/curves@1.2.0": { + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.3.2" + } + }, "@noble/curves@1.3.0": { "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", "dependencies": { "@noble/hashes": "@noble/hashes@1.3.3" } }, + "@noble/hashes@1.3.1": { + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "dependencies": {} + }, + "@noble/hashes@1.3.2": { + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dependencies": {} + }, "@noble/hashes@1.3.3": { "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "dependencies": {} + }, + "@scure/base@1.1.1": { + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "dependencies": {} + }, + "@scure/bip32@1.3.1": { + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "dependencies": { + "@noble/curves": "@noble/curves@1.1.0", + "@noble/hashes": "@noble/hashes@1.3.3", + "@scure/base": "@scure/base@1.1.1" + } + }, + "@scure/bip39@1.2.1": { + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.3.3", + "@scure/base": "@scure/base@1.1.1" + } + }, + "nostr-tools@2.3.2": { + "integrity": "sha512-8ceZ2ItkAGjR5b9+QOkkV9KWBOK0WPlpFrPPXmbWnNMcnlj9zB7rjdYPK2sV/OK4Ty9J3xL6+bvYKY77gup5EQ==", + "dependencies": { + "@noble/ciphers": "@noble/ciphers@0.5.1", + "@noble/curves": "@noble/curves@1.2.0", + "@noble/hashes": "@noble/hashes@1.3.1", + "@scure/base": "@scure/base@1.1.1", + "@scure/bip32": "@scure/bip32@1.3.1", + "@scure/bip39": "@scure/bip39@1.2.1", + "nostr-wasm": "nostr-wasm@0.1.0" + } + }, + "nostr-wasm@0.1.0": { + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "dependencies": {} } } }, @@ -117,7 +177,8 @@ "jsr:@std/streams@^0.219.1", "jsr:@std/testing@^0.219.1", "npm:@noble/curves@^1.3.0", - "npm:@noble/hashes@^1.3.3" + "npm:@noble/hashes@^1.3.3", + "npm:nostr-tools@2.3.2" ], "members": { "@lophus/core": {