querysub 0.357.0 → 0.359.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/.cursorrules +1 -0
  2. package/package.json +2 -1
  3. package/src/-a-archives/archivesDisk.ts +24 -6
  4. package/src/-a-archives/archivesMemoryCache.ts +41 -17
  5. package/src/deployManager/components/MachineDetailPage.tsx +45 -4
  6. package/src/deployManager/components/MachinesListPage.tsx +10 -2
  7. package/src/deployManager/components/ServiceDetailPage.tsx +13 -3
  8. package/src/deployManager/components/ServicesListPage.tsx +18 -6
  9. package/src/deployManager/machineApplyMainCode.ts +3 -3
  10. package/src/deployManager/machineSchema.ts +39 -0
  11. package/src/diagnostics/NodeViewer.tsx +2 -1
  12. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +124 -123
  13. package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +83 -1
  14. package/src/diagnostics/logs/IndexedLogs/BufferListStreamer.ts +2 -0
  15. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +21 -24
  16. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +1 -1
  17. package/src/diagnostics/logs/IndexedLogs/FilePathSelector.tsx +186 -25
  18. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +284 -195
  19. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +312 -108
  20. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +1 -1
  21. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +37 -7
  22. package/src/diagnostics/logs/errorNotifications2/errorNotifications2.ts +0 -0
  23. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +62 -35
  24. package/src/diagnostics/logs/lifeCycleAnalysis/test.ts +0 -180
  25. package/src/functional/limitProcessing.ts +39 -0
@@ -7,7 +7,7 @@ import { InputLabel, InputLabelURL } from "../../../library-components/InputLabe
7
7
  import { getLoggers2, getLoggers2Async, LogDatum } from "../diskLogger";
8
8
  import { list, timeInDay, keyByArray, sort, throttleFunction } from "socket-function/src/misc";
9
9
  import { formatDateTime, formatDateTimeDetailed, formatNumber, formatTime, formatPercent } from "socket-function/src/formatting/format";
10
- import { IndexedLogResults, IndexedLogs, TimeFilePathWithSize } from "./IndexedLogs";
10
+ import { IndexedLogs, TimeFilePathWithSize } from "./IndexedLogs";
11
11
  import { Querysub } from "../../../4-querysub/QuerysubController";
12
12
  import { URLParam } from "../../../library-components/URLParam";
13
13
  import { getOwnMachineId, getOwnThreadId } from "../../../-f-node-discovery/NodeDiscovery";
@@ -21,6 +21,8 @@ import { ObjectDisplay } from "../ObjectDisplay";
21
21
  import { formatDateJSX } from "../../../misc/formatJSX";
22
22
  import { PUBLIC_MOVE_THRESHOLD } from "./BufferIndexLogsOptimizationConstants";
23
23
  import { atomic } from "../../../2-proxy/PathValueProxyWatcher";
24
+ import { errorToUndefined } from "querysub/src/errors";
25
+ import { IndexedLogResults, createEmptyIndexedLogResults } from "./BufferIndexHelpers";
24
26
 
25
27
  let searchText = new URLParam("searchText", "");
26
28
  let readLiveData = new URLParam("readLiveData", false);
@@ -31,9 +33,6 @@ let savedPathsURL = new URLParam("savedPaths", "");
31
33
  let selectedFieldsURL = new URLParam("selectedFields", {} as Record<string, boolean>);
32
34
  let useRelativeTimeURL = new URLParam("useRelativeTime", true);
33
35
 
