Repository navigation
perf(net): sample send-rate estimates from one page-wide ticker #8021
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
Changes from all commits
3cf2908
7124f3e
b570223
9848fce
fc876b1
822354c
baa951c
e1492b8
2f6510e
c25bcf6
f5fa29c
6241b40
86ee8ee
1cc6727
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { expect, test } from "bun:test"; | ||
| import { Manual, Milli, Nano } from "@moq/clock"; | ||
| import { tick } from "./ticker.ts"; | ||
|
|
||
| test("subscribers share one tick and the loop stops with the last", async () => { | ||
| const clock = new Manual(); | ||
| const installed = clock.install(); | ||
| try { | ||
| const period = Nano.fromMilli(Milli(100)); | ||
| const ticks: string[] = []; | ||
| const a = tick(period, () => ticks.push("a")); | ||
| await clock.advance(Nano.fromMilli(Milli(50))); | ||
| // Joins the running loop rather than starting its own out of phase. | ||
| const b = tick(period, () => ticks.push("b")); | ||
| await clock.advance(Nano.fromMilli(Milli(50))); | ||
| expect(ticks).toEqual(["a", "b"]); | ||
|
|
||
| a(); | ||
| await clock.advance(period); | ||
| expect(ticks).toEqual(["a", "b", "b"]); | ||
|
|
||
| b(); | ||
| b(); | ||
| await clock.advance(Nano.fromMilli(Milli(500))); | ||
| expect(ticks).toEqual(["a", "b", "b"]); | ||
|
|
||
| // A stopped loop restarts for a later subscriber, rather than leaving the page unsampled. | ||
| tick(period, () => ticks.push("c")); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The restart subscriber's dispose is dropped, so the module-level 100 ms ticker keeps one subscriber after this test returns. |
||
| await clock.advance(period); | ||
| expect(ticks).toEqual(["a", "b", "b", "c"]); | ||
| } finally { | ||
| installed.close(); | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { Instant, type Nano, now, sleepUntil } from "@moq/clock"; | ||
|
|
||
| /** | ||
| * One page-wide ticker shared by every caller with the same period. | ||
| * | ||
| * Each subscriber's callback runs once per tick, all in the same task, so N | ||
| * connections sampling `getStats()` cost one timer wakeup per period instead of N | ||
| * loops ticking out of phase. The loop runs only while a subscriber is registered. | ||
| */ | ||
| class Ticker { | ||
| readonly #period: Nano; | ||
| readonly #subscribers = new Set<() => void>(); | ||
| #abort?: AbortController; | ||
|
|
||
| constructor(period: Nano) { | ||
| this.#period = period; | ||
| } | ||
|
|
||
| /** Runs `fn` on every tick until the returned dispose is called. */ | ||
| subscribe(fn: () => void): () => void { | ||
| this.#subscribers.add(fn); | ||
| if (!this.#abort) this.#start(); | ||
| return () => { | ||
| if (!this.#subscribers.delete(fn)) return; | ||
| if (this.#subscribers.size === 0) { | ||
| this.#abort?.abort(); | ||
| this.#abort = undefined; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| #start() { | ||
| const abort = new AbortController(); | ||
| this.#abort = abort; | ||
| void (async () => { | ||
| let next = Instant.add(now(), this.#period); | ||
| for (;;) { | ||
| await sleepUntil(next, { signal: abort.signal }).catch(() => {}); | ||
| if (abort.signal.aborted) return; | ||
| // Snapshot so a callback that unsubscribes doesn't skip a neighbour. | ||
| for (const fn of [...this.#subscribers]) { | ||
| try { | ||
| fn(); | ||
| } catch (err) { | ||
| console.error("ticker callback error", err); | ||
| } | ||
| } | ||
| next = Instant.add(now(), this.#period); | ||
| } | ||
| })(); | ||
| } | ||
| } | ||
|
|
||
| const tickers = new Map<Nano, Ticker>(); | ||
|
|
||
| /** Subscribes `fn` to the page-wide ticker for `period`; returns its dispose. */ | ||
| export function tick(period: Nano, fn: () => void): () => void { | ||
| if (period <= 0) throw new RangeError("ticker period must be positive"); | ||
| let ticker = tickers.get(period); | ||
|
kixelated marked this conversation as resolved.
Resolved
|
||
| if (!ticker) { | ||
| ticker = new Ticker(period); | ||
| tickers.set(period, ticker); | ||
| } | ||
| return ticker.subscribe(fn); | ||
| } | ||
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.