querysub 0.521.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/bin/function-public.js +0 -2
- package/package.json +1 -1
- package/src/-f-node-discovery/TrafficTracking.ts +52 -41
- package/src/0-path-value-core/startupAuthority.ts +2 -2
- package/src/3-path-functions/PathFunctionRunner.ts +6 -5
- package/src/3-path-functions/PathFunctionRunnerMain.ts +4 -7
- package/src/4-querysub/querysubPrediction.ts +2 -2
- package/src/config.ts +1 -3
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +72 -34
- package/src/library-components/LatencyGraph.tsx +34 -35
package/bin/function-public.js
CHANGED
package/package.json
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
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: 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.
|
|
9
|
+
|
|
5
10
|
Byte traffic comes from SocketFunction.trackMessageSizes (upload/download per connection). The connection nodeId is
|
|
6
11
|
sometimes a raw client-connection id, so we map it to the nice node id (debugNodeId) lazily at read time — the hot
|
|
7
|
-
path is just a Map increment keyed by the raw id. At read we split traffic into per-known-node
|
|
12
|
+
path is just a Map increment keyed by the raw id. At read we split traffic into per-known-node rates and a single
|
|
8
13
|
"outside the network" aggregate for anything not in our cached node list (clients, etc.).
|
|
9
14
|
*/
|
|
10
15
|
|
|
@@ -15,44 +20,40 @@ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
|
15
20
|
import { getCachedNodeIds } from "./NodeDiscovery";
|
|
16
21
|
import { debugNodeId } from "../-c-identity/IdentityController";
|
|
17
22
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
23
|
+
const BUCKET_SIZE = timeInMinute * 5;
|
|
24
|
+
const BUCKET_HISTORY = BUCKET_SIZE * 20;
|
|
20
25
|
// Sentinel key for traffic that isn't a network node (client connections, etc.). Aggregated so memory stays O(nodes),
|
|
21
26
|
// not O(client connections) — clients churn through many raw ids per hour and we never want a bucket per client.
|
|
22
27
|
const OUTSIDE_KEY = "";
|
|
23
28
|
|
|
24
29
|
export type NodeDataTraffic = { sent: number; received: number; };
|
|
30
|
+
// All numbers are PER-SECOND rates (see file header).
|
|
25
31
|
export type TrafficStats = {
|
|
26
32
|
functionsExecuted: number;
|
|
27
33
|
pathValuesSent: number;
|
|
28
34
|
pathValuesReceived: number;
|
|
29
35
|
querysubCalls: number;
|
|
30
|
-
//
|
|
36
|
+
// Byte rates to/from each known network node, plus one aggregate for everything outside the node list.
|
|
31
37
|
perNode: { [nodeId: string]: NodeDataTraffic };
|
|
32
38
|
outside: NodeDataTraffic;
|
|
33
39
|
};
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
function oldestBucket() {
|
|
39
|
-
return currentBucket() - BUCKET_COUNT + 1;
|
|
40
|
-
}
|
|
41
|
+
// bucket index => amount accumulated in that bucket
|
|
42
|
+
type BucketMap = Map<number, number>;
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
let
|
|
44
|
-
let
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let
|
|
49
|
-
let bytesReceived = new Map<string, Map<number, number>>();
|
|
44
|
+
let functionsExecuted: BucketMap = new Map();
|
|
45
|
+
let pathValuesSent: BucketMap = new Map();
|
|
46
|
+
let pathValuesReceived: BucketMap = new Map();
|
|
47
|
+
let querysubCalls: BucketMap = new Map();
|
|
48
|
+
// raw connection nodeId => buckets
|
|
49
|
+
let bytesSent = new Map<string, BucketMap>();
|
|
50
|
+
let bytesReceived = new Map<string, BucketMap>();
|
|
50
51
|
|
|
51
|
-
function addSimple(buckets:
|
|
52
|
-
let bucket =
|
|
52
|
+
function addSimple(buckets: BucketMap, amount: number) {
|
|
53
|
+
let bucket = Math.floor(Date.now() / BUCKET_SIZE) * BUCKET_SIZE;
|
|
53
54
|
buckets.set(bucket, (buckets.get(bucket) || 0) + amount);
|
|
54
55
|
}
|
|
55
|
-
function addKeyed(map: Map<string,
|
|
56
|
+
function addKeyed(map: Map<string, BucketMap>, key: string, amount: number) {
|
|
56
57
|
let buckets = map.get(key);
|
|
57
58
|
if (!buckets) {
|
|
58
59
|
buckets = new Map();
|
|
@@ -84,29 +85,39 @@ function trafficKey(nodeId: string): string {
|
|
|
84
85
|
SocketFunction.trackMessageSizes.upload.push((size, nodeId) => addKeyed(bytesSent, trafficKey(nodeId), size));
|
|
85
86
|
SocketFunction.trackMessageSizes.download.push((size, nodeId) => addKeyed(bytesReceived, trafficKey(nodeId), size));
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
94
|
let total = 0;
|
|
95
|
+
let oldestTime = now;
|
|
90
96
|
for (let [bucket, count] of Array.from(buckets)) {
|
|
91
|
-
if (bucket <
|
|
97
|
+
if (bucket < retentionOldest) {
|
|
92
98
|
buckets.delete(bucket);
|
|
93
99
|
continue;
|
|
94
100
|
}
|
|
101
|
+
if (bucket < targetTime) continue;
|
|
102
|
+
oldestTime = Math.min(oldestTime, bucket);
|
|
95
103
|
total += count;
|
|
96
104
|
}
|
|
97
|
-
|
|
105
|
+
if (oldestTime === now) {
|
|
106
|
+
oldestTime = targetTime;
|
|
107
|
+
}
|
|
108
|
+
return total / ((now - oldestTime) / 1000);
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outside: NodeDataTraffic; } {
|
|
111
|
+
function aggregateData(windowSize: number): { perNode: { [nodeId: string]: NodeDataTraffic }; outside: NodeDataTraffic; } {
|
|
101
112
|
let known = new Set(getCachedNodeIds());
|
|
102
113
|
let perNode: { [nodeId: string]: NodeDataTraffic } = {};
|
|
103
114
|
let outside: NodeDataTraffic = { sent: 0, received: 0 };
|
|
104
115
|
// At most (node count + 1) keys, so this whole pass is O(nodes). debugNodeId (a linear lookup) is only used for
|
|
105
116
|
// the rare server-style id that isn't already a known node.
|
|
106
|
-
let attribute = (rawNodeId: string, field: "sent" | "received",
|
|
107
|
-
if (
|
|
117
|
+
let attribute = (rawNodeId: string, field: "sent" | "received", rate: number) => {
|
|
118
|
+
if (rate <= 0) return;
|
|
108
119
|
if (rawNodeId === OUTSIDE_KEY) {
|
|
109
|
-
outside[field] +=
|
|
120
|
+
outside[field] += rate;
|
|
110
121
|
return;
|
|
111
122
|
}
|
|
112
123
|
let nice = known.has(rawNodeId) ? rawNodeId : debugNodeId(rawNodeId);
|
|
@@ -116,37 +127,37 @@ function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outs
|
|
|
116
127
|
entry = { sent: 0, received: 0 };
|
|
117
128
|
perNode[nice] = entry;
|
|
118
129
|
}
|
|
119
|
-
entry[field] +=
|
|
130
|
+
entry[field] += rate;
|
|
120
131
|
} else {
|
|
121
|
-
outside[field] +=
|
|
132
|
+
outside[field] += rate;
|
|
122
133
|
}
|
|
123
134
|
};
|
|
124
135
|
for (let [rawNodeId, buckets] of Array.from(bytesSent)) {
|
|
125
|
-
attribute(rawNodeId, "sent",
|
|
136
|
+
attribute(rawNodeId, "sent", bucketRate(buckets, windowSize));
|
|
126
137
|
if (buckets.size === 0) bytesSent.delete(rawNodeId);
|
|
127
138
|
}
|
|
128
139
|
for (let [rawNodeId, buckets] of Array.from(bytesReceived)) {
|
|
129
|
-
attribute(rawNodeId, "received",
|
|
140
|
+
attribute(rawNodeId, "received", bucketRate(buckets, windowSize));
|
|
130
141
|
if (buckets.size === 0) bytesReceived.delete(rawNodeId);
|
|
131
142
|
}
|
|
132
143
|
return { perNode, outside };
|
|
133
144
|
}
|
|
134
145
|
|
|
135
|
-
export function getTrafficStats(): TrafficStats {
|
|
136
|
-
let data = aggregateData();
|
|
146
|
+
export function getTrafficStats(windowSize: number): TrafficStats {
|
|
147
|
+
let data = aggregateData(windowSize);
|
|
137
148
|
return {
|
|
138
|
-
functionsExecuted:
|
|
139
|
-
pathValuesSent:
|
|
140
|
-
pathValuesReceived:
|
|
141
|
-
querysubCalls:
|
|
149
|
+
functionsExecuted: bucketRate(functionsExecuted, windowSize),
|
|
150
|
+
pathValuesSent: bucketRate(pathValuesSent, windowSize),
|
|
151
|
+
pathValuesReceived: bucketRate(pathValuesReceived, windowSize),
|
|
152
|
+
querysubCalls: bucketRate(querysubCalls, windowSize),
|
|
142
153
|
perNode: data.perNode,
|
|
143
154
|
outside: data.outside,
|
|
144
155
|
};
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
class TrafficControllerBase {
|
|
148
|
-
public async getTrafficStats(): Promise<TrafficStats> {
|
|
149
|
-
return getTrafficStats();
|
|
159
|
+
public async getTrafficStats(windowSize: number): Promise<TrafficStats> {
|
|
160
|
+
return getTrafficStats(windowSize);
|
|
150
161
|
}
|
|
151
162
|
}
|
|
152
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
|
|
|
@@ -21,7 +21,7 @@ import { getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
|
21
21
|
import { getScheduledShutdownTime } from "../-g-core-values/scheduledShutdown";
|
|
22
22
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
23
23
|
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
24
|
-
import { getDomain, isPublic
|
|
24
|
+
import { getDomain, isPublic } from "../config";
|
|
25
25
|
import { getGitRefSync, getGitURLSync } from "../4-deploy/git";
|
|
26
26
|
import type { DeployProgress } from "../4-deploy/deployFunctions";
|
|
27
27
|
import { getRoutingOverride, getRoutingOverridePart } from "../0-path-value-core/PathRouterRouteOverride";
|
|
@@ -363,14 +363,15 @@ export class PathFunctionRunner {
|
|
|
363
363
|
secondaryShardRange?: { startFraction: number, endFraction: number };
|
|
364
364
|
PermissionsChecker: PermissionsCheckType | undefined;
|
|
365
365
|
// The networks we listen to calls on. Unset is equivalent to ["default"].
|
|
366
|
-
networks
|
|
366
|
+
networks: string[];
|
|
367
367
|
}) {
|
|
368
|
+
if (!config.networks.length) throw new Error(`networks is required`);
|
|
368
369
|
PathFunctionRunner.hostControllers();
|
|
369
370
|
debugFunctionRunnerShards.push({
|
|
370
371
|
domainName: config.domainName,
|
|
371
372
|
shardRange: config.shardRange,
|
|
372
373
|
secondaryShardRange: config.secondaryShardRange,
|
|
373
|
-
networks: config.networks
|
|
374
|
+
networks: config.networks,
|
|
374
375
|
isPublic: isPublic(),
|
|
375
376
|
});
|
|
376
377
|
logErrors(this.startWatching());
|
|
@@ -401,7 +402,7 @@ export class PathFunctionRunner {
|
|
|
401
402
|
|
|
402
403
|
let outstandingCalls = 0;
|
|
403
404
|
|
|
404
|
-
let networks = this.config.networks
|
|
405
|
+
let networks = this.config.networks;
|
|
405
406
|
|
|
406
407
|
let watchModuleCalls = cache((moduleId: string) => {
|
|
407
408
|
for (let network of networks) {
|
|
@@ -691,7 +692,7 @@ export class PathFunctionRunner {
|
|
|
691
692
|
}
|
|
692
693
|
|
|
693
694
|
let callNetwork = callSpec.network;
|
|
694
|
-
let ourNetworks = this.config.networks
|
|
695
|
+
let ourNetworks = this.config.networks;
|
|
695
696
|
if (!ourNetworks.includes(callNetwork)) {
|
|
696
697
|
return;
|
|
697
698
|
}
|
|
@@ -15,7 +15,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
15
15
|
import { getThreadKeyCert } from "sliftutils/misc/https/certs";
|
|
16
16
|
import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
|
|
17
17
|
import { timeInMinute } from "socket-function/src/misc";
|
|
18
|
-
import { getDomain, getNetworks, isLocal, isPublic
|
|
18
|
+
import { getDomain, getNetworks, isLocal, isPublic } from "../config";
|
|
19
19
|
import { green, magenta } from "socket-function/src/formatting/logColors";
|
|
20
20
|
import path from "path";
|
|
21
21
|
import { IndexedLogs } from "../diagnostics/logs/IndexedLogs/IndexedLogs";
|
|
@@ -32,6 +32,9 @@ async function main() {
|
|
|
32
32
|
// ClientWatcher.DEBUG_TRIGGERS = "heavy";
|
|
33
33
|
// authorityStorage.DEBUG_UNWATCH = true;
|
|
34
34
|
|
|
35
|
+
let networks = getNetworks();
|
|
36
|
+
if (!networks.length) throw new Error(`No networks found. Use --network to specify the networks this function runner should listen on.`);
|
|
37
|
+
|
|
35
38
|
PathFunctionRunner.DEBUG_CALLS = true;
|
|
36
39
|
// debugCoreMode();
|
|
37
40
|
|
|
@@ -60,8 +63,6 @@ async function main() {
|
|
|
60
63
|
console.log(green(`Sharding from ${shardStart} to ${shardEnd}`));
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
let networks = getNetworks();
|
|
64
|
-
|
|
65
66
|
new PathFunctionRunner({
|
|
66
67
|
domainName: getDomain(),
|
|
67
68
|
shardRange: { startFraction: shardStart, endFraction: shardEnd },
|
|
@@ -76,9 +77,5 @@ async function main() {
|
|
|
76
77
|
let deployPath = path.resolve("./deploy.ts");
|
|
77
78
|
await import(deployPath);
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
if (networks.length !== 1 || networks[0] !== DEFAULT_NETWORK) {
|
|
81
|
-
console.log(magenta(`Only running functions on the network(s): ${networks.join(", ")}. Use --network ${networks[0]} in your http server to route calls here.`));
|
|
82
|
-
}
|
|
83
80
|
}
|
|
84
81
|
logErrors(main());
|
|
@@ -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
|
|
package/src/config.ts
CHANGED
|
@@ -44,8 +44,6 @@ if (isNode()) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export const DEFAULT_NETWORK = "default";
|
|
48
|
-
|
|
49
47
|
export function expandHomePath(path: string): string {
|
|
50
48
|
if (path === "~" || path.startsWith("~/") || path.startsWith("~\\")) {
|
|
51
49
|
return os.homedir() + path.slice(1);
|
|
@@ -76,7 +74,7 @@ export function getNetworks(): string[] {
|
|
|
76
74
|
result = [...result, ...networkFileNetworks()];
|
|
77
75
|
}
|
|
78
76
|
result = Array.from(new Set(result));
|
|
79
|
-
if (result.length === 0) return [
|
|
77
|
+
if (result.length === 0) return [];
|
|
80
78
|
return result;
|
|
81
79
|
}
|
|
82
80
|
|
|
@@ -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,11 +34,13 @@ 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.
|
|
40
41
|
const NODE_TABLE_COLUMNS: { key: keyof NodeRow; label: string; width: number; color?: string; perMachine?: boolean; }[] = [
|
|
41
42
|
{ key: "ip", label: "IP", width: 130, perMachine: true },
|
|
43
|
+
{ key: "networks", label: "Networks", width: 180, color: QUERYSUB_COLOR, perMachine: true },
|
|
42
44
|
{ key: "machineShort", label: "Machine", width: 96, perMachine: true },
|
|
43
45
|
{ key: "threadPort", label: "Thread:Port", width: 130 },
|
|
44
46
|
{ key: "name", label: "Name", width: 160 },
|
|
@@ -52,6 +54,7 @@ type NodeRow = {
|
|
|
52
54
|
nodeId: string;
|
|
53
55
|
machineId: string;
|
|
54
56
|
ip: string;
|
|
57
|
+
networks: string;
|
|
55
58
|
machineShort: string;
|
|
56
59
|
threadPort: string;
|
|
57
60
|
name: string;
|
|
@@ -104,9 +107,10 @@ class RoutingTablePageControllerBase {
|
|
|
104
107
|
public async getNodeLatencies(nodeId: string): Promise<{ [nodeId: string]: number } | undefined> {
|
|
105
108
|
return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, LatencyController.nodes[nodeId].getLatencies());
|
|
106
109
|
}
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
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));
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
|
|
@@ -116,7 +120,7 @@ export const RoutingTablePageController = SocketFunction.register(
|
|
|
116
120
|
() => ({
|
|
117
121
|
getAllNodeAuthoritySpecs: {},
|
|
118
122
|
getNodeLatencies: {},
|
|
119
|
-
|
|
123
|
+
getNodeTrafficRates: {},
|
|
120
124
|
getNodeIps: {},
|
|
121
125
|
}),
|
|
122
126
|
() => ({
|
|
@@ -207,13 +211,15 @@ class FunctionRunnersSection extends qreact.Component {
|
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
function nodeTotalTraffic(traffic: TrafficStats): { sent: number; received: number; outside: number; } {
|
|
210
|
-
let
|
|
211
|
-
let
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
let outsideSent = traffic.outside?.sent || 0;
|
|
215
|
+
let outsideReceived = traffic.outside?.received || 0;
|
|
216
|
+
let sent = outsideSent;
|
|
217
|
+
let received = outsideReceived;
|
|
218
|
+
for (let d of Object.values(traffic.perNode || {})) {
|
|
219
|
+
sent += d?.sent || 0;
|
|
220
|
+
received += d?.received || 0;
|
|
215
221
|
}
|
|
216
|
-
return { sent, received, outside:
|
|
222
|
+
return { sent, received, outside: outsideSent + outsideReceived };
|
|
217
223
|
}
|
|
218
224
|
|
|
219
225
|
function machineIdOf(nodeId: string): string {
|
|
@@ -235,40 +241,44 @@ function sumTraffic(threads: string[], trafficMaps: Map<string, TrafficStats>) {
|
|
|
235
241
|
let totals = nodeTotalTraffic(traffic);
|
|
236
242
|
dataSent += totals.sent;
|
|
237
243
|
dataReceived += totals.received;
|
|
238
|
-
valuesSent += traffic.pathValuesSent;
|
|
239
|
-
valuesReceived += traffic.pathValuesReceived;
|
|
240
|
-
calls += traffic.functionsExecuted;
|
|
241
|
-
addCalls += traffic.querysubCalls;
|
|
244
|
+
valuesSent += traffic.pathValuesSent || 0;
|
|
245
|
+
valuesReceived += traffic.pathValuesReceived || 0;
|
|
246
|
+
calls += traffic.functionsExecuted || 0;
|
|
247
|
+
addCalls += traffic.querysubCalls || 0;
|
|
242
248
|
}
|
|
243
249
|
return { dataSent, dataReceived, valuesSent, valuesReceived, calls, addCalls };
|
|
244
250
|
}
|
|
245
251
|
|
|
246
|
-
// A machine's graph label: the IP first (most important), then
|
|
247
|
-
// its threads and colored the same.
|
|
252
|
+
// A machine's graph label: the IP first (most important), then its function-runner networks, then the same per-piece
|
|
253
|
+
// info as the table, summed across all its threads and colored the same.
|
|
248
254
|
function machineLabelLines(config: {
|
|
249
255
|
machineId: string;
|
|
250
256
|
threads: string[];
|
|
251
257
|
trafficMaps: Map<string, TrafficStats>;
|
|
252
258
|
ip: string | undefined;
|
|
259
|
+
networks: string[];
|
|
253
260
|
}): LatencyGraphLabelLine[] {
|
|
254
|
-
let { machineId, threads, ip } = config;
|
|
261
|
+
let { machineId, threads, ip, networks } = config;
|
|
255
262
|
let totals = sumTraffic(threads, config.trafficMaps);
|
|
256
263
|
let lines: LatencyGraphLabelLine[] = [];
|
|
257
264
|
if (ip) {
|
|
258
265
|
lines.push({ text: ip });
|
|
259
266
|
}
|
|
267
|
+
if (networks.length) {
|
|
268
|
+
lines.push({ text: networks.join(" | "), color: QUERYSUB_COLOR });
|
|
269
|
+
}
|
|
260
270
|
lines.push({ text: `${machineId.slice(0, ID_CHARS)} ${threads.length} threads` });
|
|
261
271
|
if (totals.dataSent + totals.dataReceived > 0) {
|
|
262
|
-
lines.push({ text: `↑${formatNumber(totals.dataSent)}B ↓${formatNumber(totals.dataReceived)}B` });
|
|
272
|
+
lines.push({ text: `↑${formatNumber(totals.dataSent)}B/s ↓${formatNumber(totals.dataReceived)}B/s` });
|
|
263
273
|
}
|
|
264
274
|
if (totals.valuesSent || totals.valuesReceived) {
|
|
265
|
-
lines.push({ text: `↑${formatNumber(totals.valuesSent)} ↓${formatNumber(totals.valuesReceived)} values`, color: PATHVALUE_COLOR });
|
|
275
|
+
lines.push({ text: `↑${formatNumber(totals.valuesSent)}/s ↓${formatNumber(totals.valuesReceived)}/s values`, color: PATHVALUE_COLOR });
|
|
266
276
|
}
|
|
267
277
|
if (totals.calls) {
|
|
268
|
-
lines.push({ text: `${formatNumber(totals.calls)} calls`, color: FUNCTION_COLOR });
|
|
278
|
+
lines.push({ text: `${formatNumber(totals.calls)}/s calls`, color: FUNCTION_COLOR });
|
|
269
279
|
}
|
|
270
280
|
if (totals.addCalls) {
|
|
271
|
-
lines.push({ text: `${formatNumber(totals.addCalls)} addCalls`, color: QUERYSUB_COLOR });
|
|
281
|
+
lines.push({ text: `${formatNumber(totals.addCalls)}/s addCalls`, color: QUERYSUB_COLOR });
|
|
272
282
|
}
|
|
273
283
|
return lines;
|
|
274
284
|
}
|
|
@@ -280,8 +290,9 @@ function buildNodeRow(config: {
|
|
|
280
290
|
info: NodeAuthorityInfo | undefined;
|
|
281
291
|
traffic: TrafficStats | undefined;
|
|
282
292
|
machineIp: Map<string, string>;
|
|
293
|
+
machineNetworks: Map<string, string[]>;
|
|
283
294
|
}): NodeRow {
|
|
284
|
-
let { nodeId, runner, info, traffic, machineIp } = config;
|
|
295
|
+
let { nodeId, runner, info, traffic, machineIp, machineNetworks } = config;
|
|
285
296
|
let parts = decodeNodeId(nodeId, getDomain(), "allowMissingThreadId");
|
|
286
297
|
let thread = (parts?.threadId || "?").slice(0, ID_CHARS);
|
|
287
298
|
let machineId = parts?.machineId || nodeId;
|
|
@@ -294,7 +305,7 @@ function buildNodeRow(config: {
|
|
|
294
305
|
let sum = totals.sent + totals.received;
|
|
295
306
|
if (sum > 0) {
|
|
296
307
|
let outsidePct = Math.round((totals.outside / sum) * 100);
|
|
297
|
-
data = `↑${formatNumber(totals.sent)}B ↓${formatNumber(totals.received)}B ${outsidePct}%⊘`;
|
|
308
|
+
data = `↑${formatNumber(totals.sent)}B/s ↓${formatNumber(totals.received)}B/s ${outsidePct}%⊘`;
|
|
298
309
|
}
|
|
299
310
|
}
|
|
300
311
|
|
|
@@ -304,7 +315,7 @@ function buildNodeRow(config: {
|
|
|
304
315
|
for (let shard of runner.shards) {
|
|
305
316
|
fnParts.push(`fn ${shard.shardRange.startFraction.toFixed(3)}-${shard.shardRange.endFraction.toFixed(3)}`);
|
|
306
317
|
}
|
|
307
|
-
fnParts.push(`${formatNumber(traffic?.functionsExecuted ?? 0)} calls`);
|
|
318
|
+
fnParts.push(`${formatNumber(traffic?.functionsExecuted ?? 0)}/s calls`);
|
|
308
319
|
if (!runner.isPublic) fnParts.push("PRIVATE");
|
|
309
320
|
}
|
|
310
321
|
|
|
@@ -314,14 +325,15 @@ function buildNodeRow(config: {
|
|
|
314
325
|
pvParts.push(`pv ${spec.routeStart.toFixed(3)}-${spec.routeEnd.toFixed(3)}`);
|
|
315
326
|
}
|
|
316
327
|
if (traffic && (traffic.pathValuesSent || traffic.pathValuesReceived)) {
|
|
317
|
-
pvParts.push(`↑${formatNumber(traffic.pathValuesSent)} ↓${formatNumber(traffic.pathValuesReceived)} values`);
|
|
328
|
+
pvParts.push(`↑${formatNumber(traffic.pathValuesSent)}/s ↓${formatNumber(traffic.pathValuesReceived)}/s values`);
|
|
318
329
|
}
|
|
319
330
|
|
|
320
|
-
let qs = traffic?.querysubCalls ? `${formatNumber(traffic.querysubCalls)} addCalls` : "";
|
|
331
|
+
let qs = traffic?.querysubCalls ? `${formatNumber(traffic.querysubCalls)}/s addCalls` : "";
|
|
321
332
|
return {
|
|
322
333
|
nodeId,
|
|
323
334
|
machineId,
|
|
324
335
|
ip: machineIp.get(machineId) || "",
|
|
336
|
+
networks: (machineNetworks.get(machineId) || []).join(" | "),
|
|
325
337
|
machineShort: machineId.slice(0, ID_CHARS),
|
|
326
338
|
threadPort: `${thread}:${parts?.port ?? "?"}`,
|
|
327
339
|
name,
|
|
@@ -377,12 +389,13 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
377
389
|
let infoByNode = new Map(this.props.infos.map(info => [info.nodeId, info]));
|
|
378
390
|
|
|
379
391
|
// One synced call per node for each data source; each resolves independently so the graph fills in progressively.
|
|
392
|
+
let trafficWindow = trafficWindowParam.value;
|
|
380
393
|
let latencyMaps = new Map<string, { [nodeId: string]: number }>();
|
|
381
394
|
let trafficMaps = new Map<string, TrafficStats>();
|
|
382
395
|
for (let nodeId of nodeIds) {
|
|
383
396
|
let latencies = synced.getNodeLatencies(nodeId);
|
|
384
397
|
if (latencies) latencyMaps.set(nodeId, latencies);
|
|
385
|
-
let traffic = synced.
|
|
398
|
+
let traffic = synced.getNodeTrafficRates(nodeId, trafficWindow);
|
|
386
399
|
if (traffic) trafficMaps.set(nodeId, traffic);
|
|
387
400
|
}
|
|
388
401
|
// Only graph nodes that reported their latencies — ones that never respond probably don't exist.
|
|
@@ -428,14 +441,26 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
428
441
|
if (bestIp) machineIp.set(machineId, bestIp);
|
|
429
442
|
}
|
|
430
443
|
|
|
444
|
+
// The unique set of function-runner networks running on each machine (usually one, human-readable).
|
|
445
|
+
let machineNetworks = new Map<string, string[]>();
|
|
446
|
+
for (let [machineId, threads] of machineAllThreads) {
|
|
447
|
+
let unique = new Set<string>();
|
|
448
|
+
for (let thread of threads) {
|
|
449
|
+
for (let network of runnerByNode.get(thread)?.networks || []) {
|
|
450
|
+
unique.add(network);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (unique.size) machineNetworks.set(machineId, [...unique]);
|
|
454
|
+
}
|
|
455
|
+
|
|
431
456
|
// Total bytes on each thread-pair link (both directions). Both endpoints report the same total, so take the max.
|
|
432
457
|
let pairKey = (a: string, b: string) => a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
433
458
|
let pairTraffic = new Map<string, number>();
|
|
434
459
|
for (let [reporter, traffic] of trafficMaps) {
|
|
435
|
-
for (let [peer, data] of Object.entries(traffic.perNode)) {
|
|
460
|
+
for (let [peer, data] of Object.entries(traffic.perNode || {})) {
|
|
436
461
|
if (reporter === peer || !respondedSet.has(peer)) continue;
|
|
437
462
|
let key = pairKey(reporter, peer);
|
|
438
|
-
pairTraffic.set(key, Math.max(pairTraffic.get(key) || 0, data
|
|
463
|
+
pairTraffic.set(key, Math.max(pairTraffic.get(key) || 0, (data?.sent || 0) + (data?.received || 0)));
|
|
439
464
|
}
|
|
440
465
|
}
|
|
441
466
|
|
|
@@ -472,7 +497,7 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
472
497
|
let totals = sumTraffic(allThreads, trafficMaps);
|
|
473
498
|
return {
|
|
474
499
|
id: machineId,
|
|
475
|
-
labelLines: machineLabelLines({ machineId, threads: allThreads, trafficMaps, ip: machineIp.get(machineId) }),
|
|
500
|
+
labelLines: machineLabelLines({ machineId, threads: allThreads, trafficMaps, ip: machineIp.get(machineId), networks: machineNetworks.get(machineId) || [] }),
|
|
476
501
|
weight: totals.dataSent + totals.dataReceived,
|
|
477
502
|
};
|
|
478
503
|
});
|
|
@@ -484,22 +509,35 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
484
509
|
|
|
485
510
|
// One row per thread node, grouped so a machine's threads sit together; the selected machine sorts to the top.
|
|
486
511
|
// Single composite key: selected-first, then machine, then thread — the low separator keeps segments ordered.
|
|
487
|
-
let rows = nodeIds.map(nodeId => buildNodeRow({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic: trafficMaps.get(nodeId), machineIp }));
|
|
512
|
+
let rows = nodeIds.map(nodeId => buildNodeRow({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic: trafficMaps.get(nodeId), machineIp, machineNetworks }));
|
|
488
513
|
let selected = selectedMachineParam.value;
|
|
489
514
|
let sep = String.fromCharCode(1);
|
|
490
515
|
sort(rows, row => `${row.machineId === selected ? 0 : 1}${sep}${row.machineId}${sep}${row.threadPort}`);
|
|
491
516
|
|
|
492
517
|
return <div className={css.vbox(8).fillWidth}>
|
|
493
|
-
<
|
|
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>
|
|
494
531
|
<div className={css.relative.fillWidth.height(LATENCY_GRAPH_HEIGHT_PX).bord2(0, 0, 85)}>
|
|
495
532
|
<LatencyGraph
|
|
496
533
|
nodes={nodes}
|
|
497
534
|
links={links}
|
|
498
|
-
formatWeight={weight => formatNumber(weight) + "B"}
|
|
535
|
+
formatWeight={weight => formatNumber(weight) + "B/s"}
|
|
499
536
|
selectedId={selected || undefined}
|
|
500
537
|
onSelectNode={id => selectedMachineParam.value = selectedMachineParam.value === id ? "" : id}
|
|
501
538
|
/>
|
|
502
539
|
</div>
|
|
540
|
+
<div className={css.colorhsl(0, 0, 50)}>Click a node to sort its information to the top of the table below.</div>
|
|
503
541
|
<h2 className={css.margin(0)}>Nodes ({rows.length})</h2>
|
|
504
542
|
<NodeInfoTable rows={rows} selectedMachine={selected} />
|
|
505
543
|
</div>;
|
|
@@ -23,11 +23,12 @@ export type LatencyGraphProps = {
|
|
|
23
23
|
const DEFAULT_CONNECTIONS = 4;
|
|
24
24
|
// Nearest neighbors drawn per node (the layout still uses every measured latency; this only thins the drawn lines).
|
|
25
25
|
const connectionsParam = new URLParam("lgConnections", DEFAULT_CONNECTIONS);
|
|
26
|
-
const DEFAULT_LATENCY_EXPONENT =
|
|
26
|
+
const DEFAULT_LATENCY_EXPONENT = 0.5;
|
|
27
27
|
// Exponent on (latency / reference) when mapping to layout distance, so far nodes push apart harder as it grows.
|
|
28
28
|
const latencyExponentParam = new URLParam("lgLatencyExponent", DEFAULT_LATENCY_EXPONENT);
|
|
29
29
|
const geoParam = new URLParam("lgGeo", false);
|
|
30
|
-
|
|
30
|
+
// On by default: each drawn connection shows its latency and (when a weight formatter is given) its traffic.
|
|
31
|
+
const showLatenciesParam = new URLParam("lgShowLatencies", true);
|
|
31
32
|
// The config panel is collapsed by default so it doesn't eat the canvas; clicking the header expands it.
|
|
32
33
|
const configOpenParam = new URLParam("lgConfigOpen", false);
|
|
33
34
|
// Pixels per degree of latitude/longitude in geographic mode (equirectangular projection).
|
|
@@ -45,6 +46,9 @@ const POWER_EPS = 1e-9;
|
|
|
45
46
|
// with a floor. Normalized to the median so the overall scale is latency-magnitude independent.
|
|
46
47
|
const DIST_MIN_PX = 8;
|
|
47
48
|
const DIST_UNIT_PX = 90;
|
|
49
|
+
// A touch of deterministic jitter on the MDS init breaks symmetry, so a handful of nodes settle into a spread (a
|
|
50
|
+
// triangle for three) instead of collapsing onto a single line.
|
|
51
|
+
const INIT_JITTER_PX = 15;
|
|
48
52
|
|
|
49
53
|
const MIN_OPACITY = 0.05;
|
|
50
54
|
const NODE_RADIUS = 6;
|
|
@@ -65,9 +69,6 @@ const FIT_PAD = 60;
|
|
|
65
69
|
const FIT_PAD_TOP = 160;
|
|
66
70
|
// Auto-fit leaves a bit of extra breathing room by not zooming all the way in to fill the padded box.
|
|
67
71
|
const FIT_ZOOM = 0.7;
|
|
68
|
-
// Cursor must be within this many screen pixels of a node to hover/click it.
|
|
69
|
-
const HOVER_HIT_PX = 60;
|
|
70
|
-
const CLICK_HIT_PX = 40;
|
|
71
72
|
|
|
72
73
|
type Edge = { a: number; b: number; latency: number; };
|
|
73
74
|
type SolveEdge = { a: number; b: number; target: number; weight: number; };
|
|
@@ -293,6 +294,11 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
293
294
|
});
|
|
294
295
|
this.positionsX = pos.x;
|
|
295
296
|
this.positionsY = pos.y;
|
|
297
|
+
// Deterministic per-node jitter (different frequencies for x/y so it isn't itself collinear) to break symmetry.
|
|
298
|
+
for (let i = 0; i < n; i++) {
|
|
299
|
+
this.positionsX[i] += Math.sin(i * 12.9898 + 1) * INIT_JITTER_PX;
|
|
300
|
+
this.positionsY[i] += Math.cos(i * 78.233 + 1) * INIT_JITTER_PX;
|
|
301
|
+
}
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
// Classical MDS on an m-point set given a squared-target-distance function; top-2 eigenvectors.
|
|
@@ -622,6 +628,7 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
622
628
|
let hover = this.hoverNode;
|
|
623
629
|
let connections = (this.neighbors[hover] || []).map(nb => nb.edge);
|
|
624
630
|
|
|
631
|
+
// Just highlight the hovered node's connections — their latency/traffic labels are already drawn all the time.
|
|
625
632
|
ctx.lineWidth = 1.5;
|
|
626
633
|
for (let edge of connections) {
|
|
627
634
|
ctx.strokeStyle = "hsla(40, 90%, 60%, 0.6)";
|
|
@@ -630,19 +637,6 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
630
637
|
ctx.lineTo(toScreenX(this.positionsX[edge.b]), toScreenY(this.positionsY[edge.b]));
|
|
631
638
|
ctx.stroke();
|
|
632
639
|
}
|
|
633
|
-
ctx.font = "12px sans-serif";
|
|
634
|
-
ctx.textAlign = "center";
|
|
635
|
-
ctx.textBaseline = "middle";
|
|
636
|
-
for (let edge of connections) {
|
|
637
|
-
let midX = (toScreenX(this.positionsX[edge.a]) + toScreenX(this.positionsX[edge.b])) / 2;
|
|
638
|
-
let midY = (toScreenY(this.positionsY[edge.a]) + toScreenY(this.positionsY[edge.b])) / 2;
|
|
639
|
-
let label = formatTime(edge.latency);
|
|
640
|
-
let textWidth = ctx.measureText(label).width;
|
|
641
|
-
ctx.fillStyle = "hsla(0, 0%, 0%, 0.75)";
|
|
642
|
-
ctx.fillRect(midX - textWidth / 2 - 3, midY - 8, textWidth + 6, 16);
|
|
643
|
-
ctx.fillStyle = "hsl(40, 90%, 75%)";
|
|
644
|
-
ctx.fillText(label, midX, midY);
|
|
645
|
-
}
|
|
646
640
|
// Redraw the hovered node's own label last so it always sits above the connection labels.
|
|
647
641
|
let hoverLines = this.nodes[hover].labelLines;
|
|
648
642
|
if (hoverLines && hoverLines.length) {
|
|
@@ -720,7 +714,7 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
720
714
|
this.lastPointerY = e.clientY;
|
|
721
715
|
};
|
|
722
716
|
|
|
723
|
-
//
|
|
717
|
+
// Index of the node closest to a screen point (undefined only when there are no nodes).
|
|
724
718
|
nearestNodeTo(mx: number, my: number) {
|
|
725
719
|
let scale = this.viewScale;
|
|
726
720
|
let bestDist = Infinity;
|
|
@@ -728,13 +722,13 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
728
722
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
729
723
|
let sx = this.viewWidth / 2 + (this.positionsX[i] - this.centroidX) * scale + this.panX;
|
|
730
724
|
let sy = this.viewHeight / 2 + (this.positionsY[i] - this.centroidY) * scale + this.panY;
|
|
731
|
-
let dist =
|
|
725
|
+
let dist = (sx - mx) ** 2 + (sy - my) ** 2;
|
|
732
726
|
if (dist < bestDist) {
|
|
733
727
|
bestDist = dist;
|
|
734
728
|
best = i;
|
|
735
729
|
}
|
|
736
730
|
}
|
|
737
|
-
return
|
|
731
|
+
return best;
|
|
738
732
|
}
|
|
739
733
|
|
|
740
734
|
onMouseMove = (e: MouseEvent) => {
|
|
@@ -754,8 +748,8 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
754
748
|
let my = e.clientY - rect.top;
|
|
755
749
|
this.mouseX = mx;
|
|
756
750
|
this.mouseY = my;
|
|
757
|
-
|
|
758
|
-
this.hoverNode =
|
|
751
|
+
// Always hover whichever node the cursor is closest to, so moving anywhere explores the graph.
|
|
752
|
+
this.hoverNode = this.nearestNodeTo(mx, my);
|
|
759
753
|
this.scheduleFrame();
|
|
760
754
|
};
|
|
761
755
|
|
|
@@ -764,8 +758,8 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
764
758
|
if (!canvas || !this.onSelectNode) return;
|
|
765
759
|
let rect = canvas.getBoundingClientRect();
|
|
766
760
|
let nearest = this.nearestNodeTo(e.clientX - rect.left, e.clientY - rect.top);
|
|
767
|
-
if (nearest
|
|
768
|
-
this.onSelectNode(this.nodes[nearest
|
|
761
|
+
if (nearest === undefined) return;
|
|
762
|
+
this.onSelectNode(this.nodes[nearest].id);
|
|
769
763
|
};
|
|
770
764
|
|
|
771
765
|
onMouseLeave = () => {
|
|
@@ -827,22 +821,27 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
827
821
|
this.builtSig = sig;
|
|
828
822
|
this.builtWeightSig = weightSig;
|
|
829
823
|
this.build();
|
|
830
|
-
} else
|
|
831
|
-
//
|
|
832
|
-
|
|
833
|
-
this.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
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
|
+
}
|
|
839
838
|
}
|
|
840
839
|
this.scheduleFrame();
|
|
841
840
|
|
|
842
841
|
return <div className={css.relative.fillBoth.overflowHidden}>
|
|
843
842
|
<canvas
|
|
844
843
|
ref={elem => this.mountCanvas(elem ?? undefined)}
|
|
845
|
-
className={css.absolute.pos(0, 0).fillBoth}
|
|
844
|
+
className={css.absolute.pos(0, 0).fillBoth.cursor("pointer")}
|
|
846
845
|
/>
|
|
847
846
|
<div className={css.vbox(10).pad2(12).absolute.pos(0, 0).width(260).zIndex(2).hsla(0, 0, 8, 0.9).colorhsl(0, 0, 85)}>
|
|
848
847
|
<div
|