@@ -84,9 +84,15 @@ const STALE_FLAG_SUGGESTION_CAP = 4;
8484const STALE_FLAG_REFERENCES_PER_FLAG = 5 ;
8585const STALE_LOOKBACK_DAYS = 30 ;
8686
87- // Yields to the event loop between batches; without this, parse-heavy scans
88- // freeze IPC / UI on the main process.
89- const SCAN_BATCH_SIZE = 32 ;
87+ // Tree-sitter parse() is synchronous and runs on the main process event
88+ // loop. To keep IPC responsive we (1) yield after every file (not every
89+ // batch), (2) skip files past a size threshold — they're almost always
90+ // minified bundles or generated code where parsing buys nothing, and
91+ // (3) cap total parsed files so a monorepo (e.g. PostHog itself) doesn't
92+ // stall boot for tens of seconds. When the cap trips we fall back to
93+ // manifest-only install detection rather than failing outright.
94+ const MAX_FILE_BYTES = 256 * 1024 ;
95+ const MAX_FILES_TO_PARSE = 500 ;
9096
9197interface ParsedRepoEntry {
9298 langId : string ;
@@ -102,21 +108,6 @@ function yieldToEventLoop(): Promise<void> {
102108 return new Promise < void > ( ( resolve ) => setImmediate ( resolve ) ) ;
103109}
104110
105- async function processInBatches < T , R > (
106- items : T [ ] ,
107- batchSize : number ,
108- fn : ( item : T ) => Promise < R > ,
109- ) : Promise < R [ ] > {
110- const out : R [ ] = [ ] ;
111- for ( let i = 0 ; i < items . length ; i += batchSize ) {
112- const batch = items . slice ( i , i + batchSize ) ;
113- const batchResults = await Promise . all ( batch . map ( fn ) ) ;
114- for ( const r of batchResults ) out . push ( r ) ;
115- await yieldToEventLoop ( ) ;
116- }
117- return out ;
118- }
119-
120111function shouldSkipPath ( relPath : string ) : boolean {
121112 const parts = relPath . split ( / [ \\ / ] / ) ;
122113 return parts . some ( ( segment ) => SKIP_PATH_SEGMENTS . has ( segment ) ) ;
@@ -352,23 +343,40 @@ export class EnrichmentService {
352343 const langId = langIdMap [ ext ] ;
353344 if ( ! langId || ! enricher . isSupported ( langId ) ) continue ;
354345 toParse . push ( { relPath, langId } ) ;
346+ if ( toParse . length >= MAX_FILES_TO_PARSE ) {
347+ log . info ( "Capping repo parse to keep main process responsive" , {
348+ repoPath,
349+ totalCandidates : posthogFiles . length ,
350+ parseLimit : MAX_FILES_TO_PARSE ,
351+ } ) ;
352+ break ;
353+ }
355354 }
356355
357356 const files = new Map < string , ParsedRepoEntry > ( ) ;
358- await processInBatches ( toParse , SCAN_BATCH_SIZE , async ( candidate ) => {
357+ // Serial with a yield after every file. Tree-sitter parse() is sync CPU
358+ // on the event loop; batching with Promise.all stacked all parses in one
359+ // synchronous burst between yields, which froze IPC. Per-file yields cap
360+ // each blocking window at one file's parse cost.
361+ for ( const candidate of toParse ) {
362+ const absPath = path . join ( repoPath , candidate . relPath ) ;
359363 let content : string ;
360364 try {
361- content = await fs . readFile (
362- path . join ( repoPath , candidate . relPath ) ,
363- "utf-8" ,
364- ) ;
365+ const stat = await fs . stat ( absPath ) ;
366+ if ( stat . size > MAX_FILE_BYTES ) {
367+ files . set ( candidate . relPath , {
368+ langId : candidate . langId ,
369+ result : null ,
370+ } ) ;
371+ continue ;
372+ }
373+ content = await fs . readFile ( absPath , "utf-8" ) ;
365374 } catch {
366- return null ;
375+ continue ;
367376 }
368377 try {
369378 const result = await enricher . parse ( content , candidate . langId ) ;
370379 files . set ( candidate . relPath , { langId : candidate . langId , result } ) ;
371- return null ;
372380 } catch ( err ) {
373381 log . debug ( "enricher.parse threw during repo scan, skipping file" , {
374382 file : candidate . relPath ,
@@ -378,9 +386,9 @@ export class EnrichmentService {
378386 langId : candidate . langId ,
379387 result : null ,
380388 } ) ;
381- return null ;
382389 }
383- } ) ;
390+ await yieldToEventLoop ( ) ;
391+ }
384392
385393 const entry : ParsedRepoCacheEntry = { files, manifestHit } ;
386394 this . repoScanCache . set ( repoPath , entry ) ;
0 commit comments