1
+ /* eslint max-lines: ["error", 400] */
1
2
import {
2
3
Decoder ,
3
4
DecoderOptionsType ,
@@ -11,11 +12,16 @@ import {
11
12
CursorType ,
12
13
EMPTY_PAGE_RESP ,
13
14
FileSrcType ,
15
+ QueryResults ,
14
16
WORKER_RESP_CODE ,
15
17
WorkerResp ,
16
18
} from "../../typings/worker" ;
17
- import { EXPORT_LOGS_CHUNK_SIZE } from "../../utils/config" ;
19
+ import {
20
+ EXPORT_LOGS_CHUNK_SIZE ,
21
+ QUERY_CHUNK_SIZE ,
22
+ } from "../../utils/config" ;
18
23
import { getChunkNum } from "../../utils/math" ;
24
+ import { defer } from "../../utils/time" ;
19
25
import { formatSizeInBytes } from "../../utils/units" ;
20
26
import ClpIrDecoder from "../decoders/ClpIrDecoder" ;
21
27
import JsonlDecoder from "../decoders/JsonlDecoder" ;
@@ -31,35 +37,43 @@ import {
31
37
* Class to manage the retrieval and decoding of a given log file.
32
38
*/
33
39
class LogFileManager {
40
+ readonly #fileName: string ;
41
+
42
+ readonly #numEvents: number = 0 ;
43
+
34
44
readonly #pageSize: number ;
35
45
36
- readonly #fileName: string ;
46
+ #queryId: number = 0 ;
37
47
38
48
readonly #onDiskFileSizeInBytes: number ;
39
49
40
- #decoder: Decoder ;
50
+ readonly #onQueryResults: ( queryResults : QueryResults ) => void ;
41
51
42
- #numEvents: number = 0 ;
52
+ #decoder: Decoder ;
43
53
44
54
/**
45
55
* Private constructor for LogFileManager. This is not intended to be invoked publicly.
46
56
* Instead, use LogFileManager.create() to create a new instance of the class.
47
57
*
48
- * @param decoder
49
- * @param fileName
50
- * @param onDiskFileSizeInBytes
51
- * @param pageSize Page size for setting up pagination.
58
+ * @param params
59
+ * @param params.decoder
60
+ * @param params.fileName
61
+ * @param params.onDiskFileSizeInBytes
62
+ * @param params.pageSize Page size for setting up pagination.
63
+ * @param params.onQueryResults
52
64
*/
53
- constructor (
65
+ constructor ( { decoder , fileName , onDiskFileSizeInBytes , pageSize , onQueryResults } : {
54
66
decoder : Decoder ,
55
67
fileName : string ,
56
68
onDiskFileSizeInBytes : number ,
57
69
pageSize : number ,
58
- ) {
70
+ onQueryResults : ( queryResults : QueryResults ) => void ,
71
+ } ) {
72
+ this . #decoder = decoder ;
59
73
this . #fileName = fileName ;
60
- this . #onDiskFileSizeInBytes = onDiskFileSizeInBytes ;
61
74
this . #pageSize = pageSize ;
62
- this . #decoder = decoder ;
75
+ this . #onDiskFileSizeInBytes = onDiskFileSizeInBytes ;
76
+ this . #onQueryResults = onQueryResults ;
63
77
64
78
// Build index for the entire file.
65
79
const buildResult = decoder . build ( ) ;
@@ -90,17 +104,26 @@ class LogFileManager {
90
104
* File object.
91
105
* @param pageSize Page size for setting up pagination.
92
106
* @param decoderOptions Initial decoder options.
107
+ * @param onQueryResults
93
108
* @return A Promise that resolves to the created LogFileManager instance.
94
109
*/
95
110
static async create (
96
111
fileSrc : FileSrcType ,
97
112
pageSize : number ,
98
- decoderOptions : DecoderOptionsType
113
+ decoderOptions : DecoderOptionsType ,
114
+ onQueryResults : ( queryResults : QueryResults ) => void ,
99
115
) : Promise < LogFileManager > {
100
116
const { fileName, fileData} = await loadFile ( fileSrc ) ;
101
117
const decoder = await LogFileManager . #initDecoder( fileName , fileData , decoderOptions ) ;
102
118
103
- return new LogFileManager ( decoder , fileName , fileData . length , pageSize ) ;
119
+ return new LogFileManager ( {
120
+ decoder : decoder ,
121
+ fileName : fileName ,
122
+ onDiskFileSizeInBytes : fileData . length ,
123
+ pageSize : pageSize ,
124
+
125
+ onQueryResults : onQueryResults ,
126
+ } ) ;
104
127
}
105
128
106
129
/**
@@ -254,6 +277,86 @@ class LogFileManager {
254
277
} ;
255
278
}
256
279
280
+ /**
281
+ * Creates a RegExp object based on the given query string and options, and starts querying the
282
+ * first log chunk.
283
+ *
284
+ * @param queryString
285
+ * @param isRegex
286
+ * @param isCaseSensitive
287
+ */
288
+ startQuery ( queryString : string , isRegex : boolean , isCaseSensitive : boolean ) : void {
289
+ this . #queryId++ ;
290
+
291
+ // If the query string is empty, or there are no logs, return
292
+ if ( "" === queryString || 0 === this . #numEvents) {
293
+ return ;
294
+ }
295
+
296
+ // Construct query RegExp
297
+ const regexPattern = isRegex ?
298
+ queryString :
299
+ queryString . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
300
+ const regexFlags = isCaseSensitive ?
301
+ "" :
302
+ "i" ;
303
+ const queryRegex = new RegExp ( regexPattern , regexFlags ) ;
304
+
305
+ this . #queryChunkAndScheduleNext( this . #queryId, 0 , queryRegex ) ;
306
+ }
307
+
308
+ /**
309
+ * Queries a chunk of log events, sends the results, and schedules the next chunk query if more
310
+ * log events remain.
311
+ *
312
+ * @param queryId
313
+ * @param chunkBeginIdx
314
+ * @param queryRegex
315
+ */
316
+ #queryChunkAndScheduleNext (
317
+ queryId : number ,
318
+ chunkBeginIdx : number ,
319
+ queryRegex : RegExp
320
+ ) : void {
321
+ if ( queryId !== this . #queryId) {
322
+ // Current task no longer corresponds to the latest query in the LogFileManager.
323
+ return ;
324
+ }
325
+ const chunkEndIdx = Math . min ( chunkBeginIdx + QUERY_CHUNK_SIZE , this . #numEvents) ;
326
+ const results : QueryResults = new Map ( ) ;
327
+ const decodedEvents = this . #decoder. decodeRange (
328
+ chunkBeginIdx ,
329
+ chunkEndIdx ,
330
+ null !== this . #decoder. getFilteredLogEventMap ( )
331
+ ) ;
332
+
333
+ decodedEvents ?. forEach ( ( [ message , , , logEventNum ] ) => {
334
+ const matchResult = message . match ( queryRegex ) ;
335
+ if ( null !== matchResult && "number" === typeof matchResult . index ) {
336
+ const pageNum = Math . ceil ( logEventNum / this . #pageSize) ;
337
+ if ( false === results . has ( pageNum ) ) {
338
+ results . set ( pageNum , [ ] ) ;
339
+ }
340
+ results . get ( pageNum ) ?. push ( {
341
+ logEventNum : logEventNum ,
342
+ message : message ,
343
+ matchRange : [
344
+ matchResult . index ,
345
+ ( matchResult . index + matchResult [ 0 ] . length ) ,
346
+ ] ,
347
+ } ) ;
348
+ }
349
+ } ) ;
350
+
351
+ this . #onQueryResults( results ) ;
352
+
353
+ if ( chunkEndIdx < this . #numEvents) {
354
+ defer ( ( ) => {
355
+ this . #queryChunkAndScheduleNext( queryId , chunkEndIdx , queryRegex ) ;
356
+ } ) ;
357
+ }
358
+ }
359
+
257
360
/**
258
361
* Gets the data that corresponds to the cursor.
259
362
*
0 commit comments