@@ -26,15 +26,14 @@ interface TelemetryLogFile {
2626 readonly part : number ;
2727}
2828
29- interface TelemetryEventEntry {
29+ interface QueuedTelemetryEvent {
3030 readonly event : TelemetryEvent ;
31- readonly file : TelemetryLogFile ;
32- readonly lineNumber : number ;
31+ readonly timestampMs : number ;
3332}
3433
3534interface EventCursor {
36- readonly entry : TelemetryEventEntry ;
37- readonly iterator : AsyncIterator < TelemetryEventEntry > ;
35+ readonly queued : QueuedTelemetryEvent ;
36+ readonly iterator : AsyncIterator < TelemetryEvent > ;
3837}
3938
4039/** Log files whose dates could overlap `range`. */
@@ -62,41 +61,33 @@ export async function listTelemetryFilesForRange(
6261 . map ( ( { path : filePath } ) => filePath ) ;
6362}
6463
65- /** Merge per-session append streams by timestamp, buffering one event per session. */
64+ /**
65+ * Merge range-filtered, per-session event streams by timestamp. This stays
66+ * globally sorted while each session's timestamps are monotonic; if a session's
67+ * clock moves backward, that session's append order is preserved.
68+ */
6669export async function * streamTelemetryEventsSorted (
6770 filePaths : readonly string [ ] ,
6871 range : TelemetryDateRange ,
6972) : AsyncIterable < TelemetryEvent > {
73+ const iterators : Array < AsyncIterator < TelemetryEvent > > = [ ] ;
7074 const frontier : EventCursor [ ] = [ ] ;
71- for ( const files of sessionFileGroups ( filePaths ) ) {
72- const iterator = streamTelemetryEventEntries ( files , range ) [
73- Symbol . asyncIterator
74- ] ( ) ;
75- const next = await iterator . next ( ) ;
76- if ( ! next . done ) {
77- frontier . push ( { entry : next . value , iterator } ) ;
78- }
79- }
80-
81- while ( frontier . length > 0 ) {
82- frontier . sort ( ( a , b ) => compareEventEntries ( a . entry , b . entry ) ) ;
83- const cursor = frontier . shift ( ) ;
84- if ( ! cursor ) {
85- return ;
86- }
87- yield cursor . entry . event ;
88-
89- const next = await cursor . iterator . next ( ) ;
90- if ( ! next . done ) {
91- frontier . push ( { entry : next . value , iterator : cursor . iterator } ) ;
75+ try {
76+ await seedFrontier ( frontier , iterators , filePaths , range ) ;
77+ while ( frontier . length > 0 ) {
78+ const cursor = takeNextCursor ( frontier ) ;
79+ yield cursor . queued . event ;
80+ await advanceCursor ( frontier , cursor ) ;
9281 }
82+ } finally {
83+ await closeIterators ( iterators ) ;
9384 }
9485}
9586
96- async function * streamTelemetryEventEntries (
87+ async function * streamTelemetryEventsFromFiles (
9788 files : readonly TelemetryLogFile [ ] ,
9889 range : TelemetryDateRange ,
99- ) : AsyncIterable < TelemetryEventEntry > {
90+ ) : AsyncIterable < TelemetryEvent > {
10091 for ( const file of files ) {
10192 const name = path . basename ( file . path ) ;
10293 const stream = createReadStream ( file . path , { encoding : "utf8" } ) ;
@@ -113,7 +104,7 @@ async function* streamTelemetryEventEntries(
113104 }
114105 const event = parseTelemetryEventLine ( line , name , lineNumber ) ;
115106 if ( isTimestampInRange ( event . timestamp , range ) ) {
116- yield { event, file , lineNumber } ;
107+ yield event ;
117108 }
118109 }
119110 } catch ( err ) {
@@ -135,23 +126,22 @@ async function* streamTelemetryEventEntries(
135126 }
136127}
137128
138- function sessionFileGroups ( filePaths : readonly string [ ] ) : TelemetryLogFile [ ] [ ] {
139- const groups = new Map < string , TelemetryLogFile [ ] > ( ) ;
140- for ( const file of parseLogFilePaths ( filePaths ) . sort ( compareLogFiles ) ) {
141- const group = groups . get ( file . session ) ;
142- if ( group ) {
143- group . push ( file ) ;
144- } else {
145- groups . set ( file . session , [ file ] ) ;
146- }
147- }
148- return [ ...groups . values ( ) ] ;
129+ function groupFilesBySession (
130+ filePaths : readonly string [ ] ,
131+ ) : TelemetryLogFile [ ] [ ] {
132+ const files = parseLogFilePaths ( filePaths ) . sort ( compareLogFiles ) ;
133+ return [ ...Map . groupBy ( files , ( file ) => file . session ) . values ( ) ] ;
149134}
150135
151136function parseLogFilePaths ( filePaths : readonly string [ ] ) : TelemetryLogFile [ ] {
152- return filePaths . flatMap ( ( filePath ) => {
137+ return filePaths . map ( ( filePath ) => {
153138 const file = parseLogFilePath ( filePath ) ;
154- return file ? [ file ] : [ ] ;
139+ if ( ! file ) {
140+ throw new Error (
141+ `Invalid telemetry file path: ${ path . basename ( filePath ) } ` ,
142+ ) ;
143+ }
144+ return file ;
155145 } ) ;
156146}
157147
@@ -168,15 +158,68 @@ function compareLogFiles(a: TelemetryLogFile, b: TelemetryLogFile): number {
168158 ) ;
169159}
170160
171- function compareEventEntries (
172- a : TelemetryEventEntry ,
173- b : TelemetryEventEntry ,
161+ async function seedFrontier (
162+ frontier : EventCursor [ ] ,
163+ iterators : Array < AsyncIterator < TelemetryEvent > > ,
164+ filePaths : readonly string [ ] ,
165+ range : TelemetryDateRange ,
166+ ) : Promise < void > {
167+ for ( const files of groupFilesBySession ( filePaths ) ) {
168+ const iterator = streamTelemetryEventsFromFiles ( files , range ) [
169+ Symbol . asyncIterator
170+ ] ( ) ;
171+ iterators . push ( iterator ) ;
172+ await advanceCursor ( frontier , { iterator } ) ;
173+ }
174+ }
175+
176+ async function advanceCursor (
177+ frontier : EventCursor [ ] ,
178+ cursor : Pick < EventCursor , "iterator" > ,
179+ ) : Promise < void > {
180+ const next = await cursor . iterator . next ( ) ;
181+ if ( ! next . done ) {
182+ frontier . push ( {
183+ queued : queueEvent ( next . value ) ,
184+ iterator : cursor . iterator ,
185+ } ) ;
186+ }
187+ }
188+
189+ async function closeIterators (
190+ iterators : ReadonlyArray < AsyncIterator < TelemetryEvent > > ,
191+ ) : Promise < void > {
192+ await Promise . allSettled (
193+ iterators . map ( async ( iterator ) => {
194+ await iterator . return ?.( ) ;
195+ } ) ,
196+ ) ;
197+ }
198+
199+ function queueEvent ( event : TelemetryEvent ) : QueuedTelemetryEvent {
200+ return { event, timestampMs : parseTelemetryTimestampMs ( event . timestamp ) } ;
201+ }
202+
203+ function takeNextCursor ( frontier : EventCursor [ ] ) : EventCursor {
204+ let nextIndex = 0 ;
205+ for ( let i = 1 ; i < frontier . length ; i += 1 ) {
206+ if (
207+ compareQueuedEvents ( frontier [ i ] . queued , frontier [ nextIndex ] . queued ) < 0
208+ ) {
209+ nextIndex = i ;
210+ }
211+ }
212+ const cursor = frontier [ nextIndex ] ;
213+ frontier . splice ( nextIndex , 1 ) ;
214+ return cursor ;
215+ }
216+
217+ function compareQueuedEvents (
218+ a : QueuedTelemetryEvent ,
219+ b : QueuedTelemetryEvent ,
174220) : number {
175- const timestamp =
176- parseTelemetryTimestampMs ( a . event . timestamp ) -
177- parseTelemetryTimestampMs ( b . event . timestamp ) ;
178221 return (
179- timestamp ||
222+ a . timestampMs - b . timestampMs ||
180223 a . event . context . sessionId . localeCompare ( b . event . context . sessionId )
181224 ) ;
182225}
0 commit comments