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.
- package/.cursorrules +8 -0
- package/bin/movelogs.js +4 -0
- package/package.json +12 -6
- package/scripts/postinstall.js +23 -0
- package/src/-a-archives/archiveCache.ts +10 -12
- package/src/-a-archives/archives.ts +29 -0
- package/src/-a-archives/archivesBackBlaze.ts +60 -12
- package/src/-a-archives/archivesDisk.ts +27 -8
- package/src/-a-archives/archivesLimitedCache.ts +21 -0
- package/src/-a-archives/archivesMemoryCache.ts +350 -0
- package/src/-a-archives/archivesPrivateFileSystem.ts +22 -0
- package/src/-g-core-values/NodeCapabilities.ts +3 -0
- package/src/0-path-value-core/auditLogs.ts +5 -1
- package/src/0-path-value-core/pathValueCore.ts +7 -7
- package/src/4-dom/qreact.tsx +1 -0
- package/src/4-querysub/Querysub.ts +1 -5
- package/src/config.ts +5 -0
- package/src/diagnostics/MachineThreadInfo.tsx +235 -0
- package/src/diagnostics/NodeViewer.tsx +3 -2
- package/src/diagnostics/logs/FastArchiveAppendable.ts +79 -42
- package/src/diagnostics/logs/FastArchiveController.ts +102 -63
- package/src/diagnostics/logs/FastArchiveViewer.tsx +36 -8
- package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +461 -0
- package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.cpp +327 -0
- package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.d.ts +18 -0
- package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.js +1 -0
- package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +140 -0
- package/src/diagnostics/logs/IndexedLogs/BufferIndexLogsOptimizationConstants.ts +22 -0
- package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat +1145 -0
- package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat.d.ts +178 -0
- package/src/diagnostics/logs/IndexedLogs/BufferListStreamer.ts +206 -0
- package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +719 -0
- package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +146 -0
- package/src/diagnostics/logs/IndexedLogs/FilePathSelector.tsx +408 -0
- package/src/diagnostics/logs/IndexedLogs/FindProgressTracker.ts +45 -0
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +598 -0
- package/src/diagnostics/logs/IndexedLogs/LogStreamer.ts +47 -0
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +702 -0
- package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +236 -0
- package/src/diagnostics/logs/IndexedLogs/binding.gyp +23 -0
- package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +221 -0
- package/src/diagnostics/logs/IndexedLogs/moveLogsEntry.ts +10 -0
- package/src/diagnostics/logs/LogViewer2.tsx +120 -55
- package/src/diagnostics/logs/TimeRangeSelector.tsx +5 -2
- package/src/diagnostics/logs/diskLogger.ts +32 -48
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +3 -2
- package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +1 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePages.tsx +150 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +132 -15
- package/src/diagnostics/logs/lifeCycleAnalysis/test.ts +180 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/test.wat +106 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/test.wat.d.ts +2 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/testHoist.ts +5 -0
- package/src/diagnostics/logs/logViewerExtractField.ts +2 -3
- package/src/diagnostics/managementPages.tsx +10 -0
- package/src/diagnostics/trackResources.ts +1 -1
- package/src/misc/lz4_wasm_nodejs.d.ts +34 -0
- package/src/misc/lz4_wasm_nodejs.js +178 -0
- package/src/misc/lz4_wasm_nodejs_bg.js +94 -0
- package/src/misc/lz4_wasm_nodejs_bg.wasm +0 -0
- package/src/misc/lz4_wasm_nodejs_bg.wasm.d.ts +15 -0
- package/src/storage/CompressedStream.ts +13 -0
- package/src/storage/LZ4.ts +32 -0
- package/src/storage/ZSTD.ts +10 -0
- package/src/wat/watCompiler.ts +1716 -0
- package/src/wat/watGrammar.pegjs +93 -0
- package/src/wat/watHandler.ts +179 -0
- package/src/wat/watInstructions.txt +707 -0
- package/src/zip.ts +3 -89
- 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
|
+
}
|