@@ -193,7 +193,7 @@ function buildEnv(mockServer, { isDevBuild = true } = {}) {
193193 * @param {string } logsDir
194194 * @returns {string[] }
195195 */
196- function buildArgs ( userDataDir , extDir , logsDir , { isDevBuild = true } = { } ) {
196+ function buildArgs ( userDataDir , extDir , logsDir , { isDevBuild = true , extHostInspectPort = 0 } = { } ) {
197197 const args = [
198198 ROOT ,
199199 '--skip-release-notes' ,
@@ -213,6 +213,13 @@ function buildArgs(userDataDir, extDir, logsDir, { isDevBuild = true } = {}) {
213213 if ( process . platform !== 'darwin' ) {
214214 args . push ( '--disable-gpu' ) ;
215215 }
216+ if ( process . env . CI && process . platform === 'linux' ) {
217+ args . push ( '--no-sandbox' ) ;
218+ }
219+ // Enable extension host inspector for profiling/heap snapshots
220+ if ( extHostInspectPort > 0 ) {
221+ args . push ( `--inspect-extensions=${ extHostInspectPort } ` ) ;
222+ }
216223 return args ;
217224}
218225
@@ -228,6 +235,8 @@ function writeSettings(userDataDir, mockServer) {
228235 'github.copilot.advanced.debug.overrideProxyUrl' : mockServer . url ,
229236 'github.copilot.advanced.debug.overrideCapiUrl' : mockServer . url ,
230237 'chat.allowAnonymousAccess' : true ,
238+ // Start new chat sessions in agent mode so tools are available.
239+ 'chat.newSession.defaultMode' : 'agent' ,
231240 // Disable MCP servers — they start async and add unpredictable
232241 // delay that pollutes perf measurements.
233242 'chat.mcp.discovery.enabled' : false ,
@@ -275,6 +284,112 @@ function prepareRunDir(runId, mockServer) {
275284
276285// -- VS Code launch via CDP --------------------------------------------------
277286
287+ // -- Extension host inspector ------------------------------------------------
288+
289+ /** @type {number } */
290+ let nextExtHostPort = 29222 ;
291+
292+ /** @returns {number } */
293+ function getNextExtHostInspectPort ( ) {
294+ return nextExtHostPort ++ ;
295+ }
296+
297+ /**
298+ * Connect to the extension host's Node inspector via WebSocket.
299+ * The extension host must be started with `--inspect-extensions=<port>`.
300+ *
301+ * @param {number } port
302+ * @param {{ verbose?: boolean, timeoutMs?: number } } [opts]
303+ * @returns {Promise<{ send: (method: string, params?: any) => Promise<any>, on: (event: string, listener: (params: any) => void) => void, close: () => void, port: number }> }
304+ */
305+ async function connectToExtHostInspector ( port , opts = { } ) {
306+ const { verbose = false , timeoutMs = 30_000 } = opts ;
307+
308+ // Wait for the inspector endpoint to be available
309+ const deadline = Date . now ( ) + timeoutMs ;
310+ /** @type {any } */
311+ let wsUrl ;
312+ while ( Date . now ( ) < deadline ) {
313+ try {
314+ const targets = await getJson ( `http://127.0.0.1:${ port } /json` ) ;
315+ if ( targets . length > 0 && targets [ 0 ] . webSocketDebuggerUrl ) {
316+ wsUrl = targets [ 0 ] . webSocketDebuggerUrl ;
317+ break ;
318+ }
319+ } catch { }
320+ await new Promise ( r => setTimeout ( r , 500 ) ) ;
321+ }
322+ if ( ! wsUrl ) {
323+ throw new Error ( `Timed out waiting for extension host inspector on port ${ port } ` ) ;
324+ }
325+
326+ if ( verbose ) {
327+ console . log ( ` [ext-host] Connected to inspector: ${ wsUrl } ` ) ;
328+ }
329+
330+ const WebSocket = require ( 'ws' ) ;
331+ const ws = new WebSocket ( wsUrl ) ;
332+ await new Promise ( ( resolve , reject ) => {
333+ ws . once ( 'open' , resolve ) ;
334+ ws . once ( 'error' , reject ) ;
335+ } ) ;
336+
337+ let msgId = 1 ;
338+ /** @type {Map<number, { resolve: (v: any) => void, reject: (e: Error) => void }> } */
339+ const pending = new Map ( ) ;
340+ /** @type {Map<string, ((params: any) => void)[]> } */
341+ const eventListeners = new Map ( ) ;
342+
343+ ws . on ( 'message' , ( /** @type {Buffer } */ data ) => {
344+ const msg = JSON . parse ( data . toString ( ) ) ;
345+ if ( msg . id !== undefined ) {
346+ const p = pending . get ( msg . id ) ;
347+ if ( p ) {
348+ pending . delete ( msg . id ) ;
349+ if ( msg . error ) { p . reject ( new Error ( msg . error . message ) ) ; }
350+ else { p . resolve ( msg . result ) ; }
351+ }
352+ } else if ( msg . method ) {
353+ const listeners = eventListeners . get ( msg . method ) || [ ] ;
354+ for ( const listener of listeners ) { listener ( msg . params ) ; }
355+ }
356+ } ) ;
357+
358+ return {
359+ port,
360+ /**
361+ * @param {string } method
362+ * @param {any } [params]
363+ * @returns {Promise<any> }
364+ */
365+ send ( method , params ) {
366+ return new Promise ( ( resolve , reject ) => {
367+ const id = msgId ++ ;
368+ pending . set ( id , { resolve, reject } ) ;
369+ ws . send ( JSON . stringify ( { id, method, params } ) ) ;
370+ setTimeout ( ( ) => {
371+ if ( pending . has ( id ) ) {
372+ pending . delete ( id ) ;
373+ reject ( new Error ( `Inspector call timed out: ${ method } ` ) ) ;
374+ }
375+ } , 30_000 ) ;
376+ } ) ;
377+ } ,
378+ /**
379+ * @param {string } event
380+ * @param {(params: any) => void } listener
381+ */
382+ on ( event , listener ) {
383+ const list = eventListeners . get ( event ) || [ ] ;
384+ list . push ( listener ) ;
385+ eventListeners . set ( event , list ) ;
386+ } ,
387+ close ( ) {
388+ ws . close ( ) ;
389+ } ,
390+ } ;
391+ }
392+
278393/**
279394 * Fetch JSON from a URL. Used to probe the CDP endpoint.
280395 * @param {string } url
@@ -647,6 +762,10 @@ const METRIC_DEFS = [
647762 [ 'frameCount' , 'rendering' , '' ] ,
648763 [ 'compositeLayers' , 'rendering' , '' ] ,
649764 [ 'paintCount' , 'rendering' , '' ] ,
765+ [ 'extHostHeapUsedBefore' , 'extHost' , 'MB' ] ,
766+ [ 'extHostHeapUsedAfter' , 'extHost' , 'MB' ] ,
767+ [ 'extHostHeapDelta' , 'extHost' , 'MB' ] ,
768+ [ 'extHostHeapDeltaPostGC' , 'extHost' , 'MB' ] ,
650769] ;
651770
652771module . exports = {
@@ -670,4 +789,6 @@ module.exports = {
670789 summarize,
671790 markDuration,
672791 launchVSCode,
792+ getNextExtHostInspectPort,
793+ connectToExtHostInspector,
673794} ;
0 commit comments