querysub 0.522.0 → 0.523.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/-f-node-discovery/TrafficTracking.ts +37 -47
- package/src/0-path-value-core/startupAuthority.ts +2 -2
- package/src/4-querysub/querysubPrediction.ts +2 -2
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +22 -7
- package/src/library-components/LatencyGraph.tsx +14 -9
package/package.json
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
Per-server, in-memory traffic counters (never written to the database). Everything is just an integer increment
|
|
3
3
|
into a 5-minute bucket; we keep the last 60 minutes (12 buckets) and drop older ones, so memory is bounded.
|
|
4
4
|
|
|
5
|
-
Reads return PER-SECOND RATES
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Reads return PER-SECOND RATES: the window's total divided by how long data has ACTUALLY been collecting in that
|
|
6
|
+
window — from the later of the window's start and when we first saw any data, up to now (so it's dynamic, and on a
|
|
7
|
+
server that just started it divides by the real short duration, not the full window). Under a second of data falls
|
|
8
|
+
back to the window length so we never divide by ~0.
|
|
8
9
|
|
|
9
10
|
Byte traffic comes from SocketFunction.trackMessageSizes (upload/download per connection). The connection nodeId is
|
|
10
11
|
sometimes a raw client-connection id, so we map it to the nice node id (debugNodeId) lazily at read time — the hot
|
|
@@ -19,10 +20,8 @@ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
|
19
20
|
import { getCachedNodeIds } from "./NodeDiscovery";
|
|
20
21
|
import { debugNodeId } from "../-c-identity/IdentityController";
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
// Floor on the rate window so a burst in the first fraction of a second after startup can't divide out to an absurd rate.
|
|
25
|
-
const MIN_WINDOW_SECONDS = 1;
|
|
23
|
+
const BUCKET_SIZE = timeInMinute * 5;
|
|
24
|
+
const BUCKET_HISTORY = BUCKET_SIZE * 20;
|
|
26
25
|
// Sentinel key for traffic that isn't a network node (client connections, etc.). Aggregated so memory stays O(nodes),
|
|
27
26
|
// not O(client connections) — clients churn through many raw ids per hour and we never want a bucket per client.
|
|
28
27
|
const OUTSIDE_KEY = "";
|
|
@@ -39,16 +38,8 @@ export type TrafficStats = {
|
|
|
39
38
|
outside: NodeDataTraffic;
|
|
40
39
|
};
|
|
41
40
|
|
|
42
|
-
//
|
|
43
|
-
type
|
|
44
|
-
type BucketMap = Map<number, Bucket>;
|
|
45
|
-
|
|
46
|
-
function currentBucket() {
|
|
47
|
-
return Math.floor(Date.now() / BUCKET_MS);
|
|
48
|
-
}
|
|
49
|
-
function oldestBucket() {
|
|
50
|
-
return currentBucket() - BUCKET_COUNT + 1;
|
|
51
|
-
}
|
|
41
|
+
// bucket index => amount accumulated in that bucket
|
|
42
|
+
type BucketMap = Map<number, number>;
|
|
52
43
|
|
|
53
44
|
let functionsExecuted: BucketMap = new Map();
|
|
54
45
|
let pathValuesSent: BucketMap = new Map();
|
|
@@ -59,13 +50,8 @@ let bytesSent = new Map<string, BucketMap>();
|
|
|
59
50
|
let bytesReceived = new Map<string, BucketMap>();
|
|
60
51
|
|
|
61
52
|
function addSimple(buckets: BucketMap, amount: number) {
|
|
62
|
-
let bucket =
|
|
63
|
-
|
|
64
|
-
if (!entry) {
|
|
65
|
-
entry = { count: 0, firstMs: Date.now() };
|
|
66
|
-
buckets.set(bucket, entry);
|
|
67
|
-
}
|
|
68
|
-
entry.count += amount;
|
|
53
|
+
let bucket = Math.floor(Date.now() / BUCKET_SIZE) * BUCKET_SIZE;
|
|
54
|
+
buckets.set(bucket, (buckets.get(bucket) || 0) + amount);
|
|
69
55
|
}
|
|
70
56
|
function addKeyed(map: Map<string, BucketMap>, key: string, amount: number) {
|
|
71
57
|
let buckets = map.get(key);
|
|
@@ -99,26 +85,30 @@ function trafficKey(nodeId: string): string {
|
|
|
99
85
|
SocketFunction.trackMessageSizes.upload.push((size, nodeId) => addKeyed(bytesSent, trafficKey(nodeId), size));
|
|
100
86
|
SocketFunction.trackMessageSizes.download.push((size, nodeId) => addKeyed(bytesReceived, trafficKey(nodeId), size));
|
|
101
87
|
|
|
102
|
-
// Per-second rate
|
|
103
|
-
// Also prunes expired buckets as it goes.
|
|
104
|
-
function bucketRate(buckets: BucketMap): number {
|
|
105
|
-
let
|
|
88
|
+
// Per-second rate: total within the window divided by the actual span the matched buckets cover (oldest bucket's start
|
|
89
|
+
// time → now). Also prunes expired buckets as it goes.
|
|
90
|
+
function bucketRate(buckets: BucketMap, windowSize: number): number {
|
|
91
|
+
let now = Date.now();
|
|
92
|
+
let retentionOldest = now - BUCKET_HISTORY;
|
|
93
|
+
let targetTime = now - windowSize;
|
|
106
94
|
let total = 0;
|
|
107
|
-
let
|
|
108
|
-
for (let [bucket,
|
|
109
|
-
if (bucket <
|
|
95
|
+
let oldestTime = now;
|
|
96
|
+
for (let [bucket, count] of Array.from(buckets)) {
|
|
97
|
+
if (bucket < retentionOldest) {
|
|
110
98
|
buckets.delete(bucket);
|
|
111
99
|
continue;
|
|
112
100
|
}
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
if (bucket < targetTime) continue;
|
|
102
|
+
oldestTime = Math.min(oldestTime, bucket);
|
|
103
|
+
total += count;
|
|
104
|
+
}
|
|
105
|
+
if (oldestTime === now) {
|
|
106
|
+
oldestTime = targetTime;
|
|
115
107
|
}
|
|
116
|
-
|
|
117
|
-
let windowSeconds = Math.max(MIN_WINDOW_SECONDS, (Date.now() - firstMs) / 1000);
|
|
118
|
-
return total / windowSeconds;
|
|
108
|
+
return total / ((now - oldestTime) / 1000);
|
|
119
109
|
}
|
|
120
110
|
|
|
121
|
-
function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outside: NodeDataTraffic; } {
|
|
111
|
+
function aggregateData(windowSize: number): { perNode: { [nodeId: string]: NodeDataTraffic }; outside: NodeDataTraffic; } {
|
|
122
112
|
let known = new Set(getCachedNodeIds());
|
|
123
113
|
let perNode: { [nodeId: string]: NodeDataTraffic } = {};
|
|
124
114
|
let outside: NodeDataTraffic = { sent: 0, received: 0 };
|
|
@@ -143,31 +133,31 @@ function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outs
|
|
|
143
133
|
}
|
|
144
134
|
};
|
|
145
135
|
for (let [rawNodeId, buckets] of Array.from(bytesSent)) {
|
|
146
|
-
attribute(rawNodeId, "sent", bucketRate(buckets));
|
|
136
|
+
attribute(rawNodeId, "sent", bucketRate(buckets, windowSize));
|
|
147
137
|
if (buckets.size === 0) bytesSent.delete(rawNodeId);
|
|
148
138
|
}
|
|
149
139
|
for (let [rawNodeId, buckets] of Array.from(bytesReceived)) {
|
|
150
|
-
attribute(rawNodeId, "received", bucketRate(buckets));
|
|
140
|
+
attribute(rawNodeId, "received", bucketRate(buckets, windowSize));
|
|
151
141
|
if (buckets.size === 0) bytesReceived.delete(rawNodeId);
|
|
152
142
|
}
|
|
153
143
|
return { perNode, outside };
|
|
154
144
|
}
|
|
155
145
|
|
|
156
|
-
export function getTrafficStats(): TrafficStats {
|
|
157
|
-
let data = aggregateData();
|
|
146
|
+
export function getTrafficStats(windowSize: number): TrafficStats {
|
|
147
|
+
let data = aggregateData(windowSize);
|
|
158
148
|
return {
|
|
159
|
-
functionsExecuted: bucketRate(functionsExecuted),
|
|
160
|
-
pathValuesSent: bucketRate(pathValuesSent),
|
|
161
|
-
pathValuesReceived: bucketRate(pathValuesReceived),
|
|
162
|
-
querysubCalls: bucketRate(querysubCalls),
|
|
149
|
+
functionsExecuted: bucketRate(functionsExecuted, windowSize),
|
|
150
|
+
pathValuesSent: bucketRate(pathValuesSent, windowSize),
|
|
151
|
+
pathValuesReceived: bucketRate(pathValuesReceived, windowSize),
|
|
152
|
+
querysubCalls: bucketRate(querysubCalls, windowSize),
|
|
163
153
|
perNode: data.perNode,
|
|
164
154
|
outside: data.outside,
|
|
165
155
|
};
|
|
166
156
|
}
|
|
167
157
|
|
|
168
158
|
class TrafficControllerBase {
|
|
169
|
-
public async getTrafficStats(): Promise<TrafficStats> {
|
|
170
|
-
return getTrafficStats();
|
|
159
|
+
public async getTrafficStats(windowSize: number): Promise<TrafficStats> {
|
|
160
|
+
return getTrafficStats(windowSize);
|
|
171
161
|
}
|
|
172
162
|
}
|
|
173
163
|
export const TrafficController = SocketFunction.register(
|
|
@@ -34,7 +34,7 @@ export async function startupAuthority(spec: AuthoritySpec) {
|
|
|
34
34
|
pathValues: values,
|
|
35
35
|
parentSyncs: [],
|
|
36
36
|
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
37
|
-
doNotArchive:
|
|
37
|
+
doNotArchive: true,
|
|
38
38
|
});
|
|
39
39
|
console.log(blue(`Finished ingesting values from source ${source.nodeId} (values: ${formatNumber(values.length)}`), { nodeId: source.nodeId, spec: debugSpec(source), values: values.length });
|
|
40
40
|
}
|
|
@@ -62,7 +62,7 @@ export async function startupAuthority(spec: AuthoritySpec) {
|
|
|
62
62
|
let flat = Object.values(snapshot.values).flat();
|
|
63
63
|
console.log(blue(`Loaded snapshot in memory, found ${formatNumber(flat.length)} values`), { spec: debugSpec(spec), values: flat.length });
|
|
64
64
|
// NOTE: If it's on disk, it can't be rejected, so there's no point in passing it through our validStateComputer (and passing it through is VERY slow).
|
|
65
|
-
authorityStorage.ingestValues(flat, { doNotArchive:
|
|
65
|
+
authorityStorage.ingestValues(flat, { doNotArchive: true });
|
|
66
66
|
console.log(blue(`Finished ingesting values from snapshot, found ${formatNumber(flat.length)} values`), { spec: debugSpec(spec), values: flat.length });
|
|
67
67
|
})();
|
|
68
68
|
|
|
@@ -308,7 +308,7 @@ function predictCallBase(config: {
|
|
|
308
308
|
pathValues: predictions.writes,
|
|
309
309
|
parentSyncs: [],
|
|
310
310
|
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
311
|
-
doNotArchive:
|
|
311
|
+
doNotArchive: true,
|
|
312
312
|
});
|
|
313
313
|
|
|
314
314
|
if (Querysub.DEBUG_PREDICTIONS) {
|
|
@@ -337,7 +337,7 @@ function predictCallBase(config: {
|
|
|
337
337
|
pathValues: rejectedWrites,
|
|
338
338
|
parentSyncs: [],
|
|
339
339
|
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
340
|
-
doNotArchive:
|
|
340
|
+
doNotArchive: true,
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
343
|
|
|
@@ -14,7 +14,7 @@ import { getSyncedController } from "../../library-components/SyncedController";
|
|
|
14
14
|
import { assertIsManagementUser } from "../managementPages";
|
|
15
15
|
import { NodeCapabilitiesController } from "../../-g-core-values/NodeCapabilities";
|
|
16
16
|
import { timeoutToUndefinedSilent } from "../../errors";
|
|
17
|
-
import { sort } from "socket-function/src/misc";
|
|
17
|
+
import { sort, timeInMinute } from "socket-function/src/misc";
|
|
18
18
|
import type { AuthoritySpec } from "../../0-path-value-core/PathRouter";
|
|
19
19
|
import { getFunctionRunnerIndex, FunctionRunnerNodeInfo } from "../../4-querysub/FunctionRunnerTracking";
|
|
20
20
|
import { formatTime, formatNumber } from "socket-function/src/formatting/format";
|
|
@@ -34,6 +34,7 @@ const LATENCY_GRAPH_HEIGHT_PX = 720;
|
|
|
34
34
|
|
|
35
35
|
// Clicking a machine node in the graph (or a row) sorts that machine's nodes to the top of the table.
|
|
36
36
|
const selectedMachineParam = new URLParam("rtSelectedMachine", "");
|
|
37
|
+
const trafficWindowParam = new URLParam<number>("rtTrafficWindow", timeInMinute * 60);
|
|
37
38
|
|
|
38
39
|
// The per-node table columns, in order. `color` tints the function/path-value/querysub columns to match the graph;
|
|
39
40
|
// `perMachine` columns (the IP and machine id) only print on the first row of each machine group.
|
|
@@ -106,9 +107,10 @@ class RoutingTablePageControllerBase {
|
|
|
106
107
|
public async getNodeLatencies(nodeId: string): Promise<{ [nodeId: string]: number } | undefined> {
|
|
107
108
|
return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, LatencyController.nodes[nodeId].getLatencies());
|
|
108
109
|
}
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
// Values are now per-second rates over the window (were near-totals). Renamed so a stale synced-call cache from the
|
|
111
|
+
// old meaning is dropped instead of being served forever.
|
|
112
|
+
public async getNodeTrafficRates(nodeId: string, windowSize: number): Promise<TrafficStats | undefined> {
|
|
113
|
+
return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, TrafficController.nodes[nodeId].getTrafficStats(windowSize));
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -118,7 +120,7 @@ export const RoutingTablePageController = SocketFunction.register(
|
|
|
118
120
|
() => ({
|
|
119
121
|
getAllNodeAuthoritySpecs: {},
|
|
120
122
|
getNodeLatencies: {},
|
|
121
|
-
|
|
123
|
+
getNodeTrafficRates: {},
|
|
122
124
|
getNodeIps: {},
|
|
123
125
|
}),
|
|
124
126
|
() => ({
|
|
@@ -387,12 +389,13 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
387
389
|
let infoByNode = new Map(this.props.infos.map(info => [info.nodeId, info]));
|
|
388
390
|
|
|
389
391
|
// One synced call per node for each data source; each resolves independently so the graph fills in progressively.
|
|
392
|
+
let trafficWindow = trafficWindowParam.value;
|
|
390
393
|
let latencyMaps = new Map<string, { [nodeId: string]: number }>();
|
|
391
394
|
let trafficMaps = new Map<string, TrafficStats>();
|
|
392
395
|
for (let nodeId of nodeIds) {
|
|
393
396
|
let latencies = synced.getNodeLatencies(nodeId);
|
|
394
397
|
if (latencies) latencyMaps.set(nodeId, latencies);
|
|
395
|
-
let traffic = synced.
|
|
398
|
+
let traffic = synced.getNodeTrafficRates(nodeId, trafficWindow);
|
|
396
399
|
if (traffic) trafficMaps.set(nodeId, traffic);
|
|
397
400
|
}
|
|
398
401
|
// Only graph nodes that reported their latencies — ones that never respond probably don't exist.
|
|
@@ -512,7 +515,19 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
512
515
|
sort(rows, row => `${row.machineId === selected ? 0 : 1}${sep}${row.machineId}${sep}${row.threadPort}`);
|
|
513
516
|
|
|
514
517
|
return <div className={css.vbox(8).fillWidth}>
|
|
515
|
-
<
|
|
518
|
+
<div className={css.hbox(14)}>
|
|
519
|
+
<h2 className={css.margin(0)}>Latency Graph ({machineRespondedThreads.size} machines · {respondedSet.size}/{nodeIds.length} nodes reported)</h2>
|
|
520
|
+
<div className={css.hbox(0).bord2(0, 0, 70)}>
|
|
521
|
+
{([timeInMinute * 5, timeInMinute * 60]).map(windowSize => {
|
|
522
|
+
let selected = trafficWindow === windowSize;
|
|
523
|
+
return <div
|
|
524
|
+
className={css.pad2(12, 6).cursor("pointer")
|
|
525
|
+
.hsl(210, selected ? 70 : 0, selected ? 45 : 96).colorhsl(0, 0, selected ? 100 : 30)}
|
|
526
|
+
onMouseDown={() => trafficWindowParam.value = windowSize}
|
|
527
|
+
>{formatTime(windowSize)}</div>;
|
|
528
|
+
})}
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
516
531
|
<div className={css.relative.fillWidth.height(LATENCY_GRAPH_HEIGHT_PX).bord2(0, 0, 85)}>
|
|
517
532
|
<LatencyGraph
|
|
518
533
|
nodes={nodes}
|
|
@@ -821,15 +821,20 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
821
821
|
this.builtSig = sig;
|
|
822
822
|
this.builtWeightSig = weightSig;
|
|
823
823
|
this.build();
|
|
824
|
-
} else
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
this.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
this.
|
|
824
|
+
} else {
|
|
825
|
+
// Same node/link counts (same order): refresh the node objects so label text that changed as data streamed
|
|
826
|
+
// in — or when the traffic window toggled — shows up, without disturbing the settled layout/positions.
|
|
827
|
+
this.nodes = this.props.nodes.slice();
|
|
828
|
+
if (weightSig !== this.builtWeightSig) {
|
|
829
|
+
this.builtWeightSig = weightSig;
|
|
830
|
+
this.computeWeights();
|
|
831
|
+
}
|
|
832
|
+
if (latencyExponentParam.value !== this.builtLatencyExponent) {
|
|
833
|
+
this.computeSolveEdges();
|
|
834
|
+
this.applyLayout();
|
|
835
|
+
} else if (Math.max(1, Math.floor(connectionsParam.value)) !== this.builtConnections) {
|
|
836
|
+
this.computeRenderEdges();
|
|
837
|
+
}
|
|
833
838
|
}
|
|
834
839
|
this.scheduleFrame();
|
|
835
840
|
|