flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

147 lines5,238 bytesCodeBlame
1import { AsyncLocalStorage } from "node:async_hooks";
2
3import type { ServiceBinding } from "@g1t/contracts";
4
5import {
6 type Bookmarks,
7 type ServiceTiming,
8 PRIMARY_WINDOW_SECONDS,
9 bookmarkCookie,
10 coveredMs,
11 mayWrite,
12 readBookmarks,
13 rpcMethodOf,
14 serverTiming,
15 serviceDuration,
16 sessionFor,
17 SESSION_SERVICES,
18} from "./perf";
19
20/**
21 * What one request to the site did: when it started, each service call
22 * and how long it took, each loader, and the D1 bookmarks it read with and
23 * got back. Kept per request with AsyncLocalStorage, so the service
24 * clients, which are shared by every request in the isolate, can record
25 * into the right one.
26 */
27type RequestPerf = {
28 started: number;
29 /** Not GET or HEAD: an action, a form post. */
30 writing: boolean;
31 /** A service call that may have written (lib/perf.ts `mayWrite`). */
32 wrote: boolean;
33 bookmarks: Bookmarks;
34 returned: Record<string, string>;
35 intervals: [number, number][];
36 services: Record<string, ServiceTiming>;
37 loaders: { id: string; ms: number; kind: "loader" | "action" }[];
38 /** How each session-capable service was asked to read, for the header. */
39 sessions: Map<string, string>;
40};
41
42const scope = new AsyncLocalStorage<RequestPerf>();
43
44/** Runs `handle` with a fresh record for `request`. */
45export function withRequestPerf<T>(request: Request, handle: () => Promise<T>): Promise<T> {
46 const writing = request.method !== "GET" && request.method !== "HEAD";
47 return scope.run(
48 {
49 started: Date.now(),
50 writing,
51 wrote: false,
52 bookmarks: readBookmarks(request.headers.get("cookie")),
53 returned: {},
54 intervals: [],
55 services: {},
56 loaders: [],
57 sessions: new Map(),
58 },
59 handle,
60 );
61}
62
63/**
64 * `binding` with its calls timed, and, for a service that reads D1 with
65 * sessions, the `x-d1-bookmark` each call should carry (lib/perf.ts
66 * `sessionFor`). Only `fetch` is wrapped: the clients use nothing else.
67 */
68export function instrumented(name: string, binding: ServiceBinding): ServiceBinding {
69 return {
70 async fetch(input: string, init?: RequestInit) {
71 const perf = scope.getStore();
72 if (!perf) return binding.fetch(input, init);
73 const session = sessionFor(name, perf.bookmarks, perf.writing, Math.floor(Date.now() / 1000));
74 let sent = init;
75 if (session) {
76 const headers = new Headers(init?.headers);
77 headers.set("x-d1-bookmark", session);
78 sent = { ...init, headers };
79 perf.sessions.set(name, session.startsWith("first-") ? session.slice(6) : "bookmark");
80 }
81 if (SESSION_SERVICES.has(name) && mayWrite(rpcMethodOf(input))) perf.wrote = true;
82 const from = Date.now();
83 const response = await binding.fetch(input, sent);
84 const to = Date.now();
85 perf.intervals.push([from, to]);
86 const timing = (perf.services[name] ??= { calls: 0, wallMs: 0, serviceMs: 0 });
87 timing.calls += 1;
88 timing.wallMs += to - from;
89 timing.serviceMs += serviceDuration(response.headers.get("server-timing")) ?? 0;
90 const bookmark = response.headers.get("x-d1-bookmark");
91 if (bookmark && SESSION_SERVICES.has(name)) perf.returned[name] = bookmark;
92 return response;
93 },
94 };
95}
96
97/**
98 * Whether this request must read current data: it writes, or the person
99 * wrote moments ago (lib/perf.ts `PRIMARY_WINDOW_SECONDS`). Caches step
100 * aside then (lib/cache.server.ts).
101 */
102export function mustReadFresh(): boolean {
103 const perf = scope.getStore();
104 if (!perf) return true;
105 if (perf.writing) return true;
106 const at = perf.bookmarks.at;
107 return at != null && Math.floor(Date.now() / 1000) - at < PRIMARY_WINDOW_SECONDS;
108}
109
110/** Records a loader's or action's time, from the route instrumentation. */
111export function recordHandler(id: string, kind: "loader" | "action", ms: number) {
112 scope.getStore()?.loaders.push({ id, kind, ms });
113}
114
115/**
116 * `response` with the request's `Server-Timing`, and, after a request
117 * that may have written, the bookmarks its services returned, so the
118 * person's next pages read at least what they just did.
119 */
120export function finishResponse(request: Request, response: Response): Response {
121 const perf = scope.getStore();
122 if (!perf) return response;
123 // A redirect's headers cannot be changed; a copy's can.
124 const answered = new Response(response.body, response);
125 const sessions = [...perf.sessions].map(([service, how]) => `${service}=${how}`).join(" ");
126 answered.headers.append(
127 "server-timing",
128 serverTiming({
129 totalMs: Date.now() - perf.started,
130 loaders: perf.loaders,
131 rpcMs: coveredMs(perf.intervals),
132 services: perf.services,
133 sessions,
134 }),
135 );
136 // Signing in with GitHub writes on a GET; the session it starts says so.
137 const signedIn = answered.headers.getSetCookie().some((cookie) => cookie.startsWith("g1t_session="));
138 const wrote = perf.writing || perf.wrote || signedIn;
139 if (wrote) {
140 const next: Bookmarks = {
141 at: Math.floor(Date.now() / 1000),
142 services: { ...perf.bookmarks.services, ...perf.returned },
143 };
144 answered.headers.append("set-cookie", bookmarkCookie(next, new URL(request.url).protocol === "https:"));
145 }
146 return answered;
147}