@@ -40,6 +40,8 @@ pub mod session;
4040/// Custom session filesystem provider (virtualizable filesystem layer).
4141pub mod session_fs;
4242mod session_fs_dispatch;
43+ /// Per-phase timing breakdown for [`Client::start`].
44+ pub mod startup_timings;
4345/// Event subscription handles returned by `subscribe()` methods.
4446pub mod subscription;
4547/// Typed tool definition framework and dispatch router.
@@ -106,6 +108,7 @@ pub use types::*;
106108
107109mod sdk_protocol_version;
108110pub use sdk_protocol_version:: { SDK_PROTOCOL_VERSION , get_sdk_protocol_version} ;
111+ pub use startup_timings:: StartupTimings ;
109112pub use subscription:: { EventSubscription , LifecycleSubscription } ;
110113
111114/// Minimum protocol version this SDK can communicate with.
@@ -1007,6 +1010,10 @@ struct ClientInner {
10071010 /// SDK [`ClientMode`] captured at start time. Drives empty-mode safe
10081011 /// defaults inside `create_session` / `resume_session`.
10091012 pub ( crate ) mode : ClientMode ,
1013+ /// Per-phase startup timing breakdown, populated once at the end of
1014+ /// [`Client::start`]. Empty for clients built via [`Client::from_streams`]
1015+ /// or [`Client::from_transport`] directly.
1016+ startup_timings : OnceLock < StartupTimings > ,
10101017}
10111018
10121019impl Client {
@@ -1024,6 +1031,7 @@ impl Client {
10241031 /// backend.
10251032 pub async fn start ( options : ClientOptions ) -> Result < Self > {
10261033 let start_time = Instant :: now ( ) ;
1034+ let mut timings = StartupTimings :: default ( ) ;
10271035 let mut options = options;
10281036 if matches ! ( options. transport, Transport :: Default ) {
10291037 options. transport = resolve_default_transport ( & options) ?;
@@ -1119,9 +1127,16 @@ impl Client {
11191127 path. clone ( )
11201128 }
11211129 CliProgram :: Resolve => {
1130+ let resolve_start = Instant :: now ( ) ;
11221131 let resolved = resolve:: copilot_binary_with_extract_dir (
11231132 options. bundled_cli_extract_dir . as_deref ( ) ,
11241133 ) ?;
1134+ let resolve_elapsed = resolve_start. elapsed ( ) ;
1135+ timings. program_resolve_ms = Some ( StartupTimings :: millis ( resolve_elapsed) ) ;
1136+ debug ! (
1137+ elapsed_ms = resolve_elapsed. as_millis( ) ,
1138+ "Client::start CLI program resolution complete"
1139+ ) ;
11251140 info ! ( path = %resolved. display( ) , "resolved copilot CLI" ) ;
11261141 #[ cfg( windows) ]
11271142 {
@@ -1183,8 +1198,10 @@ impl Client {
11831198 port,
11841199 connection_token : _,
11851200 } => {
1186- let ( mut child, actual_port) =
1201+ let ( mut child, actual_port, spawn_elapsed , port_wait_elapsed ) =
11871202 Self :: spawn_tcp ( & program, & options, & working_directory, port) . await ?;
1203+ timings. process_spawn_ms = Some ( StartupTimings :: millis ( spawn_elapsed) ) ;
1204+ timings. port_wait_ms = Some ( StartupTimings :: millis ( port_wait_elapsed) ) ;
11881205 let connect_start = Instant :: now ( ) ;
11891206 let stream = TcpStream :: connect ( ( "127.0.0.1" , actual_port) ) . await ?;
11901207 debug ! (
@@ -1209,7 +1226,9 @@ impl Client {
12091226 ) ?
12101227 }
12111228 Transport :: Stdio => {
1212- let mut child = Self :: spawn_stdio ( & program, & options, & working_directory) ?;
1229+ let ( mut child, spawn_elapsed) =
1230+ Self :: spawn_stdio ( & program, & options, & working_directory) ?;
1231+ timings. process_spawn_ms = Some ( StartupTimings :: millis ( spawn_elapsed) ) ;
12131232 let stdin = child. stdin . take ( ) . expect ( "stdin is piped" ) ;
12141233 let stdout = child. stdout . take ( ) . expect ( "stdout is piped" ) ;
12151234 Self :: drain_stderr ( & mut child) ;
@@ -1294,7 +1313,9 @@ impl Client {
12941313 elapsed_ms = start_time. elapsed( ) . as_millis( ) ,
12951314 "Client::start transport setup complete"
12961315 ) ;
1316+ let handshake_start = Instant :: now ( ) ;
12971317 client. verify_protocol_version ( ) . await ?;
1318+ timings. handshake_ms = Some ( StartupTimings :: millis ( handshake_start. elapsed ( ) ) ) ;
12981319 debug ! (
12991320 elapsed_ms = start_time. elapsed( ) . as_millis( ) ,
13001321 "Client::start protocol verification complete"
@@ -1313,8 +1334,10 @@ impl Client {
13131334 session_state_path : cfg. session_state_path ,
13141335 } ;
13151336 client. rpc ( ) . session_fs ( ) . set_provider ( request) . await ?;
1337+ let session_fs_elapsed = session_fs_start. elapsed ( ) ;
1338+ timings. session_fs_ms = Some ( StartupTimings :: millis ( session_fs_elapsed) ) ;
13161339 debug ! (
1317- elapsed_ms = session_fs_start . elapsed ( ) . as_millis( ) ,
1340+ elapsed_ms = session_fs_elapsed . as_millis( ) ,
13181341 "Client::start session filesystem setup complete"
13191342 ) ;
13201343 }
@@ -1334,11 +1357,28 @@ impl Client {
13341357 client. inner . on_github_telemetry . clone ( ) ,
13351358 ) ;
13361359 client. rpc ( ) . llm_inference ( ) . set_provider ( ) . await ?;
1360+ let llm_inference_elapsed = llm_inference_start. elapsed ( ) ;
1361+ timings. llm_handler_ms = Some ( StartupTimings :: millis ( llm_inference_elapsed) ) ;
13371362 debug ! (
1338- elapsed_ms = llm_inference_start . elapsed ( ) . as_millis( ) ,
1363+ elapsed_ms = llm_inference_elapsed . as_millis( ) ,
13391364 "Client::start Copilot request handler registration complete"
13401365 ) ;
13411366 }
1367+ timings. total_ms = Some ( StartupTimings :: millis ( start_time. elapsed ( ) ) ) ;
1368+ // Single structured event with the full per-phase breakdown, so hosts
1369+ // can attribute startup latency to a phase without stitching together
1370+ // the individual debug lines above.
1371+ debug ! (
1372+ program_resolve_ms = ?timings. program_resolve_ms,
1373+ process_spawn_ms = ?timings. process_spawn_ms,
1374+ port_wait_ms = ?timings. port_wait_ms,
1375+ handshake_ms = ?timings. handshake_ms,
1376+ session_fs_ms = ?timings. session_fs_ms,
1377+ llm_handler_ms = ?timings. llm_handler_ms,
1378+ total_ms = ?timings. total_ms,
1379+ "Client::start timings"
1380+ ) ;
1381+ let _ = client. inner . startup_timings . set ( timings) ;
13421382 debug ! (
13431383 elapsed_ms = start_time. elapsed( ) . as_millis( ) ,
13441384 "Client::start complete"
@@ -1507,6 +1547,7 @@ impl Client {
15071547 on_get_trace_context,
15081548 effective_connection_token,
15091549 mode,
1550+ startup_timings : OnceLock :: new ( ) ,
15101551 } ) ,
15111552 } ;
15121553 client. spawn_lifecycle_dispatcher ( ) ;
@@ -1683,7 +1724,7 @@ impl Client {
16831724 program : & Path ,
16841725 options : & ClientOptions ,
16851726 working_directory : & Path ,
1686- ) -> Result < Child > {
1727+ ) -> Result < ( Child , Duration ) > {
16871728 info ! ( cwd = ?working_directory, program = %program. display( ) , "spawning copilot CLI (stdio)" ) ;
16881729 let mut command = Self :: build_command ( program, options, working_directory) ;
16891730 command
@@ -1696,19 +1737,20 @@ impl Client {
16961737 . stdin ( Stdio :: piped ( ) ) ;
16971738 let spawn_start = Instant :: now ( ) ;
16981739 let child = command. spawn ( ) ?;
1740+ let spawn_elapsed = spawn_start. elapsed ( ) ;
16991741 debug ! (
1700- elapsed_ms = spawn_start . elapsed ( ) . as_millis( ) ,
1742+ elapsed_ms = spawn_elapsed . as_millis( ) ,
17011743 "Client::spawn_stdio subprocess spawned"
17021744 ) ;
1703- Ok ( child)
1745+ Ok ( ( child, spawn_elapsed ) )
17041746 }
17051747
17061748 async fn spawn_tcp (
17071749 program : & Path ,
17081750 options : & ClientOptions ,
17091751 working_directory : & Path ,
17101752 port : u16 ,
1711- ) -> Result < ( Child , u16 ) > {
1753+ ) -> Result < ( Child , u16 , Duration , Duration ) > {
17121754 info ! ( cwd = ?working_directory, program = %program. display( ) , port = %port, "spawning copilot CLI (tcp)" ) ;
17131755 let mut command = Self :: build_command ( program, options, working_directory) ;
17141756 command
@@ -1721,8 +1763,9 @@ impl Client {
17211763 . stdin ( Stdio :: null ( ) ) ;
17221764 let spawn_start = Instant :: now ( ) ;
17231765 let mut child = command. spawn ( ) ?;
1766+ let spawn_elapsed = spawn_start. elapsed ( ) ;
17241767 debug ! (
1725- elapsed_ms = spawn_start . elapsed ( ) . as_millis( ) ,
1768+ elapsed_ms = spawn_elapsed . as_millis( ) ,
17261769 "Client::spawn_tcp subprocess spawned"
17271770 ) ;
17281771 let stdout = child. stdout . take ( ) . expect ( "stdout is piped" ) ;
@@ -1759,13 +1802,14 @@ impl Client {
17591802 . map_err ( |_| Error :: from ( ErrorKind :: Protocol ( ProtocolErrorKind :: CliStartupTimeout ) ) ) ?
17601803 . map_err ( |_| Error :: from ( ErrorKind :: Protocol ( ProtocolErrorKind :: CliStartupFailed ) ) ) ?;
17611804
1805+ let port_wait_elapsed = port_wait_start. elapsed ( ) ;
17621806 debug ! (
1763- elapsed_ms = port_wait_start . elapsed ( ) . as_millis( ) ,
1807+ elapsed_ms = port_wait_elapsed . as_millis( ) ,
17641808 port = actual_port,
17651809 "Client::spawn_tcp TCP port wait complete"
17661810 ) ;
17671811 info ! ( port = %actual_port, "CLI server listening" ) ;
1768- Ok ( ( child, actual_port) )
1812+ Ok ( ( child, actual_port, spawn_elapsed , port_wait_elapsed ) )
17691813 }
17701814
17711815 fn drain_stderr ( child : & mut Child ) {
@@ -1942,6 +1986,16 @@ impl Client {
19421986 self . inner . negotiated_protocol_version . get ( ) . copied ( )
19431987 }
19441988
1989+ /// Returns the per-phase [`StartupTimings`] breakdown captured during
1990+ /// [`start`](Self::start), if available.
1991+ ///
1992+ /// Returns `None` for clients created via
1993+ /// [`from_streams`](Self::from_streams), which bypasses the timed startup
1994+ /// sequence.
1995+ pub fn startup_timings ( & self ) -> Option < StartupTimings > {
1996+ self . inner . startup_timings . get ( ) . cloned ( )
1997+ }
1998+
19451999 /// Verify the CLI server's protocol version is within the supported range.
19462000 ///
19472001 /// Called automatically by [`start`](Self::start). Call manually after
@@ -3112,6 +3166,7 @@ mod tests {
31123166 on_get_trace_context : None ,
31133167 effective_connection_token : None ,
31143168 mode : ClientMode :: default ( ) ,
3169+ startup_timings : OnceLock :: new ( ) ,
31153170 } ) ,
31163171 }
31173172 }
0 commit comments