Skip to content

Commit 27020fd

Browse files
authored
fix: Make log exporting feature asynchronous, preventing UI freeze in the front-end (fixes #92). (#203)
1 parent 5d30e7a commit 27020fd

File tree

8 files changed

+119
-121
lines changed

8 files changed

+119
-121
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/MenuBar/ExportLogsButton.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import DownloadIcon from "@mui/icons-material/Download";
99

1010
import {StateContext} from "../../contexts/StateContextProvider";
1111
import {
12-
EXPORT_LOG_PROGRESS_VALUE_MAX,
13-
EXPORT_LOG_PROGRESS_VALUE_MIN,
12+
EXPORT_LOGS_PROGRESS_VALUE_MAX,
13+
EXPORT_LOGS_PROGRESS_VALUE_MIN,
1414
} from "../../services/LogExportManager";
1515
import {UI_ELEMENT} from "../../typings/states";
1616
import {
@@ -33,23 +33,23 @@ const ExportLogsButton = () => {
3333
className={ignorePointerIfFastLoading(uiState)}
3434
title={"Export logs"}
3535
disabled={
36-
(null !== exportProgress && EXPORT_LOG_PROGRESS_VALUE_MAX !== exportProgress) ||
36+
(null !== exportProgress && EXPORT_LOGS_PROGRESS_VALUE_MAX !== exportProgress) ||
3737
isDisabled(uiState, UI_ELEMENT.EXPORT_LOGS_BUTTON)
3838
}
3939
onClick={exportLogs}
4040
>
41-
{null === exportProgress || EXPORT_LOG_PROGRESS_VALUE_MIN === exportProgress ?
41+
{null === exportProgress || EXPORT_LOGS_PROGRESS_VALUE_MIN === exportProgress ?
4242
<DownloadIcon/> :
4343
<CircularProgress
4444
determinate={true}
4545
thickness={3}
4646
value={exportProgress * 100}
4747
variant={"solid"}
48-
color={EXPORT_LOG_PROGRESS_VALUE_MAX === exportProgress ?
48+
color={EXPORT_LOGS_PROGRESS_VALUE_MAX === exportProgress ?
4949
"success" :
5050
"primary"}
5151
>
52-
{EXPORT_LOG_PROGRESS_VALUE_MAX === exportProgress ?
52+
{EXPORT_LOGS_PROGRESS_VALUE_MAX === exportProgress ?
5353
<DownloadIcon
5454
color={"success"}
5555
sx={{fontSize: "14px"}}/> :

src/contexts/StateContextProvider.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ import React, {
1010

1111
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
1212

13-
import LogExportManager, {
14-
EXPORT_LOG_PROGRESS_VALUE_MAX,
15-
EXPORT_LOG_PROGRESS_VALUE_MIN,
16-
} from "../services/LogExportManager";
13+
import LogExportManager, {EXPORT_LOGS_PROGRESS_VALUE_MIN} from "../services/LogExportManager";
1714
import {Nullable} from "../typings/common";
1815
import {CONFIG_KEY} from "../typings/config";
1916
import {
@@ -288,9 +285,6 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
288285
if (null !== logExportManagerRef.current) {
289286
const progress = logExportManagerRef.current.appendChunk(args.logs);
290287
setExportProgress(progress);
291-
if (EXPORT_LOG_PROGRESS_VALUE_MAX === progress) {
292-
setUiState(UI_STATE.READY);
293-
}
294288
}
295289
break;
296290
case WORKER_RESP_CODE.FORMAT_POPUP:
@@ -324,9 +318,6 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
324318
case UI_STATE.FAST_LOADING:
325319
setUiState(UI_STATE.READY);
326320
break;
327-
case UI_STATE.SLOW_LOADING:
328-
setUiState(UI_STATE.READY);
329-
break;
330321
case UI_STATE.FILE_LOADING:
331322
setUiState(UI_STATE.UNOPENED);
332323
break;
@@ -389,15 +380,14 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
389380

390381
return;
391382
}
392-
setUiState(UI_STATE.SLOW_LOADING);
393-
setExportProgress(EXPORT_LOG_PROGRESS_VALUE_MIN);
383+
setExportProgress(EXPORT_LOGS_PROGRESS_VALUE_MIN);
394384
logExportManagerRef.current = new LogExportManager(
395385
Math.ceil(numEvents / EXPORT_LOGS_CHUNK_SIZE),
396386
fileName
397387
);
398388
workerPostReq(
399389
mainWorkerRef.current,
400-
WORKER_REQ_CODE.EXPORT_LOG,
390+
WORKER_REQ_CODE.EXPORT_LOGS,
401391
null
402392
);
403393
}, [

src/services/LogExportManager.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {downloadBlob} from "../utils/file";
22

33

4-
const EXPORT_LOG_PROGRESS_VALUE_MIN = 0;
5-
const EXPORT_LOG_PROGRESS_VALUE_MAX = 1;
4+
const EXPORT_LOGS_PROGRESS_VALUE_MIN = 0;
5+
const EXPORT_LOGS_PROGRESS_VALUE_MAX = 1;
66

77
/**
88
* Manager for exporting logs as a file.
@@ -40,14 +40,14 @@ class LogExportManager {
4040
if (0 === this.#numChunks) {
4141
this.#download();
4242

43-
return EXPORT_LOG_PROGRESS_VALUE_MAX;
43+
return EXPORT_LOGS_PROGRESS_VALUE_MAX;
4444
}
4545
this.#chunks.push(chunk);
4646
if (this.#chunks.length === this.#numChunks) {
4747
this.#download();
4848
this.#chunks.length = 0;
4949

50-
return EXPORT_LOG_PROGRESS_VALUE_MAX;
50+
return EXPORT_LOGS_PROGRESS_VALUE_MAX;
5151
}
5252

5353
return this.#chunks.length / this.#numChunks;
@@ -64,6 +64,6 @@ class LogExportManager {
6464

6565
export default LogExportManager;
6666
export {
67-
EXPORT_LOG_PROGRESS_VALUE_MAX,
68-
EXPORT_LOG_PROGRESS_VALUE_MIN,
67+
EXPORT_LOGS_PROGRESS_VALUE_MAX,
68+
EXPORT_LOGS_PROGRESS_VALUE_MIN,
6969
};

src/services/LogFileManager/index.ts

Lines changed: 82 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class LogFileManager {
5353

5454
readonly #onDiskFileSizeInBytes: number;
5555

56+
readonly #onExportChunk: (logs: string) => void;
57+
5658
readonly #onQueryResults: (queryProgress: number, queryResults: QueryResults) => void;
5759

5860
#decoder: Decoder;
@@ -68,19 +70,29 @@ class LogFileManager {
6870
* @param params.fileName
6971
* @param params.onDiskFileSizeInBytes
7072
* @param params.pageSize Page size for setting up pagination.
73+
* @param params.onExportChunk
7174
* @param params.onQueryResults
7275
*/
73-
constructor ({decoder, fileName, onDiskFileSizeInBytes, pageSize, onQueryResults}: {
76+
constructor ({
77+
decoder,
78+
fileName,
79+
onDiskFileSizeInBytes,
80+
pageSize,
81+
onExportChunk,
82+
onQueryResults,
83+
}: {
7484
decoder: Decoder;
7585
fileName: string;
7686
onDiskFileSizeInBytes: number;
7787
pageSize: number;
88+
onExportChunk: (logs: string) => void;
7889
onQueryResults: (queryProgress: number, queryResults: QueryResults) => void;
7990
}) {
8091
this.#decoder = decoder;
8192
this.#fileName = fileName;
8293
this.#pageSize = pageSize;
8394
this.#onDiskFileSizeInBytes = onDiskFileSizeInBytes;
95+
this.#onExportChunk = onExportChunk;
8496
this.#onQueryResults = onQueryResults;
8597

8698
// Build index for the entire file.
@@ -108,19 +120,28 @@ class LogFileManager {
108120
/**
109121
* Creates a new LogFileManager.
110122
*
111-
* @param fileSrc The source of the file to load. This can be a string representing a URL, or a
112-
* File object.
113-
* @param pageSize Page size for setting up pagination.
114-
* @param decoderOptions Initial decoder options.
115-
* @param onQueryResults
123+
* @param params
124+
* @param params.fileSrc The source of the file to load.
125+
* This can be a string representing a URL, or a File object.
126+
* @param params.pageSize Page size for setting up pagination.
127+
* @param params.decoderOptions Initial decoder options.
128+
* @param params.onExportChunk
129+
* @param params.onQueryResults
116130
* @return A Promise that resolves to the created LogFileManager instance.
117131
*/
118-
static async create (
119-
fileSrc: FileSrcType,
120-
pageSize: number,
121-
decoderOptions: DecoderOptions,
122-
onQueryResults: (queryProgress: number, queryResults: QueryResults) => void,
123-
): Promise<LogFileManager> {
132+
static async create ({
133+
fileSrc,
134+
pageSize,
135+
decoderOptions,
136+
onExportChunk,
137+
onQueryResults,
138+
}: {
139+
fileSrc: FileSrcType;
140+
pageSize: number;
141+
decoderOptions: DecoderOptions;
142+
onExportChunk: (logs: string) => void;
143+
onQueryResults: (queryProgress: number, queryResults: QueryResults) => void;
144+
}): Promise<LogFileManager> {
124145
const {fileName, fileData} = await loadFile(fileSrc);
125146
const decoder = await LogFileManager.#initDecoder(fileName, fileData, decoderOptions);
126147

@@ -130,6 +151,7 @@ class LogFileManager {
130151
onDiskFileSizeInBytes: fileData.length,
131152
pageSize: pageSize,
132153

154+
onExportChunk: onExportChunk,
133155
onQueryResults: onQueryResults,
134156
});
135157
}
@@ -187,17 +209,13 @@ class LogFileManager {
187209
}
188210

189211
/**
190-
* Loads log events in the range
191-
* [`beginLogEventIdx`, `beginLogEventIdx + EXPORT_LOGS_CHUNK_SIZE`), or all remaining log
192-
* events if `EXPORT_LOGS_CHUNK_SIZE` log events aren't available.
212+
* Exports a chunk of log events, sends the results to the renderer, and schedules the next
213+
* chunk if more log events remain.
193214
*
194215
* @param beginLogEventIdx
195-
* @return An object containing the log events as a string.
196216
* @throws {Error} if any error occurs when decoding the log events.
197217
*/
198-
loadChunk (beginLogEventIdx: number): {
199-
logs: string;
200-
} {
218+
exportChunkAndScheduleNext (beginLogEventIdx: number) {
201219
const endLogEventIdx = Math.min(beginLogEventIdx + EXPORT_LOGS_CHUNK_SIZE, this.#numEvents);
202220
const results = this.#decoder.decodeRange(
203221
beginLogEventIdx,
@@ -212,10 +230,13 @@ class LogFileManager {
212230
}
213231

214232
const messages = results.map(([msg]) => msg);
233+
this.#onExportChunk(messages.join(""));
215234

216-
return {
217-
logs: messages.join(""),
218-
};
235+
if (endLogEventIdx < this.#numEvents) {
236+
defer(() => {
237+
this.exportChunkAndScheduleNext(endLogEventIdx);
238+
});
239+
}
219240
}
220241

221242
/**
@@ -327,45 +348,6 @@ class LogFileManager {
327348
}
328349
}
329350

330-
/**
331-
* Processes decoded log events and populates the results map with matched entries.
332-
*
333-
* @param decodedEvents
334-
* @param queryRegex
335-
* @param results The map to store query results.
336-
*/
337-
#processQueryDecodedEvents (
338-
decodedEvents: DecodeResult[],
339-
queryRegex: RegExp,
340-
results: QueryResults
341-
): void {
342-
for (const [message, , , logEventNum] of decodedEvents) {
343-
const matchResult = message.match(queryRegex);
344-
if (null === matchResult || "number" !== typeof matchResult.index) {
345-
continue;
346-
}
347-
348-
const pageNum = Math.ceil(logEventNum / this.#pageSize);
349-
if (false === results.has(pageNum)) {
350-
results.set(pageNum, []);
351-
}
352-
353-
results.get(pageNum)?.push({
354-
logEventNum: logEventNum,
355-
message: message,
356-
matchRange: [
357-
matchResult.index,
358-
matchResult.index + matchResult[0].length,
359-
],
360-
});
361-
362-
this.#queryCount++;
363-
if (this.#queryCount >= MAX_QUERY_RESULT_COUNT) {
364-
break;
365-
}
366-
}
367-
}
368-
369351
/**
370352
* Queries a chunk of log events, sends the results, and schedules the next chunk query if more
371353
* log events remain.
@@ -423,6 +405,45 @@ class LogFileManager {
423405
}
424406
}
425407

408+
/**
409+
* Processes decoded log events and populates the results map with matched entries.
410+
*
411+
* @param decodedEvents
412+
* @param queryRegex
413+
* @param results The map to store query results.
414+
*/
415+
#processQueryDecodedEvents (
416+
decodedEvents: DecodeResult[],
417+
queryRegex: RegExp,
418+
results: QueryResults
419+
): void {
420+
for (const [message, , , logEventNum] of decodedEvents) {
421+
const matchResult = message.match(queryRegex);
422+
if (null === matchResult || "number" !== typeof matchResult.index) {
423+
continue;
424+
}
425+
426+
const pageNum = Math.ceil(logEventNum / this.#pageSize);
427+
if (false === results.has(pageNum)) {
428+
results.set(pageNum, []);
429+
}
430+
431+
results.get(pageNum)?.push({
432+
logEventNum: logEventNum,
433+
message: message,
434+
matchRange: [
435+
matchResult.index,
436+
matchResult.index + matchResult[0].length,
437+
],
438+
});
439+
440+
this.#queryCount++;
441+
if (this.#queryCount >= MAX_QUERY_RESULT_COUNT) {
442+
break;
443+
}
444+
}
445+
}
446+
426447
/**
427448
* Gets the data that corresponds to the cursor.
428449
*

0 commit comments

Comments
 (0)