34
- // Only auto-search if the string is long enough. Otherwise, we're going to get way too many results, which could slow down the server and won't be useful to us.
35
- const MIN_AUTO_SEARCH_LENGTH = 7;
36
-
37
36
  const defaultSelectedFields = {
38
37
  param0: true,
39
38
  time: true,
@@ -43,13 +42,61 @@ const defaultSelectedFields = {
43
42
  __entry: true,
44
43
  };
45
44
 
46
- function createTestLog(text: string, type: "log" | "warn" | "info" | "error"): LogDatum {
45
+ function mergeIndexedLogResults(existing: IndexedLogResults, incoming: IndexedLogResults): IndexedLogResults {
46
+ let readsByKey = new Map<string, typeof existing.reads[0]>();
47
+
48
+ for (let read of existing.reads) {
49
+ let key = `${read.cached}-${read.remote}`;
50
+ let existingRead = readsByKey.get(key);
51
+ if (existingRead) {
52
+ existingRead.count += read.count;
53
+ existingRead.size += read.size;
54
+ existingRead.totalSize = Math.max(existingRead.totalSize, read.totalSize);
55
+ existingRead.totalCount = Math.max(existingRead.totalCount, read.totalCount);
56
+ } else {
57
+ readsByKey.set(key, { ...read });
58
+ }
59
+ }
60
+
61
+ for (let read of incoming.reads) {
62
+ let key = `${read.cached}-${read.remote}`;
63
+ let existingRead = readsByKey.get(key);
64
+ if (existingRead) {
65
+ existingRead.count += read.count;
66
+ existingRead.size += read.size;
67
+ existingRead.totalSize = Math.max(existingRead.totalSize, read.totalSize);
68
+ existingRead.totalCount = Math.max(existingRead.totalCount, read.totalCount);
69
+ } else {
70
+ readsByKey.set(key, { ...read });
71
+ }
72
+ }
73
+
47
74
  return {
48
- time: 0,
49
- message: text,
50
- __LOG_TYPE: type,
51
- __machineId: getOwnMachineId(),
52
- __threadId: getOwnThreadId(),
75
+ matchCount: existing.matchCount + incoming.matchCount,
76
+ totalLocalFiles: existing.totalLocalFiles + incoming.totalLocalFiles,
77
+ totalBackblazeFiles: existing.totalBackblazeFiles + incoming.totalBackblazeFiles,
78
+ reads: Array.from(readsByKey.values()),
79
+ localFilesSearched: existing.localFilesSearched + incoming.localFilesSearched,
80
+ backblazeFilesSearched: existing.backblazeFilesSearched + incoming.backblazeFilesSearched,
81
+ totalBlockCount: existing.totalBlockCount + incoming.totalBlockCount,
82
+ blockCheckedCount: existing.blockCheckedCount + incoming.blockCheckedCount,
83
+ blocksCheckedCompressedSize: existing.blocksCheckedCompressedSize + incoming.blocksCheckedCompressedSize,
84
+ blocksCheckedDecompressedSize: existing.blocksCheckedDecompressedSize + incoming.blocksCheckedDecompressedSize,
85
+ blockErrors: [...existing.blockErrors, ...incoming.blockErrors],
86
+ fileErrors: [...existing.fileErrors, ...incoming.fileErrors],
87
+ remoteIndexesSearched: existing.remoteIndexesSearched + incoming.remoteIndexesSearched,
88
+ remoteIndexSize: existing.remoteIndexSize + incoming.remoteIndexSize,
89
+ localIndexesSearched: existing.localIndexesSearched + incoming.localIndexesSearched,
90
+ localIndexSize: existing.localIndexSize + incoming.localIndexSize,
91
+ timeToFirstMatch: Math.min(existing.timeToFirstMatch === 0 ? Infinity : existing.timeToFirstMatch, incoming.timeToFirstMatch === 0 ? Infinity : incoming.timeToFirstMatch),
92
+ fileFindTime: existing.fileFindTime + incoming.fileFindTime,
93
+ indexSearchTime: existing.indexSearchTime + incoming.indexSearchTime,
94
+ blockSearchTime: existing.blockSearchTime + incoming.blockSearchTime,
95
+ totalSearchTime: Math.max(existing.totalSearchTime, incoming.totalSearchTime),
96
+ remoteBlockCount: existing.remoteBlockCount + incoming.remoteBlockCount,
97
+ localBlockCount: existing.localBlockCount + incoming.localBlockCount,
98
+ remoteBlockCheckedCount: existing.remoteBlockCheckedCount + incoming.remoteBlockCheckedCount,
99
+ localBlockCheckedCount: existing.localBlockCheckedCount + incoming.localBlockCheckedCount,
53
100
  };
54
101
  }
55
102
 
@@ -60,10 +107,7 @@ export class LogViewer3 extends qreact.Component {
60
107
  enableInfos: t.boolean(true),
61
108
  enableWarnings: t.boolean(true),
62
109
  enableErrors: t.boolean(true),
63
- results: t.atomic<LogDatum[]>([
64
- { "time": 1771825252649.44, "__LOG_TYPE": "log", "__machineId": "1ed7a7340a015680", "__mountId": "1d590f0919b1f438.1ed7a7340a015680.querysubtest.com:7007", "__threadId": "1d590f0919b1f438", "__port": 7007, "__nodeId": "1d590f0919b1f438.1ed7a7340a015680.querysubtest.com:7007", "__entry": "D:\\repos\\qs-cyoa\\src\\server.ts", "__hostName": "DESKTOP-05MP449", "__accountName": "desktop-05mp449\\quent", "__externalIP": "99.250.124.91", "__os": "win32", "__pid": 75888, "param0": "new non-local WATCH", "path": ".,querysubtest._com.,PathFunctionRunner.,book.,Data.,edittingBooks.,hkwnmoiwhuyqenbi.,books.,bf0713806bf1c4338._querysubtest._com_1755390476654._5208_1.,nodes.,1770378259589._5535.,aiCalls.,", "watcher": "client:127.0.0.1:1771825250819.3994:0.634106584461968", "diskAudit": true },
65
- { "time": 1771825252649.4397, "__LOG_TYPE": "log", "__machineId": "1ed7a7340a015680", "__mountId": "1d590f0919b1f438.1ed7a7340a015680.querysubtest.com:7007", "__threadId": "x1d590f0919b1f438", "__port": 7007, "__nodeId": "1d590f0919b1f438.1ed7a7340a015680.querysubtest.com:7007", "__entry": "D:\\repos\\qs-cyoa\\src\\server.ts", "__hostName": "DESKTOP-05MP449", "__accountName": "desktop-05mp449\\quent", "__externalIP": "99.250.124.91", "__os": "win32", "__pid": 75888, "param0": "new non-local WATCH", "path": ".,querysubtest._com.,PathFunctionRunner.,book.,Data.,edittingBooks.,hkwnmoiwhuyqenbi.,books.,bf0713806bf1c4338._querysubtest._com_1755390476654._5208_1.,nodes.,1770378259589._5535.,totalTime.,", "watcher": "client:127.0.0.1:1771825250819.3994:0.634106584461968", "diskAudit": true },
66
- ]),
110
+ results: t.atomic<LogDatum[]>([]),
67
111
  searching: t.boolean,
68
112
  searchingLogs: t.boolean,
69
113
  searchingInfos: t.boolean,
@@ -72,8 +116,13 @@ export class LogViewer3 extends qreact.Component {
72
116
  paths: t.atomic<TimeFilePathWithSize[]>([]),
73
117
  loadingPaths: t.boolean,
74
118
  stats: t.atomic<IndexedLogResults | undefined>(undefined),
119
+ hasSearched: t.boolean(false),
120
+ forceMoveStartTime: t.atomic<number | undefined>(undefined),
121
+ forceMoveEndTime: t.atomic<number | undefined>(undefined),
75
122
  });
76
123
 
124
+ private searchSequenceNumber = 0;
125
+
77
126
  componentDidMount(): void {
78
127
  void this.loadPaths();
79
128
  }
@@ -118,20 +167,29 @@ export class LogViewer3 extends qreact.Component {
118
167
 
119
168
  let totalSizeRead = 0;
120
169
  let cachedSize = 0;
170
+ let cachedCount = 0;
121
171
  let uncachedSize = 0;
122
172
  let uncachedCount = 0;
123
173
  let uncachedRemoteSize = 0;
124
174
  let uncachedRemoteCount = 0;
125
175
  let totalSize = 0;
176
+ let remoteTotalSize = 0;
177
+ let localTotalSize = 0;
126
178
 
127
179
  for (let read of stats.reads) {
128
180
  totalSizeRead += read.size;
129
181
  if (read.cached) {
130
182
  cachedSize += read.size;
183
+ cachedCount += read.count;
131
184
  } else {
132
185
  uncachedSize += read.size;
133
186
  uncachedCount += read.count;
134
187
  totalSize += read.totalSize;
188
+ if (read.remote) {
189
+ remoteTotalSize += read.totalSize;
190
+ } else {
191
+ localTotalSize += read.totalSize;
192
+ }
135
193
  }
136
194
  if (read.remote && !read.cached) {
137
195
  uncachedRemoteSize += read.size;
@@ -161,21 +219,28 @@ export class LogViewer3 extends qreact.Component {
161
219
  };
162
220
 
163
221
  const fileItems = [
164
- `${formatTime(stats.fileFindTime)} | ${formatNumber(stats.localFilesSearched + stats.backblazeFilesSearched)} | ${formatNumber(totalSize)}B`,
165
- `pending ${formatNumber(stats.localFilesSearched)}`,
222
+ `${formatTime(stats.fileFindTime)} | ${formatNumber(stats.localFilesSearched + stats.backblazeFilesSearched)} / ${formatNumber(stats.totalLocalFiles + stats.totalBackblazeFiles)} | ${formatNumber(totalSize)}B`,
223
+ `remote ${formatNumber(stats.backblazeFilesSearched)} / ${formatNumber(stats.totalBackblazeFiles)} | ${formatNumber(remoteTotalSize)}B`,
224
+ `pending ${formatNumber(stats.localFilesSearched)} / ${formatNumber(stats.totalLocalFiles)} | ${formatNumber(localTotalSize)}B`,
166
225
  ];
167
226
 
227
+ const totalIndexesSearched = stats.remoteIndexesSearched + stats.localIndexesSearched;
228
+ const totalIndexSize = stats.remoteIndexSize + stats.localIndexSize;
168
229
  const indexItems = [
169
- `${formatTime(stats.indexSearchTime)} | ${formatNumber(stats.indexesSearched)} | ${formatNumber(stats.indexSize)}B`,
230
+ `${formatTime(stats.indexSearchTime)} | ${formatNumber(totalIndexesSearched)} | ${formatNumber(totalIndexSize)}B | ${formatNumber(totalSize / totalIndexSize)}X`,
231
+ `remote ${formatNumber(stats.remoteIndexesSearched)} | ${formatNumber(stats.remoteIndexSize)}B | ${formatNumber(remoteTotalSize / stats.remoteIndexSize)}X`,
232
+ `local ${formatNumber(stats.localIndexesSearched)} | ${formatNumber(stats.localIndexSize)}B | ${formatNumber(localTotalSize / stats.localIndexSize)}X`,
170
233
  ];
171
234
 
172
235
  const blockItems = [
173
- `${formatTime(stats.blockSearchTime)} | ${formatNumber(stats.blockCheckedCount)} | ${formatNumber(stats.blocksCheckedCompressedSize)}B | ${formatNumber(stats.blocksCheckedDecompressedSize / stats.blocksCheckedCompressedSize)}X`,
174
- `total blocks ${formatNumber(stats.totalBlockCount)}`,
236
+ <div title={`Scanned size = ${formatNumber(stats.blocksCheckedCompressedSize)}B, Decompressed size = ${formatNumber(stats.blocksCheckedDecompressedSize)}B, Compression ratio = ${formatNumber(stats.blocksCheckedDecompressedSize / stats.blocksCheckedCompressedSize)}X`}>{formatTime(stats.blockSearchTime)} | {formatNumber(stats.blockCheckedCount)} | {formatNumber(stats.blocksCheckedCompressedSize)}B | {formatNumber(stats.blocksCheckedDecompressedSize / stats.blocksCheckedCompressedSize)}X</div>,
237
+ `total scanned ${formatNumber(stats.totalBlockCount)} (${formatPercent(stats.blockCheckedCount / stats.totalBlockCount)})`,
238
+ `remote ${formatNumber(stats.remoteBlockCheckedCount)} / ${formatNumber(stats.remoteBlockCount)} (${formatPercent(stats.remoteBlockCheckedCount / stats.remoteBlockCount)}) | local ${formatNumber(stats.localBlockCheckedCount)} / ${formatNumber(stats.localBlockCount)} (${formatPercent(stats.localBlockCheckedCount / stats.localBlockCount)})`,
175
239
  ];
176
240
  let cacheItems = [
177
- `disk read ${formatNumber(uncachedSize)}B`,
241
+ `disk read ${formatNumber(uncachedSize)}B (${formatNumber(uncachedCount)})`,
178
242
  `remote ${formatPercent(uncachedRemoteSize / totalSizeRead)} (${formatNumber(uncachedRemoteCount)})`,
243
+ `cached ${formatNumber(cachedSize)} (${formatNumber(cachedCount)})`,
179
244
  ];
180
245
 
181
246
  const resultItems = [
@@ -194,14 +259,15 @@ export class LogViewer3 extends qreact.Component {
194
259
  {renderStage("Files", fileItems, 290, true, false)}
195
260
  {renderStage("Indexes", indexItems, 250, false, false)}
196
261
  {renderStage("Blocks", blockItems, 210, false, false)}
197
- {renderStage("Cache", cacheItems, 160, false, false)}
262
+ {renderStage("Reads", cacheItems, 160, false, false)}
198
263
  {renderStage("Results", resultItems, 120, false, false)}
199
264
  {renderStage("Done", doneItems, undefined, false, !hasErrors)}
200
265
  {(() => {
201
266
  if (!hasErrors) return undefined;
202
267
  let errorItems: preact.ComponentChild[] = [];
203
268
  if (stats.fileErrors.length > 0) {
204
- errorItems.push(<div title={stats.fileErrors.join("\n")}>{stats.fileErrors.length} files failed</div>);
269
+ const errorTitle = stats.fileErrors.map(e => `${e.path}:\n${e.error}`).join("\n\n");
270
+ errorItems.push(<div title={errorTitle}>{stats.fileErrors.length} files failed</div>);
205
271
  }
206
272
  if (stats.blockErrors.length > 0) {
207
273
  errorItems.push(<div title={stats.blockErrors.join("\n")}>{stats.blockErrors.length} blocks failed</div>);
@@ -217,7 +283,7 @@ export class LogViewer3 extends qreact.Component {
217
283
  const now = Date.now();
218
284
  const threshold = now - (2 * PUBLIC_MOVE_THRESHOLD);
219
285
 
220
- const machineWarnings: Record<string, { machineId: string; oldestTime: number }> = {};
286
+ const machineWarnings: Record<string, { machineId: string; oldestTime: number; count: number; totalSize: number }> = {};
221
287
 
222
288
  for (const path of paths) {
223
289
  if (path.logCount !== undefined) continue;
@@ -227,26 +293,102 @@ export class LogViewer3 extends qreact.Component {
227
293
  if (!path.machineId) continue;
228
294
 
229
295
  const existing = machineWarnings[path.machineId];
230
- if (!existing || path.startTime < existing.oldestTime) {
296
+ if (!existing) {
231
297
  machineWarnings[path.machineId] = {
232
298
  machineId: path.machineId,
233
299
  oldestTime: path.startTime,
300
+ count: 1,
301
+ totalSize: path.size || 0,
234
302
  };
303
+ } else {
304
+ if (path.startTime < existing.oldestTime) {
305
+ existing.oldestTime = path.startTime;
306
+ }
307
+ existing.count++;
308
+ existing.totalSize += path.size || 0;
235
309
  }
236
310
  }
237
311
 
238
312
  const warnings = Object.values(machineWarnings);
239
313
  if (warnings.length === 0) return undefined;
240
314
 
315
+ const isFrozen = !!savedPathsURL.value;
316
+
241
317
  return (
242
- <div className={css.vbox(10).pad2(10).hsl(40, 50, 50).colorhsl(60, 50, 100)}>
243
- <div className={css.fontWeight(600)}>Machines not moving logs to remote storage:</div>
244
- {warnings.map((warning) => (
245
- <div key={warning.machineId} className={css.hbox(10)}>
246
- <MachineThreadInfo machineId={warning.machineId} />
247
- <span>is not moving logs to remote storage. Logs are {formatTime(now - warning.oldestTime)} old</span>
248
- </div>
249
- ))}
318
+ <div className={css.vbox(10).pad2(10).hsl(0, 50, 50).colorhsl(60, 50, 100)}>
319
+ {isFrozen && (
320
+ <>
321
+ <div className={css.fontWeight(600)}>Frozen files are too old:</div>
322
+ {warnings.map((warning) => (
323
+ <div key={warning.machineId} className={css.hbox(10)}>
324
+ <MachineThreadInfo machineId={warning.machineId} />
325
+ <span>has {formatNumber(warning.count)} pending files ({formatNumber(warning.totalSize)}B) that are {formatTime(now - warning.oldestTime)} old</span>
326
+ </div>
327
+ ))}
328
+ <Button
329
+ onClick={() => {
330
+ this.clearFrozenPaths();
331
+ void this.loadPaths();
332
+ }}
333
+ hue={180}
334
+ >
335
+ Clear Frozen Files and Reload
336
+ </Button>
337
+ </>
338
+ )}
339
+ {!isFrozen && (
340
+ <>
341
+ <div className={css.fontWeight(600)}>FIX pending logs not being merged!</div>
342
+ {warnings.map((warning) => (
343
+ <div key={warning.machineId} className={css.hbox(10)}>
344
+ <MachineThreadInfo machineId={warning.machineId} />
345
+ <span>is not moving logs to remote storage. {formatNumber(warning.count)} files ({formatNumber(warning.totalSize)}B) are {formatTime(now - warning.oldestTime)} old</span>
346
+ </div>
347
+ ))}
348
+ <Button
349
+ onClick={async () => {
350
+ Querysub.commitLocal(() => {
351
+ this.state.forceMoveStartTime = Date.now();
352
+ this.state.forceMoveEndTime = undefined;
353
+ });
354
+
355
+ let forceMoveInterval = setInterval(() => {
356
+ this.forceUpdate();
357
+ }, 100);
358
+
359
+ try {
360
+ let loggers = await getLoggers2Async();
361
+ for (let logger of Object.values(loggers)) {
362
+ await logger.clientForceMoveLogsToPublic();
363
+ }
364
+ await this.loadPaths();
365
+ } finally {
366
+ if (forceMoveInterval) {
367
+ clearInterval(forceMoveInterval);
368
+ }
369
+
370
+ Querysub.commitLocal(() => {
371
+ this.state.forceMoveEndTime = Date.now();
372
+ });
373
+ }
374
+ }}
375
+ hue={180}
376
+ >
377
+ Force Move Logs to Public
378
+ </Button>
379
+ {(() => {
380
+ if (this.state.forceMoveStartTime !== undefined && this.state.forceMoveEndTime === undefined) {
381
+ const elapsed = Date.now() - this.state.forceMoveStartTime;
382
+ return <div>Moving logs... {formatTime(elapsed)}</div>;
383
+ }
384
+ if (this.state.forceMoveStartTime !== undefined && this.state.forceMoveEndTime !== undefined) {
385
+ const duration = this.state.forceMoveEndTime - this.state.forceMoveStartTime;
386
+ return <div>Moved logs in {formatTime(duration)}</div>;
387
+ }
388
+ return undefined;
389
+ })()}
390
+ </>
391
+ )}
250
392
  </div>
251
393
  );
252
394
  }
@@ -256,7 +398,9 @@ export class LogViewer3 extends qreact.Component {
256
398
  return;
257
399
  }
258
400
 
259
- this.state.loadingPaths = true;
401
+ Querysub.commitLocal(() => {
402
+ this.state.loadingPaths = true;
403
+ });
260
404
 
261
405
  let loggers = await getLoggers2Async();
262
406
  let selectedLoggers: typeof loggers.logLogs[] = [];
@@ -293,8 +437,21 @@ export class LogViewer3 extends qreact.Component {
293
437
  });
294
438
  }
295
439
 
440
+ cancel = async () => {
441
+ this.searchSequenceNumber++;
442
+ let loggers = await getLoggers2Async();
443
+ for (let logger of Object.values(loggers)) {
444
+ logger.clientCancelAllCallbacks();
445
+ }
446
+ Querysub.commitLocal(() => {
447
+ this.state.searching = false;
448
+ this.state.hasSearched = false;
449
+ });
450
+ };
451
+
296
452
 
297
- search = throttleFunction(200, async () => {
453
+ search = async () => {
454
+ await this.cancel();
298
455
  Querysub.commitLocal(() => {
299
456
  this.state.searching = true;
300
457
  this.state.results = [];
@@ -305,15 +462,6 @@ export class LogViewer3 extends qreact.Component {
305
462
  this.state.searchingErrors = false;
306
463
  });
307
464
 
308
- let startTime = Date.now();
309
-
310
- let hasPaths = Querysub.localRead(() => this.getPaths().length > 0);
311
- let getFilesTime = 0;
312
- if (readLiveData.value || !hasPaths) {
313
- let startTime = Date.now();
314
- await this.loadPaths();
315
- getFilesTime = Date.now() - startTime;
316
- }
317
465
 
318
466
  let loggers = await getLoggers2Async();
319
467
 
@@ -337,6 +485,20 @@ export class LogViewer3 extends qreact.Component {
337
485
  }
338
486
  });
339
487
 
488
+ this.searchSequenceNumber++;
489
+ let currentSequenceNumber = this.searchSequenceNumber;
490
+
491
+ let startTime = Date.now();
492
+
493
+ let hasPaths = Querysub.localRead(() => this.getPaths().length > 0);
494
+ let getFilesTime = 0;
495
+ if (readLiveData.value || !hasPaths) {
496
+ let startTime = Date.now();
497
+ await this.loadPaths();
498
+ getFilesTime = Date.now() - startTime;
499
+ }
500
+
501
+
340
502
  if (selectedLoggers.length === 0) {
341
503
  console.error("No log sources selected");
342
504
  Querysub.commitLocal(() => {
@@ -347,25 +509,7 @@ export class LogViewer3 extends qreact.Component {
347
509
 
348
510
  let searchBuffer = Querysub.localRead(() => Buffer.from(searchText.value, "utf8"));
349
511
  let results: LogDatum[] = [];
350
- let stats: IndexedLogResults = {
351
- matchCount: 0,
352
- reads: [],
353
- localFilesSearched: 0,
354
- backblazeFilesSearched: 0,
355
- totalBlockCount: 0,
356
- blockCheckedCount: 0,
357
- blocksCheckedCompressedSize: 0,
358
- blocksCheckedDecompressedSize: 0,
359
- blockErrors: [],
360
- fileErrors: [],
361
- indexesSearched: 0,
362
- indexSize: 0,
363
- timeToFirstMatch: 0,
364
- fileFindTime: 0,
365
- indexSearchTime: 0,
366
- blockSearchTime: 0,
367
- totalSearchTime: 0,
368
- };
512
+ let stats: IndexedLogResults = createEmptyIndexedLogResults();
369
513
  stats.fileFindTime += getFilesTime;
370
514
 
371
515
  let paths = Querysub.localRead(() => this.getPaths());
@@ -373,13 +517,33 @@ export class LogViewer3 extends qreact.Component {
373
517
  let range = Querysub.localRead(() => getTimeRange());
374
518
 
375
519
  let updateResults = throttleFunction(100, () => {
520
+ if (this.searchSequenceNumber !== currentSequenceNumber) return;
376
521
  Querysub.commitLocal(() => {
377
522
  this.state.results = results;
378
523
  });
379
524
  });
380
525
 
526
+ let loggerResults = new Map<typeof loggers.logLogs, IndexedLogResults>();
527
+
528
+ let updateStats = () => {
529
+ if (this.searchSequenceNumber !== currentSequenceNumber) return;
530
+
531
+ let mergedStats: IndexedLogResults = createEmptyIndexedLogResults();
532
+
533
+ for (let loggerResult of loggerResults.values()) {
534
+ mergedStats = mergeIndexedLogResults(mergedStats, loggerResult);
535
+ }
536
+
537
+ mergedStats.totalSearchTime = Date.now() - startTime;
538
+
539
+ Querysub.commitLocal(() => {
540
+ this.state.stats = mergedStats;
541
+ });
542
+ };
543
+
381
544
  await Promise.all(selectedLoggers.map(async (logger) => {
382
- let result = await logger.clientFind({
545
+ let done = false;
546
+ let result = await errorToUndefined(logger.clientFind({
383
547
  params: {
384
548
  startTime: range.startTime,
385
549
  endTime: range.endTime,
@@ -392,25 +556,20 @@ export class LogViewer3 extends qreact.Component {
392
556
  results.push(match);
393
557
  void updateResults();
394
558
  },
395
- });
396
- stats.reads.push(...result.reads);
397
- stats.matchCount += result.matchCount;
398
- stats.localFilesSearched += result.localFilesSearched;
399
- stats.backblazeFilesSearched += result.backblazeFilesSearched;
400
- stats.totalBlockCount += result.totalBlockCount;
401
- stats.blockCheckedCount += result.blockCheckedCount;
402
- stats.blocksCheckedCompressedSize += result.blocksCheckedCompressedSize;
403
- stats.blocksCheckedDecompressedSize += result.blocksCheckedDecompressedSize;
404
- stats.indexesSearched += result.indexesSearched;
405
- stats.indexSize += result.indexSize;
406
- stats.timeToFirstMatch = Math.min(stats.timeToFirstMatch, result.timeToFirstMatch);
407
- stats.fileFindTime += result.fileFindTime;
408
- stats.indexSearchTime += result.indexSearchTime;
409
- stats.blockSearchTime += result.blockSearchTime;
410
- stats.totalSearchTime = Date.now() - startTime;
559
+ onResults: (loggerStats: IndexedLogResults) => {
560
+ if (done) return;
561
+ loggerResults.set(logger, loggerStats);
562
+ updateStats();
563
+ },
564
+ }));
565
+ done = true;
566
+
567
+ if (result) {
568
+ loggerResults.set(logger, result);
569
+ updateStats();
570
+ }
571
+
411
572
  Querysub.commitLocal(() => {
412
- this.state.results = results;
413
- this.state.stats = stats;
414
573
  if (logger === loggers.logLogs) this.state.searchingLogs = false;
415
574
  if (logger === loggers.infoLogs) this.state.searchingInfos = false;
416
575
  if (logger === loggers.warnLogs) this.state.searchingWarnings = false;
@@ -418,14 +577,13 @@ export class LogViewer3 extends qreact.Component {
418
577
  });
419
578
  }));
420
579
 
421
- stats.totalSearchTime = Date.now() - startTime;
422
580
 
423
581
  Querysub.commitLocal(() => {
424
582
  this.state.results = results;
425
583
  this.state.searching = false;
426
- this.state.stats = stats;
584
+ this.state.hasSearched = true;
427
585
  });
428
- });
586
+ };
429
587
 
430
588
  renderResults() {
431
589
  let fieldNames = new Set<string>();
@@ -534,7 +692,7 @@ export class LogViewer3 extends qreact.Component {
534
692
  />
535
693
  </div>
536
694
 
537
- {this.state.results.length === 0 && !this.state.searching && <div className={css.hsl(40, 50, 50).colorhsl(60, 50, 100).boldStyle.pad2(10).ellipsis}>
695
+ {this.state.results.length === 0 && !this.state.searching && this.state.hasSearched && <div className={css.hsl(40, 50, 50).colorhsl(60, 50, 100).boldStyle.pad2(10).ellipsis}>
538
696
  No logs matched, try adjusting your search or time range.
539
697
  </div>}
540
698
 
@@ -629,11 +787,6 @@ export class LogViewer3 extends qreact.Component {
629
787
  fillWidth
630
788
  focusOnMount
631
789
  url={searchText}
632
- onChangeValue={(value) => {
633
- if (value.length >= MIN_AUTO_SEARCH_LENGTH) {
634
- void this.search();
635
- }
636
- }}
637
790
  onKeyDown={(e) => {
638
791
  if (e.key === "Enter") {
639
792
  searchText.value = e.currentTarget.value;
@@ -668,27 +821,78 @@ export class LogViewer3 extends qreact.Component {
668
821
  >
669
822
  Search
670
823
  </Button>
671
- </div>
672
-
673
- {this.state.loadingPaths && <div>Loading paths...</div>}
674
- {this.state.searching && <div>Searching...</div>}
675
-
676
- {this.getPaths().length > 0 && (
677
- <div className={css.hbox(10).fillWidth}>
678
- <FilePathSelector
679
- paths={this.getPaths()}
680
- onChange={(paths) => {
681
- this.state.paths = paths;
682
- }}
683
- />
824
+ {this.state.searching && (
684
825
  <Button
685
- onClick={() => this.freezePaths()}
686
- hue={180}
826
+ flavor="large"
827
+ onClick={() => void this.cancel()}
828
+ hue={60}
687
829
  >
688
- Freeze Files ({this.state.paths.length})
830
+ Cancel
689
831
  </Button>
690
- </div>
691
- )}
832
+ )}
833
+ </div>
834
+
835
+ <div className={css.hbox(10).fillWidth}>
836
+ <FilePathSelector
837
+ paths={this.getPaths()}
838
+ onChange={(paths) => {
839
+ this.state.paths = paths;
840
+ this.freezePaths();
841
+ }}
842
+ fileErrors={this.state.stats?.fileErrors}
843
+ />
844
+ <Button
845
+ onClick={() => {
846
+ if (savedPathsURL.value) {
847
+ this.clearFrozenPaths();
848
+ void this.loadPaths();
849
+ } else {
850
+ this.freezePaths();
851
+ }
852
+ }}
853
+ hue={savedPathsURL.value ? 0 : 180}
854
+ >
855
+ {savedPathsURL.value ? `Clear Frozen Files (${this.getPaths().length})` : `Freeze Files (${this.state.paths.length})`}
856
+ </Button>
857
+ {!savedPathsURL.value && (
858
+ <div className={css.pad2(8, 4).hsl(40, 60, 50).colorhsl(40, 0, 100)}>
859
+ Files frozen in memory. Click Preview Files to refresh.
860
+ </div>
861
+ )}
862
+ {(() => {
863
+ if (!savedPathsURL.value) return undefined;
864
+ const paths = this.getPaths();
865
+ const pendingCount = paths.filter(x => x.logCount === undefined).length;
866
+ if (pendingCount === 0) return undefined;
867
+ return (
868
+ <Button
869
+ onClick={() => {
870
+ const paths = this.getPaths();
871
+ const nonPendingPaths = paths.filter(x => x.logCount !== undefined);
872
+ const json = JSON.stringify(nonPendingPaths);
873
+ const buffer = Buffer.from(json, "utf8");
874
+ const compressed = LZ4.compress(buffer);
875
+ savedPathsURL.value = compressed.toString("base64");
876
+ }}
877
+ hue={40}
878
+ >
879
+ Remove Pending ({pendingCount})
880
+ </Button>
881
+ );
882
+ })()}
883
+
884
+ {this.state.loadingPaths && <div>Loading paths...</div>}
885
+ {this.state.searching && (() => {
886
+ let searchingSources: string[] = [];
887
+ if (this.state.searchingLogs) searchingSources.push("Logs");
888
+ if (this.state.searchingInfos) searchingSources.push("Infos");
889
+ if (this.state.searchingWarnings) searchingSources.push("Warnings");
890
+ if (this.state.searchingErrors) searchingSources.push("Errors");
891
+ return <div className={css.pad2(4, 2).hsl(120, 40, 50).colorhsl(120, 0, 100)}>
892
+ Searching: {searchingSources.join(", ")}
893
+ </div>;
894
+ })()}
895
+ </div>
692
896
 
693
897
  {this.renderPendingLogWarnings()}
694
898
 
@@ -70,7 +70,7 @@ const KEY_MAPPING: Record<string, { field: keyof Omit<TimeFilePath, "fullPath">;
70
70
  "compressed": { field: "compressedSize", parseAsNumber: true },
71
71
  };
72
72
 
73
- function decodeLogFilePath(path: string): TimeFilePath | undefined {
73
+ export function decodeLogFilePath(path: string): TimeFilePath | undefined {
74
74
  if (!path.endsWith(LOG_FILE_EXTENSION)) {
75
75
  return undefined;
76
76
  }