mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
The event-loop monitor starts first in register(), so loading the database client and the worker graph is inside its view, and a stall line now carries the process uptime, the startup phase, the boot step running at the time and the oldest work in flight. Each boot step is timed, and the boot ends with one boot.steps line. The five registrars and the lifecycle helpers are imported one at a time after boss.start(), with a turn of the event loop in between, instead of all at once when the worker module loads. The longest stall is then the largest registrar graph rather than the sum of all five.
105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
/**
|
|
* Named boot steps with their timings, and the step running right now.
|
|
*
|
|
* A stall at boot used to be one number: "the loop was blocked 718 ms". The
|
|
* event-loop monitor now names the step that was running when the late tick
|
|
* fired (`currentBootStep`), and the boot writes one `boot.steps` line with
|
|
* how long each step took, so a slow boot says where its time went.
|
|
*
|
|
* Kept on `globalThis`: the steps are marked from the instrumentation bundle
|
|
* and from the worker modules it loads, which are separate module copies in
|
|
* a production build.
|
|
*/
|
|
import { withBackgroundEvent } from "@/lib/logging/background";
|
|
import { annotate } from "@/lib/logging/context";
|
|
|
|
export interface BootStep {
|
|
name: string;
|
|
/** Milliseconds since the process started (`performance.timeOrigin`). */
|
|
startedAtMs: number;
|
|
durationMs: number | null;
|
|
}
|
|
|
|
const KEY = Symbol.for("healthlog.boot.steps");
|
|
|
|
interface State {
|
|
steps: BootStep[];
|
|
current: BootStep | null;
|
|
}
|
|
|
|
function state(): State {
|
|
const slot = globalThis as Record<symbol, unknown>;
|
|
let current = slot[KEY] as State | undefined;
|
|
if (!current) {
|
|
current = { steps: [], current: null };
|
|
slot[KEY] = current;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Begin `name`, ending the step before it. Steps run one after another; a
|
|
* step that starts work in the background ends when the next one begins.
|
|
*/
|
|
export function beginBootStep(
|
|
name: string,
|
|
now: number = performance.now(),
|
|
): void {
|
|
const s = state();
|
|
if (s.current && s.current.durationMs === null) {
|
|
s.current.durationMs = Math.round(now - s.current.startedAtMs);
|
|
}
|
|
const step: BootStep = {
|
|
name,
|
|
startedAtMs: Math.round(now),
|
|
durationMs: null,
|
|
};
|
|
s.steps.push(step);
|
|
s.current = step;
|
|
}
|
|
|
|
/** End the running step without starting another. */
|
|
export function endBootSteps(now: number = performance.now()): BootStep[] {
|
|
const s = state();
|
|
if (s.current && s.current.durationMs === null) {
|
|
s.current.durationMs = Math.round(now - s.current.startedAtMs);
|
|
}
|
|
s.current = null;
|
|
return [...s.steps];
|
|
}
|
|
|
|
/** The step running now, or null once the boot is through. */
|
|
export function currentBootStep(): string | null {
|
|
return state().current?.name ?? null;
|
|
}
|
|
|
|
/** Every step so far, for the boot line. */
|
|
export function bootSteps(): BootStep[] {
|
|
return [...state().steps];
|
|
}
|
|
|
|
/**
|
|
* End the boot and write one `boot.steps` line with every step and its
|
|
* duration, so a slow start says where its time went. Never throws.
|
|
*/
|
|
export async function reportBootSteps(
|
|
meta: Record<string, string | number> = {},
|
|
): Promise<void> {
|
|
const steps = endBootSteps();
|
|
try {
|
|
await withBackgroundEvent("boot.steps", async () => {
|
|
annotate({
|
|
meta: {
|
|
...meta,
|
|
boot_steps: steps
|
|
.map((s) => `${s.name}=${s.durationMs ?? 0}`)
|
|
.join(","),
|
|
boot_total_ms: Math.round(performance.now()),
|
|
},
|
|
});
|
|
});
|
|
} catch {
|
|
// A diagnostic must never break boot.
|
|
}
|
|
}
|