173 lines · 5.8 KB
import { AsyncLocalStorage } from "node:async_hooks";

/**
 * Per-unit-of-work cost accounting for hotspot detection. Every request, queue batch, cron task and Durable
 * Object alarm runs inside `withPerf`, which counts the D1 statements and scoped-object (RepoState /
 * AccountState) calls it makes and how long they took, then reports one PostHog event:
 *
 * - `server_request`: `route` (a template, never names; lib/route-template.ts), `method`, `status`, `surface`.
 * - `server_job`: `job` (cron task, queue, alarm name).
 *
 * Both carry `duration_ms`, `d1_queries`, `d1_ms`, `d1_replica_queries`, `d1_rows_read`, `d1_rows_written`,
 * `do_calls`, `do_ms`, `do_rows_read`, `do_rows_written`, `cold_start` and `sample_weight` (rows are what D1 and
 * Durable Object SQL bill by). Slow or expensive units are always sent; the rest are sampled (PERF_SAMPLE_RATE, default 2%)
 * and carry `sample_weight` = 1 / rate so sums and counts in PostHog estimate the true totals. No person
 * profile, no account ids, no paths.
 */
export type PerfCounters = {
  d1Queries: number;
  d1Ms: number;
  d1ReplicaQueries: number;
  doCalls: number;
  doMs: number;
  doRowsRead: number;
  doRowsWritten: number;
  d1RowsRead: number;
  d1RowsWritten: number;
};

type PerfScope = PerfCounters & { startedAt: number; coldStart: boolean };

const storage = new AsyncLocalStorage<PerfScope>();

const SLOW_MS = 1000;
const HEAVY_D1 = 25;
const HEAVY_DO = 25;
const DEFAULT_SAMPLE_RATE = 0.02;

let coldStart = true;

type Reporter = (event: string, properties: Record<string, string | number | boolean | null>) => void;