querysub 0.616.0 → 0.619.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.
@@ -14,54 +14,24 @@ 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, timeInMinute } from "socket-function/src/misc";
18
17
  import type { AuthoritySpec } from "../../0-path-value-core/PathRouter";
19
- import { getFunctionRunnerIndex, FunctionRunnerNodeInfo } from "../../4-querysub/FunctionRunnerTracking";
20
- import { formatTime, formatNumber } from "socket-function/src/formatting/format";
21
- import { LatencyGraph, LatencyGraphNode, LatencyGraphLink, LatencyGraphLabelLine } from "../../library-components/LatencyGraph";
22
- import { URLParam } from "../../library-components/URLParam";
23
- import { mainResets } from "../../library-components/urlResetGroups";
24
- import { FUNCTION_RUNNER_COLOR, PATHVALUE_COLOR } from "../../misc/nodeCategoryColors";
18
+ import { PathValueStatsController, PathValueTotals, PathValueDirection, PathValueSummaryState } from "../../0-path-value-core/PathValueStats";
19
+ import type { SummaryEntry } from "sliftutils/treeSummary";
20
+ import { LatencyGraphSection } from "./LatencyGraphSection";
21
+ import { PathValueServersSection } from "./PathValueServersSection";
22
+ import { FunctionRunnersSection } from "./FunctionRunnersSection";
25
23
 
26
- const ID_CHARS = 8;
24
+ // The page is split into independent section components (latency graph, path-value servers, function runners), each in
25
+ // its own file. This module keeps only what those sections share: the proxy controller (+ its synced wrapper), the
26
+ // node-id helpers, and the small AuthorityRangeBar. The sections import those back from here.
27
+
28
+ export const ID_CHARS = 8;
27
29
 
28
30
  const PROBE_TIMEOUT_MS = 5000;
29
31
  const RANGE_BAR_WIDTH_PX = 360;
30
32
  const RANGE_BAR_HEIGHT_PX = 14;
31
- const LATENCY_GRAPH_HEIGHT_PX = 720;
32
- const MACHINE_LATENCY_WIDTH_PX = 235;
33
-
34
- // Clicking a machine node in the graph (or a row) sorts that machine's nodes to the top of the table.
35
- const selectedMachineParam = new URLParam("rtSelectedMachine", "", { reset: [mainResets] });
36
- const trafficWindowParam = new URLParam<number>("rtTrafficWindow", timeInMinute * 5, { reset: [mainResets] });
37
33
 
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: FUNCTION_RUNNER_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: 380, color: FUNCTION_RUNNER_COLOR },
48
- { key: "pv", label: "Path Values", width: 250, color: PATHVALUE_COLOR },
49
- ];
50
-
51
- type NodeRow = {
52
- nodeId: string;
53
- machineId: string;
54
- ip: string;
55
- networks: string;
56
- machineShort: string;
57
- threadPort: string;
58
- name: string;
59
- data: string;
60
- fn: string;
61
- pv: string;
62
- };
63
-
64
- type NodeAuthorityInfo = {
34
+ export type NodeAuthorityInfo = {
65
35
  nodeId: string;
66
36
  entryPoint?: string;
67
37
  spec?: AuthoritySpec;
@@ -109,6 +79,17 @@ class RoutingTablePageControllerBase {
109
79
  public async getNodeTrafficRates(nodeId: string, windowSize: number): Promise<TrafficStats | undefined> {
110
80
  return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, TrafficController.nodes[nodeId].getTrafficStats(windowSize));
111
81
  }
82
+ // Cheap per-node call: just the running sent/received path-value totals, so every server row can render without
83
+ // pulling any path breakdown. The heavy per-path tree only loads when a row is expanded (getNodePathSummary).
84
+ public async getNodePathTotals(nodeId: string): Promise<PathValueTotals | undefined> {
85
+ return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, PathValueStatsController.nodes[nodeId].getTotals());
86
+ }
87
+ public async getNodePathSummary(nodeId: string, direction: PathValueDirection, maxCount: number): Promise<SummaryEntry<PathValueSummaryState>[] | undefined> {
88
+ return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, PathValueStatsController.nodes[nodeId].getSummary(direction, maxCount));
89
+ }
90
+ public async clearNodePathStats(nodeId: string): Promise<void> {
91
+ await timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, PathValueStatsController.nodes[nodeId].clear());
92
+ }
112
93
  }
113
94
 
114
95
  export const RoutingTablePageController = SocketFunction.register(
@@ -119,575 +100,36 @@ export const RoutingTablePageController = SocketFunction.register(
119
100
  getNodeLatencies: {},
120
101
  getNodeTrafficRates: {},
121
102
  getNodeIps: {},
103
+ getNodePathTotals: {},
104
+ getNodePathSummary: {},
105
+ clearNodePathStats: {},
122
106
  }),
