querysub 0.42.0 → 0.44.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 +60 -60
- package/src/-a-archives/archivesBackBlaze.ts +3 -0
- package/src/-g-core-values/NodeCapabilities.ts +6 -4
- package/src/0-path-value-core/NodePathAuthorities.ts +24 -2
- package/src/0-path-value-core/pathValueCore.ts +2221 -2216
- package/src/2-proxy/PathValueProxyWatcher.ts +41 -3
- package/src/3-path-functions/PathFunctionRunner.ts +19 -6
- package/src/3-path-functions/pathFunctionLoader.ts +7 -1
- package/src/4-querysub/QuerysubController.ts +0 -10
- package/src/5-diagnostics/GenericFormat.tsx +3 -1
- package/src/5-diagnostics/TimeGrouper.tsx +2 -0
- package/src/5-diagnostics/nodeMetadata.ts +137 -11
- package/src/diagnostics/NodeViewer.tsx +19 -8
- package/src/diagnostics/logs/DiskLoggerPage.tsx +46 -20
- package/src/diagnostics/logs/diskLogger.ts +44 -13
- package/src/diagnostics/periodic.ts +2 -2
- package/src/diagnostics/trackResources.ts +4 -3
- package/src/diagnostics/watchdog.ts +20 -6
- package/src/library-components/Input.tsx +1 -1
- package/src/zip.ts +9 -0
|
@@ -32,6 +32,8 @@ import { DEPTH_TO_DATA, MODULE_INDEX, getCurrentCall, getCurrentCallObj } from "
|
|
|
32
32
|
import { inlineNestedCalls } from "../3-path-functions/syncSchema";
|
|
33
33
|
import { interceptCalls, runCall } from "../3-path-functions/PathFunctionHelpers";
|
|
34
34
|
import { deepCloneCborx } from "../misc/cloneHelpers";
|
|
35
|
+
import { addStatPeriodic, addTimeProfileDistribution, onTimeProfile } from "../5-diagnostics/nodeMetadata";
|
|
36
|
+
import { formatPercent } from "socket-function/src/formatting/format";
|
|
35
37
|
|
|
36
38
|
// TODO: Break this into two parts:
|
|
37
39
|
// 1) Run and get accesses
|
|
@@ -158,7 +160,19 @@ interface WatcherOptions<Result> {
|
|
|
158
160
|
trackTriggers?: boolean;
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
let harvestableReadyLoopCount = 0;
|
|
164
|
+
let harvestableWaitingLoopCount = 0;
|
|
165
|
+
let lastZombieCount = 0;
|
|
166
|
+
// NOTE: Only the % waiting for active watchers. Which... is fine
|
|
167
|
+
addStatPeriodic("Watcher % Waiting", () => {
|
|
168
|
+
let totalLoop = harvestableReadyLoopCount + harvestableWaitingLoopCount;
|
|
169
|
+
if (totalLoop === 0) return 0;
|
|
170
|
+
let frac = harvestableWaitingLoopCount / totalLoop;
|
|
171
|
+
harvestableWaitingLoopCount = 0;
|
|
172
|
+
harvestableReadyLoopCount = 0;
|
|
173
|
+
return frac;
|
|
174
|
+
}, formatPercent);
|
|
175
|
+
addStatPeriodic("Stalled Watchers", () => lastZombieCount);
|
|
162
176
|
|
|
163
177
|
// Used to prevent local rejections on remote values (as local functions aren't rerun)
|
|
164
178
|
// Also used for security on servers, so they can read from untrusted domains, but can't have values
|
|
@@ -966,7 +980,19 @@ export class PathValueProxyWatcher {
|
|
|
966
980
|
watcher.dispose = dispose;
|
|
967
981
|
this.allWatchers.add(watcher);
|
|
968
982
|
|
|
969
|
-
|
|
983
|
+
let baseFunction = options.watchFunction;
|
|
984
|
+
baseFunction = measureWrap(baseFunction, watcher.debugName);
|
|
985
|
+
{
|
|
986
|
+
let base = baseFunction;
|
|
987
|
+
baseFunction = (...args: unknown[]) => {
|
|
988
|
+
let time = Date.now();
|
|
989
|
+
try {
|
|
990
|
+
return (base as any)(...args);
|
|
991
|
+
} finally {
|
|
992
|
+
onTimeProfile("Watcher", time);
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
970
996
|
|
|
971
997
|
watcher.setCurrentReadTime = (time, code) => {
|
|
972
998
|
let prev = watcher.currentReadTime;
|
|
@@ -1430,13 +1456,24 @@ export class PathValueProxyWatcher {
|
|
|
1430
1456
|
this.runningWatcher = undefined;
|
|
1431
1457
|
}
|
|
1432
1458
|
|
|
1459
|
+
const nonLooping = options.runImmediately || options.allowUnsyncedReads;
|
|
1460
|
+
|
|
1433
1461
|
const readyToCommit = (
|
|
1434
|
-
|
|
1462
|
+
nonLooping || (
|
|
1435
1463
|
watcher.lastUnsyncedAccesses.size === 0
|
|
1436
1464
|
&& watcher.lastUnsyncedParentAccesses.size === 0
|
|
1437
1465
|
&& !watcher.lastSpecialPromiseUnsynced
|
|
1438
1466
|
)
|
|
1439
1467
|
);
|
|
1468
|
+
|
|
1469
|
+
if (!nonLooping) {
|
|
1470
|
+
if (readyToCommit) {
|
|
1471
|
+
harvestableReadyLoopCount++;
|
|
1472
|
+
} else {
|
|
1473
|
+
harvestableWaitingLoopCount++;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1440
1477
|
// Commit writes BEFORE we watch. This prevents us from triggering ourself, in our first watch
|
|
1441
1478
|
// (on subsequent watches ClientWatcher will prevent self watches. It can't for the first
|
|
1442
1479
|
// watch, as it wasn't the triggerer).
|
|
@@ -1881,6 +1918,7 @@ registerPeriodic(function checkForZombieWatches() {
|
|
|
1881
1918
|
}
|
|
1882
1919
|
|
|
1883
1920
|
let zombies = Array.from(proxyWatcher.getAllWatchers()).filter(isZombieWatch);
|
|
1921
|
+
lastZombieCount = zombies.length;
|
|
1884
1922
|
if (zombies.length > 0) {
|
|
1885
1923
|
console.warn(red(`Found ${zombies.length} zombie watchers`));
|
|
1886
1924
|
let namesSet = new Set(zombies.map(x => x.debugName));
|
|
@@ -542,11 +542,14 @@ export class PathFunctionRunner {
|
|
|
542
542
|
}
|
|
543
543
|
|
|
544
544
|
let evalTimeStart = Date.now();
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
545
|
+
try {
|
|
546
|
+
let args = parseArgs(callPath);
|
|
547
|
+
overrideCurrentCall({ spec: callPath, fnc: syncedSpec, }, () => {
|
|
548
|
+
baseFunction(...args);
|
|
549
|
+
});
|
|
550
|
+
} finally {
|
|
551
|
+
evalTime += Date.now() - evalTimeStart;
|
|
552
|
+
}
|
|
550
553
|
|
|
551
554
|
// NOTE: The results are only temporarily stored, but serve as a way to let clients
|
|
552
555
|
// know figure out the dependencies for the call, which makes function call prediction
|
|
@@ -573,6 +576,7 @@ export class PathFunctionRunner {
|
|
|
573
576
|
});
|
|
574
577
|
proxyWatcher.setEventPath(() => syncedModule.Results[callPath.CallId]);
|
|
575
578
|
});
|
|
579
|
+
|
|
576
580
|
},
|
|
577
581
|
}, {
|
|
578
582
|
onWritesCommitted(writes) {
|
|
@@ -631,7 +635,16 @@ export class PathFunctionRunner {
|
|
|
631
635
|
if (PathFunctionRunner.DEBUG_CALLS) {
|
|
632
636
|
console.log(`FINISHED${nooped ? " (skipped)" : ""} ${getDebugName(callPath, functionSpec, true)}, writes: ${finalWrites?.length}`);
|
|
633
637
|
}
|
|
634
|
-
|
|
638
|
+
|
|
639
|
+
let wallTime = Date.now() - startTime;
|
|
640
|
+
let syncTime = wallTime - evalTime;
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
diskLog(`Finished FunctionRunner function`, {
|
|
644
|
+
...callPath, argsEncoded: "", functionSpec,
|
|
645
|
+
wallTime, syncTime, evalTime,
|
|
646
|
+
loops: runCount,
|
|
647
|
+
});
|
|
635
648
|
PathFunctionRunner.RUN_FINISH_COUNT++;
|
|
636
649
|
}
|
|
637
650
|
}
|
|
@@ -22,6 +22,7 @@ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
|
22
22
|
import { getControllerNodeId, getControllerNodeIdList } from "../-g-core-values/NodeCapabilities";
|
|
23
23
|
import { sha256 } from "js-sha256";
|
|
24
24
|
import os from "os";
|
|
25
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
25
26
|
|
|
26
27
|
// Get localPathRemappings using yargs, so it is easy to configure in multiple entry points
|
|
27
28
|
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
@@ -158,6 +159,8 @@ let moduleResolver = async (spec: FunctionSpec) => {
|
|
|
158
159
|
await getFileLock(lockPath, async () => {
|
|
159
160
|
if (fs.existsSync(repoPath + loadTimeIndicatorFileName)) return;
|
|
160
161
|
|
|
162
|
+
let time = Date.now();
|
|
163
|
+
|
|
161
164
|
// Remove any previous attempt to sync it
|
|
162
165
|
if (fs.existsSync(repoPath)) {
|
|
163
166
|
await fs.promises.rename(repoPath, repoPath + "_" + Date.now());
|
|
@@ -171,6 +174,9 @@ let moduleResolver = async (spec: FunctionSpec) => {
|
|
|
171
174
|
|
|
172
175
|
// Mark it as loaded. If we don't reach this point we will move the folder and try again next time
|
|
173
176
|
await fs.promises.writeFile(repoPath + loadTimeIndicatorFileName, Date.now() + "");
|
|
177
|
+
|
|
178
|
+
time = Date.now() - time;
|
|
179
|
+
console.log(blue(`Cloned and yarn installed repo`), { gitURL, time });
|
|
174
180
|
});
|
|
175
181
|
}
|
|
176
182
|
|
|
@@ -415,7 +421,7 @@ let everLoadedRepos = new Set<string>();
|
|
|
415
421
|
export async function preloadFunctions(specs: FunctionSpec[]) {
|
|
416
422
|
let nodeIds = await getControllerNodeIdList(functionPreloadController);
|
|
417
423
|
await Promise.all(nodeIds.map(async nodeId => {
|
|
418
|
-
let controller = functionPreloadController.nodes[nodeId];
|
|
424
|
+
let controller = functionPreloadController.nodes[nodeId.nodeId];
|
|
419
425
|
console.log(blue(`Preloading functions on ${nodeId}`));
|
|
420
426
|
await errorToUndefined(controller.preloadFunctions(specs));
|
|
421
427
|
console.log(blue(`Finished preloading functions on ${nodeId}`));
|
|
@@ -125,16 +125,6 @@ interface PermissionsObj {
|
|
|
125
125
|
let permissionsPerNode = new Map<string, PermissionsObj>();
|
|
126
126
|
registerResource("querysub|permissionsPerNode", permissionsPerNode);
|
|
127
127
|
|
|
128
|
-
const lastCallTime = cache((nodeId: string): { value: Time } => {
|
|
129
|
-
SocketFunction.onNextDisconnect(nodeId, () => {
|
|
130
|
-
setTimeout(() => {
|
|
131
|
-
if (SocketFunction.isNodeConnected(nodeId)) return;
|
|
132
|
-
lastCallTime.clear(nodeId);
|
|
133
|
-
}, 1000 * 60);
|
|
134
|
-
});
|
|
135
|
-
return { value: epochTime };
|
|
136
|
-
});
|
|
137
|
-
|
|
138
128
|
export const isTrustedByDomain = cache(async function isTrustedByDomain(domain: string): Promise<boolean> {
|
|
139
129
|
// NOTE: When getControllerNodeId accepts a domain we will pass them the domain we are using.
|
|
140
130
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatDateTime, formatNiceDateTime, formatNumber, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
1
|
+
import { formatDateTime, formatNiceDateTime, formatNumber, formatPercent, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
2
2
|
import { css } from "typesafecss";
|
|
3
3
|
import { canHaveChildren } from "socket-function/src/types";
|
|
4
4
|
import { qreact } from "../4-dom/qreact";
|
|
@@ -22,6 +22,7 @@ export type JSXFormatter<T = unknown, RowT extends RowType = RowType> = (
|
|
|
22
22
|
type StringFormatters = (
|
|
23
23
|
"guess"
|
|
24
24
|
| "string" | "number"
|
|
25
|
+
| "percent"
|
|
25
26
|
| "timeSpan" | "date"
|
|
26
27
|
| "error" | "link"
|
|
27
28
|
| "toSpaceCase"
|
|
@@ -38,6 +39,7 @@ const endGuessDateRange = +new Date(2040, 0, 1).getTime();
|
|
|
38
39
|
let formatters: { [formatter in StringFormatters]: (value: unknown) => preact.ComponentChild } = {
|
|
39
40
|
string: (value) => d(value, String(value || "")),
|
|
40
41
|
number: (value) => d(value, formatNumber(Number(value))),
|
|
42
|
+
percent: (value) => d(value, formatPercent(Number(value))),
|
|
41
43
|
timeSpan: (value) => d(value, formatTime(Number(value))),
|
|
42
44
|
date: (value) => d(value, formatVeryNiceDateTime(Number(value))),
|
|
43
45
|
error: (value) => d(value, <span class={errorMessage}>{String(value)}</span>),
|
|
@@ -93,6 +93,7 @@ export class TimeGrouper {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** For sampled values, ex, % CPU usage */
|
|
96
97
|
public getStateSummary(format: (value: number) => string): string {
|
|
97
98
|
return this.getSummary().map(bucket => {
|
|
98
99
|
if (bucket.duration === 0 || bucket.min === bucket.max) {
|
|
@@ -102,6 +103,7 @@ export class TimeGrouper {
|
|
|
102
103
|
return `(${bucket.name}) ${format(bucket.min)} to ${format(bucket.max)}`;
|
|
103
104
|
}).filter(isDefined).join("\n");
|
|
104
105
|
}
|
|
106
|
+
/** Sums up the values. Ex, for time waiting. */
|
|
105
107
|
public getEventSummary(format: (value: number) => string): string {
|
|
106
108
|
return this.getSummary().map(bucket => {
|
|
107
109
|
if (bucket.duration === 0) {
|
|
@@ -5,13 +5,16 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
5
5
|
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
6
6
|
import { cache } from "socket-function/src/caching";
|
|
7
7
|
import { TimeGrouper } from "./TimeGrouper";
|
|
8
|
-
import { runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
9
|
-
import { throttleFunction, timeInMinute } from "socket-function/src/misc";
|
|
8
|
+
import { runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
9
|
+
import { sort, throttleFunction, timeInMinute } from "socket-function/src/misc";
|
|
10
10
|
import { formatNumber, formatPercent, formatTime } from "socket-function/src/formatting/format";
|
|
11
11
|
import os from "os";
|
|
12
12
|
import { isNode } from "typesafecss";
|
|
13
13
|
import { diskLog } from "../diagnostics/logs/diskLogger";
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
const POLL_INTERVAL = timeInMinute * 5;
|
|
17
|
+
|
|
15
18
|
// Undefined means we infer the column
|
|
16
19
|
// Null means the column is removed
|
|
17
20
|
export type ColumnType = undefined | null | {
|
|
@@ -39,14 +42,14 @@ export function registerNodeMetadata(getter: ExtraMetadata) {
|
|
|
39
42
|
extraMetadatas.push(getter);
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
// IMPORTANT! We no longer diskLog here! diskLogging is not ideal for metrics like these. You almost only
|
|
46
|
+
// care about the current metrics OR past trends. A single value in the logs from the past is just
|
|
47
|
+
// NOT useful.
|
|
48
|
+
|
|
42
49
|
let stateValues = cache((title: string, format: (value: number) => string): { (value: number): void } => {
|
|
43
50
|
let timeGrouper = new TimeGrouper();
|
|
44
|
-
let logToDisk = throttleFunction(timeInMinute, (value: number) => {
|
|
45
|
-
diskLog(title, { value, formatted: format(value) });
|
|
46
|
-
});
|
|
47
51
|
function onValue(value: number) {
|
|
48
52
|
timeGrouper.onValueChanged(value);
|
|
49
|
-
void logToDisk(value);
|
|
50
53
|
}
|
|
51
54
|
registerNodeMetadata({
|
|
52
55
|
columnName: title,
|
|
@@ -59,12 +62,8 @@ let stateValues = cache((title: string, format: (value: number) => string): { (v
|
|
|
59
62
|
});
|
|
60
63
|
let eventValues = cache((title: string, format: (value: number) => string): { (value: number): void } => {
|
|
61
64
|
let timeGrouper = new TimeGrouper();
|
|
62
|
-
let logToDisk = throttleFunction(timeInMinute, (value: number) => {
|
|
63
|
-
diskLog(title, { value, formatted: format(value) });
|
|
64
|
-
});
|
|
65
65
|
function onValue(value: number) {
|
|
66
66
|
timeGrouper.onValueChanged(value);
|
|
67
|
-
void logToDisk(value);
|
|
68
67
|
}
|
|
69
68
|
registerNodeMetadata({
|
|
70
69
|
columnName: title,
|
|
@@ -76,15 +75,142 @@ let eventValues = cache((title: string, format: (value: number) => string): { (v
|
|
|
76
75
|
return onValue;
|
|
77
76
|
});
|
|
78
77
|
|
|
78
|
+
/** For values that should be summed to get the value. */
|
|
79
79
|
export function logNodeStats(title: string, format: (value: number) => string): { (value: number): void } {
|
|
80
80
|
return eventValues(title, format);
|
|
81
81
|
}
|
|
82
|
+
/** For state values (ex, % CPU usage), for which summing wouldn't make sense. */
|
|
82
83
|
export function logNodeStateStats(title: string, format: (value: number) => string): { (value: number): void } {
|
|
83
84
|
return stateValues(title, format);
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
let existingPeriodic = new Set<string>();
|
|
88
|
+
/** For state values (ex, % CPU usage), for which summing wouldn't make sense. */
|
|
89
|
+
export function addStatPeriodic(
|
|
90
|
+
title: string,
|
|
91
|
+
getValue: () => number,
|
|
92
|
+
format: (value: number) => string = formatNumber,
|
|
93
|
+
) {
|
|
94
|
+
let hash = JSON.stringify({ title, getValue: getValue.toString(), format: format.toString() });
|
|
95
|
+
// Ignoring duplicates, otherwise we end up logging way too much
|
|
96
|
+
if (globalThis.isHotReloading?.() && existingPeriodic.has(hash)) return;
|
|
97
|
+
|
|
98
|
+
let timeGrouper = new TimeGrouper();
|
|
99
|
+
void runInfinitePollCallAtStart(POLL_INTERVAL, () => {
|
|
100
|
+
timeGrouper.onValueChanged(getValue());
|
|
101
|
+
});
|
|
102
|
+
registerNodeMetadata({
|
|
103
|
+
columnName: title,
|
|
104
|
+
column: {},
|
|
105
|
+
getValue() {
|
|
106
|
+
return timeGrouper.getStateSummary(format);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** For values that should be summed to get the value. */
|
|
111
|
+
export function addStatSumPeriodic(
|
|
112
|
+
title: string,
|
|
113
|
+
getValue: () => number,
|
|
114
|
+
format: (value: number) => string = formatNumber,
|
|
115
|
+
) {
|
|
116
|
+
let hash = JSON.stringify({ title, getValue: getValue.toString(), format: format.toString() });
|
|
117
|
+
// Ignoring duplicates, otherwise we end up logging way too much
|
|
118
|
+
if (globalThis.isHotReloading?.() && existingPeriodic.has(hash)) return;
|
|
119
|
+
|
|
120
|
+
let timeGrouper = new TimeGrouper();
|
|
121
|
+
void runInfinitePollCallAtStart(POLL_INTERVAL, () => {
|
|
122
|
+
timeGrouper.onValueChanged(getValue());
|
|
123
|
+
});
|
|
124
|
+
registerNodeMetadata({
|
|
125
|
+
columnName: title,
|
|
126
|
+
column: {},
|
|
127
|
+
getValue() {
|
|
128
|
+
return timeGrouper.getEventSummary(format);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let pendingTimeProfiles = new Map<string, { start: number; end: number }[]>();
|
|
134
|
+
export function onTimeProfile(title: string, startTime: number) {
|
|
135
|
+
let profiles = pendingTimeProfiles.get(title);
|
|
136
|
+
if (!profiles) {
|
|
137
|
+
profiles = [];
|
|
138
|
+
pendingTimeProfiles.set(title, profiles);
|
|
139
|
+
addTimeProfileDistribution(title, () => {
|
|
140
|
+
let values = pendingTimeProfiles.get(title);
|
|
141
|
+
pendingTimeProfiles.set(title, []);
|
|
142
|
+
return values || [];
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
profiles.push({ start: startTime, end: Date.now() });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Ex, for tracking the time distribution of API calls, or watchFunction evaluation, etc.
|
|
149
|
+
* - Specifically for time, so we can handle overlap cases (such as waiting for 10 calls at once),
|
|
150
|
+
* which requires specially handling otherwise the values will be meaningless (because
|
|
151
|
+
* waiting from time 0 to 100ms for 10 calls is VERY different than waiting
|
|
152
|
+
* from time 0 to 1000ms for 1 call).
|
|
153
|
+
*
|
|
154
|
+
* NOTE: Usually you should call onTimeProfile, which will wrap this call. But, sometimes
|
|
155
|
+
* you need to harvest values from an external library, in which case, you will need
|
|
156
|
+
* this function.
|
|
157
|
+
*/
|
|
158
|
+
export function addTimeProfileDistribution(
|
|
159
|
+
title: string,
|
|
160
|
+
/** It's expected that this will return all values since the last call to harvestValues
|
|
161
|
+
* (basically, copy your value buffer and clear the old one).
|
|
162
|
+
*/
|
|
163
|
+
harvestTimeRanges: () => { start: number; end: number; }[],
|
|
164
|
+
) {
|
|
165
|
+
let hash = JSON.stringify({ title, getValues: harvestTimeRanges.toString() });
|
|
166
|
+
// Ignoring duplicates, otherwise we end up logging way too much
|
|
167
|
+
if (globalThis.isHotReloading?.() && existingPeriodic.has(hash)) return;
|
|
168
|
+
|
|
169
|
+
let fractionTimeWaitingGroup = new TimeGrouper();
|
|
170
|
+
let ratePerSecondGroup = new TimeGrouper();
|
|
171
|
+
let lastPollTime = Date.now();
|
|
172
|
+
runInfinitePoll(POLL_INTERVAL, () => {
|
|
173
|
+
let originalStartTime = lastPollTime;
|
|
174
|
+
let values = harvestTimeRanges();
|
|
175
|
+
values = values.slice();
|
|
176
|
+
sort(values, x => x.start);
|
|
177
|
+
let freeTime = 0;
|
|
178
|
+
for (let { start, end } of values) {
|
|
179
|
+
if (start > lastPollTime) {
|
|
180
|
+
freeTime += start - lastPollTime;
|
|
181
|
+
}
|
|
182
|
+
lastPollTime = end;
|
|
183
|
+
}
|
|
184
|
+
let now = Date.now();
|
|
185
|
+
freeTime += now - lastPollTime;
|
|
186
|
+
lastPollTime = now;
|
|
187
|
+
|
|
188
|
+
let duration = lastPollTime - originalStartTime;
|
|
189
|
+
let waitingTime = duration - freeTime;
|
|
190
|
+
let ratePerSecond = values.length / (duration / 1000);
|
|
191
|
+
|
|
192
|
+
fractionTimeWaitingGroup.onValueChanged(waitingTime / duration);
|
|
193
|
+
ratePerSecondGroup.onValueChanged(ratePerSecond);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
registerNodeMetadata({
|
|
197
|
+
columnName: `% Time in ${title}`,
|
|
198
|
+
column: {},
|
|
199
|
+
getValue() {
|
|
200
|
+
return fractionTimeWaitingGroup.getStateSummary(formatPercent);
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
registerNodeMetadata({
|
|
204
|
+
columnName: `${title}/S`,
|
|
205
|
+
column: {},
|
|
206
|
+
getValue() {
|
|
207
|
+
return ratePerSecondGroup.getStateSummary(formatNumber);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
86
212
|
if (isNode()) {
|
|
87
|
-
void runInfinitePollCallAtStart(
|
|
213
|
+
void runInfinitePollCallAtStart(POLL_INTERVAL, async () => {
|
|
88
214
|
// Get system memory usage
|
|
89
215
|
|
|
90
216
|
const free = os.freemem();
|
|
@@ -185,13 +185,22 @@ export class NodeViewer extends qreact.Component {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
let builtinGroups = {
|
|
188
|
-
"Default": ["buttons", "devToolsURL", "nodeId", "ip", "uptime", "loadTime", "
|
|
188
|
+
"Default": ["buttons", "devToolsURL", "nodeId", "ip", "uptime", "loadTime", "% Profiled", "All Memory", "port", "threadId", "machineId", "apiError", "live_entryPoint"],
|
|
189
|
+
"Performance": [
|
|
190
|
+
"All Memory",
|
|
191
|
+
"Heap", "Buffers",
|
|
192
|
+
"% Profiled",
|
|
193
|
+
"Blocking Lag",
|
|
194
|
+
"Top Profiled",
|
|
195
|
+
],
|
|
189
196
|
};
|
|
190
197
|
// Column => group
|
|
191
|
-
let builtInGroupsLookup = new Map<string, string>();
|
|
198
|
+
let builtInGroupsLookup = new Map<string, string[]>();
|
|
192
199
|
for (let [group, columns] of Object.entries(builtinGroups)) {
|
|
193
200
|
for (let column of columns) {
|
|
194
|
-
builtInGroupsLookup.
|
|
201
|
+
let groups = builtInGroupsLookup.get(column) || [];
|
|
202
|
+
groups.push(group);
|
|
203
|
+
builtInGroupsLookup.set(column, groups);
|
|
195
204
|
}
|
|
196
205
|
}
|
|
197
206
|
let groupedTables: {
|
|
@@ -206,12 +215,14 @@ export class NodeViewer extends qreact.Component {
|
|
|
206
215
|
for (let table of tableList) {
|
|
207
216
|
let row = table.table?.row || {};
|
|
208
217
|
for (let column in row) {
|
|
209
|
-
let
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
let groupTitles = builtInGroupsLookup.get(column) || ["Other"];
|
|
219
|
+
for (let groupTitle of groupTitles) {
|
|
220
|
+
if (column.includes("|")) {
|
|
221
|
+
groupTitle = column.split("|")[0];
|
|
222
|
+
}
|
|
223
|
+
groupedTables[groupTitle] = groupedTables[groupTitle] || {};
|
|
224
|
+
groupedTables[groupTitle][column] = true;
|
|
212
225
|
}
|
|
213
|
-
groupedTables[groupTitle] = groupedTables[groupTitle] || {};
|
|
214
|
-
groupedTables[groupTitle][column] = true;
|
|
215
226
|
}
|
|
216
227
|
}
|
|
217
228
|
}
|
|
@@ -4,7 +4,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
4
4
|
import { getSourceVSCodeLink, qreact } from "../../4-dom/qreact";
|
|
5
5
|
import { assertIsManagementUser } from "../managementPages";
|
|
6
6
|
import { LogFile, LogObj, getLogBuffer, getLogFiles, parseLogBuffer } from "./diskLogger";
|
|
7
|
-
import { t } from "../../4-querysub/Querysub";
|
|
7
|
+
import { Querysub, t } from "../../4-querysub/Querysub";
|
|
8
8
|
import { URLParam } from "../../library-components/URLParam";
|
|
9
9
|
import { getSyncedController } from "../../library-components/SyncedController";
|
|
10
10
|
import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
@@ -16,7 +16,7 @@ import { DropdownSelector } from "../../library-components/DropdownSelector";
|
|
|
16
16
|
import { Table, TableType } from "../../5-diagnostics/Table";
|
|
17
17
|
import { LogType } from "../errorLogs/ErrorLogCore";
|
|
18
18
|
import { canHaveChildren } from "socket-function/src/types";
|
|
19
|
-
import { measureBlock } from "socket-function/src/profiling/measure";
|
|
19
|
+
import { measureBlock, measureWrap } from "socket-function/src/profiling/measure";
|
|
20
20
|
import { binarySearchBasic, list, sort, timeInDay, timeInHour } from "socket-function/src/misc";
|
|
21
21
|
import { NodeViewerController, nodeViewerController } from "../NodeViewer";
|
|
22
22
|
import { red } from "socket-function/src/formatting/logColors";
|
|
@@ -31,6 +31,7 @@ import { decodeNodeId, encodeNodeId, getMachineId } from "../../-a-auth/certs";
|
|
|
31
31
|
import { Button } from "../../library-components/Button";
|
|
32
32
|
import { TimeRangeSelector } from "../../library-components/TimeRangeSelector";
|
|
33
33
|
import { BrowserLargeFileCache, cacheCalls } from "./BrowserLargeFileCache";
|
|
34
|
+
import { Zip } from "../../zip";
|
|
34
35
|
|
|
35
36
|
// TODO: Realtime log mode, by reading from the previous length forward, to add buffers
|
|
36
37
|
// to what we already read.
|
|
@@ -49,6 +50,10 @@ import { BrowserLargeFileCache, cacheCalls } from "./BrowserLargeFileCache";
|
|
|
49
50
|
// - Probably by injecting ids? Although how would they be persistent? Hmm... Maybe line numbers
|
|
50
51
|
// is fine? I think we map the typescript (I hope we do), so they should be correct?
|
|
51
52
|
|
|
53
|
+
// TODO: Use a binary format, but faster parsing.
|
|
54
|
+
// - If we could batch it, it would probably be very fast. We ARE just dealing with KVPs though,
|
|
55
|
+
// so... maybe it wouldn't be that much faster...
|
|
56
|
+
|
|
52
57
|
module.hotreload = true;
|
|
53
58
|
module.hotreload = true;
|
|
54
59
|
|
|
@@ -65,7 +70,7 @@ let inspectTimeURL = new URLParam("inspectTime", "");
|
|
|
65
70
|
|
|
66
71
|
export class DiskLoggerPage extends qreact.Component {
|
|
67
72
|
state = t.state({
|
|
68
|
-
|
|
73
|
+
downloadFilesHash: t.string,
|
|
69
74
|
});
|
|
70
75
|
render() {
|
|
71
76
|
let selectedNodesIds = selectedNodeId.value.split("|").filter(x => x);
|
|
@@ -74,8 +79,8 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
74
79
|
|
|
75
80
|
if (selectedNodesIds.length === 0) {
|
|
76
81
|
// Group by machineId
|
|
77
|
-
let grouped = new Map<string, string[]>();
|
|
78
|
-
for (let nodeId of nodeIds) {
|
|
82
|
+
let grouped = new Map<string, { nodeId: string; entryPoint: string }[]>();
|
|
83
|
+
for (let { nodeId, entryPoint } of nodeIds) {
|
|
79
84
|
let obj = decodeNodeId(nodeId);
|
|
80
85
|
if (!obj) continue;
|
|
81
86
|
let { machineId } = obj;
|
|
@@ -83,7 +88,7 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
83
88
|
if (!list) {
|
|
84
89
|
grouped.set(machineId, list = []);
|
|
85
90
|
}
|
|
86
|
-
list.push(nodeId);
|
|
91
|
+
list.push({ nodeId, entryPoint });
|
|
87
92
|
}
|
|
88
93
|
return (
|
|
89
94
|
<div class={css.pad2(10).vbox(10)}>
|
|
@@ -94,12 +99,12 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
94
99
|
<div class={css.vbox(10).wrap.maxWidth("30vw").pad2(5).hsla(0, 0, 0, 0.1)}>
|
|
95
100
|
<div class={css.fontSize(30)}>{getMachineId(machineId)}</div>
|
|
96
101
|
<div class={css.hbox(20, 5).wrap}>
|
|
97
|
-
{list.map(nodeId =>
|
|
102
|
+
{list.map(({ nodeId, entryPoint }) =>
|
|
98
103
|
<Anchor
|
|
99
104
|
class={css.hbox(10)}
|
|
100
105
|
values={[selectedNodeId.getOverride(nodeId)]}
|
|
101
106
|
>
|
|
102
|
-
{nodeId}
|
|
107
|
+
{nodeId} ({entryPoint.replaceAll("\\", "/").split("/").pop()})
|
|
103
108
|
</Anchor>
|
|
104
109
|
)}
|
|
105
110
|
</div>
|
|
@@ -135,16 +140,23 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
135
140
|
let firstTime = Number.MAX_SAFE_INTEGER;
|
|
136
141
|
let lastTime = Number.MIN_SAFE_INTEGER;
|
|
137
142
|
|
|
143
|
+
let filesHash = JSON.stringify(files.map(x => x.path));
|
|
144
|
+
|
|
138
145
|
for (let file of files) {
|
|
139
146
|
try {
|
|
140
147
|
if (file.startTime < firstTime) firstTime = file.startTime;
|
|
141
148
|
if (file.endTime > lastTime) lastTime = file.endTime;
|
|
149
|
+
if (filesHash !== this.state.downloadFilesHash) {
|
|
150
|
+
loadingCount++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
142
153
|
let buffer = controller.getRemoteLogBuffer(file.nodeId, file.path);
|
|
143
|
-
if (buffer) {
|
|
144
|
-
buffers.push(buffer);
|
|
145
|
-
} else {
|
|
154
|
+
if (!buffer) {
|
|
146
155
|
loadingCount++;
|
|
156
|
+
continue;
|
|
147
157
|
}
|
|
158
|
+
buffer = unzipCached(buffer);
|
|
159
|
+
buffers.push(buffer);
|
|
148
160
|
} catch (e) {
|
|
149
161
|
console.log("Error reading buffer", file, e);
|
|
150
162
|
}
|
|
@@ -277,9 +289,8 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
277
289
|
picked={selectedNodesIds}
|
|
278
290
|
addPicked={x => selectedNodeId.value = [...selectedNodesIds, x].join("|")}
|
|
279
291
|
removePicked={x => selectedNodeId.value = selectedNodesIds.filter(y => y !== x).join("|")}
|
|
280
|
-
options={nodeIds.map(
|
|
292
|
+
options={nodeIds.map(({ nodeId, entryPoint }) => ({ value: nodeId, label: `${nodeId} (${entryPoint.replaceAll("\\", "/").split("/").pop()})` }))}
|
|
281
293
|
/>
|
|
282
|
-
{loadingCount > 0 && <h1>Loading {loadingCount} files</h1>}
|
|
283
294
|
<div className={css.fontSize(28).boldStyle}>
|
|
284
295
|
{selectedNodesIds.length} nodes, {files.length} files, filtered logs {formatNumber(filteredCount)} / {formatNumber(totalCount)}
|
|
285
296
|
</div>
|
|
@@ -294,6 +305,13 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
294
305
|
)}
|
|
295
306
|
</div>
|
|
296
307
|
</ShowMore>
|
|
308
|
+
{filesHash !== this.state.downloadFilesHash && <div>
|
|
309
|
+
<button onClick={() => {
|
|
310
|
+
this.state.downloadFilesHash = filesHash;
|
|
311
|
+
}}>
|
|
312
|
+
Download Files
|
|
313
|
+
</button>
|
|
314
|
+
</div>}
|
|
297
315
|
{!inspectTime && <>
|
|
298
316
|
<TimeRangeSelector
|
|
299
317
|
start={startTimeURL} end={endTimeURL}
|
|
@@ -349,6 +367,7 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
349
367
|
number
|
|
350
368
|
/>
|
|
351
369
|
)}
|
|
370
|
+
{loadingCount > 0 && <h1>Loading {loadingCount} files</h1>}
|
|
352
371
|
<Table {...table} initialLimit={30} />
|
|
353
372
|
{!!inspectTime && (
|
|
354
373
|
<InputLabel
|
|
@@ -363,6 +382,8 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
363
382
|
}
|
|
364
383
|
}
|
|
365
384
|
|
|
385
|
+
const unzipCached = cacheLimited(100, Zip.gunzipSync);
|
|
386
|
+
|
|
366
387
|
|
|
367
388
|
let remainingCacheSize = 1024 * 1024 * 1024 * 2;
|
|
368
389
|
let logBufferCache = new Map<string, {
|
|
@@ -370,7 +391,8 @@ let logBufferCache = new Map<string, {
|
|
|
370
391
|
logs: LogObj[];
|
|
371
392
|
lastAccess: number;
|
|
372
393
|
}>();
|
|
373
|
-
|
|
394
|
+
|
|
395
|
+
const parseLogBufferCached = measureWrap(function parseLogBufferCached(hash: string, buffers: Buffer[]): LogObj[] {
|
|
374
396
|
let cached = logBufferCache.get(hash);
|
|
375
397
|
if (cached) {
|
|
376
398
|
cached.lastAccess = Date.now();
|
|
@@ -390,7 +412,7 @@ function parseLogBufferCached(hash: string, buffers: Buffer[]): LogObj[] {
|
|
|
390
412
|
lastAccess: Date.now()
|
|
391
413
|
});
|
|
392
414
|
return logs;
|
|
393
|
-
}
|
|
415
|
+
});
|
|
394
416
|
|
|
395
417
|
|
|
396
418
|
|
|
@@ -542,7 +564,14 @@ class DiskLoggerControllerBase {
|
|
|
542
564
|
return await getLogFiles();
|
|
543
565
|
}
|
|
544
566
|
public async getLogBuffer(path: string): Promise<Buffer | undefined> {
|
|
545
|
-
|
|
567
|
+
// Always compress it. If it is in progress it can't be compressed on disk, but
|
|
568
|
+
// the compression ratio is so high it's worth it to compress it dynamically now,
|
|
569
|
+
// to speed up the network transfer.
|
|
570
|
+
let buffer = await getLogBuffer(path);
|
|
571
|
+
if (buffer && path.endsWith(".log")) {
|
|
572
|
+
buffer = await Zip.gzip(buffer);
|
|
573
|
+
}
|
|
574
|
+
return buffer;
|
|
546
575
|
}
|
|
547
576
|
}
|
|
548
577
|
|
|
@@ -552,14 +581,11 @@ export const DiskLoggerController = SocketFunction.register(
|
|
|
552
581
|
() => ({
|
|
553
582
|
getRemoteLogFiles: {},
|
|
554
583
|
getRemoteLogBuffer: {
|
|
555
|
-
compress: true,
|
|
556
584
|
clientHooks: [cacheCalls]
|
|
557
585
|
},
|
|
558
586
|
|
|
559
587
|
getLogFiles: {},
|
|
560
|
-
getLogBuffer: {
|
|
561
|
-
compress: true,
|
|
562
|
-
},
|
|
588
|
+
getLogBuffer: {},
|
|
563
589
|
}),
|
|
564
590
|
() => ({
|
|
565
591
|
hooks: [assertIsManagementUser],
|