querysub 0.456.0 → 0.458.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/package.json +1 -1
- package/src/-a-archives/archivesBackBlaze.ts +15 -12
- package/src/-h-path-value-serialize/PathValueSerializer.ts +17 -15
- package/src/0-path-value-core/PathValueController.ts +10 -1
- package/src/0-path-value-core/PathWatcher.ts +1 -0
- package/src/0-path-value-core/pathValueCore.ts +1 -1
- package/src/3-path-functions/PathFunctionRunner.ts +7 -0
- package/src/4-querysub/QuerysubController.ts +1 -0
- package/src/5-diagnostics/gross-stats/grossStats.ts +102 -0
- package/src/deployManager/machineApplyMainCode.ts +1 -7
- package/src/diagnostics/auditDiskValues.ts +1 -1
- package/src/diagnostics/grossStats/GrossStatsController.ts +111 -0
- package/src/diagnostics/grossStats/GrossStatsInfo.tsx +95 -0
- package/src/diagnostics/grossStats/GrossStatsPage.tsx +259 -0
- package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +26 -4
- package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +22 -4
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +20 -4
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +15 -18
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +49 -69
- package/src/diagnostics/logs/diskLogger.ts +4 -0
- package/src/diagnostics/managementPages.tsx +8 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { qreact } from "../../4-dom/qreact";
|
|
3
|
+
import { css } from "../../4-dom/css";
|
|
4
|
+
import { Querysub } from "../../4-querysub/QuerysubController";
|
|
5
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
|
+
import { formatNumber } from "socket-function/src/formatting/format";
|
|
7
|
+
import { timeInHour, timeInMinute } from "socket-function/src/misc";
|
|
8
|
+
import { cacheArgsEqual } from "socket-function/src/caching";
|
|
9
|
+
import { t } from "../../2-proxy/schema2";
|
|
10
|
+
import { getSyncedController } from "../../library-components/SyncedController";
|
|
11
|
+
import { GrossStatsController } from "./GrossStatsController";
|
|
12
|
+
import {
|
|
13
|
+
GROSS_STATS_FIELDS,
|
|
14
|
+
GrossStatsField,
|
|
15
|
+
GrossStatsBucket,
|
|
16
|
+
} from "../../5-diagnostics/gross-stats/grossStats";
|
|
17
|
+
export { GrossStatsController } from "./GrossStatsController";
|
|
18
|
+
|
|
19
|
+
module.hotreload = true;
|
|
20
|
+
|
|
21
|
+
const REFRESH_INTERVAL_MS = 5 * timeInMinute;
|
|
22
|
+
const CHART_WIDTH = 1200;
|
|
23
|
+
const CHART_HEIGHT = 300;
|
|
24
|
+
|
|
25
|
+
const TIME_RANGES: { label: string; ms: number }[] = [
|
|
26
|
+
{ label: "1h", ms: timeInHour },
|
|
27
|
+
{ label: "6h", ms: 6 * timeInHour },
|
|
28
|
+
{ label: "24h", ms: 24 * timeInHour },
|
|
29
|
+
{ label: "7d", ms: 7 * 24 * timeInHour },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let pageSchema = Querysub.createLocalSchema("grossStatsPage", {
|
|
33
|
+
rangeMs: t.atomic<number>(timeInHour),
|
|
34
|
+
excludedNodes: t.atomic<Set<string>>(new Set()),
|
|
35
|
+
selectedField: t.atomic<GrossStatsField>("functionCallsInner"),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const NODE_COLORS = [
|
|
39
|
+
"hsl(0, 70%, 55%)",
|
|
40
|
+
"hsl(30, 75%, 50%)",
|
|
41
|
+
"hsl(55, 80%, 45%)",
|
|
42
|
+
"hsl(120, 50%, 45%)",
|
|
43
|
+
"hsl(170, 60%, 45%)",
|
|
44
|
+
"hsl(210, 60%, 55%)",
|
|
45
|
+
"hsl(250, 55%, 60%)",
|
|
46
|
+
"hsl(290, 55%, 55%)",
|
|
47
|
+
"hsl(330, 65%, 55%)",
|
|
48
|
+
];
|
|
49
|
+
function colorForNode(index: number): string {
|
|
50
|
+
return NODE_COLORS[index % NODE_COLORS.length];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shortNodeId(nodeId: string): string {
|
|
54
|
+
let parts = nodeId.split(".");
|
|
55
|
+
if (parts.length >= 2) return `${parts[0].slice(0, 6)}…${parts[1].slice(0, 6)}`;
|
|
56
|
+
return nodeId.slice(0, 12);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let grossStatsController = getSyncedController(GrossStatsController);
|
|
60
|
+
|
|
61
|
+
// Render a stacked bar chart to a PNG data URL. Cached by argument identity:
|
|
62
|
+
// selectedField/rangeMs/width/height are primitives; nodeIds and bucketsArrays are
|
|
63
|
+
// expected to be stable references (the bucketsArrays come from syncedController and
|
|
64
|
+
// only change when refreshed).
|
|
65
|
+
const renderChartPNG = cacheArgsEqual((
|
|
66
|
+
selectedField: GrossStatsField,
|
|
67
|
+
rangeMs: number,
|
|
68
|
+
width: number,
|
|
69
|
+
height: number,
|
|
70
|
+
nodeIds: readonly string[],
|
|
71
|
+
bucketsArrays: readonly GrossStatsBucket[][],
|
|
72
|
+
): string => {
|
|
73
|
+
let canvas = document.createElement("canvas");
|
|
74
|
+
canvas.width = width;
|
|
75
|
+
canvas.height = height;
|
|
76
|
+
let ctx = canvas.getContext("2d")!;
|
|
77
|
+
ctx.fillStyle = "hsl(0, 0%, 97%)";
|
|
78
|
+
ctx.fillRect(0, 0, width, height);
|
|
79
|
+
|
|
80
|
+
let now = Date.now();
|
|
81
|
+
let windowStart = now - rangeMs;
|
|
82
|
+
|
|
83
|
+
let perPixel: { perNode: number[]; total: number; }[] = [];
|
|
84
|
+
for (let x = 0; x < width; x++) {
|
|
85
|
+
perPixel.push({ perNode: new Array(nodeIds.length).fill(0), total: 0 });
|
|
86
|
+
}
|
|
87
|
+
let maxTotal = 0;
|
|
88
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
89
|
+
let buckets = bucketsArrays[i];
|
|
90
|
+
if (!buckets) continue;
|
|
91
|
+
for (let bucket of buckets) {
|
|
92
|
+
if (bucket.time < windowStart) continue;
|
|
93
|
+
let xFloat = (bucket.time - windowStart) / rangeMs * width;
|
|
94
|
+
let x = Math.min(width - 1, Math.max(0, Math.floor(xFloat)));
|
|
95
|
+
let cell = perPixel[x];
|
|
96
|
+
let val = bucket.deltas[selectedField] || 0;
|
|
97
|
+
cell.perNode[i] += val;
|
|
98
|
+
cell.total += val;
|
|
99
|
+
if (cell.total > maxTotal) maxTotal = cell.total;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (maxTotal <= 0) maxTotal = 1;
|
|
103
|
+
|
|
104
|
+
for (let x = 0; x < width; x++) {
|
|
105
|
+
let cell = perPixel[x];
|
|
106
|
+
if (cell.total === 0) continue;
|
|
107
|
+
let y = height;
|
|
108
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
109
|
+
let val = cell.perNode[i];
|
|
110
|
+
if (!val) continue;
|
|
111
|
+
let segHeight = (val / maxTotal) * height;
|
|
112
|
+
ctx.fillStyle = colorForNode(i);
|
|
113
|
+
ctx.fillRect(x, y - segHeight, 1, segHeight);
|
|
114
|
+
y -= segHeight;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return canvas.toDataURL();
|
|
119
|
+
}, 5);
|
|
120
|
+
|
|
121
|
+
export class GrossStatsPage extends qreact.Component {
|
|
122
|
+
refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
123
|
+
|
|
124
|
+
componentDidMount() {
|
|
125
|
+
this.refreshTimer = setInterval(() => {
|
|
126
|
+
grossStatsController.refreshAll();
|
|
127
|
+
}, REFRESH_INTERVAL_MS);
|
|
128
|
+
}
|
|
129
|
+
componentWillUnmount() {
|
|
130
|
+
if (this.refreshTimer) clearInterval(this.refreshTimer);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private renderControls(nodeIds: string[]) {
|
|
134
|
+
let state = pageSchema();
|
|
135
|
+
return <div className={css.vbox(8).pad2(8)}>
|
|
136
|
+
<div className={css.hbox(6)}>
|
|
137
|
+
<span>Range:</span>
|
|
138
|
+
{TIME_RANGES.map(r =>
|
|
139
|
+
<span
|
|
140
|
+
className={css.button.pad2(4, 2) + (state.rangeMs === r.ms ? " " + css.hsl(210, 70, 60).hslcolor(0, 0, 100) : "")}
|
|
141
|
+
onClick={() => Querysub.commit(() => { state.rangeMs = r.ms; })}
|
|
142
|
+
>{r.label}</span>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
<div className={css.hbox(6).wrap}>
|
|
146
|
+
<span>Field:</span>
|
|
147
|
+
{GROSS_STATS_FIELDS.map(f =>
|
|
148
|
+
<span
|
|
149
|
+
className={css.button.pad2(4, 2) + (state.selectedField === f ? " " + css.hsl(140, 60, 50).hslcolor(0, 0, 100) : "")}
|
|
150
|
+
onClick={() => Querysub.commit(() => { state.selectedField = f; })}
|
|
151
|
+
>{f}</span>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
<div className={css.hbox(8).wrap}>
|
|
155
|
+
<span>Servers:</span>
|
|
156
|
+
{nodeIds.map((nodeId, i) =>
|
|
157
|
+
<label className={css.hbox(4)}>
|
|
158
|
+
<input
|
|
159
|
+
type="checkbox"
|
|
160
|
+
checked={!state.excludedNodes.has(nodeId)}
|
|
161
|
+
onChange={() => {
|
|
162
|
+
Querysub.commit(() => {
|
|
163
|
+
let next = new Set(state.excludedNodes);
|
|
164
|
+
if (next.has(nodeId)) next.delete(nodeId);
|
|
165
|
+
else next.add(nodeId);
|
|
166
|
+
state.excludedNodes = next;
|
|
167
|
+
});
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
<span className={css.hslcolor(0, 0, 100).pad2(2)} style={{ background: colorForNode(i) }}>
|
|
171
|
+
{shortNodeId(nodeId)}
|
|
172
|
+
</span>
|
|
173
|
+
</label>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</div>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private renderTable(nodeIds: string[], bucketsArrays: GrossStatsBucket[][]) {
|
|
180
|
+
let state = pageSchema();
|
|
181
|
+
let now = Date.now();
|
|
182
|
+
let windowStart = now - state.rangeMs;
|
|
183
|
+
|
|
184
|
+
let perNodeTotals: Record<GrossStatsField, number>[] = [];
|
|
185
|
+
let totalsRow: Record<GrossStatsField, number> = {} as Record<GrossStatsField, number>;
|
|
186
|
+
for (let f of GROSS_STATS_FIELDS) totalsRow[f] = 0;
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
189
|
+
let totals = {} as Record<GrossStatsField, number>;
|
|
190
|
+
for (let f of GROSS_STATS_FIELDS) totals[f] = 0;
|
|
191
|
+
for (let bucket of bucketsArrays[i] ?? []) {
|
|
192
|
+
if (bucket.time < windowStart) continue;
|
|
193
|
+
for (let f of GROSS_STATS_FIELDS) {
|
|
194
|
+
totals[f] += bucket.deltas[f];
|
|
195
|
+
totalsRow[f] += bucket.deltas[f];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
perNodeTotals.push(totals);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return <table className={css.fillWidth}>
|
|
202
|
+
<thead>
|
|
203
|
+
<tr>
|
|
204
|
+
<th className={css.textAlign("left").pad2(4)}>Server</th>
|
|
205
|
+
{GROSS_STATS_FIELDS.map(f => <th className={css.textAlign("right").pad2(4)}>{f}</th>)}
|
|
206
|
+
</tr>
|
|
207
|
+
</thead>
|
|
208
|
+
<tbody>
|
|
209
|
+
<tr className={css.boldStyle}>
|
|
210
|
+
<td className={css.pad2(4)}>TOTAL</td>
|
|
211
|
+
{GROSS_STATS_FIELDS.map(f => <td className={css.textAlign("right").pad2(4)}>{formatNumber(totalsRow[f])}</td>)}
|
|
212
|
+
</tr>
|
|
213
|
+
{nodeIds.map((nodeId, i) =>
|
|
214
|
+
<tr>
|
|
215
|
+
<td className={css.pad2(4)} style={{ borderLeft: `4px solid ${colorForNode(i)}` }}>
|
|
216
|
+
{shortNodeId(nodeId)}
|
|
217
|
+
</td>
|
|
218
|
+
{GROSS_STATS_FIELDS.map(f =>
|
|
219
|
+
<td className={css.textAlign("right").pad2(4)}>{formatNumber(perNodeTotals[i][f])}</td>
|
|
220
|
+
)}
|
|
221
|
+
</tr>
|
|
222
|
+
)}
|
|
223
|
+
</tbody>
|
|
224
|
+
</table>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
render() {
|
|
228
|
+
let state = pageSchema();
|
|
229
|
+
let result = grossStatsController(SocketFunction.browserNodeId()).getGrossStats();
|
|
230
|
+
let bucketsByNode = result?.bucketsByNode ?? new Map<string, GrossStatsBucket[]>();
|
|
231
|
+
let allNodeIds = Array.from(bucketsByNode.keys()).sort();
|
|
232
|
+
|
|
233
|
+
let selectedNodeIds = allNodeIds.filter(n => !state.excludedNodes.has(n));
|
|
234
|
+
let bucketsArrays: GrossStatsBucket[][] = selectedNodeIds.map(n => bucketsByNode.get(n) ?? []);
|
|
235
|
+
let anyLoading = !result;
|
|
236
|
+
|
|
237
|
+
let chartPNG = renderChartPNG(
|
|
238
|
+
state.selectedField,
|
|
239
|
+
state.rangeMs,
|
|
240
|
+
CHART_WIDTH,
|
|
241
|
+
CHART_HEIGHT,
|
|
242
|
+
selectedNodeIds,
|
|
243
|
+
bucketsArrays,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return <div className={css.vbox(8).pad2(8).fillWidth}>
|
|
247
|
+
<h2>Cluster Stats</h2>
|
|
248
|
+
{this.renderControls(allNodeIds)}
|
|
249
|
+
<img
|
|
250
|
+
src={chartPNG}
|
|
251
|
+
width={CHART_WIDTH}
|
|
252
|
+
height={CHART_HEIGHT}
|
|
253
|
+
className={css.hsl(0, 0, 100)}
|
|
254
|
+
style={anyLoading ? { opacity: 0.5 } : undefined}
|
|
255
|
+
/>
|
|
256
|
+
{this.renderTable(selectedNodeIds, bucketsArrays)}
|
|
257
|
+
</div>;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -300,7 +300,10 @@ export class BufferIndex {
|
|
|
300
300
|
dataReader: Reader;
|
|
301
301
|
params: SearchParams;
|
|
302
302
|
keepIterating: () => boolean;
|
|
303
|
-
|
|
303
|
+
// Returns true iff the caller actually retained the value. We use that
|
|
304
|
+
// to drive the per-file matchCount cap below: see the note at the
|
|
305
|
+
// `matchesPattern(buffer)` call for why we can't blindly count emits.
|
|
306
|
+
onResult: (match: Buffer) => boolean;
|
|
304
307
|
results: IndexedLogResults;
|
|
305
308
|
allSearchUnits: Unit[][];
|
|
306
309
|
matchesPattern: (buffer: Buffer) => boolean;
|
|
@@ -395,8 +398,24 @@ export class BufferIndex {
|
|
|
395
398
|
|
|
396
399
|
let buffer = buffers[bufferIndex];
|
|
397
400
|
if (matchesPattern(buffer)) {
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
// Only count matches the caller actually kept. `onResult`
|
|
402
|
+
// routes through `FindProgressTracker.addResult`, which
|
|
403
|
+
// can reject for reasons we don't see from here — most
|
|
404
|
+
// notably time-range filtering (an entry whose time is
|
|
405
|
+
// outside the search window matched the text pattern
|
|
406
|
+
// but isn't a real hit). Counting rejected emits would
|
|
407
|
+
// let a stretch of out-of-window matches at the start
|
|
408
|
+
// of a file blow the per-file cap and short-circuit the
|
|
409
|
+
// scan before we reach the in-window region.
|
|
410
|
+
//
|
|
411
|
+
// The cost is that we keep matching and calling onResult
|
|
412
|
+
// through those out-of-window blocks (mild inefficiency).
|
|
413
|
+
// We can't skip ahead — buffers are scanned linearly and
|
|
414
|
+
// we don't know up front which entries the caller will
|
|
415
|
+
// reject — so this is the best we can do here.
|
|
416
|
+
if (config.onResult(buffer)) {
|
|
417
|
+
matchCount++;
|
|
418
|
+
}
|
|
400
419
|
}
|
|
401
420
|
}
|
|
402
421
|
} catch (e: any) {
|
|
@@ -531,7 +550,10 @@ export class BufferIndex {
|
|
|
531
550
|
params: SearchParams;
|
|
532
551
|
|
|
533
552
|
keepIterating: () => boolean;
|
|
534
|
-
onResult
|
|
553
|
+
// See the note on `findLocal.onResult` — return value drives the
|
|
554
|
+
// per-file matchCount cap so out-of-window emits don't short-circuit
|
|
555
|
+
// the scan.
|
|
556
|
+
onResult: (match: Buffer) => boolean;
|
|
535
557
|
results: IndexedLogResults;
|
|
536
558
|
}): Promise<{
|
|
537
559
|
blockSearchTime: number;
|
|
@@ -505,7 +505,10 @@ export class BufferUnitIndex {
|
|
|
505
505
|
params: SearchParams;
|
|
506
506
|
allSearchUnits: Unit[][];
|
|
507
507
|
keepIterating: () => boolean;
|
|
508
|
-
|
|
508
|
+
// Returns true iff the caller actually retained the value. Drives the
|
|
509
|
+
// `matchCounts` cap below — see the comment at the `matchesPattern`
|
|
510
|
+
// call for why we can't blindly count emits.
|
|
511
|
+
onResult: (match: Buffer) => boolean;
|
|
509
512
|
index: Buffer;
|
|
510
513
|
reader: Reader;
|
|
511
514
|
results: IndexedLogResults;
|
|
@@ -582,9 +585,24 @@ export class BufferUnitIndex {
|
|
|
582
585
|
|
|
583
586
|
const buffer = await this.getBufferFromBlock(blockReader, i);
|
|
584
587
|
if (matchesPattern(buffer)) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
+
// Only count matches the caller actually kept.
|
|
589
|
+
// `onResult` routes through
|
|
590
|
+
// `FindProgressTracker.addResult`, which can reject
|
|
591
|
+
// for reasons opaque to us — most notably
|
|
592
|
+
// time-range filtering. Counting rejected emits
|
|
593
|
+
// would let a stretch of out-of-window matches at
|
|
594
|
+
// the start of a file blow the per-file cap and
|
|
595
|
+
// short-circuit the scan before we reach the
|
|
596
|
+
// in-window region. The cost is that we keep
|
|
597
|
+
// matching and calling onResult through those
|
|
598
|
+
// out-of-window blocks (mild inefficiency); we
|
|
599
|
+
// can't skip ahead because buffers are scanned
|
|
600
|
+
// linearly and we don't know up front which
|
|
601
|
+
// entries the caller will reject.
|
|
602
|
+
if (config.onResult(buffer)) {
|
|
603
|
+
matchCount++;
|
|
604
|
+
matchCounts[blockIndex]++;
|
|
605
|
+
}
|
|
588
606
|
}
|
|
589
607
|
}
|
|
590
608
|
} catch (e: any) {
|
|
@@ -620,12 +620,28 @@ export class IndexedLogs<T> {
|
|
|
620
620
|
dataReader,
|
|
621
621
|
params: {
|
|
622
622
|
...config.params,
|
|
623
|
-
limit
|
|
623
|
+
// Per-file cap = global `limit`, NOT `limit - matchCount`.
|
|
624
|
+
// Subtracting the running count would starve parallel
|
|
625
|
+
// files: once one file fills matchCount, every other
|
|
626
|
+
// in-flight file gets limit=0 and stops scanning, even
|
|
627
|
+
// though `isSourceRelevant` may still want their earlier
|
|
628
|
+
// matches to displace the kept top-K. Within a single
|
|
629
|
+
// file blocks are time-ordered in the scan direction,
|
|
630
|
+
// so capping at `limit` per file is safe — anything
|
|
631
|
+
// past the first `limit` matches in this file couldn't
|
|
632
|
+
// survive the top-K sort.
|
|
633
|
+
limit: config.params.limit,
|
|
624
634
|
},
|
|
625
635
|
keepIterating: () => !results.cancel && progressTracker.isSourceRelevant(path),
|
|
626
|
-
onResult: (match: Buffer) => {
|
|
627
|
-
//
|
|
628
|
-
|
|
636
|
+
onResult: (match: Buffer): boolean => {
|
|
637
|
+
// Return value drives the per-file matchCount cap inside
|
|
638
|
+
// BufferIndex.find / BufferUnitIndex.find. addResult
|
|
639
|
+
// returns false when it rejects (time outside the
|
|
640
|
+
// search range, or past the kept top-K cutoff); the
|
|
641
|
+
// scanner uses that to avoid counting rejected emits
|
|
642
|
+
// against the cap. See the comment on those scanners'
|
|
643
|
+
// `matchesPattern` calls for the full reasoning.
|
|
644
|
+
return progressTracker.addResult(match, path);
|
|
629
645
|
},
|
|
630
646
|
results,
|
|
631
647
|
});
|
|
@@ -38,7 +38,7 @@ import { startTimeParam, endTimeParam } from "../TimeRangeSelector";
|
|
|
38
38
|
import { formatSearchString } from "./LogViewerParams";
|
|
39
39
|
|
|
40
40
|
let excludePendingResults = new URLParam("excludePendingResults", false);
|
|
41
|
-
let limitURL = new URLParam("limit",
|
|
41
|
+
let limitURL = new URLParam("limit", 200);
|
|
42
42
|
let enableLogsURL = new URLParam("enableLogs", true);
|
|
43
43
|
let enableInfosURL = new URLParam("enableInfos", true);
|
|
44
44
|
let enableWarningsURL = new URLParam("enableWarnings", true);
|
|
@@ -46,7 +46,6 @@ let enableErrorsURL = new URLParam("enableErrors", true);
|
|
|
46
46
|
|
|
47
47
|
let savedPathsURL = new URLParam("savedPaths", "");
|
|
48
48
|
let selectedFieldsURL = new URLParam("selectedFields", {} as Record<string, boolean>);
|
|
49
|
-
let useRelativeTimeURL = new URLParam("useRelativeTime", true);
|
|
50
49
|
|
|
51
50
|
// NOTE: Because this doesn't cache properly, it lags a lot, so we shouldn't try to use it.
|
|
52
51
|
let showLifecycleColumnURL = new URLParam("showlifecycle", false);
|
|
@@ -269,7 +268,8 @@ export class LogViewer3 extends qreact.Component {
|
|
|
269
268
|
let updateResults = throttleFunction(100, () => {
|
|
270
269
|
if (this.searchSequenceNumber !== currentSequenceNumber) return;
|
|
271
270
|
Querysub.commitLocal(() => {
|
|
272
|
-
|
|
271
|
+
// LIMIT, as the other values will be fragmented and so are confusing.
|
|
272
|
+
this.state.results = results.slice(0, limitURL.value);
|
|
273
273
|
});
|
|
274
274
|
});
|
|
275
275
|
|
|
@@ -495,17 +495,19 @@ export class LogViewer3 extends qreact.Component {
|
|
|
495
495
|
|
|
496
496
|
for (let field of selectedFields) {
|
|
497
497
|
let column: ColumnType<unknown, LogDatum> = {};
|
|
498
|
-
if (field === "time") {
|
|
499
|
-
column.formatter = (x: unknown) =>
|
|
500
|
-
} else if (field === "__machineId") {
|
|
501
|
-
column.formatter = (x: unknown, context) => {
|
|
502
|
-
if (!context?.row || !context.row.__machineId) return <ObjectDisplay value={x} />;
|
|
503
|
-
return <MachineThreadInfo machineId={context.row.__machineId} threadId={context.row.__threadId || undefined} />;
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
if (!column.formatter) {
|
|
507
|
-
column.formatter = (x: unknown) => <ObjectDisplay value={x} />;
|
|
498
|
+
if (field === "time" || field === "timeId") {
|
|
499
|
+
column.formatter = (x: unknown) => <span title={formatDateTimeDetailed(Number(x))}>{formatDateTimeDetailed(Number(x))}</span>;
|
|
508
500
|
}
|
|
501
|
+
// ObjectDisplay is too slow.
|
|
502
|
+
// else if (field === "__machineId") {
|
|
503
|
+
// column.formatter = (x: unknown, context) => {
|
|
504
|
+
// if (!context?.row || !context.row.__machineId) return <ObjectDisplay value={x} />;
|
|
505
|
+
// return <MachineThreadInfo machineId={context.row.__machineId} threadId={context.row.__threadId || undefined} />;
|
|
506
|
+
// };
|
|
507
|
+
// }
|
|
508
|
+
// if (!column.formatter) {
|
|
509
|
+
// column.formatter = (x: unknown) => <ObjectDisplay value={x} />;
|
|
510
|
+
// }
|
|
509
511
|
columns[field] = column;
|
|
510
512
|
}
|
|
511
513
|
|
|
@@ -622,11 +624,6 @@ export class LogViewer3 extends qreact.Component {
|
|
|
622
624
|
selectedFieldsURL.value = newValues;
|
|
623
625
|
}}
|
|
624
626
|
/>
|
|
625
|
-
<InputLabelURL
|
|
626
|
-
label="Use Relative Time"
|
|
627
|
-
checkbox
|
|
628
|
-
url={useRelativeTimeURL}
|
|
629
|
-
/>
|
|
630
627
|
<InputLabelURL
|
|
631
628
|
label="Show Lifecycles"
|
|
632
629
|
checkbox
|