github_copilot_sdk/startup_timings.rs
1//! Per-phase timing breakdown for [`Client::start`](crate::Client::start).
2//!
3//! `Client::start` performs several sequential phases between "spawn the CLI"
4//! and "client is ready to create sessions": resolving (and possibly
5//! extracting) the CLI binary, spawning the subprocess, waiting for the TCP
6//! port announcement, the `connect` protocol handshake, and the optional
7//! `sessionFs.setProvider` / `llmInference.setProvider` registration RPCs.
8//!
9//! Each phase is already measured internally with an [`Instant`] and logged at
10//! `debug`. [`StartupTimings`] aggregates those durations into a single value
11//! so a host can attribute total startup latency ("time to first token"
12//! groundwork) to a specific phase — e.g. separating "process exec cost" from
13//! "handshake/negotiation cost" — instead of reconstructing it from scattered
14//! log lines.
15//!
16//! Retrieve it after start via
17//! [`Client::startup_timings`](crate::Client::startup_timings).
18//!
19//! [`Instant`]: std::time::Instant
20
21use std::time::Duration;
22
23/// Millisecond breakdown of the phases of [`Client::start`](crate::Client::start).
24///
25/// Optional fields represent phases that do not run for every configuration:
26/// `program_resolve_ms` is `None` when the caller supplies an explicit CLI path
27/// (no resolution/extraction), `port_wait_ms` is `Some` only for the TCP
28/// transport, and `session_fs_ms` / `llm_handler_ms` are `Some` only when the
29/// corresponding option is configured. `process_spawn_ms` is `None` for
30/// transports that do not spawn a subprocess (external server, in-process FFI
31/// runtime). `transport_setup_ms`, `handshake_ms`, and `total_ms` are always
32/// populated for a value returned by
33/// [`Client::startup_timings`](crate::Client::startup_timings).
34///
35/// Durations are whole milliseconds, matching the existing `elapsed_ms`
36/// tracing fields.
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38#[non_exhaustive]
39pub struct StartupTimings {
40 /// Time spent in `resolve::copilot_binary_with_extract_dir` locating (and,
41 /// for a bundled CLI, extracting) the copilot binary. `None` when the
42 /// caller passes an explicit [`CliProgram::Path`](crate::CliProgram::Path).
43 pub program_resolve_ms: Option<u64>,
44 /// Time spent spawning the CLI subprocess (`command.spawn()`). `None` for
45 /// the external-server and in-process transports, which do not spawn a
46 /// child.
47 pub process_spawn_ms: Option<u64>,
48 /// Time spent waiting for the TCP server to announce its listening port on
49 /// stdout. `Some` only for the TCP transport.
50 pub port_wait_ms: Option<u64>,
51 /// Total transport setup time. This includes spawning and connecting to a
52 /// subprocess, connecting to an external server, or starting the in-process
53 /// FFI runtime. `process_spawn_ms` and `port_wait_ms` provide nested detail
54 /// for spawned transports.
55 pub transport_setup_ms: u64,
56 /// Time spent on the `connect` protocol handshake in
57 /// [`Client::verify_protocol_version`](crate::Client::verify_protocol_version),
58 /// including the fallback to the legacy `ping` RPC.
59 pub handshake_ms: u64,
60 /// Time spent registering the filesystem provider via
61 /// `sessionFs.setProvider`. `Some` only when
62 /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs) is set.
63 pub session_fs_ms: Option<u64>,
64 /// Time spent registering the LLM inference provider via
65 /// `llmInference.setProvider`. `Some` only when
66 /// [`ClientOptions::request_handler`](crate::ClientOptions::request_handler)
67 /// is set.
68 pub llm_handler_ms: Option<u64>,
69 /// Total wall-clock time for [`Client::start`](crate::Client::start), from
70 /// entry to the client being ready. Always present.
71 pub total_ms: u64,
72}
73
74impl StartupTimings {
75 /// Whole milliseconds of `duration`, saturating at [`u64::MAX`].
76 pub(crate) fn millis(duration: Duration) -> u64 {
77 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn millis_truncates_to_whole_milliseconds() {
87 assert_eq!(StartupTimings::millis(Duration::from_micros(1_999)), 1);
88 assert_eq!(StartupTimings::millis(Duration::from_millis(250)), 250);
89 assert_eq!(StartupTimings::millis(Duration::ZERO), 0);
90 }
91
92 #[test]
93 fn default_leaves_every_phase_unset() {
94 let timings = StartupTimings::default();
95 assert_eq!(timings, StartupTimings::default());
96 assert!(timings.program_resolve_ms.is_none());
97 assert!(timings.process_spawn_ms.is_none());
98 assert!(timings.port_wait_ms.is_none());
99 assert_eq!(timings.transport_setup_ms, 0);
100 assert_eq!(timings.handshake_ms, 0);
101 assert!(timings.session_fs_ms.is_none());
102 assert!(timings.llm_handler_ms.is_none());
103 assert_eq!(timings.total_ms, 0);
104 }
105}