querysub 0.46.0 → 0.50.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 +2 -2
- package/src/-0-hooks/hooks.ts +83 -0
- package/src/-a-archives/archivesBackBlaze.ts +1 -1
- package/src/0-path-value-core/NodePathAuthorities.ts +5 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +12 -12
- package/src/0-path-value-core/debugLogs.ts +6 -15
- package/src/0-path-value-core/pathValueCore.ts +53 -17
- package/src/2-proxy/PathValueProxyWatcher.ts +18 -10
- package/src/2-proxy/archiveMoveHarness.ts +7 -7
- package/src/2-proxy/garbageCollection.ts +6 -6
- package/src/4-dom/qreact.tsx +8 -1
- package/src/4-querysub/Querysub.ts +9 -4
- package/src/5-diagnostics/GenericFormat.tsx +92 -1
- package/src/5-diagnostics/Table.tsx +7 -12
- package/src/5-diagnostics/TimeGrouper.tsx +24 -15
- package/src/5-diagnostics/memoryValueAudit.ts +7 -10
- package/src/5-diagnostics/nodeMetadata.ts +92 -44
- package/src/5-diagnostics/synchronousLagTracking.ts +1 -1
- package/src/diagnostics/NodeViewer.tsx +3 -4
- package/src/diagnostics/logs/DiskLoggerPage.tsx +27 -12
- package/src/diagnostics/logs/diskLogger.ts +9 -0
- package/src/diagnostics/trackResources.ts +2 -2
- package/src/diagnostics/watchdog.ts +19 -8
- package/src/library-components/TimeRangeSelector.tsx +1 -1
|
@@ -4,6 +4,8 @@ import { canHaveChildren } from "socket-function/src/types";
|
|
|
4
4
|
import { qreact } from "../4-dom/qreact";
|
|
5
5
|
import preact, { ContextType } from "preact";
|
|
6
6
|
import { errorMessage, warnMessage } from "../library-components/colors";
|
|
7
|
+
import { URLParam } from "../library-components/URLParam";
|
|
8
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
7
9
|
|
|
8
10
|
export { errorMessage, warnMessage };
|
|
9
11
|
|
|
@@ -26,6 +28,7 @@ type StringFormatters = (
|
|
|
26
28
|
| "timeSpan" | "date"
|
|
27
29
|
| "error" | "link"
|
|
28
30
|
| "toSpaceCase"
|
|
31
|
+
| "<Selector>"
|
|
29
32
|
);
|
|
30
33
|
|
|
31
34
|
function d(value: unknown, formattedValue: preact.ComponentChild) {
|
|
@@ -44,6 +47,7 @@ let formatters: { [formatter in StringFormatters]: (value: unknown) => preact.Co
|
|
|
44
47
|
date: (value) => d(value, formatVeryNiceDateTime(Number(value))),
|
|
45
48
|
error: (value) => d(value, <span class={errorMessage}>{String(value)}</span>),
|
|
46
49
|
toSpaceCase: (value) => d(value, toSpaceCase(String(value))),
|
|
50
|
+
"<Selector>": (value) => d(value, <Selector {...JSON.parse(String(value).slice("<Selector>".length))} />),
|
|
47
51
|
link: (value) => {
|
|
48
52
|
if (value === undefined || value === null) {
|
|
49
53
|
return "";
|
|
@@ -102,6 +106,12 @@ let formatters: { [formatter in StringFormatters]: (value: unknown) => preact.Co
|
|
|
102
106
|
if (typeof value === "string" && value.startsWith("https://")) {
|
|
103
107
|
return formatters.link(value);
|
|
104
108
|
}
|
|
109
|
+
if (typeof value === "string" && value.startsWith("<Selector>")) {
|
|
110
|
+
let configParts = value.slice("<Selector>".length);
|
|
111
|
+
if (isJSON(configParts)) {
|
|
112
|
+
return formatters["<Selector>"](value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
105
115
|
// Don't render nested objects, etc, otherwise passing large arrays
|
|
106
116
|
// could just in us blowing the output up, when their intention
|
|
107
117
|
// is to just show "Array()"
|
|
@@ -111,6 +121,13 @@ let formatters: { [formatter in StringFormatters]: (value: unknown) => preact.Co
|
|
|
111
121
|
return formatters.string(value);
|
|
112
122
|
},
|
|
113
123
|
};
|
|
124
|
+
function isJSON(value: unknown) {
|
|
125
|
+
try {
|
|
126
|
+
JSON.parse(value as string);
|
|
127
|
+
return true;
|
|
128
|
+
} catch { }
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
114
131
|
function formatVArray(value: unknown[], formatter: StringFormatters) {
|
|
115
132
|
if (!Array.isArray(value)) {
|
|
116
133
|
return <span class={errorMessage}>Expected array, got {typeof value}</span>;
|
|
@@ -164,4 +181,78 @@ export function toSpaceCase(text: string) {
|
|
|
164
181
|
// Convert multiple spaces to a single space
|
|
165
182
|
.replace(/\s+/g, " ");
|
|
166
183
|
;
|
|
167
|
-
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface SelectorProps {
|
|
187
|
+
options: { value: string, urlValue: string, title: string }[];
|
|
188
|
+
urlKey: string;
|
|
189
|
+
}
|
|
190
|
+
class Selector extends qreact.Component<SelectorProps> {
|
|
191
|
+
state = {
|
|
192
|
+
expanded: false,
|
|
193
|
+
};
|
|
194
|
+
componentDidMount(): void {
|
|
195
|
+
document.body.addEventListener("click", this.onBodyClick);
|
|
196
|
+
qreact.onDispose(this.onBodyClick);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
justSetExpanded = false;
|
|
200
|
+
onBodyClick = () => {
|
|
201
|
+
if (this.justSetExpanded) {
|
|
202
|
+
this.justSetExpanded = false;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
Querysub.commit(() => {
|
|
206
|
+
this.state.expanded = false;
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
render() {
|
|
210
|
+
const { options, urlKey } = this.props;
|
|
211
|
+
if (options.length === 0) {
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
let urlParam = new URLParam(urlKey, options[0].urlValue);
|
|
215
|
+
let selectedOption = options.find(x => x.urlValue === urlParam.value);
|
|
216
|
+
let component = qreact.getRenderingComponentAssert();
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
className={css.relative.zIndex(1)}
|
|
220
|
+
onClick={() => {
|
|
221
|
+
if (this.state.expanded) return;
|
|
222
|
+
|
|
223
|
+
this.state.expanded = true;
|
|
224
|
+
this.justSetExpanded = true;
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<div
|
|
228
|
+
className={css.hsla(0, 0, 50, 0.3).pad2(6, 2).button}
|
|
229
|
+
title={selectedOption?.title}
|
|
230
|
+
>
|
|
231
|
+
{formatValue(selectedOption?.value)}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{this.state.expanded && (
|
|
235
|
+
<div
|
|
236
|
+
className={css.absolute.pos(0, 0).width("100vw").vbox0}
|
|
237
|
+
>
|
|
238
|
+
<div className={css.vbox(2).hsla(0, 0, 80, 1).pad2(6)}>
|
|
239
|
+
{options.map(x =>
|
|
240
|
+
<div
|
|
241
|
+
className={css.pad2(6, 2).hsla(0, 0, 50, 0.5).button}
|
|
242
|
+
onClick={() => {
|
|
243
|
+
urlParam.value = x.urlValue;
|
|
244
|
+
}}
|
|
245
|
+
title={x.title}
|
|
246
|
+
>({x.title}) {formatValue(x.value)}</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function encodeFormattedSelector(config: SelectorProps) {
|
|
257
|
+
return `<Selector>${JSON.stringify(config)}`;
|
|
258
|
+
}
|
|
@@ -102,7 +102,12 @@ export class Table<RowT extends RowType> extends qreact.Component<TableType<RowT
|
|
|
102
102
|
}
|
|
103
103
|
innerContent = props.children as any;
|
|
104
104
|
}
|
|
105
|
-
return <td
|
|
105
|
+
return <td
|
|
106
|
+
{...attributes}
|
|
107
|
+
data-column={columnName}
|
|
108
|
+
// If it ever becomes non-table cell, it breaks the table
|
|
109
|
+
style={{ display: "table-cell" }}
|
|
110
|
+
>
|
|
106
111
|
{innerContent}
|
|
107
112
|
</td>;
|
|
108
113
|
})}
|
|
@@ -170,14 +175,4 @@ function renderTrimmed(config: {
|
|
|
170
175
|
let close = showModal({
|
|
171
176
|
content: <FullscreenModal onCancel={() => {
|
|
172
177
|
close.close();
|
|
173
|
-
|
|
174
|
-
<div class={css.whiteSpace("pre-wrap")}>
|
|
175
|
-
{content}
|
|
176
|
-
</div>
|
|
177
|
-
</FullscreenModal>
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
innerContent: contentStr
|
|
182
|
-
};
|
|
183
|
-
}
|
|
178
|
+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import debugbreak from "debugbreak";
|
|
2
2
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
3
3
|
import { isDefined } from "../misc";
|
|
4
|
+
import { encodeFormattedSelector } from "./GenericFormat";
|
|
4
5
|
|
|
5
6
|
const SUB_BUCKET_COUNT = 100;
|
|
6
7
|
|
|
@@ -71,13 +72,14 @@ export class TimeGrouper {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
public getSummary() {
|
|
75
|
+
|
|
74
76
|
let buckets = this.buckets.map(bucket => {
|
|
75
77
|
// Reduce subBuckets within each bucket
|
|
76
78
|
let sub = bucket.subBuckets;
|
|
77
79
|
return {
|
|
78
80
|
name: bucket.name,
|
|
79
81
|
duration: bucket.duration,
|
|
80
|
-
recordedDuration: sub.at(-1)
|
|
82
|
+
recordedDuration: (sub.at(-1)?.startTime ?? 0) - (sub.at(0)?.endTime ?? 0),
|
|
81
83
|
min: sub.map(x => x.min).reduce((a, b) => Math.min(a, b), Number.POSITIVE_INFINITY),
|
|
82
84
|
max: sub.map(x => x.max).reduce((a, b) => Math.max(a, b), Number.NEGATIVE_INFINITY),
|
|
83
85
|
sum: sub.map(x => x.sum).reduce((a, b) => a + b, 0),
|
|
@@ -95,22 +97,29 @@ export class TimeGrouper {
|
|
|
95
97
|
|
|
96
98
|
/** For sampled values, ex, % CPU usage */
|
|
97
99
|
public getStateSummary(format: (value: number) => string): string {
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
return encodeFormattedSelector({
|
|
101
|
+
// Always use the same urlKey, so we can change all displays at once
|
|
102
|
+
urlKey: "timegroupergroup",
|
|
103
|
+
options: this.getSummary().map(bucket => {
|
|
104
|
+
if (bucket.duration === 0 || bucket.min === bucket.max) {
|
|
105
|
+
if (bucket.min === 0) return undefined;
|
|
106
|
+
return { value: `${format(bucket.min)}`, title: bucket.name, urlValue: bucket.name };
|
|
107
|
+
}
|
|
108
|
+
return { value: `${format(bucket.min)} to ${format(bucket.max)}`, title: bucket.name, urlValue: bucket.name };
|
|
109
|
+
}).filter(isDefined)
|
|
110
|
+
});
|
|
105
111
|
}
|
|
106
112
|
/** Sums up the values. Ex, for time waiting. */
|
|
107
113
|
public getEventSummary(format: (value: number) => string): string {
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
return encodeFormattedSelector({
|
|
115
|
+
urlKey: "timegroupergroup",
|
|
116
|
+
options: this.getSummary().map(bucket => {
|
|
117
|
+
if (bucket.duration === 0) {
|
|
118
|
+
if (bucket.sum === 0) return undefined;
|
|
119
|
+
return { value: `${format(bucket.sum)}`, title: bucket.name, urlValue: bucket.name };
|
|
120
|
+
}
|
|
121
|
+
return { value: `${format(bucket.sum)}`, title: bucket.name, urlValue: bucket.name };
|
|
122
|
+
}).filter(isDefined)
|
|
123
|
+
});
|
|
115
124
|
}
|
|
116
125
|
}
|
|
@@ -3,7 +3,7 @@ import { PathValueController } from "../0-path-value-core/PathValueController";
|
|
|
3
3
|
import { PathValue, authorityStorage, pathWatcher } from "../0-path-value-core/pathValueCore";
|
|
4
4
|
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
5
5
|
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
6
|
-
import { isNode, list, sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
6
|
+
import { QueueLimited, isNode, list, sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
7
7
|
import { getSingleSizeEstimate } from "./shared";
|
|
8
8
|
import { devDebugbreak, isDevDebugbreak } from "../config";
|
|
9
9
|
import { nodePathAuthority } from "../0-path-value-core/NodePathAuthorities";
|
|
@@ -172,12 +172,11 @@ function sampleRandom(config: {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
let lastAuditsSizes: {
|
|
175
|
+
let lastAuditsSizes = new QueueLimited<{
|
|
177
176
|
count: number;
|
|
178
177
|
size: number;
|
|
179
178
|
time: number;
|
|
180
|
-
}
|
|
179
|
+
}>(10);
|
|
181
180
|
|
|
182
181
|
async function auditSpecificPaths(config: {
|
|
183
182
|
paths: string[];
|
|
@@ -211,9 +210,10 @@ async function auditSpecificPathsBase(config: {
|
|
|
211
210
|
// audit fewer paths, up to just auditing a single path).
|
|
212
211
|
// Use getSingleSizeEstimate to get sizes
|
|
213
212
|
// If forceAudit, don't limit the paths
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let
|
|
213
|
+
let oldest = lastAuditsSizes.getOldest();
|
|
214
|
+
if (oldest && !config.forceAudit) {
|
|
215
|
+
let auditWindowTotalSize = lastAuditsSizes.getAllUnordered().map(v => v.size).reduce((a, b) => a + b, 0);
|
|
216
|
+
let auditWindowTime = Date.now() - oldest.time;
|
|
217
217
|
let auditWindowRate = auditWindowTotalSize / auditWindowTime;
|
|
218
218
|
let fractionToReduce = Math.min(1, auditWindowRate / AUDIT_MAX_BYTES_PER_SECOND);
|
|
219
219
|
let count = Math.max(1, Math.floor(paths.length * fractionToReduce));
|
|
@@ -229,9 +229,6 @@ async function auditSpecificPathsBase(config: {
|
|
|
229
229
|
+ v.path.length * 2
|
|
230
230
|
).reduce((a, b) => a + b, 0);
|
|
231
231
|
lastAuditsSizes.push({ count: paths.length, size, time: Date.now() });
|
|
232
|
-
while (lastAuditsSizes.length > AUDIT_WINDOW_SIZE) {
|
|
233
|
-
lastAuditsSizes.shift();
|
|
234
|
-
}
|
|
235
232
|
}
|
|
236
233
|
|
|
237
234
|
const THRESHOLD_TIME = Date.now() - MAX_ASSUMED_PROPAGATION_DELAY;
|
|
@@ -11,9 +11,21 @@ import { formatNumber, formatPercent, formatTime } from "socket-function/src/for
|
|
|
11
11
|
import os from "os";
|
|
12
12
|
import { isNode } from "typesafecss";
|
|
13
13
|
import { diskLog } from "../diagnostics/logs/diskLogger";
|
|
14
|
+
import { devDebugbreak } from "../config";
|
|
15
|
+
import { addStatPeriodic, addStatSumPeriodic, addTimeProfileDistribution, onTimeProfile, logNodeStats, logNodeStateStats, registerNodeMetadata } from "../-0-hooks/hooks";
|
|
16
|
+
import { magenta } from "socket-function/src/formatting/logColors";
|
|
14
17
|
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
let POLL_INTERVAL = timeInMinute * 5;
|
|
20
|
+
|
|
21
|
+
//todonext
|
|
22
|
+
// Updating didn't work?
|
|
23
|
+
// - Reboot, then try do-update again, then if it still isn't up, ssh in and see what's wrong
|
|
24
|
+
|
|
25
|
+
//todonext;
|
|
26
|
+
// Testing
|
|
27
|
+
POLL_INTERVAL = 10 * 1000;
|
|
28
|
+
|
|
17
29
|
|
|
18
30
|
// Undefined means we infer the column
|
|
19
31
|
// Null means the column is removed
|
|
@@ -37,10 +49,10 @@ export type ExtraMetadata = {
|
|
|
37
49
|
getValue: () => MaybePromise<unknown>;
|
|
38
50
|
};
|
|
39
51
|
let extraMetadatas: ExtraMetadata[] = [];
|
|
40
|
-
|
|
52
|
+
registerNodeMetadata.declare(function registerNodeMetadata(getter: ExtraMetadata) {
|
|
41
53
|
//console.log(`Registering metadata: ${getter.columnName}`);
|
|
42
54
|
extraMetadatas.push(getter);
|
|
43
|
-
}
|
|
55
|
+
});
|
|
44
56
|
|
|
45
57
|
// IMPORTANT! We no longer diskLog here! diskLogging is not ideal for metrics like these. You almost only
|
|
46
58
|
// care about the current metrics OR past trends. A single value in the logs from the past is just
|
|
@@ -76,21 +88,24 @@ let eventValues = cache((title: string, format: (value: number) => string): { (v
|
|
|
76
88
|
});
|
|
77
89
|
|
|
78
90
|
/** For values that should be summed to get the value. */
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
91
|
+
logNodeStats.declare(function registerLogNodeStats(title: string, format: (value: number) => string, value: number) {
|
|
92
|
+
eventValues(title, format)(value);
|
|
93
|
+
});
|
|
82
94
|
/** For state values (ex, % CPU usage), for which summing wouldn't make sense. */
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
95
|
+
logNodeStateStats.declare(function registerLogNodeStateStats(title: string, format: (value: number) => string, value: number) {
|
|
96
|
+
stateValues(title, format)(value);
|
|
97
|
+
});
|
|
86
98
|
|
|
87
99
|
let existingPeriodic = new Set<string>();
|
|
88
100
|
/** For state values (ex, % CPU usage), for which summing wouldn't make sense. */
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
addStatPeriodic.declare(function addStatPeriodic(
|
|
102
|
+
config: {
|
|
103
|
+
title: string,
|
|
104
|
+
getValue: () => number,
|
|
105
|
+
format?: (value: number) => string,
|
|
106
|
+
}
|
|
93
107
|
) {
|
|
108
|
+
const { title, getValue, format = formatNumber } = config;
|
|
94
109
|
let hash = JSON.stringify({ title, getValue: getValue.toString(), format: format.toString() });
|
|
95
110
|
// Ignoring duplicates, otherwise we end up logging way too much
|
|
96
111
|
if (globalThis.isHotReloading?.() && existingPeriodic.has(hash)) return;
|
|
@@ -106,13 +121,16 @@ export function addStatPeriodic(
|
|
|
106
121
|
return timeGrouper.getStateSummary(format);
|
|
107
122
|
},
|
|
108
123
|
});
|
|
109
|
-
}
|
|
124
|
+
});
|
|
110
125
|
/** For values that should be summed to get the value. */
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
addStatSumPeriodic.declare(function addStatSumPeriodic(
|
|
127
|
+
config: {
|
|
128
|
+
title: string,
|
|
129
|
+
getValue: () => number,
|
|
130
|
+
format?: ((value: number) => string),
|
|
131
|
+
}
|
|
115
132
|
) {
|
|
133
|
+
const { title, getValue, format = formatNumber } = config;
|
|
116
134
|
let hash = JSON.stringify({ title, getValue: getValue.toString(), format: format.toString() });
|
|
117
135
|
// Ignoring duplicates, otherwise we end up logging way too much
|
|
118
136
|
if (globalThis.isHotReloading?.() && existingPeriodic.has(hash)) return;
|
|
@@ -128,10 +146,10 @@ export function addStatSumPeriodic(
|
|
|
128
146
|
return timeGrouper.getEventSummary(format);
|
|
129
147
|
},
|
|
130
148
|
});
|
|
131
|
-
}
|
|
149
|
+
});
|
|
132
150
|
|
|
133
151
|
let pendingTimeProfiles = new Map<string, { start: number; end: number }[]>();
|
|
134
|
-
|
|
152
|
+
onTimeProfile.declare(function onTimeProfile(title: string, startTime: number) {
|
|
135
153
|
let profiles = pendingTimeProfiles.get(title);
|
|
136
154
|
if (!profiles) {
|
|
137
155
|
profiles = [];
|
|
@@ -143,7 +161,7 @@ export function onTimeProfile(title: string, startTime: number) {
|
|
|
143
161
|
});
|
|
144
162
|
}
|
|
145
163
|
profiles.push({ start: startTime, end: Date.now() });
|
|
146
|
-
}
|
|
164
|
+
});
|
|
147
165
|
|
|
148
166
|
/** Ex, for tracking the time distribution of API calls, or watchFunction evaluation, etc.
|
|
149
167
|
* - Specifically for time, so we can handle overlap cases (such as waiting for 10 calls at once),
|
|
@@ -155,7 +173,7 @@ export function onTimeProfile(title: string, startTime: number) {
|
|
|
155
173
|
* you need to harvest values from an external library, in which case, you will need
|
|
156
174
|
* this function.
|
|
157
175
|
*/
|
|
158
|
-
|
|
176
|
+
addTimeProfileDistribution.declare(function addTimeProfileDistribution(
|
|
159
177
|
title: string,
|
|
160
178
|
/** It's expected that this will return all values since the last call to harvestValues
|
|
161
179
|
* (basically, copy your value buffer and clear the old one).
|
|
@@ -166,8 +184,16 @@ export function addTimeProfileDistribution(
|
|
|
166
184
|
// Ignoring duplicates, otherwise we end up logging way too much
|
|
167
185
|
if (globalThis.isHotReloading?.() && existingPeriodic.has(hash)) return;
|
|
168
186
|
|
|
187
|
+
// TODO: Because we know the time ranges, we can actually provide better state values. As in,
|
|
188
|
+
// within a single call we can determine the maximum and minimum instantaneous rates. However...
|
|
189
|
+
// this is more difficult, and might not be useful (as one really fast time group shouldn't result
|
|
190
|
+
// in a spike of an infinite rate, it's better to smooth it out a bit).
|
|
191
|
+
|
|
169
192
|
let fractionTimeWaitingGroup = new TimeGrouper();
|
|
170
193
|
let ratePerSecondGroup = new TimeGrouper();
|
|
194
|
+
let timePerValueGroup = new TimeGrouper();
|
|
195
|
+
let parallelFactorGroup = new TimeGrouper();
|
|
196
|
+
let maxEstRateGroup = new TimeGrouper();
|
|
171
197
|
let lastPollTime = Date.now();
|
|
172
198
|
runInfinitePoll(POLL_INTERVAL, () => {
|
|
173
199
|
let originalStartTime = lastPollTime;
|
|
@@ -175,39 +201,61 @@ export function addTimeProfileDistribution(
|
|
|
175
201
|
values = values.slice();
|
|
176
202
|
sort(values, x => x.start);
|
|
177
203
|
let freeTime = 0;
|
|
178
|
-
|
|
204
|
+
let totalTime = 0;
|
|
205
|
+
|
|
206
|
+
console.log(magenta(`${title} Time: ${values.length}`));
|
|
207
|
+
|
|
208
|
+
for (let obj of values) {
|
|
209
|
+
if (!obj) {
|
|
210
|
+
devDebugbreak();
|
|
211
|
+
}
|
|
212
|
+
let { start, end } = obj;
|
|
179
213
|
if (start > lastPollTime) {
|
|
180
214
|
freeTime += start - lastPollTime;
|
|
181
215
|
}
|
|
182
216
|
lastPollTime = end;
|
|
217
|
+
totalTime += end - start;
|
|
183
218
|
}
|
|
184
219
|
let now = Date.now();
|
|
185
220
|
freeTime += now - lastPollTime;
|
|
186
221
|
lastPollTime = now;
|
|
187
222
|
|
|
188
223
|
let duration = lastPollTime - originalStartTime;
|
|
189
|
-
let
|
|
224
|
+
let activeTime = duration - freeTime;
|
|
190
225
|
let ratePerSecond = values.length / (duration / 1000);
|
|
191
|
-
|
|
192
|
-
|
|
226
|
+
let averageTimePerValue = totalTime / values.length;
|
|
227
|
+
let fracActive = activeTime / duration;
|
|
228
|
+
fractionTimeWaitingGroup.onValueChanged(fracActive);
|
|
193
229
|
ratePerSecondGroup.onValueChanged(ratePerSecond);
|
|
194
|
-
|
|
230
|
+
if (values.length > 0) {
|
|
231
|
+
timePerValueGroup.onValueChanged(averageTimePerValue);
|
|
232
|
+
}
|
|
195
233
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
234
|
+
let parallelFactor = 0;
|
|
235
|
+
let totalTimeOverlapped = values.map(x => x.end - x.start).reduce((a, b) => a + b, 0);
|
|
236
|
+
if (totalTimeOverlapped > 0) {
|
|
237
|
+
parallelFactor = totalTimeOverlapped / activeTime;
|
|
238
|
+
}
|
|
239
|
+
parallelFactorGroup.onValueChanged(parallelFactor);
|
|
240
|
+
|
|
241
|
+
let maxEstRate = fracActive === 0 ? 0 : ratePerSecond / fracActive;
|
|
242
|
+
maxEstRateGroup.onValueChanged(maxEstRate);
|
|
202
243
|
});
|
|
244
|
+
|
|
203
245
|
registerNodeMetadata({
|
|
204
|
-
columnName: `${title}
|
|
246
|
+
columnName: `${title} Time`,
|
|
205
247
|
column: {},
|
|
206
248
|
getValue() {
|
|
207
|
-
return
|
|
249
|
+
return [
|
|
250
|
+
fractionTimeWaitingGroup.getStateSummary(formatPercent),
|
|
251
|
+
timePerValueGroup.getStateSummary(x => formatTime(x) + "/per"),
|
|
252
|
+
ratePerSecondGroup.getStateSummary(x => formatNumber(x) + "/s"),
|
|
253
|
+
parallelFactorGroup.getStateSummary(x => formatNumber(x) + "X"),
|
|
254
|
+
maxEstRateGroup.getStateSummary(x => `est max(${formatNumber(x)}/s)`),
|
|
255
|
+
];
|
|
208
256
|
},
|
|
209
257
|
});
|
|
210
|
-
}
|
|
258
|
+
});
|
|
211
259
|
|
|
212
260
|
if (isNode()) {
|
|
213
261
|
void runInfinitePollCallAtStart(POLL_INTERVAL, async () => {
|
|
@@ -216,24 +264,24 @@ if (isNode()) {
|
|
|
216
264
|
const free = os.freemem();
|
|
217
265
|
const total = os.totalmem();
|
|
218
266
|
|
|
219
|
-
logNodeStateStats("System|Memory Free", x => formatNumber(x) + "B"
|
|
220
|
-
logNodeStateStats("System|Memory Free %", formatPercent
|
|
267
|
+
logNodeStateStats("System|Memory Free", x => formatNumber(x) + "B", free);
|
|
268
|
+
logNodeStateStats("System|Memory Free %", formatPercent, free / total);
|
|
221
269
|
|
|
222
270
|
let constrainedMemory = process.constrainedMemory();
|
|
223
271
|
if (constrainedMemory !== undefined && constrainedMemory < 2 ** 48) {
|
|
224
|
-
logNodeStateStats("System|Constrained Memory", x => formatNumber(x) + "B"
|
|
272
|
+
logNodeStateStats("System|Constrained Memory", x => formatNumber(x) + "B", constrainedMemory);
|
|
225
273
|
}
|
|
226
274
|
|
|
227
275
|
let usedCPU = process.cpuUsage();
|
|
228
|
-
logNodeStateStats("System|CPU User", formatTime
|
|
229
|
-
logNodeStateStats("System|CPU System", formatTime
|
|
276
|
+
logNodeStateStats("System|CPU User", formatTime, usedCPU.user * 1000);
|
|
277
|
+
logNodeStateStats("System|CPU System", formatTime, usedCPU.system * 1000);
|
|
230
278
|
|
|
231
279
|
let cpuCount = os.cpus().length;
|
|
232
|
-
logNodeStateStats("System|CPU Count", formatNumber
|
|
280
|
+
logNodeStateStats("System|CPU Count", formatNumber, cpuCount);
|
|
233
281
|
|
|
234
282
|
let load = os.loadavg();
|
|
235
|
-
logNodeStateStats("System|Average Load", formatPercent
|
|
236
|
-
logNodeStateStats("System|Average Free Cores", formatPercent
|
|
283
|
+
logNodeStateStats("System|Average Load", formatPercent, load[0]);
|
|
284
|
+
logNodeStateStats("System|Average Free Cores", formatPercent, (os.cpus().length - load[0]));
|
|
237
285
|
|
|
238
286
|
});
|
|
239
287
|
}
|
|
@@ -3,7 +3,7 @@ import { formatTime } from "socket-function/src/formatting/format";
|
|
|
3
3
|
import { registerMeasureInfo } from "socket-function/src/profiling/measure";
|
|
4
4
|
import { isNode } from "typesafecss";
|
|
5
5
|
import { monitorEventLoopDelay } from "perf_hooks";
|
|
6
|
-
import { registerNodeMetadata } from "
|
|
6
|
+
import { registerNodeMetadata } from "../-0-hooks/hooks";
|
|
7
7
|
import { getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
8
8
|
|
|
9
9
|
const POLL_INTERVAL = 350;
|
|
@@ -18,7 +18,7 @@ import { requiresNetworkTrustHook } from "../../src/-d-trust/NetworkTrust2";
|
|
|
18
18
|
import { getDomain, isNoNetwork } from "../../src/config";
|
|
19
19
|
import { NodeMetadataController } from "../../src/5-diagnostics/nodeMetadata";
|
|
20
20
|
import { isDefined } from "../../src/misc";
|
|
21
|
-
import { errorMessage, formatValue, toSpaceCase } from "../../src/5-diagnostics/GenericFormat";
|
|
21
|
+
import { encodeFormattedSelector, errorMessage, formatValue, toSpaceCase } from "../../src/5-diagnostics/GenericFormat";
|
|
22
22
|
import { ValueAuditController } from "../../src/5-diagnostics/memoryValueAudit";
|
|
23
23
|
import { getExternalIP } from "../../src/misc/networking";
|
|
24
24
|
import dns from "dns";
|
|
@@ -458,7 +458,6 @@ class NodeViewerControllerBase {
|
|
|
458
458
|
wrapAddTableValue("port", {}, async () => {
|
|
459
459
|
return nodeId.split(":").at(-1);
|
|
460
460
|
}),
|
|
461
|
-
//
|
|
462
461
|
wrapAddTableValue("threadId", {
|
|
463
462
|
formatter: "link:/?page=user&showingmanagement&managementpage=LogViewer&filterThreadIds=$VALUE$<Thread Errors $VALUE$>"
|
|
464
463
|
}, async () => {
|
|
@@ -493,8 +492,8 @@ class NodeViewerControllerBase {
|
|
|
493
492
|
}
|
|
494
493
|
} catch (e: any) {
|
|
495
494
|
// Any errors almost certainly mean they aren't exposing any extra metadata, which is fine.
|
|
496
|
-
|
|
497
|
-
|
|
495
|
+
columns["extraMetadatasError"] = { formatter: "error" };
|
|
496
|
+
row["extraMetadatasError"] = "Error: " + e.stack;
|
|
498
497
|
}
|
|
499
498
|
return {
|
|
500
499
|
columns,
|