Skip to main content

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}