querysub 0.356.0 → 0.357.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 (70) hide show
  1. package/.cursorrules +8 -0
  2. package/bin/movelogs.js +4 -0
  3. package/package.json +12 -6
  4. package/scripts/postinstall.js +23 -0
  5. package/src/-a-archives/archiveCache.ts +10 -12
  6. package/src/-a-archives/archives.ts +29 -0
  7. package/src/-a-archives/archivesBackBlaze.ts +60 -12
  8. package/src/-a-archives/archivesDisk.ts +27 -8
  9. package/src/-a-archives/archivesLimitedCache.ts +21 -0
  10. package/src/-a-archives/archivesMemoryCache.ts +350 -0
  11. package/src/-a-archives/archivesPrivateFileSystem.ts +22 -0
  12. package/src/-g-core-values/NodeCapabilities.ts +3 -0
  13. package/src/0-path-value-core/auditLogs.ts +5 -1
  14. package/src/0-path-value-core/pathValueCore.ts +7 -7
  15. package/src/4-dom/qreact.tsx +1 -0
  16. package/src/4-querysub/Querysub.ts +1 -5
  17. package/src/config.ts +5 -0
  18. package/src/diagnostics/MachineThreadInfo.tsx +235 -0
  19. package/src/diagnostics/NodeViewer.tsx +3 -2
  20. package/src/diagnostics/logs/FastArchiveAppendable.ts +79 -42
  21. package/src/diagnostics/logs/FastArchiveController.ts +102 -63
  22. package/src/diagnostics/logs/FastArchiveViewer.tsx +36 -8
  23. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +461 -0
  24. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.cpp +327 -0
  25. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.d.ts +18 -0
  26. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.js +1 -0
  27. package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +140 -0
  28. package/src/diagnostics/logs/IndexedLogs/BufferIndexLogsOptimizationConstants.ts +22 -0
  29. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat +1145 -0
  30. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat.d.ts +178 -0
  31. package/src/diagnostics/logs/IndexedLogs/BufferListStreamer.ts +206 -0
  32. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +719 -0
  33. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +146 -0
  34. package/src/diagnostics/logs/IndexedLogs/FilePathSelector.tsx +408 -0
  35. package/src/diagnostics/logs/IndexedLogs/FindProgressTracker.ts +45 -0
  36. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +598 -0
  37. package/src/diagnostics/logs/IndexedLogs/LogStreamer.ts +47 -0
  38. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +702 -0
  39. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +236 -0
  40. package/src/diagnostics/logs/IndexedLogs/binding.gyp +23 -0
  41. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +221 -0
  42. package/src/diagnostics/logs/IndexedLogs/moveLogsEntry.ts +10 -0
  43. package/src/diagnostics/logs/LogViewer2.tsx +120 -55
  44. package/src/diagnostics/logs/TimeRangeSelector.tsx +5 -2
  45. package/src/diagnostics/logs/diskLogger.ts +32 -48
  46. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +3 -2
  47. package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +1 -0
  48. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePages.tsx +150 -0
  49. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +132 -15
  50. package/src/diagnostics/logs/lifeCycleAnalysis/test.ts +180 -0
  51. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat +106 -0
  52. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat.d.ts +2 -0
  53. package/src/diagnostics/logs/lifeCycleAnalysis/testHoist.ts +5 -0
  54. package/src/diagnostics/logs/logViewerExtractField.ts +2 -3
  55. package/src/diagnostics/managementPages.tsx +10 -0
  56. package/src/diagnostics/trackResources.ts +1 -1
  57. package/src/misc/lz4_wasm_nodejs.d.ts +34 -0
  58. package/src/misc/lz4_wasm_nodejs.js +178 -0
  59. package/src/misc/lz4_wasm_nodejs_bg.js +94 -0
  60. package/src/misc/lz4_wasm_nodejs_bg.wasm +0 -0
  61. package/src/misc/lz4_wasm_nodejs_bg.wasm.d.ts +15 -0
  62. package/src/storage/CompressedStream.ts +13 -0
  63. package/src/storage/LZ4.ts +32 -0
  64. package/src/storage/ZSTD.ts +10 -0
  65. package/src/wat/watCompiler.ts +1716 -0
  66. package/src/wat/watGrammar.pegjs +93 -0
  67. package/src/wat/watHandler.ts +179 -0
  68. package/src/wat/watInstructions.txt +707 -0
  69. package/src/zip.ts +3 -89
  70. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +0 -125
