Skip to content
Source
Sign inSign up
Open History to see the last commit

ResourceTelemetryDiagnostics.tsx

1,386 lines50.0 KB

1import { ProcessSignalActions } from "./ProcessSignalActions";
2import { AuthEnvironmentMaintainScope } from "@t3tools/contracts";
3import { RefreshIcon } from "~/components/ui/refresh-icon";
4import {
5 ActivityIcon,
6 BatteryIcon,
7 CircleSlashIcon,
8 CpuIcon,
9 DatabaseIcon,
10 GaugeIcon,
11 HardDriveIcon,
12 MemoryStickIcon,
13 ChevronDownIcon,
14 ChevronRightIcon,
15} from "lucide-react";
16import type {
17 BackgroundBooleanState,
18 EnvironmentId,
19 ResourceAttributionEntry,
20 ResourceTelemetryAggregate,
21 ResourceTelemetryHistoryBucket,
22 ResourceTelemetryIoSemantics,
23 ResourceTelemetryProcess,
24 ResourceTelemetryProcessCategory,
25 ResourceTelemetryProcessSummary,
26 ResourceTelemetrySourceHealth,
27 ResourceTelemetrySourceStatus,
28 ServerProcessSignal,
29} from "@t3tools/contracts";
30import * as DateTime from "effect/DateTime";
31import * as Option from "effect/Option";
32import { RegistryContext } from "@effect/atom-react";
33import { useContext, useRef, useState, type ReactNode } from "react";
34import {
35 isAtomCommandInterrupted,
36 squashAtomCommandFailure,
37} from "@t3tools/client-runtime/state/runtime";
38​
39import {
40 useResourceTelemetry,
41 useResourceTelemetryHistory,
42} from "../../lib/resourceTelemetryState";
43import { cn } from "../../lib/utils";
44import { formatBytes } from "../../lib/formatBytes";
45import { ensureLocalApi } from "../../localApi";
46import { serverEnvironment } from "../../state/server";
47import { readEnvironmentScope, useEnvironmentScope } from "../../state/session";
48import { useAtomCommand } from "../../state/use-atom-command";
49import { useRelativeTimeState } from "../../hooks/useRelativeTimeState";
50import { Button } from "../ui/button";
51import { ScrollArea } from "../ui/scroll-area";
52import { StatusDot, type StatusDotTone } from "../ui/status-dot";
53import { Toggle, ToggleGroup } from "../ui/toggle-group";
54import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
55import { toastManager } from "../ui/toast";
56import {
57 processIdentityKey,
58 resourceHistoryBarHeight,
59 resourceHistoryCpuScaleMax,
60 shouldShowResourceMonitorRetry,
61 visibleResourceTelemetryProcesses,
62} from "./ResourceTelemetryDiagnostics.logic";
63import { SettingsNote, SettingsSection } from "./settingsLayout";
64​
65const HISTORY_WINDOWS = [
66 { label: "5m", windowMs: 5 * 60_000, bucketMs: 15_000 },
67 { label: "15m", windowMs: 15 * 60_000, bucketMs: 30_000 },
68 { label: "30m", windowMs: 30 * 60_000, bucketMs: 60_000 },
69 { label: "1h", windowMs: 60 * 60_000, bucketMs: 2 * 60_000 },
70] as const;
71​
72function formatRate(value: number): string {
73 return `${formatBytes(value)}/s`;
75​
76function formatCpuTime(valueMs: number): string {
77 const seconds = valueMs / 1_000;
78 if (seconds < 60) return `${seconds.toFixed(seconds >= 10 ? 1 : 2).replace(/\.0+$/, "")}s`;
79 const minutes = seconds / 60;
80 if (minutes < 60) return `${minutes.toFixed(minutes >= 10 ? 1 : 2).replace(/\.0+$/, "")}m`;
81 return `${(minutes / 60).toFixed(2).replace(/\.0+$/, "")}h`;
83​
84function formatDurationMicros(value: number): string {
85 if (value < 1_000) return `${Math.round(value)} µs`;
86 if (value < 1_000_000) return `${(value / 1_000).toFixed(2).replace(/\.0+$/, "")} ms`;
87 return `${(value / 1_000_000).toFixed(2).replace(/\.0+$/, "")} s`;
89​
90function formatSampleInterval(valueMs: number): string {
91 if (valueMs < 1_000) return `${Math.max(0, Math.round(valueMs))} ms`;
92 const seconds = valueMs / 1_000;
93 return `${seconds.toLocaleString(undefined, { maximumFractionDigits: 1 })} ${
94 seconds === 1 ? "second" : "seconds"
95 }`;
97​
98function formatProcessName(process: Pick<ResourceTelemetryProcess, "command" | "name">): string {
99 if (process.name.trim()) return process.name;
100 const firstToken = process.command.trim().split(/\s+/)[0] ?? process.command;
101 const normalized = firstToken.replace(/^['"]|['"]$/g, "");
102 return normalized.split(/[\\/]/).findLast((segment) => segment.length > 0) ?? normalized;
104​
105function categoryLabel(category: ResourceTelemetryProcessCategory): string {
106 switch (category) {
107 case "server":
108 return "Server";
109 case "server-child":
110 return "Backend child";
111 case "provider-root":
112 return "Provider";
113 case "terminal-root":
114 return "Terminal";
115 case "electron-main":
116 return "Electron main";
117 case "electron-renderer":
118 return "Renderer";
119 case "electron-gpu":
120 return "GPU";
121 case "electron-utility":
122 return "Electron utility";
123 case "resource-monitor":
124 return "Monitor";
125 case "unknown-t3":
126 return "T3 process";
127 }
129​
130function categoryDotTone(category: ResourceTelemetryProcessCategory): StatusDotTone {
131 if (category === "resource-monitor") return "warning";
132 if (category.startsWith("electron-")) return "info";
133 if (category === "server") return "merged";
134 return "success";
136​
137function ioSemanticsLabel(semantics: ResourceTelemetryIoSemantics): string {
138 switch (semantics) {
139 case "storage":
140 return "Storage bytes";
141 case "logical":
142 return "Logical bytes";
143 case "all-io":
144 return "All I/O bytes";
145 case "unavailable":
146 return "Unavailable";
147 }
149​
150function booleanStateLabel(
151 value: BackgroundBooleanState,
152 labels: { readonly true: string; readonly false: string },
153): string {
154 if (value === "true") return labels.true;
155 if (value === "false") return labels.false;
156 return "Unknown";
158​
159function sourceStatusTone(status: ResourceTelemetrySourceStatus): "default" | "warning" | "danger" {
160 if (status === "healthy") return "default";
161 if (status === "starting" || status === "degraded") return "warning";
162 return "danger";
164​
165function SourceStatusBadge({
166 label,
167 status,
168 presentation,
169}: {
170 label: string;
171 status: ResourceTelemetrySourceStatus;
172 presentation?:
173 | {
174 readonly label: string;
175 readonly tone: "neutral";
176 }
177 | undefined;
178}) {
179 const tone = presentation?.tone ?? sourceStatusTone(status);
180 return (
181 <span
182 className={cn(
183 "inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-badge border px-2.5 py-1 text-3xs font-medium",
184 tone === "neutral" && "border-transparent bg-secondary text-muted-foreground",
185 tone === "default" && "border-transparent bg-success/12 text-success-foreground",
186 tone === "warning" && "border-transparent bg-warning/12 text-warning-foreground",
187 tone === "danger" && "border-transparent bg-error/12 text-error-foreground",
188 )}
189 >
190 <span
191 className={cn(
192 "size-1.5 rounded-full",
193 tone === "neutral" && "bg-muted-foreground/55",
194 tone === "default" && "bg-success",
195 tone === "warning" && "bg-warning",
196 tone === "danger" && "bg-destructive",
197 )}
198 />
199 {label} {presentation?.label ?? status}
200 </span>
201 );
203​
204function LastSampleLabel({ sampledAt }: { sampledAt: DateTime.Utc | null }) {
205 const relative = useRelativeTimeState(sampledAt ? DateTime.formatIso(sampledAt) : null);
206 if (relative.status !== "relative") {
207 return <span className="text-2xs text-dim-foreground">Waiting for sample</span>;
208 }
209 return (
210 <span className="text-2xs text-dim-foreground">
211 Updated <span className="font-mono tabular-nums">{relative.value}</span>
212 {relative.suffix ? ` ${relative.suffix}` : ""}
213 </span>
214 );
216​
217function IconStat({
218 icon,
219 label,
220 value,
221 detail,
222 tone = "default",
223}: {
224 icon: ReactNode;
225 label: string;
226 value: string;
227 detail?: ReactNode;
228 tone?: "default" | "warning" | "danger";
229}) {
230 return (
231 <div className="group min-w-0 px-4 py-4 sm:px-5">
232 <div className="flex items-center gap-2 text-3xs font-normal text-dim-foreground">
233 <span className="text-dim-foreground transition-colors group-hover:text-foreground/65">
234 {icon}
235 </span>
236 <span className="truncate">{label}</span>
237 </div>
238 <div
239 className={cn(
240 "mt-2.5 truncate font-sans text-2xl font-semibold tabular-nums text-foreground",
241 tone === "warning" && "text-warning-foreground",
242 tone === "danger" && "text-destructive",
243 )}
244 >
245 {value}
246 </div>
247 {detail ? <div className="mt-1.5 text-meta text-muted-foreground">{detail}</div> : null}
248 </div>
249 );
251​
252function AggregateCard({
253 label,
254 accentClass,
255 aggregate,
256}: {
257 label: string;
258 accentClass: string;
259 aggregate: ResourceTelemetryAggregate;
260}) {
261 return (
262 <div className="relative overflow-hidden border-t border-divider px-4 py-4 first:border-t-0 md:border-t-0 md:border-l md:first:border-l-0 sm:px-5">
263 <span className={cn("absolute inset-x-5 top-0 h-0.5 rounded-full opacity-75", accentClass)} />
264 <div className="flex items-center justify-between gap-3">
265 <div className="text-3xs font-normal text-dim-foreground">{label}</div>
266 <div className="rounded-badge bg-secondary px-1.5 py-0.5 font-mono text-3xs tabular-nums text-dim-foreground">
267 {aggregate.processCount} {aggregate.processCount === 1 ? "process" : "processes"}
268 </div>
269 </div>
270 <div className="mt-3.5 grid grid-cols-2 gap-x-4 gap-y-2.5">
271 <MetricPair label="CPU" value={`${aggregate.currentCpuPercent.toFixed(1)}%`} />
272 <MetricPair label="Memory" value={formatBytes(aggregate.currentRssBytes)} />
273 <MetricPair label="Read" value={formatRate(aggregate.ioReadBytesPerSecond)} />
274 <MetricPair label="Write" value={formatRate(aggregate.ioWriteBytesPerSecond)} />
275 </div>
276 </div>
277 );
279​
280function MetricPair({ label, value }: { label: string; value: string }) {
281 return (
282 <div className="min-w-0">
283 <div className="text-3xs font-normal text-dim-foreground">{label}</div>
284 <div className="truncate font-mono text-xs font-medium tabular-nums text-foreground">
285 {value}
286 </div>
287 </div>
288 );
290​
291function HealthSource({ label, health }: { label: string; health: ResourceTelemetrySourceHealth }) {
292 const expectedInBrowser =
293 health.status === "unavailable" &&
294 Option.exists(health.lastError, (error) => error.includes("'web' mode"));
295 // The page states why the monitor is down once, near the top; here the reason is a tooltip.
296 const unavailable = health.status === "unavailable" && !expectedInBrowser;
297 const badge = (
298 <SourceStatusBadge
299 label=""
300 status={health.status}
301 presentation={
302 expectedInBrowser
303 ? { label: "Desktop only", tone: "neutral" }
304 : unavailable
305 ? { label: "Unavailable", tone: "neutral" }
306 : undefined
307 }
308 />
309 );
310 return (
311 <div className="flex items-start justify-between gap-4 border-t border-divider py-3 first:border-t-0">
312 <div className="min-w-0">
313 <div className="text-sm font-medium text-strong-foreground">{label}</div>
314 {unavailable ? null : (
315 <div className="mt-1 text-2xs leading-relaxed text-dim-foreground">
316 {expectedInBrowser
317 ? "Available when this page runs inside the desktop app."
318 : Option.match(health.lastError, {
319 onNone: () => "No reported errors",
320 onSome: (error) => error,
321 })}
322 </div>
323 )}
324 </div>
325 {unavailable && Option.isSome(health.lastError) ? (
326 <Tooltip>
327 <TooltipTrigger render={<span className="shrink-0" />}>{badge}</TooltipTrigger>
328 <TooltipPopup side="top">{health.lastError.value}</TooltipPopup>
329 </Tooltip>
330 ) : (
331 badge
332 )}
333 </div>
334 );
336​
337function DetailRow({
338 label,
339 value,
340 valueClassName,
341}: {
342 label: string;
343 value: ReactNode;
344 valueClassName?: string | undefined;
345}) {
346 return (
347 <div className="flex items-center justify-between gap-4 border-t border-divider py-2.5 first:border-t-0">
348 <span className="text-meta text-muted-foreground">{label}</span>
349 <span
350 className={cn(
351 "min-w-0 text-right font-mono text-meta tabular-nums text-foreground",
352 valueClassName,
353 )}
354 >
355 {value}
356 </span>
357 </div>
358 );
360​
361function HistoryWindowSelector({
362 selectedWindowMs,
363 onSelect,
364}: {
365 selectedWindowMs: number;
366 onSelect: (windowMs: number) => void;
367}) {
368 return (
369 <ToggleGroup
370 aria-label="Resource history period"
371 variant="segmented"
372 value={[String(selectedWindowMs)]}
373 onValueChange={(next) => {
374 const selected = HISTORY_WINDOWS.find((option) => String(option.windowMs) === next[0]);
375 if (selected) onSelect(selected.windowMs);
376 }}
377 >
378 {HISTORY_WINDOWS.map((option) => (
379 <Toggle key={option.windowMs} value={String(option.windowMs)}>
380 {option.label}
381 </Toggle>
382 ))}
383 </ToggleGroup>
384 );
386​
387function ResourceHistoryChart({
388 buckets,
389}: {
390 buckets: ReadonlyArray<ResourceTelemetryHistoryBucket>;
391}) {
392 const maxCpu = resourceHistoryCpuScaleMax(buckets);
393 const maxIo = Math.max(1, ...buckets.map((bucket) => bucket.ioReadBytes + bucket.ioWriteBytes));
394​
395 return (
396 <div className="border-t border-divider px-4 py-4 sm:px-5">
397 <div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-3xs text-dim-foreground">
398 <span className="inline-flex items-center gap-1.5">
399 <span className="h-1.5 w-3 rounded-full bg-foreground/70" /> CPU average
400 </span>
401 <span className="inline-flex items-center gap-1.5">
402 <span className="h-1.5 w-3 rounded-full bg-info/70" /> I/O reads
403 </span>
404 <span className="inline-flex items-center gap-1.5">
405 <span className="h-1.5 w-3 rounded-full bg-warning/80" /> I/O writes
406 </span>
407 </div>
408 <div className="flex h-32 items-end gap-1 overflow-hidden rounded-control surface-well px-2 pt-3 pb-2">
409 {buckets.map((bucket) => {
410 const cpuHeight = resourceHistoryBarHeight({
411 value: bucket.avgCpuPercent,
412 max: maxCpu,
413 minimumVisiblePercent: 2,
414 });
415 const readHeight = resourceHistoryBarHeight({
416 value: bucket.ioReadBytes,
417 max: maxIo,
418 minimumVisiblePercent: 1,
419 });
420 const writeHeight = resourceHistoryBarHeight({
421 value: bucket.ioWriteBytes,
422 max: maxIo,
423 minimumVisiblePercent: 1,
424 });
425 return (
426 <Tooltip key={DateTime.formatIso(bucket.startedAt)}>
427 <TooltipTrigger
428 render={
429 <div className="grid h-full min-w-1 flex-1 grid-cols-3 items-end gap-px">
430 <span
431 className="block rounded-t-sm bg-foreground/65"
432 style={{ height: `${cpuHeight}%` }}
433 />
434 <span
435 className="block rounded-t-sm bg-info/70"
436 style={{ height: `${readHeight}%` }}
437 />
438 <span
439 className="block rounded-t-sm bg-warning/80"
440 style={{ height: `${writeHeight}%` }}
441 />
442 </div>
443 }
444 />
445 <TooltipPopup side="top" className="text-left">
446 <div className="space-y-0.5">
447 <div>CPU avg {bucket.avgCpuPercent.toFixed(1)}%</div>
448 <div>CPU peak {bucket.maxCpuPercent.toFixed(1)}%</div>
449 <div>Read {formatBytes(bucket.ioReadBytes)}</div>
450 <div>Write {formatBytes(bucket.ioWriteBytes)}</div>
451 </div>
452 </TooltipPopup>
453 </Tooltip>
454 );
455 })}
456 </div>
457 </div>
458 );
460​
461function ProcessTreeName({
462 process,
463 collapsed,
464 onToggle,
465}: {
466 process: ResourceTelemetryProcess;
467 collapsed: boolean;
468 onToggle: (process: ResourceTelemetryProcess) => void;
469}) {
470 const name = formatProcessName(process);
471 const hasChildren = process.childPids.length > 0;
472 return (
473 <div
474 className="grid min-w-0 grid-cols-[1.25rem_0.375rem_minmax(0,1fr)] items-center gap-2"
475 style={{ paddingLeft: `${Math.min(process.depth, 7) * 10}px` }}
476 >
477 {hasChildren ? (
478 <Button
479 size="icon-micro"
480 variant="ghost-muted"
481 onClick={() => onToggle(process)}
482 aria-label={collapsed ? `Expand ${name}` : `Collapse ${name}`}
483 >
484 {collapsed ? (
485 <ChevronRightIcon className="size-3.5" />
486 ) : (
487 <ChevronDownIcon className="size-3.5" />
488 )}
489 </Button>
490 ) : (
491 <span className="size-5" aria-hidden />
492 )}
493 <StatusDot tone={categoryDotTone(process.category)} />
494 <div className="min-w-0">
495 <Tooltip>
496 <TooltipTrigger
497 render={<div className="truncate text-label text-strong-foreground">{name}</div>}
498 />
499 <TooltipPopup side="top" variant="code">
500 {process.command || process.name}
501 </TooltipPopup>
502 </Tooltip>
503 <ProcessDetail>
504 <span>{categoryLabel(process.category)}</span>
505 <span>PID {process.identity.pid}</span>
506 </ProcessDetail>
507 </div>
508 </div>
509 );
511​
512/** One quiet line saying why a measurement is missing, in place of an error banner. */
513function UnavailableNote({ children }: { children: ReactNode }) {
514 return (
515 <SettingsNote icon={<CircleSlashIcon className="text-dim-foreground" />}>
516 {children}
517 </SettingsNote>
518 );
520​
521/** A section whose data needs the monitor, once the reason has been stated above. */
522function UnavailableRows() {
523 return <div className="px-4 py-4 text-xs text-muted-foreground sm:px-5">Unavailable</div>;
525​
526/** The muted second line under a process name. */
527function ProcessDetail({ children }: { children: ReactNode }) {
528 return (
529 <div className="mt-0.5 grid gap-1 font-mono text-meta tabular-nums text-muted-foreground">
530 {children}
531 </div>
532 );
534​
535/** Read and write stacked in one cell, colored like the timeline legend. */
536function IoPair({ read, write, tooltip }: { read: string; write: string; tooltip?: ReactNode }) {
537 const pair = (
538 <div className="font-mono tabular-nums">
539 <div className="truncate text-info-foreground">{read}</div>
540 <div className="truncate text-warning-foreground">{write}</div>
541 </div>
542 );
543 if (!tooltip) return pair;
544 return (
545 <Tooltip>
546 <TooltipTrigger render={pair} />
547 <TooltipPopup side="top" className="text-left">
548 {tooltip}
549 </TooltipPopup>
550 </Tooltip>
551 );
553​
554function canSignalProcess(process: ResourceTelemetryProcess): boolean {
555 return (
556 process.category === "server-child" ||
557 process.category === "provider-root" ||
558 process.category === "terminal-root"
559 );
561​
562function ProcessActions({
563 process,
564 canMaintainEnvironment,
565 signalingKeys,
566 onSignal,
567}: {
568 process: ResourceTelemetryProcess;
569 canMaintainEnvironment: boolean;
570 signalingKeys: ReadonlySet<string>;
571 onSignal: (process: ResourceTelemetryProcess, signal: ServerProcessSignal) => void;
572}) {
573 if (!canSignalProcess(process)) {
574 return <span className="text-3xs text-dim-foreground">-</span>;
575 }
576 const isSignaling = signalingKeys.has(processIdentityKey(process));
577 return (
578 <ProcessSignalActions
579 disabled={!canMaintainEnvironment || isSignaling}
580 onSignal={(signal) => onSignal(process, signal)}
581 />
582 );
584​
585function ProcessTable({
586 processes,
587 canMaintainEnvironment,
588 signalingKeys,
589 onSignal,
590}: {
591 processes: ReadonlyArray<ResourceTelemetryProcess>;
592 canMaintainEnvironment: boolean;
593 signalingKeys: ReadonlySet<string>;
594 onSignal: (process: ResourceTelemetryProcess, signal: ServerProcessSignal) => void;
595}) {
596 const [collapsed, setCollapsed] = useState<ReadonlySet<string>>(() => new Set());
597 const visible = visibleResourceTelemetryProcesses(processes, collapsed);
598 const toggle = (process: ResourceTelemetryProcess) => {
599 const identityKey = processIdentityKey(process);
600 setCollapsed((current) => {
601 const next = new Set(current);
602 if (next.has(identityKey)) {
603 next.delete(identityKey);
604 } else {
605 next.add(identityKey);
606 }
607 return next;
608 });
609 };
610​
611 return (
612 <div className="border-t border-divider">
613 <ScrollArea
614 chainVerticalScroll
615 scrollFade
616 hideScrollbars
617 className="max-h-[min(68vh,48rem)] w-full max-w-full"
618 >
619 <table className="w-full table-fixed text-left text-xs">
620 <colgroup>
621 <col />
622 <col className="w-16" />
623 <col className="w-18" />
624 <col className="w-20" />
625 <col className="w-24" />
626 <col className="w-24" />
627 </colgroup>
628 <thead className="sticky top-0 z-10 border-b border-divider bg-card text-3xs text-dim-foreground">
629 <tr>
630 <th className="px-4 py-2 font-semibold sm:pl-5">Process</th>
631 <th className="px-3 py-2 text-right font-semibold">CPU</th>
632 <th className="px-3 py-2 text-right font-semibold">CPU time</th>
633 <th className="px-3 py-2 text-right font-semibold">Memory</th>
634 <th className="px-3 py-2 text-right font-semibold">I/O per sec</th>
635 <th className="px-2 py-2 font-semibold sm:pr-4">
636 <span className="sr-only">Signal</span>
637 </th>
638 </tr>
639 </thead>
640 <tbody className="divide-y divide-divider">
641 {visible.length === 0 ? (
642 <tr>
643 <td colSpan={6} className="px-4 py-5 text-xs text-muted-foreground sm:px-5">
644 Waiting for the native process monitor.
645 </td>
646 </tr>
647 ) : null}
648 {visible.map((process) => (
649 <tr key={processIdentityKey(process)} className="hover:surface-ghost-hover">
650 <td className="px-4 py-2 sm:pl-5">
651 <ProcessTreeName
652 process={process}
653 collapsed={collapsed.has(processIdentityKey(process))}
654 onToggle={toggle}
655 />
656 </td>
657 <td className="px-3 py-2 text-right font-mono tabular-nums">
658 {process.cpuPercent.toFixed(1)}%
659 </td>
660 <td className="px-3 py-2 text-right font-mono tabular-nums">
661 {formatCpuTime(process.cpuTimeMs)}
662 </td>
663 <td className="px-3 py-2 text-right font-mono tabular-nums">
664 {formatBytes(process.residentBytes)}
665 </td>
666 <td className="px-3 py-2 text-right">
667 <IoPair
668 read={formatRate(process.ioReadBytesPerSecond)}
669 write={formatRate(process.ioWriteBytesPerSecond)}
670 tooltip={
671 <div className="space-y-0.5">
672 <div>Read {formatRate(process.ioReadBytesPerSecond)}</div>
673 <div>Write {formatRate(process.ioWriteBytesPerSecond)}</div>
674 <div>Read total {formatBytes(process.ioReadBytes)}</div>
675 <div>Write total {formatBytes(process.ioWriteBytes)}</div>
676 <div>{ioSemanticsLabel(process.ioSemantics)}</div>
677 </div>
678 }
679 />
680 </td>
681 <td className="px-2 py-2 text-right sm:pr-4">
682 <ProcessActions
683 process={process}
684 canMaintainEnvironment={canMaintainEnvironment}
685 signalingKeys={signalingKeys}
686 onSignal={onSignal}
687 />
688 </td>
689 </tr>
690 ))}
691 </tbody>
692 </table>
693 </ScrollArea>
694 </div>
695 );
697​
698function HistoryProcessTable({
699 processes,
700}: {
701 processes: ReadonlyArray<ResourceTelemetryProcessSummary>;
702}) {
703 return (
704 <div className="border-t border-divider">
705 <ScrollArea
706 chainVerticalScroll
707 scrollFade
708 hideScrollbars
709 className="max-h-[28rem] w-full max-w-full"
710 >
711 <table className="w-full table-fixed text-left text-xs">
712 <colgroup>
713 <col />
714 <col className="w-20" />
715 <col className="w-20" />
716 <col className="w-22" />
717 <col className="w-24" />
718 </colgroup>
719 <thead className="sticky top-0 z-10 border-b border-divider bg-card text-3xs text-dim-foreground">
720 <tr>
721 <th className="px-4 py-2 font-semibold sm:pl-5">Process</th>
722 <th className="px-3 py-2 text-right font-semibold">CPU time</th>
723 <th className="px-3 py-2 text-right font-semibold">Peak CPU</th>
724 <th className="px-3 py-2 text-right font-semibold">Peak mem</th>
725 <th className="px-3 py-2 text-right font-semibold sm:pr-5">I/O</th>
726 </tr>
727 </thead>
728 <tbody className="divide-y divide-divider">
729 {processes.length === 0 ? (
730 <tr>
731 <td colSpan={5} className="px-4 py-5 text-xs text-muted-foreground sm:px-5">
732 No retained process samples in this window.
733 </td>
734 </tr>
735 ) : null}
736 {processes.map((process) => (
737 <tr key={processIdentityKey(process)} className="hover:surface-ghost-hover">
738 <td className="px-4 py-2 sm:pl-5">
739 <Tooltip>
740 <TooltipTrigger
741 render={
742 <span className="block truncate text-label text-strong-foreground">
743 {process.name || process.command}
744 </span>
745 }
746 />
747 <TooltipPopup side="top" variant="code">
748 {process.command || process.name}
749 </TooltipPopup>
750 </Tooltip>
751 <ProcessDetail>
752 <span>{categoryLabel(process.category)}</span>
753 <span>PID {process.identity.pid}</span>
754 <span>{process.sampleCount} samples</span>
755 </ProcessDetail>
756 </td>
757 <td className="px-3 py-2 text-right font-mono tabular-nums">
758 {formatCpuTime(process.cpuTimeMs)}
759 </td>
760 <td className="px-3 py-2 text-right font-mono tabular-nums">
761 {process.maxCpuPercent.toFixed(1)}%
762 </td>
763 <td className="px-3 py-2 text-right font-mono tabular-nums">
764 {formatBytes(process.peakRssBytes)}
765 </td>
766 <td className="px-3 py-2 text-right sm:pr-5">
767 <IoPair
768 read={formatBytes(process.ioReadBytes)}
769 write={formatBytes(process.ioWriteBytes)}
770 tooltip={
771 <div className="space-y-0.5">
772 <div>Read {formatBytes(process.ioReadBytes)}</div>
773 <div>Write {formatBytes(process.ioWriteBytes)}</div>
774 </div>
775 }
776 />
777 </td>
778 </tr>
779 ))}
780 </tbody>
781 </table>
782 </ScrollArea>
783 </div>
784 );
786​
787function AttributionTable({ entries }: { entries: ReadonlyArray<ResourceAttributionEntry> }) {
788 return (
789 <div className="border-t border-divider">
790 <table className="w-full table-fixed text-left text-xs">
791 <colgroup>
792 <col />
793 <col className="w-22" />
794 <col className="w-22" />
795 <col className="w-18" />
796 <col className="w-20" />
797 </colgroup>
798 <thead className="border-b border-divider text-3xs text-dim-foreground">
799 <tr>
800 <th className="px-4 py-2 font-semibold sm:pl-5">Operation</th>
801 <th className="px-3 py-2 text-right font-semibold">Read</th>
802 <th className="px-3 py-2 text-right font-semibold">Write</th>
803 <th className="px-3 py-2 text-right font-semibold">Count</th>
804 <th className="px-3 py-2 text-right font-semibold sm:pr-5">Time</th>
805 </tr>
806 </thead>
807 <tbody className="divide-y divide-divider">
808 {entries.length === 0 ? (
809 <tr>
810 <td colSpan={5} className="px-4 py-5 text-xs text-muted-foreground sm:px-5">
811 No instrumented application I/O has been recorded yet.
812 </td>
813 </tr>
814 ) : null}
815 {entries.map((entry) => (
816 <tr key={`${entry.component}:${entry.operation}`} className="hover:surface-ghost-hover">
817 <td className="px-4 py-2 sm:pl-5">
818 <div className="truncate text-label text-strong-foreground">{entry.operation}</div>
819 <ProcessDetail>{entry.component}</ProcessDetail>
820 </td>
821 <td className="px-3 py-2 text-right font-mono tabular-nums text-info-foreground">
822 {formatBytes(entry.logicalReadBytes)}
823 </td>
824 <td className="px-3 py-2 text-right font-mono tabular-nums text-warning-foreground">
825 {formatBytes(entry.logicalWriteBytes)}
826 </td>
827 <td className="px-3 py-2 text-right font-mono tabular-nums">{entry.count}</td>
828 <td className="px-3 py-2 text-right font-mono tabular-nums text-muted-foreground sm:pr-5">
829 {(entry.durationMs / 1_000).toFixed(2).replace(/\.0+$/, "")}s
830 </td>
831 </tr>
832 ))}
833 </tbody>
834 </table>
835 </div>
836 );
838​
839export function ResourceTelemetryDiagnostics({
840 environmentId,
841}: {
842 environmentId: EnvironmentId | null;
843}) {
844 // Pending signals and retries belong to one environment, so a switch starts fresh.
845 return (
846 <ResourceTelemetryDiagnosticsBody key={environmentId ?? ""} environmentId={environmentId} />
847 );
849​
850function ResourceTelemetryDiagnosticsBody({
851 environmentId,
852}: {
853 environmentId: EnvironmentId | null;
854}) {
855 const registry = useContext(RegistryContext);
856 const [windowMs, setWindowMs] = useState(15 * 60_000);
857 const selectedWindow =
858 HISTORY_WINDOWS.find((option) => option.windowMs === windowMs) ?? HISTORY_WINDOWS[1];
859 const canMaintainEnvironment = useEnvironmentScope(environmentId, AuthEnvironmentMaintainScope);
860 const telemetry = useResourceTelemetry(environmentId);
861 const retryTelemetry = telemetry.retry;
862 const history = useResourceTelemetryHistory(
863 {
864 windowMs: selectedWindow.windowMs,
865 bucketMs: selectedWindow.bucketMs,
866 },
867 environmentId,
868 );
869 const signalServerProcess = useAtomCommand(serverEnvironment.signalProcess, {
870 reportFailure: false,
871 });
872 const [signalingKeys, setSignalingKeys] = useState<ReadonlySet<string>>(() => new Set());
873 const signalingKeysRef = useRef<ReadonlySet<string>>(new Set());
874 const [isRetrying, setIsRetrying] = useState(false);
875 const snapshot = telemetry.data;
876 // With the native monitor down and nothing sampled, the zeroed totals are unknown, not zero.
877 const nativeUnavailableReason =
878 snapshot &&
879 (snapshot.health.native.status === "unavailable" ||
880 snapshot.health.native.status === "stopped") &&
881 snapshot.processes.length === 0
882 ? Option.getOrElse(
883 snapshot.health.native.lastError,
884 () => "The native process monitor is unavailable.",
885 )
886 : null;
887 const measured = nativeUnavailableReason === null ? snapshot?.groups.allT3 : undefined;
888 const pendingValue = nativeUnavailableReason === null ? "..." : "-";
889 const historyProcesses = history.data?.topProcesses ?? [];
890​
891 const signalProcess = async (process: ResourceTelemetryProcess, signal: ServerProcessSignal) => {
892 const targetEnvironmentId = environmentId;
893 if (
894 targetEnvironmentId === null ||
895 !readEnvironmentScope(targetEnvironmentId, AuthEnvironmentMaintainScope)
896 )
897 return;
898 const identityKey = processIdentityKey(process);
899 if (signalingKeysRef.current.has(identityKey)) return;
900 const nextSignalingKeys = new Set(signalingKeysRef.current).add(identityKey);
901 signalingKeysRef.current = nextSignalingKeys;
902 setSignalingKeys(nextSignalingKeys);
903 const clearSignaling = () => {
904 const next = new Set(signalingKeysRef.current);
905 next.delete(identityKey);
906 signalingKeysRef.current = next;
907 setSignalingKeys(next);
908 };
909​
910 if (signal === "SIGKILL") {
911 let confirmed = false;
912 try {
913 confirmed = await ensureLocalApi().dialogs.confirm(
914 `Send SIGKILL to process ${process.identity.pid}? This cannot be handled by the process.`,
915 { variant: "destructive" },
916 );
917 } catch (error) {
918 clearSignaling();
919 toastManager.add({
920 type: "error",
921 title: "Could not confirm signal",
922 description: error instanceof Error ? error.message : `Failed to send ${signal}.`,
923 });
924 return;
925 }
926 if (!confirmed) {
927 clearSignaling();
928 return;
929 }
930 }
931 if (!readEnvironmentScope(targetEnvironmentId, AuthEnvironmentMaintainScope)) {
932 clearSignaling();
933 return;
934 }
935 void signalServerProcess({
936 environmentId: targetEnvironmentId,
937 input: {
938 pid: process.identity.pid,
939 startTimeMs: process.identity.startTimeMs,
940 signal,
941 },
942 })
943 .then((result) => {
944 if (result._tag === "Failure") {
945 if (isAtomCommandInterrupted(result)) return;
946 throw squashAtomCommandFailure(result);
947 }
948 if (result.value.signaled) return;
949 toastManager.add({
950 type: "error",
951 title: `Could not send ${signal}`,
952 description: Option.getOrElse(
953 result.value.message,
954 () => `Failed to send ${signal} to process ${process.identity.pid}.`,
955 ),
956 });
957 })
958 .catch((error: unknown) => {
959 toastManager.add({
960 type: "error",
961 title: `Could not send ${signal}`,
962 description: error instanceof Error ? error.message : `Failed to send ${signal}.`,
963 });
964 })
965 .finally(() => {
966 clearSignaling();
967 });
968 };
969​
970 const retryCollector = () => {
971 if (
972 environmentId === null ||
973 !readEnvironmentScope(environmentId, AuthEnvironmentMaintainScope)
974 )
975 return;
976 setIsRetrying(true);
977 void retryTelemetry()
978 .then(() => {
979 // The user can pick another window while the retry runs; refreshing every window
980 // re-reads the one on screen and only invalidates the rest.
981 for (const option of HISTORY_WINDOWS) {
982 registry.refresh(
983 serverEnvironment.resourceTelemetryHistory({
984 environmentId,
985 input: { windowMs: option.windowMs, bucketMs: option.bucketMs },
986 }),
987 );
988 }
989 })
990 .catch((error: unknown) => {
991 toastManager.add({
992 type: "error",
993 title: "Could not restart resource monitor",
994 description:
995 error instanceof Error ? error.message : "The resource monitor retry failed.",
996 });
997 })
998 .finally(() => {
999 setIsRetrying(false);
1000 });
1001 };
1002​
1003 const speedLimit = snapshot ? Option.getOrNull(snapshot.speedLimitPercent) : null;
1004 const collectorNeedsRetry = shouldShowResourceMonitorRetry({
1005 nativeStatus: snapshot?.health.native.status ?? null,
1006 error: telemetry.error,
1007 });
1008 const hasHostPowerSignal =
1009 snapshot !== null &&
1010 (snapshot.power.onBattery !== "unknown" ||
1011 snapshot.power.lowPowerMode !== "unknown" ||
1012 snapshot.power.idle !== "unknown" ||
1013 snapshot.power.locked !== "unknown" ||
1014 snapshot.power.thermalState !== "unknown");
1015​
1016 return (
1017 <>
1018 <SettingsSection
1019 title="Resource monitor"
1020 headerAction={
1021 <div className="flex items-center gap-2">
1022 {snapshot && nativeUnavailableReason === null ? (
1023 <SourceStatusBadge label="Native" status={snapshot.health.native.status} />
1024 ) : null}
1025 <LastSampleLabel sampledAt={snapshot?.readAt ?? null} />
1026 <Tooltip>
1027 <TooltipTrigger
1028 render={
1029 <Button
1030 size="icon-micro"
1031 variant="ghost"
1032 disabled={telemetry.isPending}
1033 onClick={telemetry.refresh}
1034 aria-label="Refresh resource telemetry"
1036 <RefreshIcon size="xs" refreshing={telemetry.isPending} />
1037 </Button>
1039 />
1040 <TooltipPopup side="top">Refresh telemetry snapshot</TooltipPopup>
1041 </Tooltip>
1042 </div>
1045 <div className="overflow-hidden rounded-card border border-transparent surface-card-settings">
1046 <div className="flex flex-col gap-3 border-b border-divider surface-card-settings px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-5">
1047 <div>
1048 <div className="text-3xs font-normal text-dim-foreground">T3 system footprint</div>
1049 <p className="mt-1 max-w-xl text-xs leading-relaxed text-muted-foreground">
1050 Live native counters for the server, providers, terminals, desktop processes, and
1051 the monitor itself.
1052 </p>
1053 </div>
1054 {nativeUnavailableReason ? (
1055 <div className="flex items-center gap-2 text-3xs text-dim-foreground">
1056 <StatusDot tone="neutral" label={nativeUnavailableReason} />
1057 Unavailable
1058 </div>
1059 ) : (
1060 <div className="flex items-center gap-2 text-3xs text-dim-foreground">
1061 <StatusDot tone="success" />
1062 Sampling every {snapshot ? formatSampleInterval(snapshot.sampleIntervalMs) : "..."}
1063 </div>
1064 )}
1065 </div>
1066 {/* Tiles draw their right and bottom edges; the 1px overhang is clipped by the card so only inner dividers show. */}
1067 <div className="-mr-px -mb-px grid grid-cols-2 md:grid-cols-3 [&>*]:border-r [&>*]:border-b [&>*]:border-divider">
1068 <IconStat
1069 icon={<CpuIcon className="size-3.5" />}
1070 label="Current CPU"
1071 value={measured ? `${measured.currentCpuPercent.toFixed(1)}%` : pendingValue}
1072 detail={
1073 measured ? `${formatCpuTime(measured.cpuTimeMs)} observed CPU time` : undefined
1075 />
1076 <IconStat
1077 icon={<MemoryStickIcon className="size-3.5" />}
1078 label="Resident memory"
1079 value={measured ? formatBytes(measured.currentRssBytes) : pendingValue}
1080 detail={
1081 measured
1082 ? `${formatBytes(measured.peakRssBytes)} combined process peaks`
1083 : undefined
1085 />
1086 <IconStat
1087 icon={<ActivityIcon className="size-3.5" />}
1088 label="Process count"
1089 value={measured ? String(measured.processCount) : pendingValue}
1090 detail={
1091 measured ? (
1092 <span className="grid gap-1">
1093 <span>{measured.processStarts} starts</span>
1094 <span>{measured.processExits} exits</span>
1095 </span>
1096 ) : undefined
1098 />
1099 <IconStat
1100 icon={<HardDriveIcon className="size-3.5" />}
1101 label="Read throughput"
1102 value={measured ? formatRate(measured.ioReadBytesPerSecond) : pendingValue}
1103 detail={measured ? `${formatBytes(measured.ioReadBytes)} observed` : undefined}
1104 />
1105 <IconStat
1106 icon={<DatabaseIcon className="size-3.5" />}
1107 label="Write throughput"
1108 value={measured ? formatRate(measured.ioWriteBytesPerSecond) : pendingValue}
1109 detail={measured ? `${formatBytes(measured.ioWriteBytes)} observed` : undefined}
1110 tone={
1111 measured && measured.ioWriteBytesPerSecond >= 10 * 1_024 * 1_024
1112 ? "danger"
1113 : measured && measured.ioWriteBytesPerSecond >= 1_024 * 1_024
1114 ? "warning"
1115 : "default"
1117 />
1118 <IconStat
1119 icon={<GaugeIcon className="size-3.5" />}
1120 label="CPU speed limit"
1121 value={snapshot ? (speedLimit === null ? "-" : `${speedLimit.toFixed(0)}%`) : "..."}
1122 detail={
1123 snapshot && snapshot.power.thermalState !== "unknown"
1124 ? `${snapshot.power.thermalState} thermal state`
1125 : undefined
1127 tone={speedLimit !== null && speedLimit < 80 ? "warning" : "default"}
1128 />
1129 </div>
1130 {telemetry.error ? (
1131 <div className="border-t border-divider">
1132 <UnavailableNote>{telemetry.error}</UnavailableNote>
1133 </div>
1134 ) : null}
1135 {snapshot && nativeUnavailableReason === null ? (
1136 <div className="grid border-t border-divider bg-muted/10 md:grid-cols-3">
1137 <AggregateCard
1138 label="Backend + agents"
1139 accentClass="bg-success/80"
1140 aggregate={snapshot.groups.backend}
1141 />
1142 <AggregateCard
1143 label="Desktop"
1144 accentClass="bg-info/80"
1145 aggregate={snapshot.groups.electron}
1146 />
1147 <AggregateCard
1148 label="Monitor overhead"
1149 accentClass="bg-warning/80"
1150 aggregate={snapshot.groups.monitor}
1151 />
1152 </div>
1153 ) : null}
1154 </div>
1155 </SettingsSection>
1156​
1157 <SettingsSection
1158 title="Host & collection"
1159 headerAction={
1160 collectorNeedsRetry ? (
1161 <Button size="xs" variant="secondary" disabled={isRetrying} onClick={retryCollector}>
1162 <RefreshIcon size="xs" refreshing={isRetrying} />
1163 Retry monitor
1164 </Button>
1165 ) : null
1168 {/* Without host signals the host side is one note, so the two sides stack instead of leaving a tall empty column. */}
1169 <div
1170 className={cn(
1171 "grid overflow-hidden rounded-card border border-transparent surface-card-settings",
1172 hasHostPowerSignal && "md:grid-cols-2 md:divide-x md:divide-divider",
1173 )}
1175 <div className="px-4 py-4 sm:px-5">
1176 <div className="mb-3 flex items-center gap-2 text-3xs font-normal text-dim-foreground">
1177 <span className="flex size-6 items-center justify-center rounded-md bg-muted/60">
1178 <BatteryIcon className="size-3.5" />
1179 </span>
1180 Host state
1181 </div>
1182 {hasHostPowerSignal && snapshot ? (
1183 <>
1184 <DetailRow
1185 label="Power source"
1186 value={booleanStateLabel(snapshot.power.onBattery, {
1187 true: "Battery",
1188 false: "External power",
1189 })}
1190 />
1191 <DetailRow
1192 label="Low power mode"
1193 value={booleanStateLabel(snapshot.power.lowPowerMode, {
1194 true: "Enabled",
1195 false: "Disabled",
1196 })}
1197 />
1198 <DetailRow
1199 label="Idle"
1200 value={
1201 <span className="grid justify-items-end gap-1">
1202 <span>
1203 {booleanStateLabel(snapshot.power.idle, { true: "Idle", false: "Active" })}
1204 </span>
1205 {snapshot.power.idleSeconds === null ? null : (
1206 <span>{Math.round(snapshot.power.idleSeconds)}s</span>
1207 )}
1208 </span>
1210 />
1211 <DetailRow
1212 label="Session"
1213 value={
1214 snapshot.power.suspended
1215 ? "Suspended"
1216 : booleanStateLabel(snapshot.power.locked, {
1217 true: "Locked",
1218 false: "Unlocked",
1219 })
1221 />
1222 <DetailRow
1223 label="Thermal"
1224 value={snapshot.power.thermalState}
1225 valueClassName={
1226 snapshot.power.thermalState === "serious" ||
1227 snapshot.power.thermalState === "critical"
1228 ? "text-destructive"
1229 : undefined
1231 />
1232 </>
1233 ) : (
1234 <div>
1235 <div className="text-sm font-medium text-strong-foreground">
1236 Desktop host signals not connected
1237 </div>
1238 <p className="mt-1 text-2xs leading-relaxed text-dim-foreground">
1239 Power, idle, lock, and thermal state are supplied by the desktop host.
1240 {snapshot?.health.native.status === "healthy" ||
1241 snapshot?.health.native.status === "degraded"
1242 ? " Process telemetry remains fully active in this browser session."
1243 : null}
1244 </p>
1245 </div>
1246 )}
1247 </div>
1248 <div
1249 className={cn(
1250 "border-t border-divider px-4 py-4 sm:px-5",
1251 hasHostPowerSignal && "md:border-t-0",
1252 )}
1254 <div className="mb-3 flex items-center gap-2 text-3xs font-normal text-dim-foreground">
1255 <span className="flex size-6 items-center justify-center rounded-md bg-muted/60">
1256 <GaugeIcon className="size-3.5" />
1257 </span>
1258 Collection health
1259 </div>
1260 {snapshot ? (
1261 <>
1262 <HealthSource label="Native process monitor" health={snapshot.health.native} />
1263 <HealthSource label="Electron main process" health={snapshot.health.desktop} />
1264 <DetailRow
1265 label="Collection time"
1266 value={
1267 nativeUnavailableReason
1268 ? "-"
1269 : formatDurationMicros(snapshot.health.collectionDurationMicros)
1271 />
1272 <DetailRow
1273 label="Process scan"
1274 value={
1275 nativeUnavailableReason
1276 ? "-"
1277 : `${snapshot.health.retainedProcessCount}/${snapshot.health.scannedProcessCount} retained`
1279 />
1280 <DetailRow
1281 label="Inaccessible"
1282 value={
1283 nativeUnavailableReason ? "-" : String(snapshot.health.inaccessibleProcessCount)
1285 valueClassName={
1286 nativeUnavailableReason === null && snapshot.health.inaccessibleProcessCount > 0
1287 ? "text-warning-foreground"
1288 : undefined
1290 />
1291 <DetailRow
1292 label="Sidecar"
1293 value={
1294 <span className="grid justify-items-end gap-1">
1295 <span>{Option.getOrElse(snapshot.health.sidecarVersion, () => "-")}</span>
1296 {Option.isSome(snapshot.health.sidecarPid) ? (
1297 <span>PID {snapshot.health.sidecarPid.value}</span>
1298 ) : null}
1299 </span>
1301 />
1302 <DetailRow label="Restarts" value={String(snapshot.health.restartCount)} />
1303 </>
1304 ) : (
1305 <div className="py-4 text-xs text-muted-foreground">
1306 Waiting for collector health.
1307 </div>
1308 )}
1309 </div>
1310 </div>
1311 </SettingsSection>
1312​
1313 <SettingsSection
1314 title="Resource timeline"
1315 headerAction={
1316 <div className="flex items-center gap-2">
1317 <HistoryWindowSelector selectedWindowMs={windowMs} onSelect={setWindowMs} />
1318 <Button
1319 size="icon-micro"
1320 variant="ghost"
1321 disabled={history.isPending}
1322 onClick={history.refresh}
1323 aria-label="Refresh resource history"
1325 <RefreshIcon size="xs" refreshing={history.isPending} />
1326 </Button>
1327 </div>
1330 <div className="overflow-hidden rounded-card border border-transparent surface-card-settings">
1331 {historyProcesses.length === 0 && nativeUnavailableReason ? (
1332 <UnavailableRows />
1333 ) : (
1334 <>
1335 {history.error ? <UnavailableNote>{history.error}</UnavailableNote> : null}
1336 {historyProcesses.length > 0 ? (
1337 <ResourceHistoryChart buckets={history.data?.buckets ?? []} />
1338 ) : null}
1339 <HistoryProcessTable processes={historyProcesses} />
1340 </>
1341 )}
1342 </div>
1343 </SettingsSection>
1344​
1345 <SettingsSection
1346 title="Live process tree"
1347 headerAction={
1348 snapshot ? (
1349 <span className="text-3xs text-dim-foreground">
1350 Identity: <span className="font-mono">PID + start time</span>
1351 </span>
1352 ) : null
1355 <div className="overflow-hidden rounded-card border border-transparent surface-card-settings">
1356 {nativeUnavailableReason ? (
1357 <UnavailableRows />
1358 ) : (
1359 <ProcessTable
1360 processes={snapshot?.processes ?? []}
1361 canMaintainEnvironment={canMaintainEnvironment}
1362 signalingKeys={signalingKeys}
1363 onSignal={signalProcess}
1364 />
1365 )}
1366 </div>
1367 </SettingsSection>
1368​
1369 <SettingsSection
1370 title="Instrumented application I/O"
1371 headerAction={
1372 <span className="text-3xs text-dim-foreground">Logical bytes by operation</span>
1375 <div className="overflow-hidden rounded-card border border-transparent surface-card-settings">
1376 <div className="bg-muted/15 px-4 py-3 text-2xs leading-relaxed text-muted-foreground sm:px-5">
1377 Native counters identify which process is reading or writing. These application-level
1378 counters identify known T3 operations so process spikes can be correlated with specific
1379 persistence and logging paths.
1380 </div>
1381 <AttributionTable entries={snapshot?.attribution.entries ?? []} />
1382 </div>
1383 </SettingsSection>
1384 </>
1385 );