Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: create bench dir #36

Merged
merged 4 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions bench/README.md
Original file line number Diff line number Diff line change
@@ -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
```
61 changes: 61 additions & 0 deletions bench/bench.ts
Original file line number Diff line number Diff line change
@@ -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<void>((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();
},
});
}
15 changes: 15 additions & 0 deletions bench/lophus.ts
Original file line number Diff line number Diff line change
@@ -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<NostrEvent<1>>) {
await sub.getReader().read();
}
19 changes: 19 additions & 0 deletions bench/nostr_tools.ts
Original file line number Diff line number Diff line change
@@ -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<void>((resolve) => {
received = resolve;
});
}
3 changes: 3 additions & 0 deletions bench/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface BenchContext {
WebSocket: typeof WebSocket;
}
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:[email protected]"
},
"tasks": {
"build": "mkdir -p ./dist && deno run -A ./bin/bundle.ts",
Expand Down
65 changes: 63 additions & 2 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 43 additions & 26 deletions lib/testing.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
type MessageEventData = string | ArrayBufferLike | Blob | ArrayBufferView;

export class MockWebSocket extends EventTarget implements WebSocket {
/**
* A list of all instances of MockWebSocket.
* An instance is removed from this list when it is closed.
*/
static get instances(): MockWebSocket[] {
return Array.from(this.#instances);
}
static #instances = new Set<MockWebSocket>();

static get first(): MockWebSocket | undefined {
return this.instances[0];
/** An AsyncGenerator that yields WebSocket instances. */
static async *instances() {
for (;;) {
if (this.#instances.length > 0) {
yield this.#instances.shift()!;
}
await new Promise<void>((resolve) => {
this.#pushed = resolve;
});
this.#pushed = undefined;
}
}
static readonly #instances: MockWebSocket[] = [];
static #pushed: (() => void) | undefined;

constructor(url?: string | URL, protocols?: string | string[]) {
constructor(
url?: string | URL,
protocols?: string | string[],
isRemote = false,
) {
super();
this.url = url?.toString() ?? "";
this.protocol = protocols ? [...protocols].flat()[0] : "";
MockWebSocket.#instances.add(this);
// Simulate async behavior of WebSocket as much as possible.
queueMicrotask(() => {
this.#readyState = 1;
this.dispatchEvent(new Event("open"));
const ev = new Event("open");
this.dispatchEvent(ev);
this.onopen?.(ev);
});
if (!isRemote) {
MockWebSocket.#instances.push(this);
MockWebSocket.#pushed?.();
}
}

binaryType: "blob" | "arraybuffer" = "blob";
Expand All @@ -44,7 +55,7 @@ export class MockWebSocket extends EventTarget implements WebSocket {

get remote(): MockWebSocket {
if (!this.#remote) {
this.#remote = new MockWebSocket(this.url);
this.#remote = new MockWebSocket(import.meta.url, undefined, true);
this.#remote.#remote = this;
}
return this.#remote;
Expand All @@ -53,30 +64,36 @@ export class MockWebSocket extends EventTarget implements WebSocket {

close(code?: number, reason?: string): void {
this.#readyState = 2;
if (this.#remote) {
this.#remote.#readyState = 2;
}
// Simulate async behavior of WebSocket as much as possible.
queueMicrotask(() => {
const ev = new CloseEvent("close", { code, reason });
if (this.#remote) {
this.#remote.#readyState = 3;
this.#remote.dispatchEvent(new CloseEvent("close", { code, reason }));
MockWebSocket.#instances.delete(this.#remote);
this.#remote.dispatchEvent(ev);
this.#remote.onclose?.(ev);
}
this.#readyState = 3;
this.dispatchEvent(new CloseEvent("close", { code, reason }));
MockWebSocket.#instances.delete(this);
this.dispatchEvent(ev);
this.onclose?.(ev);
});
}

send(data: MessageEventData): void {
// Simulate async behavior of WebSocket as much as possible.
queueMicrotask(() =>
this.#remote?.dispatchEvent(new MessageEvent("message", { data }))
);
queueMicrotask(() => {
const ev = new MessageEvent("message", { data });
this.#remote?.dispatchEvent(ev);
this.#remote?.onmessage?.(ev);
});
}

onclose = null;
onerror = null;
onmessage = null;
onopen = null;
onclose: WebSocket["onclose"] = null;
onerror: WebSocket["onerror"] = null;
onmessage: WebSocket["onmessage"] = null;
onopen: WebSocket["onopen"] = null;

addEventListener: WebSocket["addEventListener"] = super.addEventListener;
removeEventListener: WebSocket["removeEventListener"] = super
Expand Down
Loading