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.
- package/package.json +1 -1
- package/src/-a-archives/archives2.ts +6 -6
- package/src/-f-node-discovery/NodeDiscovery.ts +2 -2
- package/src/0-path-value-core/PathValueController.ts +4 -0
- package/src/0-path-value-core/PathValueStats.ts +83 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +0 -5
- package/src/1-path-client/RemoteWatcher.ts +4 -2
- package/src/deployManager/MachinesPage.tsx +3 -0
- package/src/deployManager/urlParams.ts +1 -1
- package/src/diagnostics/managementPages.tsx +4 -6
- package/src/diagnostics/misc-pages/FunctionRunnersSection.tsx +38 -0
- package/src/diagnostics/misc-pages/LatencyGraphSection.tsx +524 -0
- package/src/diagnostics/misc-pages/PathValueServersSection.tsx +154 -0
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +34 -597
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +1 -2
|
@@ -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 {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
391
|
-
let
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
<
|
|
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
|
}
|