querysub 0.520.0 → 0.522.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 +2 -2
- package/src/-f-node-discovery/TrafficTracking.ts +49 -28
- package/src/3-path-functions/PathFunctionRunner.ts +6 -5
- package/src/3-path-functions/PathFunctionRunnerMain.ts +4 -7
- package/src/config.ts +1 -3
- package/src/deployManager/components/ServiceDetailPage.tsx +32 -8
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +303 -76
- package/src/library-components/LatencyGraph.tsx +156 -736
|
@@ -8,6 +8,8 @@ import { LatencyController } from "../../-f-node-discovery/LatencyTracking";
|
|
|
8
8
|
import { TrafficController, TrafficStats } from "../../-f-node-discovery/TrafficTracking";
|
|
9
9
|
import { getDomain } from "../../config";
|
|
10
10
|
import { decodeNodeId } from "sliftutils/misc/https/certs";
|
|
11
|
+
import { getNodeIdDomainMaybeUndefined } from "socket-function/src/nodeCache";
|
|
12
|
+
import dns from "dns";
|
|
11
13
|
import { getSyncedController } from "../../library-components/SyncedController";
|
|
12
14
|
import { assertIsManagementUser } from "../managementPages";
|
|
13
15
|
import { NodeCapabilitiesController } from "../../-g-core-values/NodeCapabilities";
|
|
@@ -17,6 +19,7 @@ import type { AuthoritySpec } from "../../0-path-value-core/PathRouter";
|
|
|
17
19
|
import { getFunctionRunnerIndex, FunctionRunnerNodeInfo } from "../../4-querysub/FunctionRunnerTracking";
|
|
18
20
|
import { formatTime, formatNumber } from "socket-function/src/formatting/format";
|
|
19
21
|
import { LatencyGraph, LatencyGraphNode, LatencyGraphLink, LatencyGraphLabelLine } from "../../library-components/LatencyGraph";
|
|
22
|
+
import { URLParam } from "../../library-components/URLParam";
|
|
20
23
|
|
|
21
24
|
const ID_CHARS = 8;
|
|
22
25
|
// Green means querysub, blue means path value, purple means function runner.
|
|
@@ -29,13 +32,68 @@ const RANGE_BAR_WIDTH_PX = 360;
|
|
|
29
32
|
const RANGE_BAR_HEIGHT_PX = 14;
|
|
30
33
|
const LATENCY_GRAPH_HEIGHT_PX = 720;
|
|
31
34
|
|
|
35
|
+
// Clicking a machine node in the graph (or a row) sorts that machine's nodes to the top of the table.
|
|
36
|
+
const selectedMachineParam = new URLParam("rtSelectedMachine", "");
|
|
37
|
+
|
|
38
|
+
// The per-node table columns, in order. `color` tints the function/path-value/querysub columns to match the graph;
|
|
39
|
+
// `perMachine` columns (the IP and machine id) only print on the first row of each machine group.
|
|
40
|
+
const NODE_TABLE_COLUMNS: { key: keyof NodeRow; label: string; width: number; color?: string; perMachine?: boolean; }[] = [
|
|
41
|
+
{ key: "ip", label: "IP", width: 130, perMachine: true },
|
|
42
|
+
{ key: "networks", label: "Networks", width: 180, color: QUERYSUB_COLOR, perMachine: true },
|
|
43
|
+
{ key: "machineShort", label: "Machine", width: 96, perMachine: true },
|
|
44
|
+
{ key: "threadPort", label: "Thread:Port", width: 130 },
|
|
45
|
+
{ key: "name", label: "Name", width: 160 },
|
|
46
|
+
{ key: "data", label: "Data", width: 210 },
|
|
47
|
+
{ key: "fn", label: "Function", width: 320, color: FUNCTION_COLOR },
|
|
48
|
+
{ key: "pv", label: "Path Values", width: 250, color: PATHVALUE_COLOR },
|
|
49
|
+
{ key: "qs", label: "Querysub", width: 120, color: QUERYSUB_COLOR },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
type NodeRow = {
|
|
53
|
+
nodeId: string;
|
|
54
|
+
machineId: string;
|
|
55
|
+
ip: string;
|
|
56
|
+
networks: string;
|
|
57
|
+
machineShort: string;
|
|
58
|
+
threadPort: string;
|
|
59
|
+
name: string;
|
|
60
|
+
data: string;
|
|
61
|
+
fn: string;
|
|
62
|
+
pv: string;
|
|
63
|
+
qs: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
32
66
|
type NodeAuthorityInfo = {
|
|
33
67
|
nodeId: string;
|
|
34
68
|
entryPoint?: string;
|
|
35
69
|
spec?: AuthoritySpec;
|
|
36
70
|
};
|
|
37
71
|
|
|
72
|
+
// A machine's IP never changes, so we cache each hostname's resolved IP forever (keyed by hostname, shared across all
|
|
73
|
+
// the machine's threads). DNS resolution only runs on the server node this controller executes on.
|
|
74
|
+
const nodeIpCache = new Map<string, string>();
|
|
75
|
+
|
|
76
|
+
async function resolveNodeIp(nodeId: string): Promise<string | undefined> {
|
|
77
|
+
let hostname = getNodeIdDomainMaybeUndefined(nodeId);
|
|
78
|
+
if (!hostname) return undefined;
|
|
79
|
+
let cached = nodeIpCache.get(hostname);
|
|
80
|
+
if (cached) return cached;
|
|
81
|
+
let result = await timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, dns.promises.lookup(hostname));
|
|
82
|
+
if (!result) return undefined;
|
|
83
|
+
nodeIpCache.set(hostname, result.address);
|
|
84
|
+
return result.address;
|
|
85
|
+
}
|
|
86
|
+
|
|
38
87
|
class RoutingTablePageControllerBase {
|
|
88
|
+
// Resolves every node's IP in parallel (cached). A machine's IP is then the most common IP across its threads.
|
|
89
|
+
public async getNodeIps(nodeIds: string[]): Promise<{ [nodeId: string]: string }> {
|
|
90
|
+
let entries = await Promise.all(nodeIds.map(async nodeId => [nodeId, await resolveNodeIp(nodeId)] as const));
|
|
91
|
+
let result: { [nodeId: string]: string } = {};
|
|
92
|
+
for (let [nodeId, ip] of entries) {
|
|
93
|
+
if (ip) result[nodeId] = ip;
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
39
97
|
public async getAllNodeAuthoritySpecs(): Promise<NodeAuthorityInfo[]> {
|
|
40
98
|
let nodes = await getAllNodeIds();
|
|
41
99
|
return Promise.all(nodes.map(async (nodeId): Promise<NodeAuthorityInfo> => {
|
|
@@ -61,6 +119,7 @@ export const RoutingTablePageController = SocketFunction.register(
|
|
|
61
119
|
getAllNodeAuthoritySpecs: {},
|
|
62
120
|
getNodeLatencies: {},
|
|
63
121
|
getNodeTrafficStats: {},
|
|
122
|
+
getNodeIps: {},
|
|
64
123
|
}),
|
|
65
124
|
() => ({
|
|
66
125
|
hooks: [assertIsManagementUser],
|
|
@@ -149,68 +208,170 @@ class FunctionRunnersSection extends qreact.Component {
|
|
|
149
208
|
}
|
|
150
209
|
}
|
|
151
210
|
|
|
152
|
-
function
|
|
211
|
+
function nodeTotalTraffic(traffic: TrafficStats): { sent: number; received: number; outside: number; } {
|
|
212
|
+
let outsideSent = traffic.outside?.sent || 0;
|
|
213
|
+
let outsideReceived = traffic.outside?.received || 0;
|
|
214
|
+
let sent = outsideSent;
|
|
215
|
+
let received = outsideReceived;
|
|
216
|
+
for (let d of Object.values(traffic.perNode || {})) {
|
|
217
|
+
sent += d?.sent || 0;
|
|
218
|
+
received += d?.received || 0;
|
|
219
|
+
}
|
|
220
|
+
return { sent, received, outside: outsideSent + outsideReceived };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function machineIdOf(nodeId: string): string {
|
|
224
|
+
let parts = decodeNodeId(nodeId, getDomain(), "allowMissingThreadId");
|
|
225
|
+
return parts?.machineId || nodeId;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Summed traffic metrics across a set of nodes (a machine's threads).
|
|
229
|
+
function sumTraffic(threads: string[], trafficMaps: Map<string, TrafficStats>) {
|
|
230
|
+
let dataSent = 0;
|
|
231
|
+
let dataReceived = 0;
|
|
232
|
+
let valuesSent = 0;
|
|
233
|
+
let valuesReceived = 0;
|
|
234
|
+
let calls = 0;
|
|
235
|
+
let addCalls = 0;
|
|
236
|
+
for (let id of threads) {
|
|
237
|
+
let traffic = trafficMaps.get(id);
|
|
238
|
+
if (!traffic) continue;
|
|
239
|
+
let totals = nodeTotalTraffic(traffic);
|
|
240
|
+
dataSent += totals.sent;
|
|
241
|
+
dataReceived += totals.received;
|
|
242
|
+
valuesSent += traffic.pathValuesSent || 0;
|
|
243
|
+
valuesReceived += traffic.pathValuesReceived || 0;
|
|
244
|
+
calls += traffic.functionsExecuted || 0;
|
|
245
|
+
addCalls += traffic.querysubCalls || 0;
|
|
246
|
+
}
|
|
247
|
+
return { dataSent, dataReceived, valuesSent, valuesReceived, calls, addCalls };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// A machine's graph label: the IP first (most important), then its function-runner networks, then the same per-piece
|
|
251
|
+
// info as the table, summed across all its threads and colored the same.
|
|
252
|
+
function machineLabelLines(config: {
|
|
253
|
+
machineId: string;
|
|
254
|
+
threads: string[];
|
|
255
|
+
trafficMaps: Map<string, TrafficStats>;
|
|
256
|
+
ip: string | undefined;
|
|
257
|
+
networks: string[];
|
|
258
|
+
}): LatencyGraphLabelLine[] {
|
|
259
|
+
let { machineId, threads, ip, networks } = config;
|
|
260
|
+
let totals = sumTraffic(threads, config.trafficMaps);
|
|
261
|
+
let lines: LatencyGraphLabelLine[] = [];
|
|
262
|
+
if (ip) {
|
|
263
|
+
lines.push({ text: ip });
|
|
264
|
+
}
|
|
265
|
+
if (networks.length) {
|
|
266
|
+
lines.push({ text: networks.join(" | "), color: QUERYSUB_COLOR });
|
|
267
|
+
}
|
|
268
|
+
lines.push({ text: `${machineId.slice(0, ID_CHARS)} ${threads.length} threads` });
|
|
269
|
+
if (totals.dataSent + totals.dataReceived > 0) {
|
|
270
|
+
lines.push({ text: `↑${formatNumber(totals.dataSent)}B/s ↓${formatNumber(totals.dataReceived)}B/s` });
|
|
271
|
+
}
|
|
272
|
+
if (totals.valuesSent || totals.valuesReceived) {
|
|
273
|
+
lines.push({ text: `↑${formatNumber(totals.valuesSent)}/s ↓${formatNumber(totals.valuesReceived)}/s values`, color: PATHVALUE_COLOR });
|
|
274
|
+
}
|
|
275
|
+
if (totals.calls) {
|
|
276
|
+
lines.push({ text: `${formatNumber(totals.calls)}/s calls`, color: FUNCTION_COLOR });
|
|
277
|
+
}
|
|
278
|
+
if (totals.addCalls) {
|
|
279
|
+
lines.push({ text: `${formatNumber(totals.addCalls)}/s addCalls`, color: QUERYSUB_COLOR });
|
|
280
|
+
}
|
|
281
|
+
return lines;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// One table row per thread node: each piece of info that used to live on the graph label becomes its own column.
|
|
285
|
+
function buildNodeRow(config: {
|
|
153
286
|
nodeId: string;
|
|
154
287
|
runner: FunctionRunnerNodeInfo | undefined;
|
|
155
288
|
info: NodeAuthorityInfo | undefined;
|
|
156
289
|
traffic: TrafficStats | undefined;
|
|
157
|
-
|
|
158
|
-
|
|
290
|
+
machineIp: Map<string, string>;
|
|
291
|
+
machineNetworks: Map<string, string[]>;
|
|
292
|
+
}): NodeRow {
|
|
293
|
+
let { nodeId, runner, info, traffic, machineIp, machineNetworks } = config;
|
|
159
294
|
let parts = decodeNodeId(nodeId, getDomain(), "allowMissingThreadId");
|
|
160
295
|
let thread = (parts?.threadId || "?").slice(0, ID_CHARS);
|
|
161
|
-
let
|
|
162
|
-
let lines: LatencyGraphLabelLine[] = [{ text: `${thread}:${parts?.port ?? "?"}` }];
|
|
163
|
-
// The last segment of the entry-point path is a good human name; shown on one line with the server (machine) id.
|
|
296
|
+
let machineId = parts?.machineId || nodeId;
|
|
164
297
|
let entryPoint = info?.entryPoint || runner?.entryPoint;
|
|
165
|
-
let name = entryPoint
|
|
166
|
-
|
|
167
|
-
|
|
298
|
+
let name = entryPoint && entryPoint.split(/[\\/]/).filter(Boolean).at(-1) || "";
|
|
299
|
+
|
|
300
|
+
let data = "";
|
|
168
301
|
if (traffic) {
|
|
169
302
|
let totals = nodeTotalTraffic(traffic);
|
|
170
303
|
let sum = totals.sent + totals.received;
|
|
171
304
|
if (sum > 0) {
|
|
172
305
|
let outsidePct = Math.round((totals.outside / sum) * 100);
|
|
173
|
-
|
|
306
|
+
data = `↑${formatNumber(totals.sent)}B/s ↓${formatNumber(totals.received)}B/s ${outsidePct}%⊘`;
|
|
174
307
|
}
|
|
175
308
|
}
|
|
176
|
-
|
|
309
|
+
|
|
310
|
+
let fnParts: string[] = [];
|
|
177
311
|
if (runner) {
|
|
178
|
-
let fnParts: string[] = [];
|
|
179
312
|
if (runner.networks.length) fnParts.push(runner.networks.join(" "));
|
|
180
313
|
for (let shard of runner.shards) {
|
|
181
314
|
fnParts.push(`fn ${shard.shardRange.startFraction.toFixed(3)}-${shard.shardRange.endFraction.toFixed(3)}`);
|
|
182
315
|
}
|
|
183
|
-
fnParts.push(`${formatNumber(traffic?.functionsExecuted ?? 0)} calls`);
|
|
316
|
+
fnParts.push(`${formatNumber(traffic?.functionsExecuted ?? 0)}/s calls`);
|
|
184
317
|
if (!runner.isPublic) fnParts.push("PRIVATE");
|
|
185
|
-
lines.push({ text: fnParts.join(" "), color: FUNCTION_COLOR });
|
|
186
318
|
}
|
|
187
|
-
|
|
188
|
-
let spec = info?.spec;
|
|
319
|
+
|
|
189
320
|
let pvParts: string[] = [];
|
|
321
|
+
let spec = info?.spec;
|
|
190
322
|
if (spec && spec.routeStart >= 0 && spec.routeEnd >= 0) {
|
|
191
323
|
pvParts.push(`pv ${spec.routeStart.toFixed(3)}-${spec.routeEnd.toFixed(3)}`);
|
|
192
324
|
}
|
|
193
325
|
if (traffic && (traffic.pathValuesSent || traffic.pathValuesReceived)) {
|
|
194
|
-
pvParts.push(`↑${formatNumber(traffic.pathValuesSent)} ↓${formatNumber(traffic.pathValuesReceived)} values`);
|
|
195
|
-
}
|
|
196
|
-
if (pvParts.length) {
|
|
197
|
-
lines.push({ text: pvParts.join(" "), color: PATHVALUE_COLOR });
|
|
326
|
+
pvParts.push(`↑${formatNumber(traffic.pathValuesSent)}/s ↓${formatNumber(traffic.pathValuesReceived)}/s values`);
|
|
198
327
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
328
|
+
|
|
329
|
+
let qs = traffic?.querysubCalls ? `${formatNumber(traffic.querysubCalls)}/s addCalls` : "";
|
|
330
|
+
return {
|
|
331
|
+
nodeId,
|
|
332
|
+
machineId,
|
|
333
|
+
ip: machineIp.get(machineId) || "",
|
|
334
|
+
networks: (machineNetworks.get(machineId) || []).join(" | "),
|
|
335
|
+
machineShort: machineId.slice(0, ID_CHARS),
|
|
336
|
+
threadPort: `${thread}:${parts?.port ?? "?"}`,
|
|
337
|
+
name,
|
|
338
|
+
data,
|
|
339
|
+
fn: fnParts.join(" "),
|
|
340
|
+
pv: pvParts.join(" "),
|
|
341
|
+
qs,
|
|
342
|
+
};
|
|
204
343
|
}
|
|
205
344
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
345
|
+
class NodeInfoTable extends qreact.Component<{ rows: NodeRow[]; selectedMachine: string }> {
|
|
346
|
+
render() {
|
|
347
|
+
let rows = this.props.rows;
|
|
348
|
+
let selected = this.props.selectedMachine;
|
|
349
|
+
return <div className={css.vbox(0).fillWidth.overflowAuto.bord2(0, 0, 85)}>
|
|
350
|
+
<div className={css.hbox(0).hsl(0, 0, 96).colorhsl(0, 0, 20).boldStyle}>
|
|
351
|
+
{NODE_TABLE_COLUMNS.map(col =>
|
|
352
|
+
<div className={css.width(col.width).flexShrink0.pad2(6).ellipsis}>{col.label}</div>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
{rows.map((row, i) => {
|
|
356
|
+
let firstOfMachine = i === 0 || rows[i - 1].machineId !== row.machineId;
|
|
357
|
+
let isSelected = row.machineId === selected;
|
|
358
|
+
return <div
|
|
359
|
+
className={css.hbox(0).button.fillWidth.hsl(0, 0, isSelected ? 92 : 99)
|
|
360
|
+
.borderTop(firstOfMachine ? "1px solid hsl(0, 0%, 80%)" : "1px solid hsl(0, 0%, 93%)")}
|
|
361
|
+
onClick={() => selectedMachineParam.value = selected === row.machineId ? "" : row.machineId}
|
|
362
|
+
>
|
|
363
|
+
{NODE_TABLE_COLUMNS.map(col => {
|
|
364
|
+
// Per-machine columns (IP, machine id) only print on the first row of each group, so it reads clearly.
|
|
365
|
+
let text = col.perMachine && !firstOfMachine ? "" : row[col.key];
|
|
366
|
+
return <div
|
|
367
|
+
className={css.width(col.width).flexShrink0.pad2(6).ellipsis.color(col.color || "hsl(0, 0%, 25%)")}
|
|
368
|
+
title={text}
|
|
369
|
+
>{text}</div>;
|
|
370
|
+
})}
|
|
371
|
+
</div>;
|
|
372
|
+
})}
|
|
373
|
+
</div>;
|
|
212
374
|
}
|
|
213
|
-
return { sent, received, outside: traffic.outside.sent + traffic.outside.received };
|
|
214
375
|
}
|
|
215
376
|
|
|
216
377
|
class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: NodeAuthorityInfo[] }> {
|
|
@@ -234,70 +395,136 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
234
395
|
let traffic = synced.getNodeTrafficStats(nodeId);
|
|
235
396
|
if (traffic) trafficMaps.set(nodeId, traffic);
|
|
236
397
|
}
|
|
237
|
-
// Only
|
|
398
|
+
// Only graph nodes that reported their latencies — ones that never respond probably don't exist.
|
|
238
399
|
let respondedSet = new Set(latencyMaps.keys());
|
|
239
400
|
|
|
240
|
-
//
|
|
241
|
-
|
|
401
|
+
// Group every thread by its machine; the graph shows one node per machine that has a responding thread.
|
|
402
|
+
let machineAllThreads = new Map<string, string[]>();
|
|
403
|
+
let machineRespondedThreads = new Map<string, string[]>();
|
|
404
|
+
let pushInto = (map: Map<string, string[]>, key: string, value: string) => {
|
|
405
|
+
let list = map.get(key);
|
|
406
|
+
if (!list) {
|
|
407
|
+
list = [];
|
|
408
|
+
map.set(key, list);
|
|
409
|
+
}
|
|
410
|
+
list.push(value);
|
|
411
|
+
};
|
|
412
|
+
for (let nodeId of nodeIds) {
|
|
413
|
+
let machineId = machineIdOf(nodeId);
|
|
414
|
+
pushInto(machineAllThreads, machineId, nodeId);
|
|
415
|
+
if (respondedSet.has(nodeId)) {
|
|
416
|
+
pushInto(machineRespondedThreads, machineId, nodeId);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// A machine's IP is the most common IP across its threads (they should all match, but resolve robustly).
|
|
421
|
+
let nodeIps = synced.getNodeIps(nodeIds);
|
|
422
|
+
let machineIp = new Map<string, string>();
|
|
423
|
+
for (let [machineId, threads] of machineAllThreads) {
|
|
424
|
+
let counts = new Map<string, number>();
|
|
425
|
+
for (let thread of threads) {
|
|
426
|
+
let ip = nodeIps?.[thread];
|
|
427
|
+
if (!ip) continue;
|
|
428
|
+
counts.set(ip, (counts.get(ip) || 0) + 1);
|
|
429
|
+
}
|
|
430
|
+
let bestIp = "";
|
|
431
|
+
let bestCount = 0;
|
|
432
|
+
for (let [ip, count] of counts) {
|
|
433
|
+
if (count > bestCount) {
|
|
434
|
+
bestCount = count;
|
|
435
|
+
bestIp = ip;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (bestIp) machineIp.set(machineId, bestIp);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// The unique set of function-runner networks running on each machine (usually one, human-readable).
|
|
442
|
+
let machineNetworks = new Map<string, string[]>();
|
|
443
|
+
for (let [machineId, threads] of machineAllThreads) {
|
|
444
|
+
let unique = new Set<string>();
|
|
445
|
+
for (let thread of threads) {
|
|
446
|
+
for (let network of runnerByNode.get(thread)?.networks || []) {
|
|
447
|
+
unique.add(network);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (unique.size) machineNetworks.set(machineId, [...unique]);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Total bytes on each thread-pair link (both directions). Both endpoints report the same total, so take the max.
|
|
242
454
|
let pairKey = (a: string, b: string) => a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
243
455
|
let pairTraffic = new Map<string, number>();
|
|
244
456
|
for (let [reporter, traffic] of trafficMaps) {
|
|
245
|
-
for (let [peer, data] of Object.entries(traffic.perNode)) {
|
|
457
|
+
for (let [peer, data] of Object.entries(traffic.perNode || {})) {
|
|
246
458
|
if (reporter === peer || !respondedSet.has(peer)) continue;
|
|
247
459
|
let key = pairKey(reporter, peer);
|
|
248
|
-
pairTraffic.set(key, Math.max(pairTraffic.get(key) || 0, data
|
|
460
|
+
pairTraffic.set(key, Math.max(pairTraffic.get(key) || 0, (data?.sent || 0) + (data?.received || 0)));
|
|
249
461
|
}
|
|
250
462
|
}
|
|
251
463
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
464
|
+
// Machine-to-machine latency = mean over all cross-machine thread-pair latencies; traffic = summed pair traffic.
|
|
465
|
+
let machinePairLatency = new Map<string, { sum: number; count: number; }>();
|
|
466
|
+
for (let [sourceId, latencies] of latencyMaps) {
|
|
467
|
+
let sourceMachine = machineIdOf(sourceId);
|
|
468
|
+
for (let [destId, latencyMs] of Object.entries(latencies)) {
|
|
469
|
+
if (!respondedSet.has(destId) || !Number.isFinite(latencyMs)) continue;
|
|
470
|
+
let destMachine = machineIdOf(destId);
|
|
471
|
+
if (sourceMachine === destMachine) continue;
|
|
472
|
+
let key = pairKey(sourceMachine, destMachine);
|
|
473
|
+
let agg = machinePairLatency.get(key);
|
|
474
|
+
if (!agg) {
|
|
475
|
+
agg = { sum: 0, count: 0 };
|
|
476
|
+
machinePairLatency.set(key, agg);
|
|
477
|
+
}
|
|
478
|
+
agg.sum += latencyMs;
|
|
479
|
+
agg.count++;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
let machinePairTraffic = new Map<string, number>();
|
|
483
|
+
for (let [key, weight] of pairTraffic) {
|
|
484
|
+
let [a, b] = key.split("|");
|
|
485
|
+
let ma = machineIdOf(a);
|
|
486
|
+
let mb = machineIdOf(b);
|
|
487
|
+
if (ma === mb) continue;
|
|
488
|
+
let mk = pairKey(ma, mb);
|
|
489
|
+
machinePairTraffic.set(mk, (machinePairTraffic.get(mk) || 0) + weight);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let nodes: LatencyGraphNode[] = [...machineRespondedThreads.keys()].map(machineId => {
|
|
493
|
+
let allThreads = machineAllThreads.get(machineId) || [];
|
|
494
|
+
let totals = sumTraffic(allThreads, trafficMaps);
|
|
255
495
|
return {
|
|
256
|
-
id:
|
|
257
|
-
labelLines:
|
|
258
|
-
weight: totals
|
|
496
|
+
id: machineId,
|
|
497
|
+
labelLines: machineLabelLines({ machineId, threads: allThreads, trafficMaps, ip: machineIp.get(machineId), networks: machineNetworks.get(machineId) || [] }),
|
|
498
|
+
weight: totals.dataSent + totals.dataReceived,
|
|
259
499
|
};
|
|
260
500
|
});
|
|
261
501
|
let links: LatencyGraphLink[] = [];
|
|
262
|
-
for (let [
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
links.push({ source: sourceId, destination: destId, latencyMs, weight: pairTraffic.get(pairKey(sourceId, destId)) || 0 });
|
|
266
|
-
}
|
|
502
|
+
for (let [key, agg] of machinePairLatency) {
|
|
503
|
+
let [source, destination] = key.split("|");
|
|
504
|
+
links.push({ source, destination, latencyMs: agg.sum / agg.count, weight: machinePairTraffic.get(key) || 0 });
|
|
267
505
|
}
|
|
268
506
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let dataSent = 0;
|
|
276
|
-
let dataReceived = 0;
|
|
277
|
-
for (let id of memberIds) {
|
|
278
|
-
let traffic = trafficMaps.get(id);
|
|
279
|
-
if (!traffic) continue;
|
|
280
|
-
sent += traffic.pathValuesSent;
|
|
281
|
-
received += traffic.pathValuesReceived;
|
|
282
|
-
calls += traffic.functionsExecuted;
|
|
283
|
-
addCalls += traffic.querysubCalls;
|
|
284
|
-
let totals = nodeTotalTraffic(traffic);
|
|
285
|
-
dataSent += totals.sent;
|
|
286
|
-
dataReceived += totals.received;
|
|
287
|
-
}
|
|
288
|
-
let summary: LatencyGraphLabelLine[] = [];
|
|
289
|
-
summary.push({ text: `↑${formatNumber(dataSent)}B ↓${formatNumber(dataReceived)}B` });
|
|
290
|
-
summary.push({ text: `↑${formatNumber(sent)} ↓${formatNumber(received)} values`, color: PATHVALUE_COLOR });
|
|
291
|
-
summary.push({ text: `${formatNumber(calls)} calls`, color: FUNCTION_COLOR });
|
|
292
|
-
summary.push({ text: `${formatNumber(addCalls)} addCalls`, color: QUERYSUB_COLOR });
|
|
293
|
-
return summary;
|
|
294
|
-
};
|
|
507
|
+
// One row per thread node, grouped so a machine's threads sit together; the selected machine sorts to the top.
|
|
508
|
+
// Single composite key: selected-first, then machine, then thread — the low separator keeps segments ordered.
|
|
509
|
+
let rows = nodeIds.map(nodeId => buildNodeRow({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic: trafficMaps.get(nodeId), machineIp, machineNetworks }));
|
|
510
|
+
let selected = selectedMachineParam.value;
|
|
511
|
+
let sep = String.fromCharCode(1);
|
|
512
|
+
sort(rows, row => `${row.machineId === selected ? 0 : 1}${sep}${row.machineId}${sep}${row.threadPort}`);
|
|
295
513
|
|
|
296
514
|
return <div className={css.vbox(8).fillWidth}>
|
|
297
|
-
<h2 className={css.margin(0)}>Latency Graph ({respondedSet.size}/{nodeIds.length} nodes reported)</h2>
|
|
515
|
+
<h2 className={css.margin(0)}>Latency Graph ({machineRespondedThreads.size} machines · {respondedSet.size}/{nodeIds.length} nodes reported)</h2>
|
|
298
516
|
<div className={css.relative.fillWidth.height(LATENCY_GRAPH_HEIGHT_PX).bord2(0, 0, 85)}>
|
|
299
|
-
<LatencyGraph
|
|
517
|
+
<LatencyGraph
|
|
518
|
+
nodes={nodes}
|
|
519
|
+
links={links}
|
|
520
|
+
formatWeight={weight => formatNumber(weight) + "B/s"}
|
|
521
|
+
selectedId={selected || undefined}
|
|
522
|
+
onSelectNode={id => selectedMachineParam.value = selectedMachineParam.value === id ? "" : id}
|
|
523
|
+
/>
|
|
300
524
|
</div>
|
|
525
|
+
<div className={css.colorhsl(0, 0, 50)}>Click a node to sort its information to the top of the table below.</div>
|
|
526
|
+
<h2 className={css.margin(0)}>Nodes ({rows.length})</h2>
|
|
527
|
+
<NodeInfoTable rows={rows} selectedMachine={selected} />
|
|
301
528
|
</div>;
|
|
302
529
|
}
|
|
303
530
|
}
|