g1t/apps/web/app/lib/perf.ts

213 lines9,280 bytesCodeBlame
1/**
2 * The pure parts of request timing and D1 read consistency for the site:
3 * the `g1t_d1` cookie, which session each service call asks for, which
4 * calls may write, and the `Server-Timing` header. perf.server.ts holds
5 * the per-request state; docs/PERFORMANCE.md explains the whole.
6 */
7
8/** The cookie that carries D1 bookmarks between a person's requests. */
9export const D1_COOKIE = "g1t_d1";
10
11/**
12 * How long after a write the services a request did not get a bookmark
13 * from read their primary. A write can reach a service the site did not
14 * call itself (work writing to repos, say), whose bookmark the site never
15 * sees; D1 replicas trail the primary by well under a second, so 30
16 * seconds covers that with room to spare.
17 */
18export const PRIMARY_WINDOW_SECONDS = 30;
19
20/** How long the bookmarks are kept: far longer than any replica trails. */
21export const D1_COOKIE_MAX_AGE = 300;
22
23/**
24 * The services whose RPCs open a D1 session (crates/kit/src/d1.rs and
25 * @g1t/contracts d1.ts), by the name the site gives their binding.
26 */
27export const SESSION_SERVICES = new Set(["identity", "repos", "work", "search", "billing", "projects", "deployments"]);
28
29/** What the site remembers from a person's last writes. */
30export type Bookmarks = {
31 /** Unix seconds of the last request that may have written, if recent. */
32 at: number | null;
33 /** The latest bookmark each service returned after it. */
34 services: Record<string, string>;
35};
36
37const BOOKMARK = /^[0-9A-Za-z-]{1,256}$/;
38const SERVICE = /^[a-z]{1,32}$/;
39
40/** Reads the `g1t_d1` cookie; anything malformed is dropped. */
41export function readBookmarks(cookieHeader: string | null): Bookmarks {
42 const empty: Bookmarks = { at: null, services: {} };
43 if (!cookieHeader) return empty;
44 const match = new RegExp(`(?:^|;\\s*)${D1_COOKIE}=([^;]*)`).exec(cookieHeader);
45 if (!match) return empty;
46 const found: Bookmarks = { at: null, services: {} };
47 for (const entry of match[1].split("~")) {
48 const colon = entry.indexOf(":");
49 if (colon < 1) continue;
50 const key = entry.slice(0, colon);
51 const value = entry.slice(colon + 1);
52 if (key === "at") {
53 const at = Number(value);
54 if (Number.isSafeInteger(at) && at > 0) found.at = at;
55 } else if (SERVICE.test(key) && SESSION_SERVICES.has(key) && BOOKMARK.test(value)) {
56 found.services[key] = value;
57 }
58 }
59 return found;
60}
61
62/** The `g1t_d1` cookie's value. */
63export function writeBookmarks(bookmarks: Bookmarks): string {
64 const entries = bookmarks.at ? [`at:${bookmarks.at}`] : [];
65 for (const [service, bookmark] of Object.entries(bookmarks.services).sort()) {
66 if (SESSION_SERVICES.has(service) && BOOKMARK.test(bookmark)) entries.push(`${service}:${bookmark}`);
67 }
68 return entries.join("~");
69}
70
71/** The `Set-Cookie` value for `bookmarks`. */
72export function bookmarkCookie(bookmarks: Bookmarks, secure: boolean): string {
73 return `${D1_COOKIE}=${writeBookmarks(bookmarks)}; Path=/; HttpOnly;${secure ? " Secure;" : ""} SameSite=Lax; Max-Age=${D1_COOKIE_MAX_AGE}`;
74}
75
76/**
77 * What a call to `service` sends as `x-d1-bookmark`, or null for none
78 * (that service reads its primary, as before sessions):
79 *
80 * - A request that writes (any method but GET and HEAD) starts every
81 * session on the primary, so what it checks before writing is current.
82 * - Within `PRIMARY_WINDOW_SECONDS` of the person's last write, the
83 * primary, for every service: that write may have reached a service
84 * through another one, whose bookmark the site never saw.
85 * - Otherwise the bookmark that service returned after that write: never
86 * older than what they did, however far a replica trails.
87 * - Otherwise the nearest copy.
88 */
89export function sessionFor(service: string, bookmarks: Bookmarks, writing: boolean, nowSeconds: number): string | null {
90 if (!SESSION_SERVICES.has(service)) return null;
91 if (writing) return "first-primary";
92 if (bookmarks.at && nowSeconds - bookmarks.at < PRIMARY_WINDOW_SECONDS) return "first-primary";
93 return bookmarks.services[service] ?? "first-unconstrained";
94}
95
96/**
97 * Methods that only read. Anything not listed is taken to write, so a new
98 * method errs towards a cookie and a primary read, never a stale page.
99 * `get`, `list`, `queue` and `pulls_for_repos` were missing: every project
100 * page called `get` (repos, projects), so every one set the cookie, was
101 * never kept in the public cache, and sent the next 30 s of the person's
102 * reads to the primary.
103 */
104const READS = new Set(
105 (
106 "account active_agents all_ids get list queue pulls_for_repos blame blob branches by_author by_repo catalog check_invite check_limit " +
107 "check_workspace_deletion check_workspace_rename collaborator_permission compare counts deleted deliveries " +
108 "dependencies domains entitlements entity explore features git_access graph has_feature invoices ledger limit " +
109 "limit_requests links log logs managed_pulls memories_by_id memory_context my_repo_invitations " +
110 "outside_collaborators overview path_by_id prices profile profile_workspaces public_namespaces read_session " +
111 "readable ready_issues references registration repo_access resolve resolve_branch resolve_path resolve_slug " +
112 "routes run run_context run_cost runner_groups runner_settings runners runs scorecards search search_memories " +
113 "settings statement statement_entries status status_by_id suggest tree usage usage_meters user_by_username " +
114 "user_for_session usernames waiting_workspaces workflows workspace workspace_invites github_enabled"
115 ).split(" "),
116);
117
118/** Whether an RPC to `method` may write. */
119export function mayWrite(method: string): boolean {
120 if (READS.has(method)) return false;
121 return !(method.startsWith("get_") || method.startsWith("list_"));
122}
123
124/** The method name of an RPC URL (`https://service/rpc/<method>`). */
125export function rpcMethodOf(input: string): string {
126 const at = input.indexOf("/rpc/");
127 return at < 0 ? "" : input.slice(at + 5).split(/[?#]/)[0];
128}
129
130/** One service's calls during a request. */
131export type ServiceTiming = {
132 calls: number;
133 /** Summed wall time of its calls, from here. */
134 wallMs: number;
135 /** Summed time its own `server-timing: svc;dur` reported. */
136 serviceMs: number;
137 /**
138 * Of that, summed time it reported waiting on its database
139 * (`db;dur`, crates/kit/src/d1.rs `Timing`) and in how many round trips.
140 * Absent for services that do not time them.
141 */
142 dbMs?: number;
143 dbTrips?: number;
144};
145
146/** The `svc;dur=N` a service reports, or null. */
147export function serviceDuration(header: string | null): number | null {
148 if (!header) return null;
149 const match = /(?:^|,)\s*svc;dur=([0-9.]+)/.exec(header);
150 return match ? Number(match[1]) : null;
151}
152
153/**
154 * The `db;dur=N;desc="T round trips, …"` a service reports: its summed
155 * database time and round trips, or null.
156 */
157export function databaseTime(header: string | null): { ms: number; trips: number } | null {
158 if (!header) return null;
159 const match = /(?:^|,)\s*db;dur=([0-9.]+)(?:;desc="(\d+) round trips?)?/.exec(header);
160 return match ? { ms: Number(match[1]), trips: Number(match[2] ?? 0) } : null;
161}
162
163/** Total time covered by overlapping [start, end] intervals. */
164export function coveredMs(intervals: [number, number][]): number {
165 const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
166 let total = 0;
167 let end = -Infinity;
168 let start = -Infinity;
169 for (const [from, to] of sorted) {
170 if (from > end) {
171 if (end > start) total += end - start;
172 start = from;
173 end = to;
174 } else if (to > end) {
175 end = to;
176 }
177 }
178 if (end > start) total += end - start;
179 return total;
180}
181
182/** A Server-Timing metric name: a token, so `/` and spaces become `.`. */
183export function metricName(name: string): string {
184 return name.replace(/^routes\//, "").replace(/[^A-Za-z0-9_.-]+/g, ".");
185}
186
187/**
188 * The `Server-Timing` header for a request: the whole, the loaders, the
189 * time spent waiting on services (overlap counted once), then each
190 * service. DevTools shows them in this order under Network → Timing.
191 */
192export function serverTiming(input: {
193 totalMs: number;
194 loaders: { id: string; ms: number; kind: "loader" | "action" }[];
195 rpcMs: number;
196 services: Record<string, ServiceTiming>;
197 sessions: string;
198}): string {
199 const parts = [`total;dur=${input.totalMs};desc="web to first byte"`];
200 for (const loader of input.loaders) {
201 parts.push(`${loader.kind}.${metricName(loader.id)};dur=${loader.ms}`);
202 }
203 const calls = Object.values(input.services).reduce((sum, timing) => sum + timing.calls, 0);
204 if (calls > 0) parts.push(`rpc;dur=${input.rpcMs};desc="${calls} service calls, overlap counted once"`);
205 const ranked = Object.entries(input.services).sort((a, b) => b[1].wallMs - a[1].wallMs);
206 for (const [name, timing] of ranked) {
207 const db = timing.dbTrips ? `, db ${timing.dbMs ?? 0}ms in ${timing.dbTrips} round trip${timing.dbTrips === 1 ? "" : "s"}` : "";
208 const inside = timing.serviceMs > 0 ? `, ${timing.serviceMs}ms inside${db}` : "";
209 parts.push(`${metricName(name)};dur=${timing.wallMs};desc="${timing.calls} call${timing.calls === 1 ? "" : "s"}${inside}"`);
210 }
211 if (input.sessions) parts.push(`d1;desc="${input.sessions}"`);
212 return parts.join(", ");
213}