@@ -0,0 +1,146 @@
1
+ import { formatPercent, formatNumber } from "socket-function/src/formatting/format";
2
+ import { red } from "socket-function/src/formatting/logColors";
3
+ import { measureFnc } from "socket-function/src/profiling/measure";
4
+ import { Unit } from "./BufferIndexHelpers";
5
+
6
+ export class UnitSet {
7
+ // Hash-based set for checking unit membership (no positions stored)
8
+ // Similar to the hash table approach in UnitRefList.encode, but only stores presence
9
+
10
+ @measureFnc
11
+ static encode(blocks: Buffer[][]): Buffer {
12
+ const MAX_FILL_RATIO = 0.65;
13
+
14
+ // First pass: count total units
15
+ let totalUnits = 0;
16
+ for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
17
+ const block = blocks[blockIndex];
18
+ for (let bufferIndex = 0; bufferIndex < block.length; bufferIndex++) {
19
+ const buffer = block[bufferIndex];
20
+ if (buffer.length >= 4) {
21
+ totalUnits += buffer.length - 3;
22
+ }
23
+ }
24
+ }
25
+
26
+ if (totalUnits === 0) {
27
+ return Buffer.from(new Uint32Array([0]).buffer);
28
+ }
29
+
30
+ function tryEncodeWithRatio(estimatedUniqueRatio: number): { hashTable: Uint32Array; collisions: number; totalInserts: number; } | undefined {
31
+ const HASH_FACTOR = 2;
32
+ const estimatedUnique = Math.max(Math.ceil(totalUnits / estimatedUniqueRatio), 16);
33
+ const desiredCapacity = estimatedUnique * HASH_FACTOR;
34
+ const hashTableCount = Math.pow(2, Math.ceil(Math.log2(desiredCapacity)));
35
+
36
+ const maxUniqueCount = Math.floor(hashTableCount * MAX_FILL_RATIO);
37
+
38
+ // Each entry is just a single uint32 (the unit value, 0 means empty)
39
+ const hashTable = new Uint32Array(hashTableCount);
40
+
41
+ function getHashIndex(unit: number): number {
42
+ const hash = Math.imul(unit, 2654435761);
43
+ return (hash >>> 16) & (hashTableCount - 1);
44
+ }
45
+
46
+ function getNextIndex(index: number): number {
47
+ return (index + 1) & (hashTableCount - 1);
48
+ }
49
+
50
+ let uniqueCount = 0;
51
+ let collisions = 0;
52
+ let totalInserts = 0;
53
+
54
+ // Iterate and add to hash table directly (no first pass)
55
+ for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
56
+ const block = blocks[blockIndex];
57
+ for (let bufferIndex = 0; bufferIndex < block.length; bufferIndex++) {
58
+ const buffer = block[bufferIndex];
59
+ // Extract units directly
60
+ for (let i = 0; i <= buffer.length - 4; i++) {
61
+ const unit = buffer.readUint32LE(i);
62
+ if (!unit) continue;
63
+
64
+ totalInserts++;
65
+ let index = getHashIndex(unit);
66
+ // Linear probing
67
+ while (true) {
68
+ if (hashTable[index] === 0) {
69
+ // Empty slot - insert
70
+ hashTable[index] = unit;
71
+ uniqueCount++;
72
+ if (uniqueCount > maxUniqueCount) {
73
+ return undefined; // Failed, exceeded fill ratio
74
+ }
75
+ break;
76
+ }
77
+ if (hashTable[index] === unit) {
78
+ // Already present
79
+ break;
80
+ }
81
+ // Collision, probe next
82
+ collisions++;
83
+ index = getNextIndex(index);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ return { hashTable, collisions, totalInserts };
90
+ }
91
+
92
+ // Try with increasingly conservative ratios until we succeed
93
+ let ratio = 1000;
94
+ let result: ReturnType<typeof tryEncodeWithRatio>;
95
+ while (true) {
96
+ result = tryEncodeWithRatio(ratio);
97
+ if (result) break;
98
+ ratio /= 2;
99
+ }
100
+
101
+ const collisionRate = result.totalInserts > 0 ? result.collisions / result.totalInserts : 0;
102
+
103
+ if (collisionRate > 0.5) {
104
+ console.warn(red(`WARNING: UnitSet collision rate is over 50%: ${formatPercent(collisionRate)}`));
105
+ }
106
+
107
+ // Store capacity as first uint32, followed by the hash table
108
+ const output = new Uint32Array(1 + result.hashTable.length);
109
+ output[0] = result.hashTable.length;
110
+ output.set(result.hashTable, 1);
111
+
112
+ return Buffer.from(output.buffer);
113
+ }
114
+
115
+ @measureFnc
116
+ static has(data: Buffer, unit: Unit): boolean {
117
+ const hashTableCount = data.readUInt32LE(0);
118
+
119
+ if (hashTableCount === 0) return false;
120
+
121
+ function getHashIndex(unit: number): number {
122
+ const hash = Math.imul(unit, 2654435761);
123
+ return (hash >>> 16) & (hashTableCount - 1);
124
+ }
125
+
126
+ function getNextIndex(index: number): number {
127
+ return (index + 1) & (hashTableCount - 1);
128
+ }
129
+
130
+ let index = getHashIndex(unit);
131
+ const maxProbes = hashTableCount;
132
+
133
+ for (let probes = 0; probes < maxProbes; probes++) {
134
+ const value = data.readUInt32LE((1 + index) * 4);
135
+ if (value === 0) return false; // Empty slot, unit not present
136
+ if (value === unit) return true; // Found it
137
+ index = getNextIndex(index);
138
+ }
139
+
140
+ return false; // Shouldn't reach here if hash table isn't full
141
+ }
142
+ }
143
+
144
+
145
+
146
+
@@ -0,0 +1,408 @@
1
+ import { formatDateTime, formatNumber } from "socket-function/src/formatting/format";
2
+ import { css } from "typesafecss";
3
+ import { t } from "../../../2-proxy/schema2";
4
+ import { qreact } from "../../../4-dom/qreact";
5
+ import { Button } from "../../../library-components/Button";
6
+ import { TimeFilePathWithSize } from "./IndexedLogs";
7
+ import { keyByArray } from "socket-function/src/misc";
8
+ import { MachineThreadInfo } from "../../MachineThreadInfo";
9
+
10
+ export type FilePathsByThread = {
11
+ threadId: string;
12
+ files: TimeFilePathWithSize[];
13
+ totalSize: number;
14
+ totalLogCount: number;
15
+ };
16
+
17
+ export type FilePathsByMachine = {
18
+ machineId: string;
19
+ threads: FilePathsByThread[];
20
+ totalSize: number;
21
+ totalLogCount: number;
22
+ };
23
+
24
+
25
+ export class FilePathSelector extends qreact.Component<{
26
+ paths: TimeFilePathWithSize[];
27
+ onChange: (paths: TimeFilePathWithSize[]) => void;
28
+ }> {
29
+ state = t.state({
30
+ showFullScreenModal: t.boolean,
31
+ });
32
+
33
+ getGroupedByMachine(): FilePathsByMachine[] {
34
+ let byMachine = keyByArray(this.props.paths, (path) => path.machineId || "unknown");
35
+
36
+ let result: FilePathsByMachine[] = [];
37
+ for (let [machineId, files] of byMachine) {
38
+ let byThread = keyByArray(files, (file) => file.threadId || "unknown");
39
+
40
+ let threads: FilePathsByThread[] = [];
41
+ let totalSize = 0;
42
+ let totalLogCount = 0;
43
+
44
+ for (let [threadId, threadFiles] of byThread) {
45
+ let threadSize = 0;
46
+ let threadLogCount = 0;
47
+ for (let file of threadFiles) {
48
+ threadSize += file.size;
49
+ threadLogCount += file.logCount || 0;
50
+ }
51
+ threads.push({ threadId, files: threadFiles, totalSize: threadSize, totalLogCount: threadLogCount });
52
+ totalSize += threadSize;
53
+ totalLogCount += threadLogCount;
54
+ }
55
+
56
+ result.push({ machineId, threads, totalSize, totalLogCount });
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ getPendingCount(): number {
63
+ return this.props.paths.filter(p => p.threadId !== undefined).length;
64
+ }
65
+
66
+ getUniqueThreadCount(): number {
67
+ let uniqueThreads = new Set<string>();
68
+ for (let path of this.props.paths) {
69
+ if (path.threadId !== undefined) {
70
+ uniqueThreads.add(path.threadId);
71
+ }
72
+ }
73
+ return uniqueThreads.size;
74
+ }
75
+
76
+ getTotals() {
77
+ let totalSize = 0;
78
+ let totalLogCount = 0;
79
+ for (let path of this.props.paths) {
80
+ totalSize += path.size;
81
+ totalLogCount += path.logCount || 0;
82
+ }
83
+ return { totalSize, totalLogCount, totalPending: this.getPendingCount(), uniqueThreads: this.getUniqueThreadCount() };
84
+ }
85
+
86
+ formatBytes(bytes: number): string {
87
+ return formatNumber(bytes) + "B";
88
+ }
89
+
90
+ render() {
91
+ let grouped = this.getGroupedByMachine();
92
+ let totals = this.getTotals();
93
+
94
+ return (
95
+ <div className={css.vbox(10)}>
96
+ <div
97
+ className={css.hbox(10).pad2(10).bord(1, { h: 0, s: 0, l: 80 }).hsl(240, 20, 95).hslhover(240, 30, 90).cursor("pointer")}
98
+ onClick={() => this.state.showFullScreenModal = true}
99
+ >
100
+ <div>Machines: {formatNumber(grouped.length)} |</div>
101
+ <div>Threads: {formatNumber(totals.uniqueThreads)} |</div>
102
+ <div>Files: {formatNumber(this.props.paths.length)} |</div>
103
+ <div>Pending: {formatNumber(totals.totalPending)} |</div>
104
+ <div>Size: {this.formatBytes(totals.totalSize)} |</div>
105
+ <div>Log Count: {formatNumber(totals.totalLogCount)}</div>
106
+ </div>
107
+
108
+ {this.state.showFullScreenModal && (
109
+ <div
110
+ className={css.fixed.pos(0, 0).size("100%", "100%").hsla(0, 0, 0, 0.5).vbox(0).zIndex(1000)}
111
+ onClick={() => this.state.showFullScreenModal = false}
112
+ >
113
+ <FilePathSelectorModal
114
+ allPaths={this.props.paths}
115
+ grouped={grouped}
116
+ onSave={(selectedPaths) => {
117
+ this.props.onChange(selectedPaths);
118
+ this.state.showFullScreenModal = false;
119
+ }}
120
+ onCancel={() => this.state.showFullScreenModal = false}
121
+ formatBytes={(bytes) => this.formatBytes(bytes)}
122
+ />
123
+ </div>
124
+ )}
125
+ </div>
126
+ );
127
+ }
128
+ }
129
+
130
+
131
+ export class FilePathSelectorModal extends qreact.Component<{
132
+ allPaths: TimeFilePathWithSize[];
133
+ grouped: FilePathsByMachine[];
134
+ onSave: (selectedPaths: TimeFilePathWithSize[]) => void;
135
+ onCancel: () => void;
136
+ formatBytes: (bytes: number) => string;
137
+ }> {
138
+ state = t.state({
139
+ expandedMachines: t.lookup({
140
+ expanded: t.boolean
141
+ }),
142
+ expandedThreads: t.lookup({
143
+ expanded: t.boolean
144
+ }),
145
+ selectedPaths: t.atomic<Set<string>>(new Set()),
146
+ });
147
+
148
+ componentDidMount(): void {
149
+ this.initializeSelectedPaths();
150
+ this.initializeExpandedMachines();
151
+ }
152
+
153
+ initializeSelectedPaths() {
154
+ let selected = new Set<string>();
155
+ for (let path of this.props.allPaths) {
156
+ selected.add(path.fullPath);
157
+ }
158
+ this.state.selectedPaths = selected;
159
+ }
160
+
161
+ initializeExpandedMachines() {
162
+ for (let machine of this.props.grouped) {
163
+ this.state.expandedMachines[machine.machineId] = { expanded: true };
164
+ }
165
+ }
166
+
167
+ toggleMachine(machineId: string) {
168
+ let machine = this.props.grouped.find(m => m.machineId === machineId);
169
+ if (!machine) return;
170
+
171
+ let allFiles = machine.threads.flatMap(t => t.files);
172
+ let allSelected = allFiles.every(f => this.state.selectedPaths.has(f.fullPath));
173
+ let newSelected = new Set(this.state.selectedPaths);
174
+
175
+ if (allSelected) {
176
+ for (let file of allFiles) {
177
+ newSelected.delete(file.fullPath);
178
+ }
179
+ } else {
180
+ for (let file of allFiles) {
181
+ newSelected.add(file.fullPath);
182
+ }
183
+ }
184
+
185
+ this.state.selectedPaths = newSelected;
186
+ }
187
+
188
+ toggleThread(machineId: string, threadId: string) {
189
+ let machine = this.props.grouped.find(m => m.machineId === machineId);
190
+ if (!machine) return;
191
+
192
+ let thread = machine.threads.find(t => t.threadId === threadId);
193
+ if (!thread) return;
194
+
195
+ let allSelected = thread.files.every(f => this.state.selectedPaths.has(f.fullPath));
196
+ let newSelected = new Set(this.state.selectedPaths);
197
+
198
+ if (allSelected) {
199
+ for (let file of thread.files) {
200
+ newSelected.delete(file.fullPath);
201
+ }
202
+ } else {
203
+ for (let file of thread.files) {
204
+ newSelected.add(file.fullPath);
205
+ }
206
+ }
207
+
208
+ this.state.selectedPaths = newSelected;
209
+ }
210
+
211
+ toggleFile(fullPath: string) {
212
+ let newSelected = new Set(this.state.selectedPaths);
213
+ if (newSelected.has(fullPath)) {
214
+ newSelected.delete(fullPath);
215
+ } else {
216
+ newSelected.add(fullPath);
217
+ }
218
+ this.state.selectedPaths = newSelected;
219
+ }
220
+
221
+ save() {
222
+ let selectedPaths = this.props.allPaths.filter(p => this.state.selectedPaths.has(p.fullPath));
223
+ this.props.onSave(selectedPaths);
224
+ }
225
+
226
+ getSelectedSummary() {
227
+ let selectedFiles = this.props.allPaths.filter(p => this.state.selectedPaths.has(p.fullPath));
228
+ let totalSize = 0;
229
+ let totalLogCount = 0;
230
+ let uniqueMachines = new Set<string>();
231
+ let uniqueThreads = new Set<string>();
232
+
233
+ for (let file of selectedFiles) {
234
+ totalSize += file.size;
235
+ totalLogCount += file.logCount || 0;
236
+ if (file.machineId) {
237
+ uniqueMachines.add(file.machineId);
238
+ }
239
+ if (file.threadId) {
240
+ uniqueThreads.add(file.threadId);
241
+ }
242
+ }
243
+
244
+ return {
245
+ fileCount: selectedFiles.length,
246
+ machineCount: uniqueMachines.size,
247
+ threadCount: uniqueThreads.size,
248
+ totalSize,
249
+ totalLogCount
250
+ };
251
+ }
252
+
253
+ renderFile(file: TimeFilePathWithSize, machineId: string) {
254
+ let isSelected = this.state.selectedPaths.has(file.fullPath);
255
+ return (
256
+ <tr key={file.fullPath}>
257
+ <td className={css.pad2(2)}>
258
+ <Button
259
+ onClick={() => this.toggleFile(file.fullPath)}
260
+ className={css.minWidth(100).pad2(5, 2)}
261
+ hue={isSelected ? 120 : undefined}
262
+ >
263
+ {isSelected ? "Selected" : "Not Selected"}
264
+ </Button>
265
+ </td>
266
+ <td className={css.pad2(2)}>{this.props.formatBytes(file.size)}</td>
267
+ <td className={css.pad2(2)}>{file.logCount !== undefined ? formatNumber(file.logCount) : ""}</td>
268
+ <td className={css.pad2(2)}>{file.dedupe}</td>
269
+ <td className={css.pad2(2)}>{formatDateTime(file.startTime)}</td>
270
+ </tr>
271
+ );
272
+ }
273
+
274
+ render() {
275
+ let summary = this.getSelectedSummary();
276
+
277
+ return (
278
+ <div
279
+ className={css.vbox(10).pad2(10).hsl(0, 0, 100).overflowAuto.width("75vw").height("75vh").marginAuto}
280
+ onClick={(e) => e.stopPropagation()}
281
+ >
282
+ <div className={css.hbox(10)}>
283
+ <Button onClick={() => this.state.selectedPaths = new Set()}>
284
+ Unselect All
285
+ </Button>
286
+ <Button onClick={() => this.state.selectedPaths = new Set(this.props.allPaths.map(p => p.fullPath))}>
287
+ Select All
288
+ </Button>
289
+ <div>Machines: {formatNumber(summary.machineCount)}</div>
290
+ <div>Threads: {formatNumber(summary.threadCount)}</div>
291
+ <div>Files: {formatNumber(summary.fileCount)}</div>
292
+ <div>Size: {this.props.formatBytes(summary.totalSize)}</div>
293
+ <div>Logs: {formatNumber(summary.totalLogCount)}</div>
294
+ </div>
295
+
296
+ {this.props.grouped.map((machine) => {
297
+ let isMachineExpanded = machine.machineId in this.state.expandedMachines;
298
+ let allFiles = machine.threads.flatMap(t => t.files);
299
+ let allSelected = allFiles.every(f => this.state.selectedPaths.has(f.fullPath));
300
+ let someSelected = allFiles.some(f => this.state.selectedPaths.has(f.fullPath));
301
+ let totalFileCount = allFiles.length;
302
+
303
+ return (
304
+ <div key={machine.machineId} className={css.vbox(0).bord(1, { h: 0, s: 0, l: 80 })}>
305
+ <div
306
+ className={css.hbox(5).button}
307
+ onMouseDown={() => {
308
+ if (isMachineExpanded) {
309
+ delete this.state.expandedMachines[machine.machineId];
310
+ } else {
311
+ this.state.expandedMachines[machine.machineId] = { expanded: true };
312
+ }
313
+ }}
314
+ >
315
+ <div>
316
+ {isMachineExpanded ? "▼" : "▶"}
317
+ </div>
318
+ <Button
319
+ onClick={(e) => {
320
+ e.stopPropagation();
321
+ this.toggleMachine(machine.machineId);
322
+ }}
323
+ onMouseDown={(e) => e.stopPropagation()}
324
+ className={css.minWidth(100)}
325
+ hue={allSelected ? 120 : undefined}
326
+ >
327
+ {allSelected ? "Selected" : someSelected ? "Partial" : "Not Selected"}
328
+ </Button>
329
+ <MachineThreadInfo machineId={machine.machineId} />
330
+ <div>Threads: {formatNumber(machine.threads.length)}</div>
331
+ <div className={css.minWidth(80)}>Files: {formatNumber(totalFileCount)}</div>
332
+ <div className={css.minWidth(100)}>Size: {this.props.formatBytes(machine.totalSize)}</div>
333
+ <div className={css.minWidth(100)}>Logs: {formatNumber(machine.totalLogCount)}</div>
334
+ </div>
335
+
336
+ {isMachineExpanded && machine.threads.map((thread) => {
337
+ let threadKey = `${machine.machineId}:${thread.threadId}`;
338
+ let isThreadExpanded = threadKey in this.state.expandedThreads;
339
+ let allThreadSelected = thread.files.every(f => this.state.selectedPaths.has(f.fullPath));
340
+ let someThreadSelected = thread.files.some(f => this.state.selectedPaths.has(f.fullPath));
341
+
342
+ return (
343
+ <div key={threadKey} className={css.vbox(0).bord(1, { h: 0, s: 0, l: 85 }).marginLeft(10)}>
344
+ <div
345
+ className={css.hbox(5).button}
346
+ onMouseDown={() => {
347
+ if (isThreadExpanded) {
348
+ delete this.state.expandedThreads[threadKey];
349
+ } else {
350
+ this.state.expandedThreads[threadKey] = { expanded: true };
351
+ }
352
+ }}
353
+ >
354
+ <div>
355
+ {isThreadExpanded ? "▼" : "▶"}
356
+ </div>
357
+ <Button
358
+ onClick={(e) => {
359
+ e.stopPropagation();
360
+ this.toggleThread(machine.machineId, thread.threadId);
361
+ }}
362
+ onMouseDown={(e) => e.stopPropagation()}
363
+ className={css.minWidth(100)}
364
+ hue={allThreadSelected ? 120 : undefined}
365
+ >
366
+ {allThreadSelected ? "Selected" : someThreadSelected ? "Partial" : "Not Selected"}
367
+ </Button>
368
+ <div className={css.minWidth(80)}>Files: {formatNumber(thread.files.length)}</div>
369
+ <div className={css.minWidth(100)}>Size: {this.props.formatBytes(thread.totalSize)}</div>
370
+ <div className={css.minWidth(100)}>Logs: {formatNumber(thread.totalLogCount)}</div>
371
+ <MachineThreadInfo machineId={machine.machineId} threadId={thread.threadId} onlyShowThread />
372
+ </div>
373
+
374
+ {isThreadExpanded && (
375
+ <table className={css.width("100%")}>
376
+ <thead>
377
+ <tr>
378
+ <th className={css.pad2(2).textAlign("left")}>Selection</th>
379
+ <th className={css.pad2(2).textAlign("left")}>Size</th>
380
+ <th className={css.pad2(2).textAlign("left")}>Logs</th>
381
+ <th className={css.pad2(2).textAlign("left")}>Dedupe</th>
382
+ <th className={css.pad2(2).textAlign("left")}>Start Time</th>
383
+ </tr>
384
+ </thead>
385
+ <tbody>
386
+ {thread.files.map((file) => this.renderFile(file, machine.machineId))}
387
+ </tbody>
388
+ </table>
389
+ )}
390
+ </div>
391
+ );
392
+ })}
393
+ </div>
394
+ );
395
+ })}
396
+
397
+ <div className={css.hbox(10)}>
398
+ <Button onClick={() => this.save()} hue={120}>
399
+ Save
400
+ </Button>
401
+ <Button onClick={() => this.props.onCancel()}>
402
+ Cancel
403
+ </Button>
404
+ </div>
405
+ </div>
406
+ );
407
+ }
408
+ }
@@ -0,0 +1,45 @@
1
+ import { binarySearchBasic2 } from "socket-function/src/misc";
2
+ import { SearchParams } from "./BufferIndexHelpers";
3
+
4
+ export class FindProgressTracker<T> {
5
+ public constructor(private config: {
6
+ params: SearchParams;
7
+ deserialize: (result: Buffer) => T;
8
+ getTime: (result: T) => number | undefined;
9
+ onResult: (result: T) => void;
10
+ }) { }
11
+
12
+
13
+ private results: { time: number; result: T }[] = [];
14
+
15
+ public addResult(buffer: Buffer, source: { startTime: number; endTime: number; }): boolean {
16
+ let result = this.config.deserialize(buffer);
17
+ let time = this.config.getTime(result) ?? source.endTime;
18
+
19
+ if (time < this.config.params.startTime) return false;
20
+ if (time >= this.config.params.endTime) return false;
21
+
22
+ if (this.results.length >= this.config.params.limit && time < this.results[0].time) {
23
+ return false;
24
+ }
25
+
26
+ let newObj = { time, result };
27
+ let index = binarySearchBasic2(this.results, x => x.time, newObj);
28
+ if (index < 0) index = ~index;
29
+ this.results.splice(index, 0, newObj);
30
+
31
+ if (this.results.length > this.config.params.limit) {
32
+ this.results = this.results.slice(-this.config.params.limit);
33
+ }
34
+
35
+ this.config.onResult(result);
36
+ return true;
37
+ }
38
+
39
+ public isSourceRelevant(source: { startTime: number; endTime: number; }): boolean {
40
+ if (this.results.length >= this.config.params.limit && source.endTime < this.results[0].time) {
41
+ return false;
42
+ }
43
+ return source.startTime <= this.config.params.endTime && source.endTime >= this.config.params.startTime;
44
+ }
45
+ }