/** * Receiver for Godot's own remote-debugger profiler stream. * 来源:Erodenn-godot-mcp-runtime/src/utils/profiler.ts 整文件移植(2026-09-11 P2 批; [上游 license:MIT,2026-10-01 GitHub API 核实,见 THIRD_PARTY_NOTICES.md] * 除文件头/import/logger 三处适配外逐行一致——该文件经真引擎验证,刻意不重写)。 * * `run_project({ profiling: true })` binds this listener first and passes * `--remote-debug tcp://127.0.0.1:` to the spawned engine, so the * measurements are the stock editor ones — no engine build, no addon, and * nothing injected into the project. Attached sessions cannot profile: the * debugger channel only exists if it was on the command line at launch. * * Godot pauses the game on a script error or `breakpoint` while a debugger is * connected, so every `debug_enter` is answered with `continue` — profiling * must never turn a runtime error into a frozen window. */ import { type Variant } from './godot-variant.js'; export type ProfilerErrorCode = 'bad_args' | 'profile_busy' | 'profile_not_started' | 'profile_timeout' | 'profile_disconnected' | 'profile_no_frames' | 'profile_bad_frame'; export declare class ProfilerError extends Error { readonly code: ProfilerErrorCode; constructor(code: ProfilerErrorCode, message: string); } export type ProfileSort = 'selfMs' | 'totalMs' | 'calls'; export declare const PROFILE_SORTS: readonly ProfileSort[]; export declare const PROFILE_MAX_SECONDS = 60; export declare const CAPTURE_LIMIT_MIN = 16; export declare const CAPTURE_LIMIT_MAX = 512; export declare const PROFILE_TOP_MAX = 100; export interface ProfilePeak { frame: number; calls: number; selfMs: number; totalMs: number; } export interface ProfileRow { signature: string; function: string; file: string; line: number; sourceResolved: boolean; calls: number; selfMs: number; totalMs: number; callsPerFrame: number; selfMsPerFrame: number; totalMsPerFrame: number; msPerCall: number; /** Inclusive share of an average frame, the editor's "Frame %" measure. */ percentOfFrame: number; peak: ProfilePeak | null; } /** Average and worst value of one per-frame measurement across the capture. */ export interface ProfileStat { avg: number; max: number; } /** The engine's own frame breakdown — the editor's "Frame Time" category. */ export interface FrameTimings { frameMs: number; processMs: number; physicsMs: number; physicsFrameMs: number; scriptMs: number; } export interface ProfileServer { name: string; msPerFrame: number; functions: Array<{ name: string; msPerFrame: number; }>; } export interface ProfileStartResult { active: boolean; maxSeconds: number; firstFrame: number | null; captureLimit: number; } export interface ProfileResult { seconds: number; frames: number; framesReceived: number; firstFrame: number | null; lastFrame: number | null; frameGaps: number; /** * Debugger packets dropped because the codec could not represent them. A * non-zero value means the capture may be missing frames it was sent. */ undecodablePackets: number; captureLimit: number; limitReached: boolean; sort: ProfileSort; functionsReceived: number; unresolvedFunctions: number; /** Per-frame engine breakdown, averaged over the capture and at its worst. */ frame: Record; /** Server-side timings (physics, audio, …), averaged per frame. */ servers: ProfileServer[]; rows: ProfileRow[]; worstFrame: ({ frame: number; } & FrameTimings & { rows: FrameRow[]; }) | null; } /** One function's numbers inside a single received frame. */ interface FrameRow { signature: string; function: string; file: string; line: number; sourceResolved: boolean; calls: number; selfMs: number; totalMs: number; } /** @internal 测试用(W10 批4 组A):导出供 parseFrame 单测直接覆盖 */ export declare function badFrame(what: string): ProfilerError; export declare function asNumber(value: Variant | undefined): number; /** * Loop bounds and element counts must be real non-negative integers. Plain * `asNumber` would accept a float — and a layout shift that lands a timing * value where a count belongs makes `for (i < 0.016)` run once instead of * throwing, walking `offset` off silently. Fail loudly on version drift. * @internal 测试用(W10 批4 组A) */ export declare function asCount(value: Variant | undefined, limit: number): number; /** Trim float noise from the summary — these are milliseconds, not physics. * @internal 测试用(W10 批4 组A) */ export declare function roundNumbers(value: T): T; /** One received frame, in the same three parts the editor's tree shows. */ interface FrameSample { frame: number; timings: FrameTimings; servers: Array<{ name: string; functions: Array<{ name: string; ms: number; }>; }>; rows: FrameRow[]; /** * Rows the engine actually sent, before zero-call rows are dropped. The * engine's `captureLimit` applies to this count, so the truncation check * has to use it rather than the filtered `rows.length`. */ rawRowCount: number; } /** * Split the engine's per-frame array. Layout: the frame number, five timing * fields, a server count, that many `name, entryCount, ...name/time pairs` * blocks, then the flattened function rows preceded by their own length. * Verified against `ServersProfilerFrame::serialize()`; `internal_time` at * `i + 4` is deliberately skipped (the editor's "internal functions" toggle). * @internal 测试用(W10 批4 组A):帧解析纯函数,布局漂移防线核心 */ export declare function parseFrame(data: Variant[], signatures: Map): FrameSample; export declare class DebuggerProfiler { private readonly server; readonly port: number; private socket; /** Pending bytes, joined only once a whole frame has arrived (see `receive`). */ private rxChunks; private rxLength; private threadId; private processId; private state; private error; private closed; private lastMessage; private lastDecodeError; private undecodable; private signatures; private capture; private autoStopTimer; private waiters; private constructor(); /** Bind a loopback listener the spawned engine will dial back into. */ static create(): Promise; /** * A finished capture is readable even once the engine is gone — `stop` only * re-ranks data already folded, and the capture worth reading is often the * one taken right before a crash. */ get hasResult(): boolean; get connected(): boolean; /** * Enable the engine profiler and return once frames are arriving. The * capture stops itself after `seconds` so a forgotten `start_profiler` * cannot profile the rest of the session. */ start(seconds: number, captureLimit: number): Promise; /** * Stop an active capture (or re-read a finished one) and rank the functions. * The engine's own accumulated totals close the capture, so this waits for * the `profile_total` packet rather than summing the last frame. */ stop(top: number, sort: ProfileSort): Promise; /** * Wait out a known capture's close and rank it. Takes the capture rather than * re-reading `this.capture` so a caller that snapshotted one cannot be handed * a different capture's numbers. */ private finish; /** `start` + wait out the window + `stop`, for a one-shot capture. */ captureWindow(seconds: number, top: number, sort: ProfileSort, captureLimit?: number): Promise; close(): void; private accept; /** Read one pending byte without joining the chunk list. */ private byteAt; private receive; private handle; private summarize; /** Read behind a call so `start`'s own state assignment doesn't narrow it. */ private isCapturing; private send; private write; private autoStop; private armAutoStop; private clearAutoStop; /** * Close a capture out. Called on the engine's `profile_total`, and again if * that packet never arrives — a capture left in `stopping` would reject every * later `start` as busy while `stop` kept timing out, and the advice on that * error points straight back at `stop`. */ private finalize; private fail; private wait; private notify; private rejectWaiters; } export {};