123
107
  () => ({
124
108
  hooks: [assertIsManagementUser],
125
109
  }),
126
- {
127
- noAutoExpose: true,
128
- }
129
110
  );
130
111
 
131
- const RoutingTableSynced = getSyncedController(RoutingTablePageController, {
112
+ export const RoutingTableSynced = getSyncedController(RoutingTablePageController, {
132
113
  reads: { getAllNodeAuthoritySpecs: ["nodeAuthoritySpecs"] },
133
114
  writes: {},
134
115
  });
135
116
 
136
- class AuthorityRangeBar extends qreact.Component<{ start: number; end: number }> {
137
- render() {
138
- let startPct = this.props.start * 100;
139
- let widthPct = (this.props.end - this.props.start) * 100;
140
- return <div className={css.relative.size(RANGE_BAR_WIDTH_PX, RANGE_BAR_HEIGHT_PX).flexShrink0.hsl(0, 0, 92).bord2(0, 0, 75)}>
141
- <div className={css.absolute.top(0).left(`${startPct}%`).size(`${widthPct}%`, "100%").hsl(210, 65, 50)} />
142
- </div>;
143
- }
144
- }
145
-
146
- class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo }> {
147
- state = {
148
- expanded: false,
149
- };
150
- render() {
151
- let info = this.props.info;
152
- let spec = info.spec!;
153
- let expanded = this.state.expanded;
154
- return <div
155
- className={css.button.vbox(6).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}
156
- onClick={() => this.state.expanded = !expanded}
157
- >
158
- <div className={css.hbox(10).fillWidth}>
159
- <span>{expanded ? "▼" : "▶"}</span>
160
- <span className={css.boldStyle}>{info.nodeId}</span>
161
- <span>{spec.routeStart.toFixed(4)} - {spec.routeEnd.toFixed(4)}</span>
162
- <AuthorityRangeBar start={spec.routeStart} end={spec.routeEnd} />
163
- <span className={css.colorhsl(0, 0, 50)}>width {(spec.routeEnd - spec.routeStart).toFixed(4)}</span>
164
- {spec.excludeDefault && <span className={css.colorhsl(0, 70, 35)}>(excludes default)</span>}
165
- <span className={css.colorhsl(0, 0, 40).ellipsis.flexFillWidth}>{info.entryPoint || "(no entry point)"}</span>
166
- </div>
167
- {expanded &&
168
- <div className={css.vbox(2)}>
169
- <div className={css.boldStyle}>Prefixes ({spec.prefixes.length}):</div>
170
- {spec.prefixes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(none)</div>}
171
- {spec.prefixes.map(p =>
172
- <div key={p.originalPrefix} className={css.colorhsl(0, 0, 25)}>{p.originalPrefix}</div>
173
- )}
174
- </div>
175
- }
176
- </div>;
177
- }
178
- }
179
-
180
- class FunctionRunnersSection extends qreact.Component {
181
- render() {
182
- let index = getFunctionRunnerIndex();
183
- let nodes = index?.nodes || [];
184
- return <div className={css.vbox(8).fillWidth}>
185
- <h2>Function Runners ({nodes.length})</h2>
186
- {nodes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(no function runners found yet)</div>}
187
- {nodes.map(node =>
188
- <div className={css.vbox(4).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}>
189
- <div className={css.hbox(10).fillWidth}>
190
- <span className={css.boldStyle}>{node.nodeId}</span>
191
- <span className={css.color(FUNCTION_RUNNER_COLOR)}>networks: {node.networks.join(", ")}</span>
192
- {!node.isPublic && <span className={css.colorhsl(0, 70, 35)}>(non-public)</span>}
193
- <span>latency {formatTime(node.averageLatency)}</span>
194
- <span>up for {formatTime(Date.now() - node.startupTime)}</span>
195
- <span className={css.colorhsl(0, 0, 40).ellipsis}>{node.entryPoint}</span>
196
- </div>
197
- {node.shards.map(shard =>
198
- <div className={css.hbox(10).fillWidth}>
199
- <span>{shard.shardRange.startFraction.toFixed(4)} - {shard.shardRange.endFraction.toFixed(4)}</span>
200
- <AuthorityRangeBar start={shard.shardRange.startFraction} end={shard.shardRange.endFraction} />
201
- {shard.secondaryShardRange && <span className={css.colorhsl(0, 0, 50)}>secondary {shard.secondaryShardRange.startFraction.toFixed(4)} - {shard.secondaryShardRange.endFraction.toFixed(4)}</span>}
202
- </div>
203
- )}
204
- </div>
205
- )}
206
- </div>;
207
- }
208
- }
209
-
210
- function nodeTotalTraffic(traffic: TrafficStats): { sent: number; received: number; outside: number; } {
211
- let outsideSent = traffic.outside?.sent || 0;
212
- let outsideReceived = traffic.outside?.received || 0;
213
- let sent = outsideSent;
214
- let received = outsideReceived;
215
- for (let d of Object.values(traffic.perNode || {})) {
216
- sent += d?.sent || 0;
217
- received += d?.received || 0;
218
- }
219
- return { sent, received, outside: outsideSent + outsideReceived };
220
- }
221
-
222
- function machineIdOf(nodeId: string): string {
117
+ export function machineIdOf(nodeId: string): string {
223
118
  let parts = decodeNodeId(nodeId, getDomain(), "allowMissingThreadId");
224
119
  return parts?.machineId || nodeId;
225
120
  }
226
121
 
227
- function threadLabel(nodeId: string): string {
122
+ export function threadLabel(nodeId: string): string {
228
123
  let parts = decodeNodeId(nodeId, getDomain(), "allowMissingThreadId");
229
124
  return `${(parts?.threadId || "?").slice(0, ID_CHARS)}:${parts?.port ?? "?"}`;
230
125
  }
231
126
 
232
- // min · median · max of one source node's latencies to a machine's threads (plus a tooltip naming the farthest
233
- // thread), and the node's total path values sent/received to that machine (summed — the per-thread split isn't interesting).
234
- function machineLatencyCell(config: {
235
- sourceLatencies: { [nodeId: string]: number } | undefined;
236
- threads: string[];
237
- traffic: TrafficStats | undefined;
238
- }): { text: string; tooltip: string } {
239
- let { sourceLatencies, threads, traffic } = config;
240
- if (!sourceLatencies) return { text: "", tooltip: "" };
241
- let entries: { thread: string; ms: number }[] = [];
242
- for (let thread of threads) {
243
- let ms = sourceLatencies[thread];
244
- if (Number.isFinite(ms)) entries.push({ thread, ms });
245
- }
246
- if (!entries.length) return { text: "", tooltip: "" };
247
- sort(entries, entry => entry.ms);
248
- let farthest = entries[entries.length - 1];
249
- let text = `${formatTime(entries[0].ms)} · ${formatTime(entries[Math.floor(entries.length / 2)].ms)} · ${formatTime(farthest.ms)}`;
250
- let pvSent = 0;
251
- let pvReceived = 0;
252
- for (let thread of threads) {
253
- let data = traffic?.perNode?.[thread];
254
- if (!data) continue;
255
- pvSent += data.pathValuesSent || 0;
256
- pvReceived += data.pathValuesReceived || 0;
257
- }
258
- if (pvSent || pvReceived) {
259
- text += ` ↑${formatNumber(pvSent)}/s ↓${formatNumber(pvReceived)}/s values`;
260
- }
261
- return { text, tooltip: `${text}\nfarthest: ${threadLabel(farthest.thread)} (${formatTime(farthest.ms)})` };
262
- }
263
-
264
- // Summed traffic metrics across a set of nodes (a machine's threads).
265
- function sumTraffic(threads: string[], trafficMaps: Map<string, TrafficStats>) {
266
- let dataSent = 0;
267
- let dataReceived = 0;
268
- let valuesSent = 0;
269
- let valuesReceived = 0;
270
- let calls = 0;
271
- let addCalls = 0;
272
- for (let id of threads) {
273
- let traffic = trafficMaps.get(id);
274
- if (!traffic) continue;
275
- let totals = nodeTotalTraffic(traffic);
276
- dataSent += totals.sent;
277
- dataReceived += totals.received;
278
- valuesSent += traffic.pathValuesSent || 0;
279
- valuesReceived += traffic.pathValuesReceived || 0;
280
- calls += traffic.functionsExecuted || 0;
281
- addCalls += traffic.querysubCalls || 0;
282
- }
283
- return { dataSent, dataReceived, valuesSent, valuesReceived, calls, addCalls };
284
- }
285
-
286
- // A machine's graph label: the IP first (most important), then its function-runner networks, then the same per-piece
287
- // info as the table, summed across all its threads and colored the same.
288
- function machineLabelLines(config: {
289
- machineId: string;
290
- threads: string[];
291
- trafficMaps: Map<string, TrafficStats>;
292
- ip: string | undefined;
293
- networks: string[];
294
- }): LatencyGraphLabelLine[] {
295
- let { machineId, threads, ip, networks } = config;
296
- let totals = sumTraffic(threads, config.trafficMaps);
297
- let lines: LatencyGraphLabelLine[] = [];
298
- if (ip) {
299
- lines.push({ text: ip });
300
- }
301
- if (networks.length) {
302
- lines.push({ text: networks.join(" | "), color: FUNCTION_RUNNER_COLOR });
303
- }
304
- lines.push({ text: `${machineId.slice(0, ID_CHARS)} ${threads.length} threads` });
305
- if (totals.dataSent + totals.dataReceived > 0) {
306
- lines.push({ text: `↑${formatNumber(totals.dataSent)}B/s ↓${formatNumber(totals.dataReceived)}B/s` });
307
- }
308
- if (totals.valuesSent || totals.valuesReceived) {
309
- lines.push({ text: `↑${formatNumber(totals.valuesSent)}/s ↓${formatNumber(totals.valuesReceived)}/s values`, color: PATHVALUE_COLOR });
310
- }
311
- if (totals.calls) {
312
- lines.push({ text: `${formatNumber(totals.calls)}/s calls`, color: FUNCTION_RUNNER_COLOR });
313
- }
314
- if (totals.addCalls) {
315
- lines.push({ text: `${formatNumber(totals.addCalls)}/s querysub addCalls`, color: FUNCTION_RUNNER_COLOR });
316
- }
317
- return lines;
318
- }
319
-
320
- // One table row per thread node: each piece of info that used to live on the graph label becomes its own column.
321
- function buildNodeRow(config: {
322
- nodeId: string;
323
- runner: FunctionRunnerNodeInfo | undefined;
324
- info: NodeAuthorityInfo | undefined;
325
- traffic: TrafficStats | undefined;
326
- machineIp: Map<string, string>;
327
- machineNetworks: Map<string, string[]>;
328
- }): NodeRow {
329
- let { nodeId, runner, info, traffic, machineIp, machineNetworks } = config;
330
- let parts = decodeNodeId(nodeId, getDomain(), "allowMissingThreadId");
331
- let thread = (parts?.threadId || "?").slice(0, ID_CHARS);
332
- let machineId = parts?.machineId || nodeId;
333
- let entryPoint = info?.entryPoint || runner?.entryPoint;
334
- let name = entryPoint && entryPoint.split(/[\\/]/).filter(Boolean).at(-1) || "";
335
-
336
- let data = "";
337
- if (traffic) {
338
- let totals = nodeTotalTraffic(traffic);
339
- let sum = totals.sent + totals.received;
340
- if (sum > 0) {
341
- let outsidePct = Math.round((totals.outside / sum) * 100);
342
- data = `↑${formatNumber(totals.sent)}B/s ↓${formatNumber(totals.received)}B/s ${outsidePct}%⊘`;
343
- }
344
- }
345
-
346
- let fnParts: string[] = [];
347
- if (runner) {
348
- if (runner.networks.length) fnParts.push(runner.networks.join(" "));
349
- for (let shard of runner.shards) {
350
- fnParts.push(`${shard.shardRange.startFraction.toFixed(3)}-${shard.shardRange.endFraction.toFixed(3)}`);
351
- }
352
- fnParts.push(`${formatNumber(traffic?.functionsExecuted ?? 0)}/s calls`);
353
- if (!runner.isPublic) fnParts.push("PRIVATE");
354
- }
355
- // A slightly different source than function calls, but still function calls, so it lives in the function column
356
- if (traffic?.querysubCalls) {
357
- fnParts.push(`${formatNumber(traffic.querysubCalls)}/s querysub addCalls`);
358
- }
359
-
360
- let pvParts: string[] = [];
361
- let spec = info?.spec;
362
- if (spec && spec.routeStart >= 0 && spec.routeEnd >= 0) {
363
- pvParts.push(`${spec.routeStart.toFixed(3)}-${spec.routeEnd.toFixed(3)}`);
364
- }
365
- if (traffic && (traffic.pathValuesSent || traffic.pathValuesReceived)) {
366
- pvParts.push(`↑${formatNumber(traffic.pathValuesSent)}/s ↓${formatNumber(traffic.pathValuesReceived)}/s values`);
367
- }
368
-
369
- return {
370
- nodeId,
371
- machineId,
372
- ip: machineIp.get(machineId) || "",
373
- networks: (machineNetworks.get(machineId) || []).join(" | "),
374
- machineShort: machineId.slice(0, ID_CHARS),
375
- threadPort: `${thread}:${parts?.port ?? "?"}`,
376
- name,
377
- data,
378
- fn: fnParts.join(" "),
379
- pv: pvParts.join(" "),
380
- };
381
- }
382
-
383
- class NodeInfoTable extends qreact.Component<{
384
- rows: NodeRow[];
385
- selectedMachine: string;
386
- machineColumns: { id: string; label: string }[];
387
- nodeMachineLatency: Map<string, { [machineId: string]: { text: string; tooltip: string } }>;
388
- }> {
127
+ export class AuthorityRangeBar extends qreact.Component<{ start: number; end: number }> {
389
128
  render() {
390
- let rows = this.props.rows;
391
- let selected = this.props.selectedMachine;
392
- let machineColumns = this.props.machineColumns;
393
- let nodeMachineLatency = this.props.nodeMachineLatency;
394
- return <div className={css.vbox(0).fillWidth.overflowAuto.bord2(0, 0, 85)}>
395
- <div className={css.hbox(0).hsl(0, 0, 96).colorhsl(0, 0, 20).boldStyle}>
396
- {NODE_TABLE_COLUMNS.map(col =>
397
- <div className={css.width(col.width).flexShrink0.pad2(6).ellipsis}>{col.label}</div>
398
- )}
399
- {machineColumns.map(col =>
400
- <div className={css.width(MACHINE_LATENCY_WIDTH_PX).flexShrink0.pad2(6).ellipsis} title={`latency and path values to ${col.label}`}>→ {col.label}</div>
401
- )}
402
- </div>
403
- {rows.map((row, i) => {
404
- let firstOfMachine = i === 0 || rows[i - 1].machineId !== row.machineId;
405
- let isSelected = row.machineId === selected;
406
- let latencies = nodeMachineLatency.get(row.nodeId) || {};
407
- return <div
408
- className={css.hbox(0).button.fillWidth.hsl(0, 0, isSelected ? 92 : 99)
409
- .borderTop(firstOfMachine ? "1px solid hsl(0, 0%, 80%)" : "1px solid hsl(0, 0%, 93%)")}
410
- onClick={() => selectedMachineParam.value = selected === row.machineId ? "" : row.machineId}
411
- >
412
- {NODE_TABLE_COLUMNS.map(col => {
413
- // Per-machine columns (IP, machine id) only print on the first row of each group, so it reads clearly.
414
- let text = col.perMachine && !firstOfMachine ? "" : row[col.key];
415
- return <div
416
- className={css.width(col.width).flexShrink0.pad2(6).ellipsis.color(col.color || "hsl(0, 0%, 25%)")}
417
- title={text}
418
- >{text}</div>;
419
- })}
420
- {machineColumns.map(col => {
421
- let cell = latencies[col.id];
422
- return <div
423
- className={css.width(MACHINE_LATENCY_WIDTH_PX).flexShrink0.pad2(6).ellipsis.colorhsl(0, 0, 25)}
424
- title={cell?.tooltip || ""}
425
- >{cell?.text || ""}</div>;
426
- })}
427
- </div>;
428
- })}
429
- </div>;
430
- }
431
- }
432
-
433
- // Nodes that never reported their latencies — all we have is their nodeId (and possibly a DNS-resolved machine IP), so this is a much narrower table than NodeInfoTable, still grouped by machine.
434
- class UnresponsiveNodesTable extends qreact.Component<{ nodeIds: string[]; machineIp: Map<string, string> }> {
435
- render() {
436
- let rows = this.props.nodeIds.map(nodeId => ({ nodeId, machineId: machineIdOf(nodeId) }));
437
- let sep = String.fromCharCode(1);
438
- sort(rows, row => `${row.machineId}${sep}${threadLabel(row.nodeId)}`);
439
- return <div className={css.vbox(0).fillWidth.overflowAuto.bord2(0, 0, 85)}>
440
- <div className={css.hbox(0).hsl(0, 0, 96).colorhsl(0, 0, 20).boldStyle}>
441
- <div className={css.width(130).flexShrink0.pad2(6).ellipsis}>IP</div>
442
- <div className={css.width(96).flexShrink0.pad2(6).ellipsis}>Machine</div>
443
- <div className={css.width(130).flexShrink0.pad2(6).ellipsis}>Thread:Port</div>
444
- <div className={css.width(520).flexShrink0.pad2(6).ellipsis}>Node Id</div>
445
- </div>
446
- {rows.map((row, i) => {
447
- let firstOfMachine = i === 0 || rows[i - 1].machineId !== row.machineId;
448
- return <div
449
- className={css.hbox(0).fillWidth.hsl(0, 0, 99)
450
- .borderTop(firstOfMachine ? "1px solid hsl(0, 0%, 80%)" : "1px solid hsl(0, 0%, 93%)")}
451
- >
452
- <div className={css.width(130).flexShrink0.pad2(6).ellipsis}>{firstOfMachine && this.props.machineIp.get(row.machineId) || ""}</div>
453
- <div className={css.width(96).flexShrink0.pad2(6).ellipsis}>{firstOfMachine && row.machineId.slice(0, ID_CHARS) || ""}</div>
454
- <div className={css.width(130).flexShrink0.pad2(6).ellipsis}>{threadLabel(row.nodeId)}</div>
455
- <div className={css.width(520).flexShrink0.pad2(6).ellipsis.colorhsl(0, 0, 45)} title={row.nodeId}>{row.nodeId}</div>
456
- </div>;
457
- })}
458
- </div>;
459
- }
460
- }
461
-
462
- class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: NodeAuthorityInfo[] }> {
463
- render() {
464
- let nodeIds = this.props.nodeIds;
465
- let synced = RoutingTableSynced(getBrowserUrlNode());
466
-
467
- let index = getFunctionRunnerIndex();
468
- let runnerByNode = new Map<string, FunctionRunnerNodeInfo>();
469
- for (let node of index?.nodes ?? []) {
470
- runnerByNode.set(node.nodeId, node);
471
- }
472
- let infoByNode = new Map(this.props.infos.map(info => [info.nodeId, info]));
473
-
474
- // One synced call per node for each data source; each resolves independently so the graph fills in progressively.
475
- let trafficWindow = trafficWindowParam.value;
476
- let latencyMaps = new Map<string, { [nodeId: string]: number }>();
477
- let trafficMaps = new Map<string, TrafficStats>();
478
- for (let nodeId of nodeIds) {
479
- let latencies = synced.getNodeLatencies(nodeId);
480
- if (latencies) latencyMaps.set(nodeId, latencies);
481
- let traffic = synced.getNodeTrafficRates(nodeId, trafficWindow);
482
- if (traffic) trafficMaps.set(nodeId, traffic);
483
- }
484
- // Only graph nodes that reported their latencies — ones that never respond probably don't exist.
485
- let respondedSet = new Set(latencyMaps.keys());
486
-
487
- // Group every thread by its machine; the graph shows one node per machine that has a responding thread.
488
- let machineAllThreads = new Map<string, string[]>();
489
- let machineRespondedThreads = new Map<string, string[]>();
490
- let pushInto = (map: Map<string, string[]>, key: string, value: string) => {
491
- let list = map.get(key);
492
- if (!list) {
493
- list = [];
494
- map.set(key, list);
495
- }
496
- list.push(value);
497
- };
498
- for (let nodeId of nodeIds) {
499
- let machineId = machineIdOf(nodeId);
500
- pushInto(machineAllThreads, machineId, nodeId);
501
- if (respondedSet.has(nodeId)) {
502
- pushInto(machineRespondedThreads, machineId, nodeId);
503
- }
504
- }
505
-
506
- // A machine's IP is the most common IP across its threads (they should all match, but resolve robustly).
507
- let nodeIps = synced.getNodeIps(nodeIds);
508
- let machineIp = new Map<string, string>();
509
- for (let [machineId, threads] of machineAllThreads) {
510
- let counts = new Map<string, number>();
511
- for (let thread of threads) {
512
- let ip = nodeIps?.[thread];
513
- if (!ip) continue;
514
- counts.set(ip, (counts.get(ip) || 0) + 1);
515
- }
516
- let bestIp = "";
517
- let bestCount = 0;
518
- for (let [ip, count] of counts) {
519
- if (count > bestCount) {
520
- bestCount = count;
521
- bestIp = ip;
522
- }
523
- }
524
- if (bestIp) machineIp.set(machineId, bestIp);
525
- }
526
-
527
- // The unique set of function-runner networks running on each machine (usually one, human-readable).
528
- let machineNetworks = new Map<string, string[]>();
529
- for (let [machineId, threads] of machineAllThreads) {
530
- let unique = new Set<string>();
531
- for (let thread of threads) {
532
- for (let network of runnerByNode.get(thread)?.networks || []) {
533
- unique.add(network);
534
- }
535
- }
536
- if (unique.size) machineNetworks.set(machineId, [...unique]);
537
- }
538
-
539
- // One latency column per machine: each row (node) gets its own min · median · max latency to that machine's
540
- // threads (a machine has several threads, so a cell is a range, not one number).
541
- let machineColumns = [...machineAllThreads.keys()].map(id => {
542
- let networks = machineNetworks.get(id) || [];
543
- let label = networks.length ? `${id.slice(0, ID_CHARS)} (${networks.join(" | ")})` : id.slice(0, ID_CHARS);
544
- return { id, label, threads: machineAllThreads.get(id) || [] };
545
- });
546
- sort(machineColumns, column => column.id);
547
- let nodeMachineLatency = new Map<string, { [machineId: string]: { text: string; tooltip: string } }>();
548
- for (let nodeId of nodeIds) {
549
- let sourceLatencies = latencyMaps.get(nodeId);
550
- let traffic = trafficMaps.get(nodeId);
551
- let byMachine: { [machineId: string]: { text: string; tooltip: string } } = {};
552
- for (let column of machineColumns) {
553
- let cell = machineLatencyCell({ sourceLatencies, threads: column.threads, traffic });
554
- if (cell.text) byMachine[column.id] = cell;
555
- }
556
- nodeMachineLatency.set(nodeId, byMachine);
557
- }
558
-
559
- // Total bytes on each thread-pair link (both directions). Both endpoints report the same total, so take the max.
560
- let pairKey = (a: string, b: string) => a < b ? `${a}|${b}` : `${b}|${a}`;
561
- let pairTraffic = new Map<string, number>();
562
- // Path values per thread pair, split by direction (fwd = lower id → higher id). Both endpoints report each
563
- // direction (one as sent, one as received), so take the max of the two estimates.
564
- let pairPv = new Map<string, { fwd: number; back: number }>();
565
- for (let [reporter, traffic] of trafficMaps) {
566
- for (let [peer, data] of Object.entries(traffic.perNode || {})) {
567
- if (reporter === peer || !respondedSet.has(peer)) continue;
568
- let key = pairKey(reporter, peer);
569
- pairTraffic.set(key, Math.max(pairTraffic.get(key) || 0, (data?.sent || 0) + (data?.received || 0)));
570
- let pv = pairPv.get(key);
571
- if (!pv) {
572
- pv = { fwd: 0, back: 0 };
573
- pairPv.set(key, pv);
574
- }
575
- let sentRate = data?.pathValuesSent || 0;
576
- let receivedRate = data?.pathValuesReceived || 0;
577
- if (reporter < peer) {
578
- pv.fwd = Math.max(pv.fwd, sentRate);
579
- pv.back = Math.max(pv.back, receivedRate);
580
- } else {
581
- pv.fwd = Math.max(pv.fwd, receivedRate);
582
- pv.back = Math.max(pv.back, sentRate);
583
- }
584
- }
585
- }
586
-
587
- // Machine-to-machine latency = minimum over all cross-machine thread-pair latencies; traffic = summed pair traffic.
588
- let machinePairLatency = new Map<string, number>();
589
- for (let [sourceId, latencies] of latencyMaps) {
590
- let sourceMachine = machineIdOf(sourceId);
591
- for (let [destId, latencyMs] of Object.entries(latencies)) {
592
- if (!respondedSet.has(destId) || !Number.isFinite(latencyMs)) continue;
593
- let destMachine = machineIdOf(destId);
594
- if (sourceMachine === destMachine) continue;
595
- let key = pairKey(sourceMachine, destMachine);
596
- let existing = machinePairLatency.get(key);
597
- machinePairLatency.set(key, existing === undefined ? latencyMs : Math.min(existing, latencyMs));
598
- }
599
- }
600
- let machinePairTraffic = new Map<string, number>();
601
- for (let [key, weight] of pairTraffic) {
602
- let [a, b] = key.split("|");
603
- let ma = machineIdOf(a);
604
- let mb = machineIdOf(b);
605
- if (ma === mb) continue;
606
- let mk = pairKey(ma, mb);
607
- machinePairTraffic.set(mk, (machinePairTraffic.get(mk) || 0) + weight);
608
- }
609
- // Sum thread-pair path value flows up to machine pairs, keeping direction (fwd = lower machine id → higher).
610
- let machinePairPv = new Map<string, { fwd: number; back: number }>();
611
- for (let [key, pv] of pairPv) {
612
- let [a, b] = key.split("|");
613
- let ma = machineIdOf(a);
614
- let mb = machineIdOf(b);
615
- if (ma === mb) continue;
616
- let mk = pairKey(ma, mb);
617
- let entry = machinePairPv.get(mk);
618
- if (!entry) {
619
- entry = { fwd: 0, back: 0 };
620
- machinePairPv.set(mk, entry);
621
- }
622
- // The thread pair's fwd direction may be flipped relative to the machine pair's ordering.
623
- if (ma < mb) {
624
- entry.fwd += pv.fwd;
625
- entry.back += pv.back;
626
- } else {
627
- entry.fwd += pv.back;
628
- entry.back += pv.fwd;
629
- }
630
- }
631
-
632
- let nodes: LatencyGraphNode[] = [...machineRespondedThreads.keys()].map(machineId => {
633
- let allThreads = machineAllThreads.get(machineId) || [];
634
- let totals = sumTraffic(allThreads, trafficMaps);
635
- return {
636
- id: machineId,
637
- labelLines: machineLabelLines({ machineId, threads: allThreads, trafficMaps, ip: machineIp.get(machineId), networks: machineNetworks.get(machineId) || [] }),
638
- weight: totals.valuesSent + totals.valuesReceived,
639
- };
640
- });
641
- let links: LatencyGraphLink[] = [];
642
- for (let [key, latencyMs] of machinePairLatency) {
643
- let [source, destination] = key.split("|");
644
- let pv = machinePairPv.get(key);
645
- let extraLabel: LatencyGraphLabelLine | undefined;
646
- if (pv && (pv.fwd || pv.back)) {
647
- extraLabel = { text: `↑${formatNumber(pv.fwd)}/s ↓${formatNumber(pv.back)}/s values`, color: PATHVALUE_COLOR };
648
- }
649
- links.push({ source, destination, latencyMs, weight: machinePairTraffic.get(key) || 0, valueWeight: pv && pv.fwd + pv.back || 0, extraLabel });
650
- }
651
-
652
- // One row per thread node, grouped so a machine's threads sit together; the selected machine sorts to the top.
653
- // Single composite key: selected-first, then machine, then thread — the low separator keeps segments ordered.
654
- let rows = nodeIds.filter(nodeId => respondedSet.has(nodeId)).map(nodeId => buildNodeRow({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic: trafficMaps.get(nodeId), machineIp, machineNetworks }));
655
- let selected = selectedMachineParam.value;
656
- let sep = String.fromCharCode(1);
657
- sort(rows, row => `${row.machineId === selected ? 0 : 1}${sep}${row.machineId}${sep}${row.threadPort}`);
658
-
659
- let unresponsiveNodeIds = nodeIds.filter(nodeId => !respondedSet.has(nodeId));
660
-
661
- return <div className={css.vbox(8).fillWidth}>
662
- <div className={css.hbox(14)}>
663
- <h2 className={css.margin(0)}>Latency Graph ({machineRespondedThreads.size} machines · {respondedSet.size}/{nodeIds.length} nodes reported)</h2>
664
- <div className={css.hbox(0).bord2(0, 0, 70)}>
665
- {([timeInMinute * 5, timeInMinute * 60]).map(windowSize => {
666
- let selected = trafficWindow === windowSize;
667
- return <div
668
- className={css.pad2(12, 6).cursor("pointer")
669
- .hsl(210, selected ? 70 : 0, selected ? 45 : 96).colorhsl(0, 0, selected ? 100 : 30)}
670
- onMouseDown={() => trafficWindowParam.value = windowSize}
671
- >{formatTime(windowSize)}</div>;
672
- })}
673
- </div>
674
- </div>
675
- <div className={css.relative.fillWidth.height(LATENCY_GRAPH_HEIGHT_PX).bord2(0, 0, 85)}>
676
- <LatencyGraph
677
- nodes={nodes}
678
- links={links}
679
- formatWeight={weight => formatNumber(weight) + "B/s"}
680
- selectedId={selected || undefined}
681
- onSelectNode={id => selectedMachineParam.value = selectedMachineParam.value === id ? "" : id}
682
- />
683
- </div>
684
- <div className={css.colorhsl(0, 0, 50)}>Click a node to sort its information to the top of the table below.</div>
685
- <h2 className={css.margin(0)}>Nodes ({rows.length})</h2>
686
- <NodeInfoTable rows={rows} selectedMachine={selected} machineColumns={machineColumns} nodeMachineLatency={nodeMachineLatency} />
687
- {unresponsiveNodeIds.length > 0 && <>
688
- <h2 className={css.margin(0)}>Unresponsive Nodes ({unresponsiveNodeIds.length})</h2>
689
- <UnresponsiveNodesTable nodeIds={unresponsiveNodeIds} machineIp={machineIp} />
690
- </>}
129
+ let startPct = this.props.start * 100;
130
+ let widthPct = (this.props.end - this.props.start) * 100;
131
+ return <div className={css.relative.size(RANGE_BAR_WIDTH_PX, RANGE_BAR_HEIGHT_PX).flexShrink0.hsl(0, 0, 92).bord2(0, 0, 75)}>
132
+ <div className={css.absolute.top(0).left(`${startPct}%`).size(`${widthPct}%`, "100%").hsl(210, 65, 50)} />
691
133
  </div>;
692
134
  }
693
135
  }
@@ -699,15 +141,10 @@ export class RoutingTablePage extends qreact.Component {
699
141
  return <div className={css.pad2(16)}>Loading routing table...</div>;
700
142
  }
701
143
  let allNodeIds = infos.map(x => x.nodeId);
702
- let routableInfos = infos.filter(x => x.spec && x.spec.routeStart >= 0 && x.spec.routeEnd >= 0);
703
- sort(routableInfos, x => x.spec!.routeStart);
704
144
 
705
145
  return <div className={css.vbox(12).pad2(16).fillWidth}>
706
146
  <LatencyGraphSection nodeIds={allNodeIds} infos={infos} />
707
- <h2>Routing Table ({routableInfos.length})</h2>
708
- <div className={css.vbox(8).fillWidth}>
709
- {routableInfos.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
710
- </div>
147
+ <PathValueServersSection infos={infos} />
711
148
  <FunctionRunnersSection />
712
149
  </div>;
713
150
  }