返回 DeepSeek-Reasonix
rendererDiagnostics.ts
根目录 / desktop / electron / src / main / rendererDiagnostics.ts
1 export interface CpuProfile {
2 nodes: { id: number; callFrame: { functionName: string; url: string; lineNumber: number; columnNumber?: number } }[];
3 samples?: number[];
4 timeDeltas?: number[];
5 startTime: number;
6 endTime: number;
7 }
8 export interface ProfileFrame { label: string; samples: number; selfMs: number }
9 export interface RendererProfileResult {
10 status: "captured" | "inactive" | "busy" | "cooldown" | "limit" | "unavailable" | "cancelled" | "failed";
11 durationMs?: number;
12 frames?: ProfileFrame[];
13 }
14 export interface ProfileTarget {
15 debugger: {
16 isAttached(): boolean;
17 attach(): void;
18 detach(): void;
19 sendCommand(method: string, params?: Record<string, unknown>): Promise<any>;
20 on(event: "detach", listener: () => void): unknown;
21 removeListener(event: "detach", listener: () => void): unknown;
22 };
23 isDestroyed(): boolean;
24 isDevToolsOpened(): boolean;
25 }
26 interface Dependencies {
27 target(): ProfileTarget | null;
28 isForeground(): boolean;
29 onInvalidated(listener: () => void): () => void;
30 analyse(profile: CpuProfile): Promise<ProfileFrame[]>;
31 now?: () => number;
32 durationMs?: number;
33 cooldownMs?: number;
34 maxCaptures?: number;
35 commandTimeoutMs?: number;
36 }
37
38 // Own only the trusted renderer's debugger connection. Never share or steal a
39 // connection from DevTools or another caller; cancellation retains ownership
40 // until stop/detach has completed, so a late reply cannot affect a new capture.
41 export class RendererDiagnostics {
42 private active: { requestId?: string; cancel(): void } | undefined;
43 private lastAttempt = -Infinity;
44 private captures = 0;
45 private disposed = false;
46 constructor(private readonly deps: Dependencies) {}
47 get busy(): boolean { return this.active !== undefined; }
48
49 cancel(requestId?: string): void {
50 if (requestId !== undefined && this.active?.requestId !== requestId) return;
51 this.active?.cancel();
52 }
53 dispose(): void { this.disposed = true; this.cancel(); }
54
55 async capture(requestId?: string): Promise<RendererProfileResult> {
56 if (this.disposed) return { status: "unavailable" };
57 if (this.active) return { status: "busy" };
58 if (!this.deps.isForeground()) return { status: "inactive" };
59 const now = (this.deps.now ?? (() => performance.now()))();
60 if (this.captures >= (this.deps.maxCaptures ?? 3)) return { status: "limit" };
61 if (now - this.lastAttempt < (this.deps.cooldownMs ?? 600_000)) return { status: "cooldown" };
62 const target = this.deps.target();
63 if (!target || target.isDestroyed() || target.isDevToolsOpened() || target.debugger.isAttached()) return { status: "unavailable" };
64 this.lastAttempt = now;
65 this.captures++;
66 let cancelled = false;
67 let ownsConnection = false;
68 let wake!: () => void;
69 const interrupted = new Promise<void>((resolve) => { wake = resolve; });
70 const owner = { requestId, cancel: () => { cancelled = true; wake(); } };
71 this.active = owner;
72 const onDetach = () => { ownsConnection = false; owner.cancel(); };
73 let removeInvalidated = () => {};
74 const inspector = target.debugger;
75 const command = <T>(method: string, params?: Record<string, unknown>) => deadline<T>(
76 inspector.sendCommand(method, params), this.deps.commandTimeoutMs ?? 1500,
77 );
78 let timer: ReturnType<typeof setTimeout> | undefined;
79 const startedAt = (this.deps.now ?? (() => performance.now()))();
80 try {
81 removeInvalidated = this.deps.onInvalidated(owner.cancel);
82 if (cancelled) return { status: "cancelled" };
83 inspector.attach();
84 ownsConnection = true;
85 inspector.on("detach", onDetach);
86 await command("Profiler.enable");
87 if (cancelled) return { status: "cancelled" };
88 await command("Profiler.setSamplingInterval", { interval: 10_000 });
89 if (cancelled) return { status: "cancelled" };
90 await command("Profiler.start");
91 await Promise.race([
92 interrupted,
93 new Promise<void>((resolve) => { timer = setTimeout(resolve, this.deps.durationMs ?? 5000); }),
94 ]);
95 if (!ownsConnection || target.isDestroyed()) return { status: "cancelled" };
96 const result = await command<{ profile: CpuProfile }>("Profiler.stop");
97 if (cancelled) return { status: "cancelled" };
98 // The raw profile never enters the renderer or its report payload.
99 const frames = await this.deps.analyse(result.profile);
100 if (cancelled) return { status: "cancelled" };
101 return { status: "captured", durationMs: (this.deps.now ?? (() => performance.now()))() - startedAt, frames };
102 } catch {
103 return { status: cancelled ? "cancelled" : "failed" };
104 } finally {
105 clearTimeout(timer);
106 try { removeInvalidated(); } catch { /* Cleanup cannot strand the lease. */ }
107 if (ownsConnection) {
108 try { inspector.detach(); } catch { /* Renderer exit also ends the session. */ }
109 }
110 try { inspector.removeListener("detach", onDetach); } catch { /* A destroyed target may reject cleanup. */ }
111 if (this.active === owner) this.active = undefined;
112 }
113 }
114 }
115
116 function deadline<T>(work: Promise<T>, ms: number): Promise<T> {
117 return new Promise((resolve, reject) => {
118 const timer = setTimeout(() => reject(new Error("diagnostic command timeout")), ms);
119 work.then(resolve, reject).finally(() => clearTimeout(timer));
120 });
121 }
122
122 lines TYPESCRIPT