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

191 lines8,211 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 */
100const READS = new Set(
101 (
102 "account active_agents all_ids blame blob branches by_author by_repo catalog check_invite check_limit " +
103 "check_workspace_deletion check_workspace_rename collaborator_permission compare counts deleted deliveries " +
104 "dependencies domains entitlements entity explore features git_access graph has_feature invoices ledger limit " +
105 "limit_requests links log logs managed_pulls memories_by_id memory_context my_repo_invitations " +
106 "outside_collaborators overview path_by_id prices profile profile_workspaces public_namespaces read_session " +
107 "readable ready_issues references registration repo_access resolve resolve_branch resolve_path resolve_slug " +
108 "routes run run_context run_cost runner_groups runner_settings runners runs scorecards search search_memories " +
109 "settings statement statement_entries status status_by_id suggest tree usage usage_meters user_by_username " +
110 "user_for_session usernames waiting_workspaces workflows workspace workspace_invites github_enabled"
111 ).split(" "),
112);
113
114/** Whether an RPC to `method` may write. */
115export function mayWrite(method: string): boolean {
116 if (READS.has(method)) return false;
117 return !(method.startsWith("get_") || method.startsWith("list_"));
118}
119
120/** The method name of an RPC URL (`https://service/rpc/<method>`). */
121export function rpcMethodOf(input: string): string {
122 const at = input.indexOf("/rpc/");
123 return at < 0 ? "" : input.slice(at + 5).split(/[?#]/)[0];
124}
125
126/** One service's calls during a request. */
127export type ServiceTiming = {
128 calls: number;
129 /** Summed wall time of its calls, from here. */
130 wallMs: number;
131 /** Summed time its own `server-timing: svc;dur` reported. */
132 serviceMs: number;
133};
134
135/** The `svc;dur=N` a service reports, or null. */
136export function serviceDuration(header: string | null): number | null {
137 if (!header) return null;
138 const match = /(?:^|,)\s*svc;dur=([0-9.]+)/.exec(header);
139 return match ? Number(match[1]) : null;
140}
141
142/** Total time covered by overlapping [start, end] intervals. */
143export function coveredMs(intervals: [number, number][]): number {
144 const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
145 let total = 0;
146 let end = -Infinity;
147 let start = -Infinity;
148 for (const [from, to] of sorted) {
149 if (from > end) {
150 if (end > start) total += end - start;
151 start = from;
152 end = to;
153 } else if (to > end) {
154 end = to;
155 }
156 }
157 if (end > start) total += end - start;
158 return total;
159}
160
161/** A Server-Timing metric name: a token, so `/` and spaces become `.`. */
162export function metricName(name: string): string {
163 return name.replace(/^routes\//, "").replace(/[^A-Za-z0-9_.-]+/g, ".");
164}
165
166/**
167 * The `Server-Timing` header for a request: the whole, the loaders, the
168 * time spent waiting on services (overlap counted once), then each
169 * service. DevTools shows them in this order under Network → Timing.
170 */
171export function serverTiming(input: {
172 totalMs: number;
173 loaders: { id: string; ms: number; kind: "loader" | "action" }[];
174 rpcMs: number;
175 services: Record<string, ServiceTiming>;
176 sessions: string;
177}): string {
178 const parts = [`total;dur=${input.totalMs};desc="web to first byte"`];
179 for (const loader of input.loaders) {
180 parts.push(`${loader.kind}.${metricName(loader.id)};dur=${loader.ms}`);
181 }
182 const calls = Object.values(input.services).reduce((sum, timing) => sum + timing.calls, 0);
183 if (calls > 0) parts.push(`rpc;dur=${input.rpcMs};desc="${calls} service calls, overlap counted once"`);
184 const ranked = Object.entries(input.services).sort((a, b) => b[1].wallMs - a[1].wallMs);
185 for (const [name, timing] of ranked) {
186 const inside = timing.serviceMs > 0 ? `, ${timing.serviceMs}ms inside` : "";
187 parts.push(`${metricName(name)};dur=${timing.wallMs};desc="${timing.calls} call${timing.calls === 1 ? "" : "s"}${inside}"`);
188 }
189 if (input.sessions) parts.push(`d1;desc="${input.sessions}"`);
190 return parts.join(", ");
191}