Remove ELK and baseline Rust solver performance
This commit is contained in:
parent
5b2bccf417
commit
1b3a07f09a
17 changed files with 6219 additions and 886 deletions
12
README.md
12
README.md
|
|
@ -6,10 +6,10 @@ The live package is `web/epiphany-graph-viewer`. It is what clients and agents
|
|||
should reach for when they want to render `architecture`, `dataflow`, and typed
|
||||
cross-links in a UI.
|
||||
|
||||
The older .NET generator, MSAGL SVG renderer, MCP server, and ELK probe still exist
|
||||
as support tools for note exports and layout experiments. They are not competing
|
||||
client surfaces. Do not make consumers choose between engines like this repo is a
|
||||
sad little trade show booth.
|
||||
The older .NET generator, MSAGL SVG renderer, and MCP server still exist as support
|
||||
tools for note exports and legacy inspection. They are not competing client surfaces.
|
||||
Do not make consumers choose between engines like this repo is a sad little trade
|
||||
show booth.
|
||||
|
||||
## React Viewer
|
||||
|
||||
|
|
@ -211,10 +211,6 @@ That keeps structurally separated note graphs from vanishing just because they n
|
|||
- `msagl`: the stock MSAGL SVG, useful as the plain baseline
|
||||
- `rich`: a custom SVG renderer that consumes MSAGL graph geometry and semantic metadata, then draws something with more visual hierarchy and less beige despair
|
||||
|
||||
## ELK Probe
|
||||
|
||||
There is also a small web-side `elkjs` probe under `web/elk-probe/`. It inspects which algorithms and options are actually present in the installed `elkjs` build and can relayout existing `source-tree.json` / `control-flow.json` outputs without crossing the C# boundary.
|
||||
|
||||
## Support Tool: Epiphany Graph Viewer Demo
|
||||
|
||||
The web-side viewer package lives under `web/epiphany-graph-viewer/`.
|
||||
|
|
|
|||
13
benchmarks/realtime/rust-solver-convergence.svg
Normal file
13
benchmarks/realtime/rust-solver-convergence.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="460" viewBox="0 0 920 460">
|
||||
<rect width="100%" height="100%" fill="#071019" />
|
||||
<text x="74" y="32" fill="#e5eef8" font-family="Arial, sans-serif" font-size="22" font-weight="700">Rust Solver Convergence Loss By Iterations</text>
|
||||
<line x1="74" y1="390" x2="884" y2="390" stroke="#64748b" />
|
||||
<line x1="74" y1="58" x2="74" y2="390" stroke="#64748b" />
|
||||
<text x="479" y="438" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="14" text-anchor="middle">iterations</text>
|
||||
<text x="22" y="224" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="14" text-anchor="middle" transform="rotate(-90 22 224)">normalized loss</text>
|
||||
<text x="74" y="412" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12">32</text>
|
||||
<text x="884" y="412" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12" text-anchor="end">224</text>
|
||||
<text x="64" y="390" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12" text-anchor="end">0</text>
|
||||
<text x="64" y="62" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12" text-anchor="end">0.2308</text>
|
||||
<path d="M 74.00 93.57 L 344.00 154.87 L 884.00 214.72" fill="none" stroke="#f472b6" stroke-width="3" /><circle cx="74.00" cy="93.57" r="4" fill="#f472b6" /><circle cx="344.00" cy="154.87" r="4" fill="#f472b6" /><circle cx="884.00" cy="214.72" r="4" fill="#f472b6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
13
benchmarks/realtime/rust-solver-latency.svg
Normal file
13
benchmarks/realtime/rust-solver-latency.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="460" viewBox="0 0 920 460">
|
||||
<rect width="100%" height="100%" fill="#071019" />
|
||||
<text x="74" y="32" fill="#e5eef8" font-family="Arial, sans-serif" font-size="22" font-weight="700">Rust Solver P95 Latency By Graph Size</text>
|
||||
<line x1="74" y1="390" x2="884" y2="390" stroke="#64748b" />
|
||||
<line x1="74" y1="58" x2="74" y2="390" stroke="#64748b" />
|
||||
<text x="479" y="438" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="14" text-anchor="middle">nodes</text>
|
||||
<text x="22" y="224" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="14" text-anchor="middle" transform="rotate(-90 22 224)">p95 ms</text>
|
||||
<text x="74" y="412" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12">24</text>
|
||||
<text x="884" y="412" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12" text-anchor="end">1024</text>
|
||||
<text x="64" y="390" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12" text-anchor="end">0</text>
|
||||
<text x="64" y="62" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12" text-anchor="end">212.6332</text>
|
||||
<path d="M 74.00 389.84 L 106.40 389.19 L 158.24 386.69 L 261.92 371.72 L 469.28 333.64 L 884.00 93.57" fill="none" stroke="#22d3ee" stroke-width="3" /><circle cx="74.00" cy="389.84" r="4" fill="#22d3ee" /><circle cx="106.40" cy="389.19" r="4" fill="#22d3ee" /><circle cx="158.24" cy="386.69" r="4" fill="#22d3ee" /><circle cx="261.92" cy="371.72" r="4" fill="#22d3ee" /><circle cx="469.28" cy="333.64" r="4" fill="#22d3ee" /><circle cx="884.00" cy="93.57" r="4" fill="#22d3ee" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
271
benchmarks/realtime/rust-solver-performance-baseline.csv
Normal file
271
benchmarks/realtime/rust-solver-performance-baseline.csv
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
family,nodeCount,edgeCount,profileId,iterations,rankGap,nodeGap,edgeLength,latencyMs,p95LatencyMs,loss,edgeStress,referenceIterations
|
||||
chain,24,23,fast-flat,32,96,76,130,0.2488,0.2488,0.2791,0.254,360
|
||||
chain,24,23,fast-flat,96,96,76,130,0.5909,0.5909,0.2788,0.2526,360
|
||||
chain,24,23,fast-flat,224,96,76,130,1.1465,1.1465,0.2788,0.2525,360
|
||||
chain,24,23,balanced-hierarchy,32,136,92,170,0.1267,0.1267,0.1465,0.1953,360
|
||||
chain,24,23,balanced-hierarchy,96,136,92,170,0.2492,0.2492,0.1463,0.1945,360
|
||||
chain,24,23,balanced-hierarchy,224,136,92,170,0.5487,0.5487,0.1463,0.1945,360
|
||||
chain,24,23,quality-spacious,32,180,118,230,0.1107,0.1107,0.0003,0.2131,360
|
||||
chain,24,23,quality-spacious,96,180,118,230,0.2452,0.2452,0,0.2123,360
|
||||
chain,24,23,quality-spacious,224,180,118,230,0.5297,0.5297,0,0.2123,360
|
||||
chain,64,63,fast-flat,32,96,76,130,0.7609,0.7609,0.273,0.2587,360
|
||||
chain,64,63,fast-flat,96,96,76,130,1.6521,1.6521,0.273,0.2582,360
|
||||
chain,64,63,fast-flat,224,96,76,130,5.399,5.399,0.273,0.2582,360
|
||||
chain,64,63,balanced-hierarchy,32,136,92,170,0.5904,0.5904,0.1431,0.1983,360
|
||||
chain,64,63,balanced-hierarchy,96,136,92,170,2.5069,2.5069,0.143,0.198,360
|
||||
chain,64,63,balanced-hierarchy,224,136,92,170,4.2243,4.2243,0.143,0.198,360
|
||||
chain,64,63,quality-spacious,32,180,118,230,2.9015,2.9015,0.0001,0.2158,360
|
||||
chain,64,63,quality-spacious,96,180,118,230,1.7075,1.7075,0,0.2155,360
|
||||
chain,64,63,quality-spacious,224,180,118,230,3.5914,3.5914,0,0.2155,360
|
||||
chain,128,127,fast-flat,32,96,76,130,2.8866,2.8866,0.2712,0.2602,360
|
||||
chain,128,127,fast-flat,96,96,76,130,13.7612,13.7612,0.2712,0.2599,360
|
||||
chain,128,127,fast-flat,224,96,76,130,46.0941,46.0941,0.2712,0.2599,360
|
||||
chain,128,127,balanced-hierarchy,32,136,92,170,5.6582,5.6582,0.1421,0.1992,360
|
||||
chain,128,127,balanced-hierarchy,96,136,92,170,38.0374,38.0374,0.1421,0.199,360
|
||||
chain,128,127,balanced-hierarchy,224,136,92,170,35.1158,35.1158,0.1421,0.199,360
|
||||
chain,128,127,quality-spacious,32,180,118,230,30.4521,30.4521,0,0.2166,360
|
||||
chain,128,127,quality-spacious,96,180,118,230,49.3048,49.3048,0,0.2165,360
|
||||
chain,128,127,quality-spacious,224,180,118,230,37.4902,37.4902,0,0.2165,360
|
||||
chain,256,255,fast-flat,32,96,76,130,14.7115,14.7115,0.2703,0.2608,360
|
||||
chain,256,255,fast-flat,96,96,76,130,83.9211,83.9211,0.2703,0.2607,360
|
||||
chain,256,255,fast-flat,224,96,76,130,195.3656,195.3656,0.2703,0.2607,360
|
||||
chain,256,255,balanced-hierarchy,32,136,92,170,18.5755,18.5755,0.1416,0.1996,360
|
||||
chain,256,255,balanced-hierarchy,96,136,92,170,24.3222,24.3222,0.1416,0.1995,360
|
||||
chain,256,255,balanced-hierarchy,224,136,92,170,192.6467,192.6467,0.1416,0.1995,360
|
||||
chain,256,255,quality-spacious,32,180,118,230,32.1363,32.1363,0,0.217,360
|
||||
chain,256,255,quality-spacious,96,180,118,230,59.932,59.932,0,0.2169,360
|
||||
chain,256,255,quality-spacious,224,180,118,230,112.9374,112.9374,0,0.2169,360
|
||||
chain,512,511,fast-flat,32,96,76,130,131.2833,131.2833,0.2699,0.2612,280
|
||||
chain,512,511,fast-flat,96,96,76,130,354.3509,354.3509,0.2699,0.2611,280
|
||||
chain,512,511,fast-flat,224,96,76,130,458.5302,458.5302,0.2699,0.2611,280
|
||||
chain,512,511,balanced-hierarchy,32,136,92,170,55.493,55.493,0.1414,0.1998,280
|
||||
chain,512,511,balanced-hierarchy,96,136,92,170,184.8303,184.8303,0.1414,0.1998,280
|
||||
chain,512,511,balanced-hierarchy,224,136,92,170,632.1172,632.1172,0.1414,0.1997,280
|
||||
chain,512,511,quality-spacious,32,180,118,230,44.7984,44.7984,0,0.2172,280
|
||||
chain,512,511,quality-spacious,96,180,118,230,140.1735,140.1735,0,0.2172,280
|
||||
chain,512,511,quality-spacious,224,180,118,230,564.6244,564.6244,0,0.2172,280
|
||||
chain,1024,1023,fast-flat,32,96,76,130,263.6441,263.6441,0.2697,0.2614,220
|
||||
chain,1024,1023,fast-flat,96,96,76,130,1133.1209,1133.1209,0.2697,0.2613,220
|
||||
chain,1024,1023,fast-flat,224,96,76,130,1582.2375,1582.2375,0.2697,0.2613,220
|
||||
chain,1024,1023,balanced-hierarchy,32,136,92,170,207.5773,207.5773,0.1412,0.1999,220
|
||||
chain,1024,1023,balanced-hierarchy,96,136,92,170,991.8572,991.8572,0.1412,0.1999,220
|
||||
chain,1024,1023,balanced-hierarchy,224,136,92,170,2407.9727,2407.9727,0.1412,0.1999,220
|
||||
chain,1024,1023,quality-spacious,32,180,118,230,221.886,221.886,0,0.2173,220
|
||||
chain,1024,1023,quality-spacious,96,180,118,230,728.9886,728.9886,0,0.2173,220
|
||||
chain,1024,1023,quality-spacious,224,180,118,230,2212.4852,2212.4852,0,0.2173,220
|
||||
layered-dag,24,34,fast-flat,32,96,76,130,0.1299,0.1299,0.3017,0.1345,360
|
||||
layered-dag,24,34,fast-flat,96,96,76,130,0.2868,0.2868,0.2756,0.1621,360
|
||||
layered-dag,24,34,fast-flat,224,96,76,130,1.0623,1.0623,0.2529,0.1792,360
|
||||
layered-dag,24,34,balanced-hierarchy,32,136,92,170,0.1949,0.1949,0.2016,0.1023,360
|
||||
layered-dag,24,34,balanced-hierarchy,96,136,92,170,0.3345,0.3345,0.1768,0.1165,360
|
||||
layered-dag,24,34,balanced-hierarchy,224,136,92,170,0.6188,0.6188,0.149,0.1318,360
|
||||
layered-dag,24,34,quality-spacious,32,180,118,230,0.114,0.114,0.092,0.1122,360
|
||||
layered-dag,24,34,quality-spacious,96,180,118,230,0.2849,0.2849,0.0692,0.106,360
|
||||
layered-dag,24,34,quality-spacious,224,180,118,230,0.7908,0.7908,0.0328,0.1169,360
|
||||
layered-dag,64,105,fast-flat,32,96,76,130,2.2729,2.2729,0.291,0.1266,360
|
||||
layered-dag,64,105,fast-flat,96,96,76,130,93.8521,93.8521,0.2708,0.1509,360
|
||||
layered-dag,64,105,fast-flat,224,96,76,130,7.0965,7.0965,0.25,0.1829,360
|
||||
layered-dag,64,105,balanced-hierarchy,32,136,92,170,0.5319,0.5319,0.1864,0.0994,360
|
||||
layered-dag,64,105,balanced-hierarchy,96,136,92,170,1.5299,1.5299,0.1684,0.1086,360
|
||||
layered-dag,64,105,balanced-hierarchy,224,136,92,170,6.9493,6.9493,0.1457,0.1267,360
|
||||
layered-dag,64,105,quality-spacious,32,180,118,230,0.6223,0.6223,0.0719,0.1225,360
|
||||
layered-dag,64,105,quality-spacious,96,180,118,230,1.5497,1.5497,0.0552,0.1139,360
|
||||
layered-dag,64,105,quality-spacious,224,180,118,230,81.4445,81.4445,0.0262,0.1108,360
|
||||
layered-dag,128,223,fast-flat,32,96,76,130,2.2184,2.2184,0.2708,0.1296,360
|
||||
layered-dag,128,223,fast-flat,96,96,76,130,9.1519,9.1519,0.2566,0.1477,360
|
||||
layered-dag,128,223,fast-flat,224,96,76,130,112.1169,112.1169,0.2407,0.1831,360
|
||||
layered-dag,128,223,balanced-hierarchy,32,136,92,170,3.6201,3.6201,0.1661,0.1019,360
|
||||
layered-dag,128,223,balanced-hierarchy,96,136,92,170,7.4542,7.4542,0.1539,0.1062,360
|
||||
layered-dag,128,223,balanced-hierarchy,224,136,92,170,99.9972,99.9972,0.1375,0.1247,360
|
||||
layered-dag,128,223,quality-spacious,32,180,118,230,3.806,3.806,0.0533,0.1273,360
|
||||
layered-dag,128,223,quality-spacious,96,180,118,230,10.8485,10.8485,0.0413,0.1196,360
|
||||
layered-dag,128,223,quality-spacious,224,180,118,230,32.5006,32.5006,0.02,0.114,360
|
||||
layered-dag,256,465,fast-flat,32,96,76,130,17.6227,17.6227,0.2708,0.1327,360
|
||||
layered-dag,256,465,fast-flat,96,96,76,130,107.2987,107.2987,0.2593,0.1362,360
|
||||
layered-dag,256,465,fast-flat,224,96,76,130,219.4445,219.4445,0.2449,0.1659,360
|
||||
layered-dag,256,465,balanced-hierarchy,32,136,92,170,92.2619,92.2619,0.1623,0.1038,360
|
||||
layered-dag,256,465,balanced-hierarchy,96,136,92,170,35.3534,35.3534,0.1529,0.1023,360
|
||||
layered-dag,256,465,balanced-hierarchy,224,136,92,170,341.3665,341.3665,0.1393,0.1147,360
|
||||
layered-dag,256,465,quality-spacious,32,180,118,230,22.812,22.812,0.0419,0.1304,360
|
||||
layered-dag,256,465,quality-spacious,96,180,118,230,84.4691,84.4691,0.0329,0.1243,360
|
||||
layered-dag,256,465,quality-spacious,224,180,118,230,157.2866,157.2866,0.0162,0.1184,360
|
||||
layered-dag,512,956,fast-flat,32,96,76,130,171.0027,171.0027,0.2519,0.1371,280
|
||||
layered-dag,512,956,fast-flat,96,96,76,130,440.7858,440.7858,0.2439,0.1353,280
|
||||
layered-dag,512,956,fast-flat,224,96,76,130,894.2877,894.2877,0.2333,0.1559,280
|
||||
layered-dag,512,956,balanced-hierarchy,32,136,92,170,101.5174,101.5174,0.1453,0.1062,280
|
||||
layered-dag,512,956,balanced-hierarchy,96,136,92,170,301.8864,301.8864,0.1391,0.1034,280
|
||||
layered-dag,512,956,balanced-hierarchy,224,136,92,170,681.1257,681.1257,0.1299,0.1093,280
|
||||
layered-dag,512,956,quality-spacious,32,180,118,230,112.146,112.146,0.0231,0.1328,280
|
||||
layered-dag,512,956,quality-spacious,96,180,118,230,274.3755,274.3755,0.0168,0.1278,280
|
||||
layered-dag,512,956,quality-spacious,224,180,118,230,487.8526,487.8526,0.0049,0.1226,280
|
||||
layered-dag,1024,1953,fast-flat,32,96,76,130,265.6747,265.6747,0.2579,0.1416,220
|
||||
layered-dag,1024,1953,fast-flat,96,96,76,130,1034.2477,1034.2477,0.2521,0.1369,220
|
||||
layered-dag,1024,1953,fast-flat,224,96,76,130,2487.4745,2487.4745,0.244,0.1456,220
|
||||
layered-dag,1024,1953,balanced-hierarchy,32,136,92,170,341.3757,341.3757,0.1448,0.1084,220
|
||||
layered-dag,1024,1953,balanced-hierarchy,96,136,92,170,557.8116,557.8116,0.1405,0.1051,220
|
||||
layered-dag,1024,1953,balanced-hierarchy,224,136,92,170,1428.6917,1428.6917,0.1338,0.1065,220
|
||||
layered-dag,1024,1953,quality-spacious,32,180,118,230,259.2804,259.2804,0.0134,0.1349,220
|
||||
layered-dag,1024,1953,quality-spacious,96,180,118,230,679.6793,679.6793,0.0087,0.131,220
|
||||
layered-dag,1024,1953,quality-spacious,224,180,118,230,1677.3771,1677.3771,0.0003,0.1265,220
|
||||
binary-tree,24,23,fast-flat,32,96,76,130,0.0994,0.0994,0.2315,0.2278,360
|
||||
binary-tree,24,23,fast-flat,96,96,76,130,0.3308,0.3308,0.2102,0.1893,360
|
||||
binary-tree,24,23,fast-flat,224,96,76,130,0.6879,0.6879,0.1871,0.1854,360
|
||||
binary-tree,24,23,balanced-hierarchy,32,136,92,170,0.1173,0.1173,0.1641,0.1949,360
|
||||
binary-tree,24,23,balanced-hierarchy,96,136,92,170,0.2445,0.2445,0.1411,0.1605,360
|
||||
binary-tree,24,23,balanced-hierarchy,224,136,92,170,0.6525,0.6525,0.1158,0.1338,360
|
||||
binary-tree,24,23,quality-spacious,32,180,118,230,0.1089,0.1089,0.1231,0.1869,360
|
||||
binary-tree,24,23,quality-spacious,96,180,118,230,0.302,0.302,0.0851,0.1552,360
|
||||
binary-tree,24,23,quality-spacious,224,180,118,230,1.4987,1.4987,0.0361,0.1217,360
|
||||
binary-tree,64,63,fast-flat,32,96,76,130,0.5334,0.5334,0.2169,1.4057,360
|
||||
binary-tree,64,63,fast-flat,96,96,76,130,1.5077,1.5077,0.1861,0.7332,360
|
||||
binary-tree,64,63,fast-flat,224,96,76,130,47.4958,47.4958,0.1748,0.3761,360
|
||||
binary-tree,64,63,balanced-hierarchy,32,136,92,170,0.5165,0.5165,0.1994,1.3141,360
|
||||
binary-tree,64,63,balanced-hierarchy,96,136,92,170,2.4395,2.4395,0.133,0.6676,360
|
||||
binary-tree,64,63,balanced-hierarchy,224,136,92,170,6.9034,6.9034,0.1084,0.3358,360
|
||||
binary-tree,64,63,quality-spacious,32,180,118,230,0.6857,0.6857,0.2604,1.3039,360
|
||||
binary-tree,64,63,quality-spacious,96,180,118,230,4.0524,4.0524,0.1344,0.6388,360
|
||||
binary-tree,64,63,quality-spacious,224,180,118,230,4.8413,4.8413,0.0435,0.3092,360
|
||||
binary-tree,128,127,fast-flat,32,96,76,130,3.3911,3.3911,0.2462,3.8579,360
|
||||
binary-tree,128,127,fast-flat,96,96,76,130,8.1517,8.1517,0.1893,1.93,360
|
||||
binary-tree,128,127,fast-flat,224,96,76,130,21.4976,21.4976,0.1745,0.7793,360
|
||||
binary-tree,128,127,balanced-hierarchy,32,136,92,170,4.0164,4.0164,0.2648,3.7141,360
|
||||
binary-tree,128,127,balanced-hierarchy,96,136,92,170,66.2096,66.2096,0.1642,1.9737,360
|
||||
binary-tree,128,127,balanced-hierarchy,224,136,92,170,20.7004,20.7004,0.1075,0.7572,360
|
||||
binary-tree,128,127,quality-spacious,32,180,118,230,2.358,2.358,0.3624,3.6958,360
|
||||
binary-tree,128,127,quality-spacious,96,180,118,230,12.2222,12.2222,0.2339,2.1938,360
|
||||
binary-tree,128,127,quality-spacious,224,180,118,230,21.1299,21.1299,0.0693,0.8203,360
|
||||
binary-tree,256,255,fast-flat,32,96,76,130,16.9305,16.9305,0.1864,9.7142,360
|
||||
binary-tree,256,255,fast-flat,96,96,76,130,110.6497,110.6497,0.1505,6.5718,360
|
||||
binary-tree,256,255,fast-flat,224,96,76,130,161.4961,161.4961,0.1504,2.7779,360
|
||||
binary-tree,256,255,balanced-hierarchy,32,136,92,170,14.3811,14.3811,0.1986,9.2064,360
|
||||
binary-tree,256,255,balanced-hierarchy,96,136,92,170,57.5473,57.5473,0.1411,6.6632,360
|
||||
binary-tree,256,255,balanced-hierarchy,224,136,92,170,106.9944,106.9944,0.0831,3.181,360
|
||||
binary-tree,256,255,quality-spacious,32,180,118,230,37.3606,37.3606,0.2771,8.9666,360
|
||||
binary-tree,256,255,quality-spacious,96,180,118,230,77.6774,77.6774,0.2118,6.9749,360
|
||||
binary-tree,256,255,quality-spacious,224,180,118,230,156.5553,156.5553,0.0971,3.9117,360
|
||||
binary-tree,512,511,fast-flat,32,96,76,130,122.1017,122.1017,0.1328,21.9711,280
|
||||
binary-tree,512,511,fast-flat,96,96,76,130,187.3812,187.3812,0.1354,18.1953,280
|
||||
binary-tree,512,511,fast-flat,224,96,76,130,571.7195,571.7195,0.1568,11.8925,280
|
||||
binary-tree,512,511,balanced-hierarchy,32,136,92,170,53.5575,53.5575,0.0927,20.5908,280
|
||||
binary-tree,512,511,balanced-hierarchy,96,136,92,170,148.5947,148.5947,0.0829,17.6309,280
|
||||
binary-tree,512,511,balanced-hierarchy,224,136,92,170,398.2566,398.2566,0.0925,12.4995,280
|
||||
binary-tree,512,511,quality-spacious,32,180,118,230,36.0988,36.0988,0.1106,19.794,280
|
||||
binary-tree,512,511,quality-spacious,96,180,118,230,181.5769,181.5769,0.0809,17.5481,280
|
||||
binary-tree,512,511,quality-spacious,224,180,118,230,528.5091,528.5091,0.0239,13.5086,280
|
||||
binary-tree,1024,1023,fast-flat,32,96,76,130,210.2692,210.2692,0.1369,46.7937,220
|
||||
binary-tree,1024,1023,fast-flat,96,96,76,130,610.4159,610.4159,0.1432,42.6892,220
|
||||
binary-tree,1024,1023,fast-flat,224,96,76,130,2249.5615,2249.5615,0.1586,35.0941,220
|
||||
binary-tree,1024,1023,balanced-hierarchy,32,136,92,170,271.652,271.652,0.0798,43.5894,220
|
||||
binary-tree,1024,1023,balanced-hierarchy,96,136,92,170,1021.4519,1021.4519,0.0841,40.4131,220
|
||||
binary-tree,1024,1023,balanced-hierarchy,224,136,92,170,3800.4651,3800.4651,0.0984,34.4479,220
|
||||
binary-tree,1024,1023,quality-spacious,32,180,118,230,332.3558,332.3558,0.042,41.6159,220
|
||||
binary-tree,1024,1023,quality-spacious,96,180,118,230,1071.4013,1071.4013,0.0276,39.238,220
|
||||
binary-tree,1024,1023,quality-spacious,224,180,118,230,2616.4795,2616.4795,0.0009,34.7045,220
|
||||
hub-spoke,24,29,fast-flat,32,96,76,130,0.1137,0.1137,0.1814,0.8226,360
|
||||
hub-spoke,24,29,fast-flat,96,96,76,130,0.4694,0.4694,0.1492,0.4697,360
|
||||
hub-spoke,24,29,fast-flat,224,96,76,130,1.098,1.098,0.1585,0.4307,360
|
||||
hub-spoke,24,29,balanced-hierarchy,32,136,92,170,0.2271,0.2271,0.2224,0.8247,360
|
||||
hub-spoke,24,29,balanced-hierarchy,96,136,92,170,0.4943,0.4943,0.0881,0.4002,360
|
||||
hub-spoke,24,29,balanced-hierarchy,224,136,92,170,0.8968,0.8968,0.0954,0.3494,360
|
||||
hub-spoke,24,29,quality-spacious,32,180,118,230,0.882,0.882,0.4151,0.9344,360
|
||||
hub-spoke,24,29,quality-spacious,96,180,118,230,0.3203,0.3203,0.1192,0.3564,360
|
||||
hub-spoke,24,29,quality-spacious,224,180,118,230,16.0776,16.0776,0.0234,0.2954,360
|
||||
hub-spoke,64,78,fast-flat,32,96,76,130,0.6576,0.6576,0.8482,5.8995,360
|
||||
hub-spoke,64,78,fast-flat,96,96,76,130,2.0299,2.0299,0.3601,2.2776,360
|
||||
hub-spoke,64,78,fast-flat,224,96,76,130,6.3586,6.3586,0.1055,0.7324,360
|
||||
hub-spoke,64,78,balanced-hierarchy,32,136,92,170,0.99,0.99,1.1505,5.8157,360
|
||||
hub-spoke,64,78,balanced-hierarchy,96,136,92,170,11.8964,11.8964,0.6505,2.653,360
|
||||
hub-spoke,64,78,balanced-hierarchy,224,136,92,170,5.8031,5.8031,0.0686,0.6729,360
|
||||
hub-spoke,64,78,quality-spacious,32,180,118,230,1.5412,1.5412,1.6506,5.9046,360
|
||||
hub-spoke,64,78,quality-spacious,96,180,118,230,6.3829,6.3829,1.1423,3.252,360
|
||||
hub-spoke,64,78,quality-spacious,224,180,118,230,37.8104,37.8104,0.3026,0.8043,360
|
||||
hub-spoke,128,162,fast-flat,32,96,76,130,2.4179,2.4179,0.3497,16.0025,360
|
||||
hub-spoke,128,162,fast-flat,96,96,76,130,23.8242,23.8242,0.2072,10.6282,360
|
||||
hub-spoke,128,162,fast-flat,224,96,76,130,68.7851,68.7851,0.1036,3.654,360
|
||||
hub-spoke,128,162,balanced-hierarchy,32,136,92,170,5.8344,5.8344,0.4665,15.2361,360
|
||||
hub-spoke,128,162,balanced-hierarchy,96,136,92,170,10.0863,10.0863,0.3271,10.9589,360
|
||||
hub-spoke,128,162,balanced-hierarchy,224,136,92,170,22.1724,22.1724,0.0739,4.7465,360
|
||||
hub-spoke,128,162,quality-spacious,32,180,118,230,3.3698,3.3698,0.6685,14.894,360
|
||||
hub-spoke,128,162,quality-spacious,96,180,118,230,36.8697,36.8697,0.5377,11.6044,360
|
||||
hub-spoke,128,162,quality-spacious,224,180,118,230,44.4775,44.4775,0.2847,6.3452,360
|
||||
hub-spoke,256,328,fast-flat,32,96,76,130,33.5236,33.5236,0.1669,36.4441,360
|
||||
hub-spoke,256,328,fast-flat,96,96,76,130,88.0661,88.0661,0.1327,30.3648,360
|
||||
hub-spoke,256,328,fast-flat,224,96,76,130,116.2219,116.2219,0.1471,19.9784,360
|
||||
hub-spoke,256,328,balanced-hierarchy,32,136,92,170,33.1609,33.1609,0.1822,34.19,360
|
||||
hub-spoke,256,328,balanced-hierarchy,96,136,92,170,36.7797,36.7797,0.1303,29.4519,360
|
||||
hub-spoke,256,328,balanced-hierarchy,224,136,92,170,122.1986,122.1986,0.0724,21.1357,360
|
||||
hub-spoke,256,328,quality-spacious,32,180,118,230,54.9571,54.9571,0.2674,32.8891,360
|
||||
hub-spoke,256,328,quality-spacious,96,180,118,230,72.35,72.35,0.2149,29.3306,360
|
||||
hub-spoke,256,328,quality-spacious,224,180,118,230,184.0544,184.0544,0.1114,22.8709,360
|
||||
hub-spoke,512,664,fast-flat,32,96,76,130,96.2749,96.2749,0.1496,78.9376,280
|
||||
hub-spoke,512,664,fast-flat,96,96,76,130,304.5229,304.5229,0.1523,72.5247,280
|
||||
hub-spoke,512,664,fast-flat,224,96,76,130,566.0394,566.0394,0.1742,60.5199,280
|
||||
hub-spoke,512,664,balanced-hierarchy,32,136,92,170,88.6981,88.6981,0.0964,73.5417,280
|
||||
hub-spoke,512,664,balanced-hierarchy,96,136,92,170,269.6925,269.6925,0.0906,68.597,280
|
||||
hub-spoke,512,664,balanced-hierarchy,224,136,92,170,715.2272,715.2272,0.103,59.2111,280
|
||||
hub-spoke,512,664,quality-spacious,32,180,118,230,107.2466,107.2466,0.0946,70.2089,280
|
||||
hub-spoke,512,664,quality-spacious,96,180,118,230,385.9852,385.9852,0.0702,66.5199,280
|
||||
hub-spoke,512,664,quality-spacious,224,180,118,230,666.3786,666.3786,0.0213,59.4487,280
|
||||
hub-spoke,1024,1338,fast-flat,32,96,76,130,333.8144,333.8144,0.1596,164.564,220
|
||||
hub-spoke,1024,1338,fast-flat,96,96,76,130,710.1158,710.1158,0.1637,157.9969,220
|
||||
hub-spoke,1024,1338,fast-flat,224,96,76,130,2184.0934,2184.0934,0.1755,145.2562,220
|
||||
hub-spoke,1024,1338,balanced-hierarchy,32,136,92,170,416.9324,416.9324,0.0953,152.8116,220
|
||||
hub-spoke,1024,1338,balanced-hierarchy,96,136,92,170,1319.5015,1319.5015,0.0981,147.7718,220
|
||||
hub-spoke,1024,1338,balanced-hierarchy,224,136,92,170,2762.4865,2762.4865,0.1087,137.9328,220
|
||||
hub-spoke,1024,1338,quality-spacious,32,180,118,230,569.0331,569.0331,0.035,145.3619,220
|
||||
hub-spoke,1024,1338,quality-spacious,96,180,118,230,1520.0452,1520.0452,0.0231,141.6236,220
|
||||
hub-spoke,1024,1338,quality-spacious,224,180,118,230,2587.3624,2587.3624,0.0008,134.2803,220
|
||||
clustered-modules,24,42,fast-flat,32,96,76,130,0.1194,0.1194,0.2896,0.3399,360
|
||||
clustered-modules,24,42,fast-flat,96,96,76,130,0.2845,0.2845,0.2659,0.296,360
|
||||
clustered-modules,24,42,fast-flat,224,96,76,130,0.5939,0.5939,0.2521,0.2863,360
|
||||
clustered-modules,24,42,balanced-hierarchy,32,136,92,170,0.1046,0.1046,0.2055,0.3352,360
|
||||
clustered-modules,24,42,balanced-hierarchy,96,136,92,170,0.2615,0.2615,0.1716,0.291,360
|
||||
clustered-modules,24,42,balanced-hierarchy,224,136,92,170,0.7611,0.7611,0.145,0.2739,360
|
||||
clustered-modules,24,42,quality-spacious,32,180,118,230,0.1697,0.1697,0.1496,0.3424,360
|
||||
clustered-modules,24,42,quality-spacious,96,180,118,230,0.296,0.296,0.0954,0.2844,360
|
||||
clustered-modules,24,42,quality-spacious,224,180,118,230,1.4378,1.4378,0.0376,0.2652,360
|
||||
clustered-modules,64,120,fast-flat,32,96,76,130,0.6757,0.6757,0.2325,0.3366,360
|
||||
clustered-modules,64,120,fast-flat,96,96,76,130,5.7168,5.7168,0.2231,0.3168,360
|
||||
clustered-modules,64,120,fast-flat,224,96,76,130,12.3975,12.3975,0.2147,0.3104,360
|
||||
clustered-modules,64,120,balanced-hierarchy,32,136,92,170,7.0531,7.0531,0.1446,0.3242,360
|
||||
clustered-modules,64,120,balanced-hierarchy,96,136,92,170,3.4456,3.4456,0.133,0.2895,360
|
||||
clustered-modules,64,120,balanced-hierarchy,224,136,92,170,6.3231,6.3231,0.1198,0.2798,360
|
||||
clustered-modules,64,120,quality-spacious,32,180,118,230,1.0192,1.0192,0.0823,0.331,360
|
||||
clustered-modules,64,120,quality-spacious,96,180,118,230,15.0834,15.0834,0.0585,0.2839,360
|
||||
clustered-modules,64,120,quality-spacious,224,180,118,230,4.7643,4.7643,0.0268,0.2703,360
|
||||
clustered-modules,128,244,fast-flat,32,96,76,130,2.7995,2.7995,0.2106,0.3718,360
|
||||
clustered-modules,128,244,fast-flat,96,96,76,130,23.0144,23.0144,0.2056,0.3566,360
|
||||
clustered-modules,128,244,fast-flat,224,96,76,130,25.9841,25.9841,0.1997,0.3588,360
|
||||
clustered-modules,128,244,balanced-hierarchy,32,136,92,170,2.1172,2.1172,0.1217,0.3577,360
|
||||
clustered-modules,128,244,balanced-hierarchy,96,136,92,170,6.8458,6.8458,0.1175,0.3296,360
|
||||
clustered-modules,128,244,balanced-hierarchy,224,136,92,170,43.6508,43.6508,0.1095,0.326,360
|
||||
clustered-modules,128,244,quality-spacious,32,180,118,230,5.6318,5.6318,0.0631,0.3736,360
|
||||
clustered-modules,128,244,quality-spacious,96,180,118,230,24.4066,24.4066,0.0413,0.324,360
|
||||
clustered-modules,128,244,quality-spacious,224,180,118,230,86.6985,86.6985,0.0186,0.3121,360
|
||||
clustered-modules,256,496,fast-flat,32,96,76,130,30.7665,30.7665,0.2075,0.4111,360
|
||||
clustered-modules,256,496,fast-flat,96,96,76,130,86.1928,86.1928,0.2057,0.4007,360
|
||||
clustered-modules,256,496,fast-flat,224,96,76,130,121.0846,121.0846,0.202,0.3988,360
|
||||
clustered-modules,256,496,balanced-hierarchy,32,136,92,170,15.2616,15.2616,0.1129,0.4058,360
|
||||
clustered-modules,256,496,balanced-hierarchy,96,136,92,170,71.2521,71.2521,0.1135,0.3776,360
|
||||
clustered-modules,256,496,balanced-hierarchy,224,136,92,170,135.0564,135.0564,0.1091,0.3705,360
|
||||
clustered-modules,256,496,quality-spacious,32,180,118,230,11.7053,11.7053,0.0559,0.4309,360
|
||||
clustered-modules,256,496,quality-spacious,96,180,118,230,50.246,50.246,0.0306,0.3761,360
|
||||
clustered-modules,256,496,quality-spacious,224,180,118,230,133.2498,133.2498,0.0145,0.3616,360
|
||||
clustered-modules,512,1000,fast-flat,32,96,76,130,172.2378,172.2378,0.2014,0.4756,280
|
||||
clustered-modules,512,1000,fast-flat,96,96,76,130,288.0959,288.0959,0.2022,0.4434,280
|
||||
clustered-modules,512,1000,fast-flat,224,96,76,130,502.8001,502.8001,0.1996,0.4416,280
|
||||
clustered-modules,512,1000,balanced-hierarchy,32,136,92,170,95.3391,95.3391,0.107,0.4747,280
|
||||
clustered-modules,512,1000,balanced-hierarchy,96,136,92,170,369.7732,369.7732,0.1089,0.4271,280
|
||||
clustered-modules,512,1000,balanced-hierarchy,224,136,92,170,689.7119,689.7119,0.1069,0.4151,280
|
||||
clustered-modules,512,1000,quality-spacious,32,180,118,230,141.8101,141.8101,0.0517,0.4906,280
|
||||
clustered-modules,512,1000,quality-spacious,96,180,118,230,262.9639,262.9639,0.0188,0.4305,280
|
||||
clustered-modules,512,1000,quality-spacious,224,180,118,230,456.8799,456.8799,0.0047,0.4081,280
|
||||
clustered-modules,1024,2016,fast-flat,32,96,76,130,370.416,370.416,0.2011,0.5064,220
|
||||
clustered-modules,1024,2016,fast-flat,96,96,76,130,1037.4821,1037.4821,0.2025,0.4666,220
|
||||
clustered-modules,1024,2016,fast-flat,224,96,76,130,1788.4993,1788.4993,0.2006,0.47,220
|
||||
clustered-modules,1024,2016,balanced-hierarchy,32,136,92,170,189.8511,189.8511,0.1064,0.5016,220
|
||||
clustered-modules,1024,2016,balanced-hierarchy,96,136,92,170,660.9775,660.9775,0.1077,0.454,220
|
||||
clustered-modules,1024,2016,balanced-hierarchy,224,136,92,170,1581.5562,1581.5562,0.1071,0.4426,220
|
||||
clustered-modules,1024,2016,quality-spacious,32,180,118,230,246.5,246.5,0.0437,0.5135,220
|
||||
clustered-modules,1024,2016,quality-spacious,96,180,118,230,596.422,596.422,0.0164,0.4659,220
|
||||
clustered-modules,1024,2016,quality-spacious,224,180,118,230,2067.5246,2067.5246,0.0003,0.437,220
|
||||
|
5390
benchmarks/realtime/rust-solver-performance-baseline.json
Normal file
5390
benchmarks/realtime/rust-solver-performance-baseline.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"generatedAtUtc": "2026-05-19T13:48:35.155Z",
|
||||
"generatedAtUtc": "2026-05-19T19:25:01.505Z",
|
||||
"description": "Smoke baseline for React viewer realtime simulation profile. Measures local JS simulation cost/error over synthetic expansions of mock Epiphany state.",
|
||||
"method": {
|
||||
"iterations": 180,
|
||||
|
|
@ -15,8 +15,8 @@
|
|||
"edgeCount": 10,
|
||||
"fullSample": {
|
||||
"id": "full",
|
||||
"averageCostMs": 0.0128,
|
||||
"p95CostMs": 0.0257,
|
||||
"averageCostMs": 0.0113,
|
||||
"p95CostMs": 0.0186,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 10,
|
||||
"anchorError": 9.6281,
|
||||
|
|
@ -25,8 +25,8 @@
|
|||
"candidates": [
|
||||
{
|
||||
"id": "quality",
|
||||
"averageCostMs": 0.0036,
|
||||
"p95CostMs": 0.0038,
|
||||
"averageCostMs": 0.0045,
|
||||
"p95CostMs": 0.0049,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 10,
|
||||
"anchorError": 9.6281,
|
||||
|
|
@ -34,8 +34,8 @@
|
|||
},
|
||||
{
|
||||
"id": "balanced",
|
||||
"averageCostMs": 0.0042,
|
||||
"p95CostMs": 0.0067,
|
||||
"averageCostMs": 0.0037,
|
||||
"p95CostMs": 0.0062,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 10,
|
||||
"anchorError": 9.6281,
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "fast",
|
||||
"averageCostMs": 0.0034,
|
||||
"p95CostMs": 0.0038,
|
||||
"p95CostMs": 0.004,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 10,
|
||||
"anchorError": 9.6281,
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
"fullSample": {
|
||||
"id": "full",
|
||||
"averageCostMs": 0.0177,
|
||||
"p95CostMs": 0.0351,
|
||||
"p95CostMs": 0.0212,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 40,
|
||||
"anchorError": 9.5761,
|
||||
|
|
@ -68,8 +68,8 @@
|
|||
"candidates": [
|
||||
{
|
||||
"id": "quality",
|
||||
"averageCostMs": 0.0258,
|
||||
"p95CostMs": 0.0762,
|
||||
"averageCostMs": 0.0134,
|
||||
"p95CostMs": 0.014,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 40,
|
||||
"anchorError": 9.5761,
|
||||
|
|
@ -77,8 +77,8 @@
|
|||
},
|
||||
{
|
||||
"id": "balanced",
|
||||
"averageCostMs": 0.0112,
|
||||
"p95CostMs": 0.0109,
|
||||
"averageCostMs": 0.0126,
|
||||
"p95CostMs": 0.0116,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 40,
|
||||
"anchorError": 9.5761,
|
||||
|
|
@ -86,8 +86,8 @@
|
|||
},
|
||||
{
|
||||
"id": "fast",
|
||||
"averageCostMs": 0.0126,
|
||||
"p95CostMs": 0.013,
|
||||
"averageCostMs": 0.0128,
|
||||
"p95CostMs": 0.0171,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 40,
|
||||
"anchorError": 9.5761,
|
||||
|
|
@ -101,8 +101,8 @@
|
|||
"edgeCount": 120,
|
||||
"fullSample": {
|
||||
"id": "full",
|
||||
"averageCostMs": 0.1851,
|
||||
"p95CostMs": 0.1878,
|
||||
"averageCostMs": 0.0468,
|
||||
"p95CostMs": 0.0435,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 120,
|
||||
"anchorError": 9.534,
|
||||
|
|
@ -111,30 +111,30 @@
|
|||
"candidates": [
|
||||
{
|
||||
"id": "quality",
|
||||
"averageCostMs": 0.2182,
|
||||
"p95CostMs": 0.15,
|
||||
"budgetHitRate": 0.9944,
|
||||
"averageCostMs": 0.036,
|
||||
"p95CostMs": 0.0723,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 120,
|
||||
"anchorError": 9.534,
|
||||
"baselineError": 0
|
||||
},
|
||||
{
|
||||
"id": "balanced",
|
||||
"averageCostMs": 0.5637,
|
||||
"p95CostMs": 0.8305,
|
||||
"budgetHitRate": 0.9778,
|
||||
"averageNodeBudget": 109.75,
|
||||
"anchorError": 9.179,
|
||||
"baselineError": 0.6506
|
||||
"averageCostMs": 0.0357,
|
||||
"p95CostMs": 0.0521,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 120,
|
||||
"anchorError": 9.534,
|
||||
"baselineError": 0
|
||||
},
|
||||
{
|
||||
"id": "fast",
|
||||
"averageCostMs": 0.659,
|
||||
"p95CostMs": 1.8007,
|
||||
"budgetHitRate": 0.9778,
|
||||
"averageNodeBudget": 76.8833,
|
||||
"anchorError": 6.6998,
|
||||
"baselineError": 3.0346
|
||||
"averageCostMs": 0.195,
|
||||
"p95CostMs": 0.4638,
|
||||
"budgetHitRate": 1,
|
||||
"averageNodeBudget": 90,
|
||||
"anchorError": 7.161,
|
||||
"baselineError": 2.373
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -142,8 +142,8 @@
|
|||
"candidates": [
|
||||
{
|
||||
"id": "quality",
|
||||
"budgetHitRate": 0.9981333333333332,
|
||||
"averageCostMs": 0.08253333333333333,
|
||||
"budgetHitRate": 1,
|
||||
"averageCostMs": 0.017966666666666666,
|
||||
"baselineError": 0,
|
||||
"anchorError": 9.5794,
|
||||
"profile": {
|
||||
|
|
@ -162,10 +162,10 @@
|
|||
},
|
||||
{
|
||||
"id": "balanced",
|
||||
"budgetHitRate": 0.9926,
|
||||
"averageCostMs": 0.1930333333333333,
|
||||
"baselineError": 0.21686666666666665,
|
||||
"anchorError": 9.461066666666667,
|
||||
"budgetHitRate": 1,
|
||||
"averageCostMs": 0.017333333333333336,
|
||||
"baselineError": 0,
|
||||
"anchorError": 9.5794,
|
||||
"profile": {
|
||||
"id": "balanced",
|
||||
"simulationBudgetMs": 4,
|
||||
|
|
@ -182,10 +182,10 @@
|
|||
},
|
||||
{
|
||||
"id": "fast",
|
||||
"budgetHitRate": 0.9926,
|
||||
"averageCostMs": 0.225,
|
||||
"baselineError": 1.0115333333333334,
|
||||
"anchorError": 8.634666666666666,
|
||||
"budgetHitRate": 1,
|
||||
"averageCostMs": 0.0704,
|
||||
"baselineError": 0.791,
|
||||
"anchorError": 8.788400000000001,
|
||||
"profile": {
|
||||
"id": "fast",
|
||||
"simulationBudgetMs": 2.5,
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
# ELK Probe
|
||||
|
||||
Small local `elkjs` probe for EpiphanyGraph.
|
||||
|
||||
This is not the final viewer. It is a migration scout:
|
||||
|
||||
- inspect which algorithms and options are actually present in the installed `elkjs` build
|
||||
- adapt existing EpiphanyGraph partition JSON into ELK graphs
|
||||
- run sample layouts without crossing the C# boundary
|
||||
- emit simple SVG artifacts under `out/elk-probe/` for side-by-side comparison
|
||||
|
||||
## Install
|
||||
|
||||
```powershell
|
||||
cd E:\Projects\EpiphanyGraph\web\elk-probe
|
||||
npm install
|
||||
```
|
||||
|
||||
## Inspect Available ELK Surface
|
||||
|
||||
```powershell
|
||||
npm run inspect
|
||||
```
|
||||
|
||||
Writes:
|
||||
|
||||
- `out/elk-probe/inspect/known-layout-algorithms.json`
|
||||
- `out/elk-probe/inspect/known-layout-options.json`
|
||||
|
||||
## Probe Existing Graph Output
|
||||
|
||||
Point the probe at a render directory that already contains `source-tree.json` and `control-flow.json`:
|
||||
|
||||
```powershell
|
||||
npm run probe -- --render-dir "E:\Projects\EpiphanyGraph\out\benchmarks\aetheria-source-tree-map\20260425T082539Z\candidates\sugiyama-rich\render"
|
||||
```
|
||||
|
||||
Optional flags:
|
||||
|
||||
- `--render-dir <path>` explicit input directory; if omitted, the probe tries to use the newest benchmark candidate render it can find
|
||||
- `--out-dir <path>` custom output directory
|
||||
- `--source-tree-algorithm <id>` defaults to `org.eclipse.elk.layered`
|
||||
- `--control-flow-algorithm <id>` defaults to `org.eclipse.elk.stress`
|
||||
|
||||
Writes one folder per partition under `out/elk-probe/<timestamp>/` with:
|
||||
|
||||
- `input.json`
|
||||
- `elk-layout.json`
|
||||
- `layout.svg`
|
||||
- `layout-summary.json`
|
||||
|
||||
The SVG renderer here is deliberately plain. The point is to test layout integration and algorithm availability, not to declare victory with decorative sludge.
|
||||
21
web/elk-probe/package-lock.json
generated
21
web/elk-probe/package-lock.json
generated
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name": "@epiphanygraph/elk-probe",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@epiphanygraph/elk-probe",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"elkjs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/elkjs": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz",
|
||||
"integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==",
|
||||
"license": "EPL-2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "@epiphanygraph/elk-probe",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Local elkjs probe for EpiphanyGraph layout experiments.",
|
||||
"scripts": {
|
||||
"probe": "node ./src/cli.mjs probe",
|
||||
"inspect": "node ./src/cli.mjs inspect"
|
||||
},
|
||||
"dependencies": {
|
||||
"elkjs": "^0.11.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,502 +0,0 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ELK from "elkjs/lib/elk.bundled.js";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
const defaultInspectDir = path.join(repoRoot, "out", "elk-probe", "inspect");
|
||||
|
||||
const command = process.argv[2] ?? "inspect";
|
||||
const args = parseArgs(process.argv.slice(3));
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case "inspect":
|
||||
await runInspect(args);
|
||||
break;
|
||||
case "probe":
|
||||
await runProbe(args);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown command '${command}'. Use 'inspect' or 'probe'.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
async function runInspect(args) {
|
||||
const outDir = resolveMaybeRelative(args["out-dir"] ?? defaultInspectDir);
|
||||
const elk = createElk();
|
||||
const algorithms = await elk.knownLayoutAlgorithms();
|
||||
const options = await elk.knownLayoutOptions();
|
||||
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
await writeJson(path.join(outDir, "known-layout-algorithms.json"), algorithms);
|
||||
await writeJson(path.join(outDir, "known-layout-options.json"), options);
|
||||
|
||||
console.log(`Wrote algorithm inventory to ${outDir}`);
|
||||
console.log("");
|
||||
console.log("Algorithms:");
|
||||
for (const algorithm of algorithms) {
|
||||
console.log(`- ${algorithm.id}`);
|
||||
}
|
||||
console.log("");
|
||||
console.log(`Options: ${options.length}`);
|
||||
}
|
||||
|
||||
async function runProbe(args) {
|
||||
const renderDir = args["render-dir"]
|
||||
? resolveMaybeRelative(args["render-dir"])
|
||||
: await findLatestBenchmarkRenderDir();
|
||||
const outDir = resolveMaybeRelative(
|
||||
args["out-dir"] ?? path.join(repoRoot, "out", "elk-probe", timestampNow()),
|
||||
);
|
||||
const sourceTreeAlgorithm = args["source-tree-algorithm"] ?? "org.eclipse.elk.layered";
|
||||
const controlFlowAlgorithm = args["control-flow-algorithm"] ?? "org.eclipse.elk.stress";
|
||||
|
||||
await assertFile(path.join(renderDir, "source-tree.json"));
|
||||
await assertFile(path.join(renderDir, "control-flow.json"));
|
||||
|
||||
const elk = createElk();
|
||||
const algorithms = await elk.knownLayoutAlgorithms();
|
||||
const algorithmIds = new Set(algorithms.map((algorithm) => algorithm.id));
|
||||
|
||||
ensureAlgorithmAvailable(sourceTreeAlgorithm, algorithmIds);
|
||||
ensureAlgorithmAvailable(controlFlowAlgorithm, algorithmIds);
|
||||
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
await writeJson(path.join(outDir, "known-layout-algorithms.json"), algorithms);
|
||||
|
||||
const partitions = [
|
||||
{
|
||||
graphKey: "source-tree",
|
||||
algorithm: sourceTreeAlgorithm,
|
||||
},
|
||||
{
|
||||
graphKey: "control-flow",
|
||||
algorithm: controlFlowAlgorithm,
|
||||
},
|
||||
];
|
||||
|
||||
for (const partition of partitions) {
|
||||
const inputPath = path.join(renderDir, `${partition.graphKey}.json`);
|
||||
const report = JSON.parse(await fs.readFile(inputPath, "utf8"));
|
||||
const partitionOutDir = path.join(outDir, partition.graphKey);
|
||||
await fs.mkdir(partitionOutDir, { recursive: true });
|
||||
|
||||
const elkGraph = buildElkGraph(report, partition.algorithm);
|
||||
const layout = await elk.layout(elkGraph);
|
||||
const summary = buildLayoutSummary(report, layout, partition.algorithm);
|
||||
const svg = renderSvg(layout, report, partition.algorithm);
|
||||
|
||||
await writeJson(path.join(partitionOutDir, "input.json"), report);
|
||||
await writeJson(path.join(partitionOutDir, "elk-graph.json"), elkGraph);
|
||||
await writeJson(path.join(partitionOutDir, "elk-layout.json"), layout);
|
||||
await writeJson(path.join(partitionOutDir, "layout-summary.json"), summary);
|
||||
await fs.writeFile(path.join(partitionOutDir, "layout.svg"), svg, "utf8");
|
||||
|
||||
console.log(
|
||||
`[${partition.graphKey}] ${partition.algorithm} -> ${path.join(partitionOutDir, "layout.svg")}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log(`Probe output: ${outDir}`);
|
||||
}
|
||||
|
||||
async function findLatestBenchmarkRenderDir() {
|
||||
const benchmarkRoot = path.join(repoRoot, "out", "benchmarks", "aetheria-source-tree-map");
|
||||
const runEntries = await fs.readdir(benchmarkRoot, { withFileTypes: true });
|
||||
const runNames = runEntries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
for (const runName of runNames) {
|
||||
const candidateRoot = path.join(benchmarkRoot, runName, "candidates");
|
||||
try {
|
||||
const candidateEntries = await fs.readdir(candidateRoot, { withFileTypes: true });
|
||||
const candidateNames = candidateEntries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort((left, right) => {
|
||||
const leftScore = left.includes("rich") ? 0 : 1;
|
||||
const rightScore = right.includes("rich") ? 0 : 1;
|
||||
return leftScore - rightScore || left.localeCompare(right);
|
||||
});
|
||||
|
||||
for (const candidateName of candidateNames) {
|
||||
const renderDir = path.join(candidateRoot, candidateName, "render");
|
||||
try {
|
||||
await assertFile(path.join(renderDir, "source-tree.json"));
|
||||
await assertFile(path.join(renderDir, "control-flow.json"));
|
||||
return renderDir;
|
||||
} catch {
|
||||
// Keep walking until we find a valid render directory.
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore missing candidate folders and keep scanning older runs.
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Could not find a benchmark render directory automatically. Pass --render-dir explicitly.",
|
||||
);
|
||||
}
|
||||
|
||||
function createElk() {
|
||||
return new ELK();
|
||||
}
|
||||
|
||||
function buildElkGraph(report, algorithm) {
|
||||
return {
|
||||
id: report.GraphKey,
|
||||
layoutOptions: {
|
||||
"elk.algorithm": algorithm,
|
||||
...baseGraphOptions(report.GraphKey, algorithm),
|
||||
},
|
||||
children: report.Nodes.map((node) => ({
|
||||
id: node.Id,
|
||||
width: normalizeSize(node.Bounds.Width, 68, 260),
|
||||
height: normalizeSize(node.Bounds.Height, 34, 110),
|
||||
labels: [
|
||||
{
|
||||
text: node.Title,
|
||||
},
|
||||
],
|
||||
layoutOptions: {
|
||||
...nodeOptions(node, report.GraphKey),
|
||||
},
|
||||
})),
|
||||
edges: report.Edges.map((edge, index) => ({
|
||||
id: `${report.GraphKey}-edge-${index}`,
|
||||
sources: [edge.SourceId],
|
||||
targets: [edge.TargetId],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function baseGraphOptions(graphKey, algorithm) {
|
||||
const layered = algorithm === "org.eclipse.elk.layered";
|
||||
if (graphKey === "source-tree") {
|
||||
return layered
|
||||
? {
|
||||
"elk.direction": "DOWN",
|
||||
"elk.layered.spacing.nodeNodeBetweenLayers": "92",
|
||||
"elk.spacing.nodeNode": "42",
|
||||
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
|
||||
"elk.edgeRouting": "SPLINES",
|
||||
}
|
||||
: {
|
||||
"elk.spacing.nodeNode": "48",
|
||||
"elk.padding": "[top=40,left=40,bottom=40,right=40]",
|
||||
};
|
||||
}
|
||||
|
||||
return layered
|
||||
? {
|
||||
"elk.direction": "DOWN",
|
||||
"elk.layered.spacing.nodeNodeBetweenLayers": "124",
|
||||
"elk.spacing.nodeNode": "84",
|
||||
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
|
||||
"elk.edgeRouting": "SPLINES",
|
||||
}
|
||||
: {
|
||||
"elk.spacing.nodeNode": "80",
|
||||
"elk.padding": "[top=60,left=60,bottom=60,right=60]",
|
||||
};
|
||||
}
|
||||
|
||||
function nodeOptions(node, graphKey) {
|
||||
if (graphKey === "source-tree") {
|
||||
return {
|
||||
"elk.nodeSize.constraints": "MINIMUM_SIZE",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
"elk.nodeSize.constraints": "MINIMUM_SIZE",
|
||||
};
|
||||
}
|
||||
|
||||
function buildLayoutSummary(report, layout, algorithm) {
|
||||
return {
|
||||
graphKey: report.GraphKey,
|
||||
graphTitle: report.GraphTitle,
|
||||
nodeCount: report.NodeCount,
|
||||
edgeCount: report.EdgeCount,
|
||||
algorithm,
|
||||
width: layout.width ?? 0,
|
||||
height: layout.height ?? 0,
|
||||
children: layout.children?.length ?? 0,
|
||||
edgesWithSections:
|
||||
layout.edges?.filter((edge) => Array.isArray(edge.sections) && edge.sections.length > 0).length ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function renderSvg(layout, report, algorithm) {
|
||||
const nodeLookup = new Map(report.Nodes.map((node) => [node.Id, node]));
|
||||
const width = Math.max(1280, Math.ceil((layout.width ?? 0) + 120));
|
||||
const height = Math.max(860, Math.ceil((layout.height ?? 0) + 140));
|
||||
const headerHeight = 92;
|
||||
const offsetX = 60;
|
||||
const offsetY = headerHeight;
|
||||
|
||||
const children = [...(layout.children ?? [])];
|
||||
const edges = [...(layout.edges ?? [])];
|
||||
|
||||
const svg = [];
|
||||
svg.push(`<?xml version="1.0" encoding="utf-8"?>`);
|
||||
svg.push(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="title desc">`,
|
||||
);
|
||||
svg.push(` <title id="title">${escapeXml(report.GraphTitle)} ELK layout</title>`);
|
||||
svg.push(
|
||||
` <desc id="desc">ELK probe render for ${escapeXml(report.GraphKey)} using ${escapeXml(algorithm)}.</desc>`,
|
||||
);
|
||||
svg.push(` <defs>`);
|
||||
svg.push(` <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">`);
|
||||
svg.push(` <stop offset="0%" stop-color="#09111c" />`);
|
||||
svg.push(` <stop offset="100%" stop-color="${report.GraphKey === "control-flow" ? "#190d12" : "#111827"}" />`);
|
||||
svg.push(` </linearGradient>`);
|
||||
svg.push(` <marker id="arrow" viewBox="0 0 12 12" refX="10" refY="6" markerWidth="7" markerHeight="7" orient="auto-start-reverse">`);
|
||||
svg.push(` <path d="M 0 0 L 12 6 L 0 12 z" fill="${report.GraphKey === "control-flow" ? "#fb7185" : "#22d3ee"}" />`);
|
||||
svg.push(` </marker>`);
|
||||
svg.push(` </defs>`);
|
||||
svg.push(` <rect x="0" y="0" width="${width}" height="${height}" fill="url(#bg)" />`);
|
||||
svg.push(
|
||||
` <rect x="20" y="20" width="${width - 40}" height="54" rx="18" fill="#0c1626" stroke="${report.GraphKey === "control-flow" ? "#fb7185" : "#22d3ee"}" stroke-opacity="0.5" />`,
|
||||
);
|
||||
svg.push(
|
||||
` <text x="38" y="44" fill="#f8fafc" font-family="Bahnschrift, Segoe UI, sans-serif" font-size="28" font-weight="700">${escapeXml(report.GraphTitle)}</text>`,
|
||||
);
|
||||
svg.push(
|
||||
` <text x="38" y="62" fill="${report.GraphKey === "control-flow" ? "#fdba74" : "#a5f3fc"}" font-family="Bahnschrift, Segoe UI, sans-serif" font-size="12" letter-spacing="0.16em">${escapeXml(algorithm)}</text>`,
|
||||
);
|
||||
|
||||
svg.push(` <g id="edges" fill="none">`);
|
||||
for (const edge of edges) {
|
||||
const points = edgeSectionsToPoints(edge.sections, offsetX, offsetY);
|
||||
if (points.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
svg.push(
|
||||
` <path d="${polylinePath(points)}" stroke="${report.GraphKey === "control-flow" ? "#fb7185" : "#22d3ee"}" stroke-opacity="0.42" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow)" />`,
|
||||
);
|
||||
}
|
||||
svg.push(` </g>`);
|
||||
|
||||
svg.push(` <g id="nodes">`);
|
||||
for (const child of children) {
|
||||
const x = (child.x ?? 0) + offsetX;
|
||||
const y = (child.y ?? 0) + offsetY;
|
||||
const widthValue = child.width ?? 80;
|
||||
const heightValue = child.height ?? 42;
|
||||
const node = nodeLookup.get(child.id);
|
||||
const color = nodeColor(node, report.GraphKey);
|
||||
const lines = wrapLabel(node?.Title ?? child.id, widthValue < 120 ? 16 : 20, 2);
|
||||
|
||||
svg.push(
|
||||
` <rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(widthValue)}" height="${fmt(heightValue)}" rx="16" fill="#101827" fill-opacity="0.92" stroke="${color}" stroke-width="${node?.IsEntry ? "2.2" : "1.4"}" />`,
|
||||
);
|
||||
if (node?.IsEntry) {
|
||||
svg.push(
|
||||
` <rect x="${fmt(x - 5)}" y="${fmt(y - 5)}" width="${fmt(widthValue + 10)}" height="${fmt(heightValue + 10)}" rx="20" fill="none" stroke="#fde68a" stroke-width="2.1" stroke-opacity="0.9" />`,
|
||||
);
|
||||
}
|
||||
svg.push(
|
||||
` <circle cx="${fmt(x + 14)}" cy="${fmt(y + 14)}" r="9" fill="${color}" stroke="#e5e7eb" stroke-opacity="0.4" />`,
|
||||
);
|
||||
svg.push(
|
||||
` <text x="${fmt(x + 14)}" y="${fmt(y + 18)}" text-anchor="middle" fill="#0b1220" font-family="Bahnschrift, Segoe UI, sans-serif" font-size="9" font-weight="700">${escapeXml(tokenFor(node))}</text>`,
|
||||
);
|
||||
|
||||
for (const [index, line] of lines.entries()) {
|
||||
svg.push(
|
||||
` <text x="${fmt(x + widthValue / 2)}" y="${fmt(y + 24 + index * 14)}" text-anchor="middle" fill="#f8fafc" font-family="Bahnschrift, Segoe UI, sans-serif" font-size="11.5" font-weight="${node?.IsEntry ? "700" : "600"}">${escapeXml(line)}</text>`,
|
||||
);
|
||||
}
|
||||
}
|
||||
svg.push(` </g>`);
|
||||
svg.push(`</svg>`);
|
||||
return svg.join("\n");
|
||||
}
|
||||
|
||||
function edgeSectionsToPoints(sections = [], offsetX, offsetY) {
|
||||
const points = [];
|
||||
for (const section of sections ?? []) {
|
||||
if (section.startPoint) {
|
||||
points.push({
|
||||
x: section.startPoint.x + offsetX,
|
||||
y: section.startPoint.y + offsetY,
|
||||
});
|
||||
}
|
||||
for (const bendPoint of section.bendPoints ?? []) {
|
||||
points.push({
|
||||
x: bendPoint.x + offsetX,
|
||||
y: bendPoint.y + offsetY,
|
||||
});
|
||||
}
|
||||
if (section.endPoint) {
|
||||
points.push({
|
||||
x: section.endPoint.x + offsetX,
|
||||
y: section.endPoint.y + offsetY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return dedupeSequential(points);
|
||||
}
|
||||
|
||||
function polylinePath(points) {
|
||||
const [first, ...rest] = points;
|
||||
return `M ${fmt(first.x)} ${fmt(first.y)} ${rest.map((point) => `L ${fmt(point.x)} ${fmt(point.y)}`).join(" ")}`;
|
||||
}
|
||||
|
||||
function dedupeSequential(points) {
|
||||
const deduped = [];
|
||||
for (const point of points) {
|
||||
const previous = deduped[deduped.length - 1];
|
||||
if (!previous || previous.x !== point.x || previous.y !== point.y) {
|
||||
deduped.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function nodeColor(node, graphKey) {
|
||||
if (node?.IsEntry) {
|
||||
return "#fde68a";
|
||||
}
|
||||
if (node?.Role === "anchor") {
|
||||
return "#94a3b8";
|
||||
}
|
||||
if (graphKey === "control-flow") {
|
||||
return node?.Title?.includes("Server") ? "#60a5fa" : "#fb7185";
|
||||
}
|
||||
if (node?.Title?.includes("Shared")) {
|
||||
return "#67e8f9";
|
||||
}
|
||||
if (node?.Title?.includes("UI") || node?.Title?.includes("Client")) {
|
||||
return "#4ade80";
|
||||
}
|
||||
if (node?.Title?.includes("Simulation") || node?.Title?.includes("Zone") || node?.Title?.includes("Galaxy")) {
|
||||
return "#c084fc";
|
||||
}
|
||||
return "#22d3ee";
|
||||
}
|
||||
|
||||
function tokenFor(node) {
|
||||
if (!node) {
|
||||
return "N";
|
||||
}
|
||||
if (node.IsEntry) {
|
||||
return "E";
|
||||
}
|
||||
if (node.Role === "anchor") {
|
||||
return "A";
|
||||
}
|
||||
const words = node.Title.split(/[\s/_-]+/).filter(Boolean);
|
||||
if (words.length === 1) {
|
||||
return words[0].slice(0, 2).toUpperCase();
|
||||
}
|
||||
return `${words[0][0] ?? "N"}${words[1][0] ?? ""}`.toUpperCase();
|
||||
}
|
||||
|
||||
function wrapLabel(label, maxChars, maxLines) {
|
||||
const words = label.split(/\s+/).filter(Boolean);
|
||||
const lines = [];
|
||||
let current = "";
|
||||
for (const word of words) {
|
||||
const candidate = current ? `${current} ${word}` : word;
|
||||
if (candidate.length > maxChars && current) {
|
||||
lines.push(current);
|
||||
current = word;
|
||||
if (lines.length === maxLines) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
current = candidate;
|
||||
}
|
||||
}
|
||||
if (current && lines.length < maxLines) {
|
||||
lines.push(current);
|
||||
}
|
||||
if (lines.length === 0) {
|
||||
lines.push(label);
|
||||
}
|
||||
if (lines.length === maxLines && words.length > 0) {
|
||||
const last = lines[lines.length - 1];
|
||||
if (last.length > maxChars - 3) {
|
||||
lines[lines.length - 1] = `${last.slice(0, Math.max(1, maxChars - 3))}...`;
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function ensureAlgorithmAvailable(algorithm, knownAlgorithms) {
|
||||
if (!knownAlgorithms.has(algorithm)) {
|
||||
throw new Error(
|
||||
`Algorithm '${algorithm}' is not available in this elkjs build.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertFile(filePath) {
|
||||
await fs.access(filePath);
|
||||
}
|
||||
|
||||
async function writeJson(filePath, value) {
|
||||
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function normalizeSize(value, min, max) {
|
||||
return Math.max(min, Math.min(max, Math.round(value)));
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const token = argv[index];
|
||||
if (!token.startsWith("--")) {
|
||||
continue;
|
||||
}
|
||||
const key = token.slice(2);
|
||||
const next = argv[index + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
args[key] = true;
|
||||
continue;
|
||||
}
|
||||
args[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function resolveMaybeRelative(targetPath) {
|
||||
return path.isAbsolute(targetPath) ? targetPath : path.resolve(repoRoot, targetPath);
|
||||
}
|
||||
|
||||
function timestampNow() {
|
||||
const now = new Date();
|
||||
const pad = (value) => String(value).padStart(2, "0");
|
||||
return `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}T${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}Z`;
|
||||
}
|
||||
|
||||
function escapeXml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function fmt(value) {
|
||||
return Number(value).toFixed(2).replace(/\.00$/, "");
|
||||
}
|
||||
|
|
@ -53,7 +53,8 @@ Regenerate it after changing `E:\Projects\epiphany-graph-rs`:
|
|||
npm run build:solver:wasm
|
||||
```
|
||||
|
||||
ELK remains only as a fallback if the Rust solver fails to instantiate.
|
||||
If the Rust solver fails to instantiate, layout fails loudly. There is no shadow
|
||||
engine behind the component.
|
||||
|
||||
## Use In Another App
|
||||
|
||||
|
|
@ -86,7 +87,7 @@ The package exports:
|
|||
- `EpiphanyGraphViewer`
|
||||
- state, selection, node, edge, layout mode, motion, performance, and event payload types
|
||||
|
||||
The package does not export demo data, validation helpers, ELK probes, SVG renderers,
|
||||
The package does not export demo data, validation helpers, probes, SVG renderers,
|
||||
or engine controls. Those are implementation furniture. Consumers get the couch, not
|
||||
the warehouse inventory.
|
||||
|
||||
|
|
@ -178,13 +179,12 @@ the rendered state closer to the current layout truth:
|
|||
- high-error nodes: farthest from their base layout position
|
||||
- kinetic nodes: nodes carrying the most residual velocity
|
||||
|
||||
The base ELK layout is the current ground-truth anchor for browser motion. Combined
|
||||
The base Rust solver layout is the current ground-truth anchor for browser motion. Combined
|
||||
force is a live presentation layer around that anchor, not a second source of graph
|
||||
truth.
|
||||
|
||||
The existing benchmark harness under `benchmarks/` measures rendered graph usefulness
|
||||
against blind graph-truth probes. It does not yet prove realtime sampling optimality.
|
||||
The first realtime smoke baseline is generated by:
|
||||
against blind graph-truth probes. The realtime smoke baseline is generated by:
|
||||
|
||||
```powershell
|
||||
npm run tune:simulation
|
||||
|
|
@ -192,6 +192,14 @@ npm run tune:simulation
|
|||
|
||||
That writes `benchmarks/realtime/simulation-profile-baseline.json` with cost,
|
||||
budget-hit, anchor-error, and full-sample-baseline-error data for synthetic expansions
|
||||
of the mock graph. The viewer's default profile mirrors that baseline. Do not call
|
||||
the realtime heuristics "extensively tuned" until broader profiled budget sweeps
|
||||
exist for real client corpora and their results are checked in.
|
||||
of the mock graph.
|
||||
|
||||
The Rust solver performance baseline is generated by:
|
||||
|
||||
```powershell
|
||||
npm run profile:solver
|
||||
```
|
||||
|
||||
That writes `benchmarks/realtime/rust-solver-performance-baseline.json`, CSV rows,
|
||||
and SVG charts covering hierarchy families, graph sizes, solver latency windows,
|
||||
meta-heuristic parameters, and convergence error against a high-iteration reference.
|
||||
|
|
|
|||
9
web/epiphany-graph-viewer/package-lock.json
generated
9
web/epiphany-graph-viewer/package-lock.json
generated
|
|
@ -7,9 +7,6 @@
|
|||
"": {
|
||||
"name": "@epiphanygraph/epiphany-graph-viewer",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"elkjs": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
|
|
@ -1484,12 +1481,6 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/elkjs": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz",
|
||||
"integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==",
|
||||
"license": "EPL-2.0"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
|
|
|
|||
|
|
@ -24,15 +24,13 @@
|
|||
"build:solver:wasm": "powershell -ExecutionPolicy Bypass -File ./scripts/Build-RustSolverWasm.ps1",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"tune:simulation": "node ./scripts/tune-simulation-profile.mjs",
|
||||
"profile:solver": "node ./scripts/profile-rust-solver-performance.mjs",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"elkjs": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,459 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "..", "..", "..");
|
||||
const packageRoot = path.resolve(import.meta.dirname, "..");
|
||||
const wasmBytesPath = path.join(packageRoot, "src", "lib", "solver-wasm-bytes.ts");
|
||||
const outputDir = path.join(repoRoot, "benchmarks", "realtime");
|
||||
const outputJsonPath = path.join(outputDir, "rust-solver-performance-baseline.json");
|
||||
const outputCsvPath = path.join(outputDir, "rust-solver-performance-baseline.csv");
|
||||
const latencySvgPath = path.join(outputDir, "rust-solver-latency.svg");
|
||||
const convergenceSvgPath = path.join(outputDir, "rust-solver-convergence.svg");
|
||||
|
||||
const solver = await loadSolver();
|
||||
|
||||
const graphSizes = [24, 64, 128, 256, 512, 1024];
|
||||
const graphFamilies = [
|
||||
"chain",
|
||||
"layered-dag",
|
||||
"binary-tree",
|
||||
"hub-spoke",
|
||||
"clustered-modules",
|
||||
];
|
||||
const latencyWindowsMs = [1.5, 3, 6, 12];
|
||||
const parameterProfiles = [
|
||||
{ id: "fast-flat", rankGap: 96, nodeGap: 76, edgeLength: 130 },
|
||||
{ id: "balanced-hierarchy", rankGap: 136, nodeGap: 92, edgeLength: 170 },
|
||||
{ id: "quality-spacious", rankGap: 180, nodeGap: 118, edgeLength: 230 },
|
||||
];
|
||||
const iterationBudgets = [32, 96, 224];
|
||||
|
||||
const run = {
|
||||
generatedAtUtc: new Date().toISOString(),
|
||||
description:
|
||||
"Smoke baseline for the Rust WASM graph solver used by the React viewer. Measures graph family, size, latency-window, meta-heuristic parameter, and convergence tradeoffs.",
|
||||
method: {
|
||||
graphSizes,
|
||||
graphFamilies,
|
||||
latencyWindowsMs,
|
||||
parameterProfiles,
|
||||
iterationBudgets,
|
||||
samplesPerCandidate: 1,
|
||||
warmupRuns: 0,
|
||||
reference: "quality-spacious parameters at a size-capped high iteration count",
|
||||
loss:
|
||||
"Normalized RMS node displacement from the reference layout after aligning centroids. Lower is closer to the high-iteration solver result.",
|
||||
},
|
||||
rows: [],
|
||||
latencyWindowFrontier: [],
|
||||
summary: {},
|
||||
};
|
||||
|
||||
for (const family of graphFamilies) {
|
||||
for (const nodeCount of graphSizes) {
|
||||
const graph = buildGraph(family, nodeCount);
|
||||
const referenceIterations = nodeCount <= 256 ? 360 : nodeCount <= 512 ? 280 : 220;
|
||||
const reference = layoutGraph(graph, {
|
||||
...parameterProfiles[2],
|
||||
iterations: referenceIterations,
|
||||
});
|
||||
|
||||
for (const profile of parameterProfiles) {
|
||||
for (const iterations of iterationBudgets) {
|
||||
const candidate = measureCandidate(graph, {
|
||||
...profile,
|
||||
iterations,
|
||||
});
|
||||
run.rows.push({
|
||||
family,
|
||||
nodeCount,
|
||||
edgeCount: graph.edgePairs.length / 2,
|
||||
profileId: profile.id,
|
||||
iterations,
|
||||
rankGap: profile.rankGap,
|
||||
nodeGap: profile.nodeGap,
|
||||
edgeLength: profile.edgeLength,
|
||||
latencyMs: round(candidate.latencyMs),
|
||||
p95LatencyMs: round(candidate.p95LatencyMs),
|
||||
loss: round(layoutLoss(candidate.coordinates, reference.coordinates)),
|
||||
edgeStress: round(edgeStress(graph, candidate.coordinates, profile.edgeLength)),
|
||||
referenceIterations,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const family of graphFamilies) {
|
||||
for (const nodeCount of graphSizes) {
|
||||
const rows = run.rows.filter((row) => row.family === family && row.nodeCount === nodeCount);
|
||||
for (const budgetMs of latencyWindowsMs) {
|
||||
const eligible = rows
|
||||
.filter((row) => row.p95LatencyMs <= budgetMs)
|
||||
.sort((left, right) => left.loss - right.loss || left.p95LatencyMs - right.p95LatencyMs);
|
||||
const fallback = rows
|
||||
.slice()
|
||||
.sort((left, right) => left.p95LatencyMs - right.p95LatencyMs || left.loss - right.loss);
|
||||
const best = eligible[0] ?? fallback[0];
|
||||
run.latencyWindowFrontier.push({
|
||||
family,
|
||||
nodeCount,
|
||||
budgetMs,
|
||||
metBudget: Boolean(eligible[0]),
|
||||
profileId: best.profileId,
|
||||
iterations: best.iterations,
|
||||
p95LatencyMs: best.p95LatencyMs,
|
||||
loss: best.loss,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run.summary = summarize(run.rows, run.latencyWindowFrontier);
|
||||
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
await fs.writeFile(`${outputJsonPath}.tmp`, `${JSON.stringify(run, null, 2)}\n`);
|
||||
await fs.rename(`${outputJsonPath}.tmp`, outputJsonPath);
|
||||
await fs.writeFile(`${outputCsvPath}.tmp`, csvRows(run.rows));
|
||||
await fs.rename(`${outputCsvPath}.tmp`, outputCsvPath);
|
||||
await fs.writeFile(`${latencySvgPath}.tmp`, latencySvg(run.rows));
|
||||
await fs.rename(`${latencySvgPath}.tmp`, latencySvgPath);
|
||||
await fs.writeFile(`${convergenceSvgPath}.tmp`, convergenceSvg(run.rows));
|
||||
await fs.rename(`${convergenceSvgPath}.tmp`, convergenceSvgPath);
|
||||
|
||||
console.log(`Wrote ${outputJsonPath}`);
|
||||
console.log(`Wrote ${outputCsvPath}`);
|
||||
console.log(`Wrote ${latencySvgPath}`);
|
||||
console.log(`Wrote ${convergenceSvgPath}`);
|
||||
|
||||
async function loadSolver() {
|
||||
const source = await fs.readFile(wasmBytesPath, "utf8");
|
||||
const match = source.match(/epiphanyGraphSolverWasmBase64\s*=\s*"([^"]+)"/);
|
||||
if (!match) {
|
||||
throw new Error(`Could not read embedded solver WASM from ${wasmBytesPath}`);
|
||||
}
|
||||
const bytes = Buffer.from(match[1], "base64");
|
||||
const result = await WebAssembly.instantiate(bytes, {});
|
||||
const exports = result.instance.exports;
|
||||
for (const name of [
|
||||
"memory",
|
||||
"epiphany_graph_alloc_f32",
|
||||
"epiphany_graph_dealloc_f32",
|
||||
"epiphany_graph_alloc_u32",
|
||||
"epiphany_graph_dealloc_u32",
|
||||
"epiphany_graph_layout_2d",
|
||||
]) {
|
||||
if (!(name in exports)) {
|
||||
throw new Error(`Rust graph solver wasm is missing ${name}`);
|
||||
}
|
||||
}
|
||||
return exports;
|
||||
}
|
||||
|
||||
function measureCandidate(graph, config) {
|
||||
const samples = [];
|
||||
let coordinates = null;
|
||||
for (let index = 0; index < 1; index += 1) {
|
||||
const start = performance.now();
|
||||
coordinates = layoutGraph(graph, config).coordinates;
|
||||
samples.push(performance.now() - start);
|
||||
}
|
||||
|
||||
return {
|
||||
coordinates,
|
||||
latencyMs: average(samples),
|
||||
p95LatencyMs: percentile(samples, 0.95),
|
||||
};
|
||||
}
|
||||
|
||||
function layoutGraph(graph, config) {
|
||||
const nodeCount = graph.nodeWeights.length;
|
||||
const edgeCount = graph.edgePairs.length / 2;
|
||||
const nodeWeightsPtr = solver.epiphany_graph_alloc_f32(nodeCount);
|
||||
const edgePairsPtr = solver.epiphany_graph_alloc_u32(graph.edgePairs.length);
|
||||
const outputPtr = solver.epiphany_graph_alloc_f32(nodeCount * 4);
|
||||
|
||||
try {
|
||||
new Float32Array(solver.memory.buffer, nodeWeightsPtr, nodeCount).set(graph.nodeWeights);
|
||||
new Uint32Array(solver.memory.buffer, edgePairsPtr, graph.edgePairs.length).set(graph.edgePairs);
|
||||
const status = solver.epiphany_graph_layout_2d(
|
||||
nodeWeightsPtr,
|
||||
nodeCount,
|
||||
edgePairsPtr,
|
||||
edgeCount,
|
||||
outputPtr,
|
||||
config.iterations,
|
||||
config.rankGap,
|
||||
config.nodeGap,
|
||||
config.edgeLength,
|
||||
);
|
||||
if (status !== 0) {
|
||||
throw new Error(`Rust graph solver failed with status ${status}`);
|
||||
}
|
||||
const coordinates = new Float32Array(nodeCount * 2);
|
||||
const raw = new Float32Array(solver.memory.buffer, outputPtr, nodeCount * 4);
|
||||
for (let index = 0; index < nodeCount; index += 1) {
|
||||
coordinates[index * 2] = raw[index * 4];
|
||||
coordinates[index * 2 + 1] = raw[index * 4 + 1];
|
||||
}
|
||||
return { coordinates };
|
||||
} finally {
|
||||
solver.epiphany_graph_dealloc_f32(nodeWeightsPtr, nodeCount);
|
||||
solver.epiphany_graph_dealloc_u32(edgePairsPtr, graph.edgePairs.length);
|
||||
solver.epiphany_graph_dealloc_f32(outputPtr, nodeCount * 4);
|
||||
}
|
||||
}
|
||||
|
||||
function buildGraph(family, nodeCount) {
|
||||
const edges = [];
|
||||
if (family === "chain") {
|
||||
for (let index = 1; index < nodeCount; index += 1) {
|
||||
edges.push(index - 1, index);
|
||||
}
|
||||
} else if (family === "layered-dag") {
|
||||
const width = Math.max(4, Math.round(Math.sqrt(nodeCount)));
|
||||
for (let index = 0; index < nodeCount; index += 1) {
|
||||
const nextLayer = index + width;
|
||||
if (nextLayer < nodeCount) {
|
||||
edges.push(index, nextLayer);
|
||||
}
|
||||
if (index % width !== width - 1 && nextLayer + 1 < nodeCount) {
|
||||
edges.push(index, nextLayer + 1);
|
||||
}
|
||||
}
|
||||
} else if (family === "binary-tree") {
|
||||
for (let index = 1; index < nodeCount; index += 1) {
|
||||
edges.push(Math.floor((index - 1) / 2), index);
|
||||
}
|
||||
} else if (family === "hub-spoke") {
|
||||
const hubs = Math.max(2, Math.floor(Math.sqrt(nodeCount) / 2));
|
||||
for (let index = hubs; index < nodeCount; index += 1) {
|
||||
edges.push(index % hubs, index);
|
||||
if (index + hubs < nodeCount && index % 3 === 0) {
|
||||
edges.push(index, index + hubs);
|
||||
}
|
||||
}
|
||||
} else if (family === "clustered-modules") {
|
||||
const clusterSize = Math.max(6, Math.round(Math.sqrt(nodeCount)));
|
||||
for (let index = 0; index < nodeCount; index += 1) {
|
||||
const clusterStart = Math.floor(index / clusterSize) * clusterSize;
|
||||
const next = clusterStart + ((index - clusterStart + 1) % clusterSize);
|
||||
if (next < nodeCount && next !== index) {
|
||||
edges.push(index, next);
|
||||
}
|
||||
if (index + clusterSize < nodeCount) {
|
||||
edges.push(index, index + clusterSize);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown graph family ${family}`);
|
||||
}
|
||||
|
||||
const degrees = new Uint32Array(nodeCount);
|
||||
for (let index = 0; index < edges.length; index += 2) {
|
||||
degrees[edges[index]] += 1;
|
||||
degrees[edges[index + 1]] += 1;
|
||||
}
|
||||
|
||||
const nodeWeights = new Float32Array(nodeCount);
|
||||
for (let index = 0; index < nodeCount; index += 1) {
|
||||
nodeWeights[index] = 1 + degrees[index] * 0.18 + deterministicNoise(family, index) * 0.24;
|
||||
}
|
||||
|
||||
return {
|
||||
nodeWeights,
|
||||
edgePairs: new Uint32Array(edges),
|
||||
};
|
||||
}
|
||||
|
||||
function layoutLoss(candidate, reference) {
|
||||
const count = candidate.length / 2;
|
||||
const candidateCenter = centroid(candidate);
|
||||
const referenceCenter = centroid(reference);
|
||||
let sum = 0;
|
||||
let maxReferenceRadius = 1;
|
||||
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const candidateX = candidate[index * 2] - candidateCenter.x;
|
||||
const candidateY = candidate[index * 2 + 1] - candidateCenter.y;
|
||||
const referenceX = reference[index * 2] - referenceCenter.x;
|
||||
const referenceY = reference[index * 2 + 1] - referenceCenter.y;
|
||||
maxReferenceRadius = Math.max(maxReferenceRadius, Math.hypot(referenceX, referenceY));
|
||||
sum += (candidateX - referenceX) ** 2 + (candidateY - referenceY) ** 2;
|
||||
}
|
||||
|
||||
return Math.sqrt(sum / count) / maxReferenceRadius;
|
||||
}
|
||||
|
||||
function edgeStress(graph, coordinates, targetLength) {
|
||||
const stresses = [];
|
||||
for (let index = 0; index < graph.edgePairs.length; index += 2) {
|
||||
const source = graph.edgePairs[index];
|
||||
const target = graph.edgePairs[index + 1];
|
||||
const length = Math.hypot(
|
||||
coordinates[source * 2] - coordinates[target * 2],
|
||||
coordinates[source * 2 + 1] - coordinates[target * 2 + 1],
|
||||
);
|
||||
stresses.push(Math.abs(length - targetLength) / Math.max(1, targetLength));
|
||||
}
|
||||
return average(stresses);
|
||||
}
|
||||
|
||||
function summarize(rows, frontier) {
|
||||
const bySize = graphSizes.map((nodeCount) => {
|
||||
const sizeRows = rows.filter((row) => row.nodeCount === nodeCount);
|
||||
return {
|
||||
nodeCount,
|
||||
bestP95LatencyMs: round(Math.min(...sizeRows.map((row) => row.p95LatencyMs))),
|
||||
bestLoss: round(Math.min(...sizeRows.map((row) => row.loss))),
|
||||
medianP95LatencyMs: round(percentile(sizeRows.map((row) => row.p95LatencyMs), 0.5)),
|
||||
};
|
||||
});
|
||||
const largestRealtime = [...frontier]
|
||||
.filter((row) => row.metBudget)
|
||||
.sort((left, right) => right.nodeCount - left.nodeCount || left.budgetMs - right.budgetMs)[0];
|
||||
|
||||
return {
|
||||
bySize,
|
||||
largestBudgetMeetingRun: largestRealtime ?? null,
|
||||
bestOverallLoss: rows.slice().sort((left, right) => left.loss - right.loss)[0],
|
||||
fastestOverall: rows.slice().sort((left, right) => left.p95LatencyMs - right.p95LatencyMs)[0],
|
||||
};
|
||||
}
|
||||
|
||||
function csvRows(rows) {
|
||||
const header = [
|
||||
"family",
|
||||
"nodeCount",
|
||||
"edgeCount",
|
||||
"profileId",
|
||||
"iterations",
|
||||
"rankGap",
|
||||
"nodeGap",
|
||||
"edgeLength",
|
||||
"latencyMs",
|
||||
"p95LatencyMs",
|
||||
"loss",
|
||||
"edgeStress",
|
||||
"referenceIterations",
|
||||
];
|
||||
return [
|
||||
header.join(","),
|
||||
...rows.map((row) => header.map((key) => row[key]).join(",")),
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function latencySvg(rows) {
|
||||
const fastestBySize = graphSizes.map((nodeCount) => ({
|
||||
x: nodeCount,
|
||||
y: Math.min(...rows.filter((row) => row.nodeCount === nodeCount).map((row) => row.p95LatencyMs)),
|
||||
}));
|
||||
return chartSvg({
|
||||
title: "Rust Solver P95 Latency By Graph Size",
|
||||
xLabel: "nodes",
|
||||
yLabel: "p95 ms",
|
||||
series: [{ id: "fastest", color: "#22d3ee", points: fastestBySize }],
|
||||
});
|
||||
}
|
||||
|
||||
function convergenceSvg(rows) {
|
||||
const byIteration = iterationBudgets.map((iterations) => ({
|
||||
x: iterations,
|
||||
y: average(rows.filter((row) => row.iterations === iterations).map((row) => row.loss)),
|
||||
}));
|
||||
return chartSvg({
|
||||
title: "Rust Solver Convergence Loss By Iterations",
|
||||
xLabel: "iterations",
|
||||
yLabel: "normalized loss",
|
||||
series: [{ id: "average loss", color: "#f472b6", points: byIteration }],
|
||||
});
|
||||
}
|
||||
|
||||
function chartSvg({ title, xLabel, yLabel, series }) {
|
||||
const width = 920;
|
||||
const height = 460;
|
||||
const padding = { left: 74, right: 36, top: 58, bottom: 70 };
|
||||
const allPoints = series.flatMap((item) => item.points);
|
||||
const xMin = Math.min(...allPoints.map((point) => point.x));
|
||||
const xMax = Math.max(...allPoints.map((point) => point.x));
|
||||
const yMin = 0;
|
||||
const yMax = Math.max(0.001, Math.max(...allPoints.map((point) => point.y)) * 1.12);
|
||||
const plotWidth = width - padding.left - padding.right;
|
||||
const plotHeight = height - padding.top - padding.bottom;
|
||||
const sx = (value) => padding.left + ((value - xMin) / Math.max(1, xMax - xMin)) * plotWidth;
|
||||
const sy = (value) => padding.top + plotHeight - ((value - yMin) / Math.max(0.001, yMax - yMin)) * plotHeight;
|
||||
const paths = series.map((item) => {
|
||||
const d = item.points
|
||||
.map((point, index) => `${index === 0 ? "M" : "L"} ${sx(point.x).toFixed(2)} ${sy(point.y).toFixed(2)}`)
|
||||
.join(" ");
|
||||
const circles = item.points
|
||||
.map((point) => `<circle cx="${sx(point.x).toFixed(2)}" cy="${sy(point.y).toFixed(2)}" r="4" fill="${item.color}" />`)
|
||||
.join("");
|
||||
return `<path d="${d}" fill="none" stroke="${item.color}" stroke-width="3" />${circles}`;
|
||||
}).join("");
|
||||
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<rect width="100%" height="100%" fill="#071019" />
|
||||
<text x="${padding.left}" y="32" fill="#e5eef8" font-family="Arial, sans-serif" font-size="22" font-weight="700">${escapeXml(title)}</text>
|
||||
<line x1="${padding.left}" y1="${padding.top + plotHeight}" x2="${padding.left + plotWidth}" y2="${padding.top + plotHeight}" stroke="#64748b" />
|
||||
<line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${padding.top + plotHeight}" stroke="#64748b" />
|
||||
<text x="${padding.left + plotWidth / 2}" y="${height - 22}" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="14" text-anchor="middle">${escapeXml(xLabel)}</text>
|
||||
<text x="22" y="${padding.top + plotHeight / 2}" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="14" text-anchor="middle" transform="rotate(-90 22 ${padding.top + plotHeight / 2})">${escapeXml(yLabel)}</text>
|
||||
<text x="${padding.left}" y="${padding.top + plotHeight + 22}" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12">${xMin}</text>
|
||||
<text x="${padding.left + plotWidth}" y="${padding.top + plotHeight + 22}" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12" text-anchor="end">${xMax}</text>
|
||||
<text x="${padding.left - 10}" y="${padding.top + plotHeight}" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12" text-anchor="end">${round(yMin)}</text>
|
||||
<text x="${padding.left - 10}" y="${padding.top + 4}" fill="#94a3b8" font-family="Arial, sans-serif" font-size="12" text-anchor="end">${round(yMax)}</text>
|
||||
${paths}
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
function centroid(coordinates) {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
const count = coordinates.length / 2;
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
x += coordinates[index * 2];
|
||||
y += coordinates[index * 2 + 1];
|
||||
}
|
||||
return { x: x / count, y: y / count };
|
||||
}
|
||||
|
||||
function deterministicNoise(family, index) {
|
||||
let hash = 2166136261;
|
||||
for (const char of `${family}:${index}`) {
|
||||
hash ^= char.charCodeAt(0);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return ((hash >>> 0) % 1000) / 1000;
|
||||
}
|
||||
|
||||
function average(values) {
|
||||
const finite = values.filter(Number.isFinite);
|
||||
if (finite.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return finite.reduce((sum, value) => sum + value, 0) / finite.length;
|
||||
}
|
||||
|
||||
function percentile(values, fraction) {
|
||||
const sorted = values.filter(Number.isFinite).sort((left, right) => left - right);
|
||||
if (sorted.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * fraction))];
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Number(value.toFixed(4));
|
||||
}
|
||||
|
||||
function escapeXml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
|
@ -600,7 +600,7 @@ export function EpiphanyGraphViewer({
|
|||
<div>Wheel to zoom. Drag the void to pan. Click something interesting and the side panel stops being decorative.</div>
|
||||
</div>
|
||||
|
||||
{status === "loading" && <StageMessage tone="#67e8f9">Laying out the graph. ELK is thinking noble thoughts.</StageMessage>}
|
||||
{status === "loading" && <StageMessage tone="#67e8f9">Laying out the graph. Rust solver is taking the hill.</StageMessage>}
|
||||
{status === "error" && <StageMessage tone="#fca5a5">{errorMessage ?? "Layout failed."}</StageMessage>}
|
||||
{status === "ready" && activeLayout && (
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import ELK from "elkjs/lib/elk.bundled.js";
|
||||
import { layoutWithRustSolver } from "./solver-wasm";
|
||||
import type {
|
||||
EpiphanyGraph,
|
||||
|
|
@ -14,33 +13,7 @@ import type {
|
|||
PositionedPoint,
|
||||
} from "./types";
|
||||
|
||||
const elk = new ELK();
|
||||
|
||||
type ElkSection = {
|
||||
startPoint?: { x: number; y: number };
|
||||
endPoint?: { x: number; y: number };
|
||||
bendPoints?: Array<{ x: number; y: number }>;
|
||||
};
|
||||
|
||||
type ElkLayoutNode = {
|
||||
id: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
type ElkLayoutEdge = {
|
||||
id?: string;
|
||||
sections?: ElkSection[];
|
||||
};
|
||||
|
||||
type ElkLayoutGraph = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
children?: ElkLayoutNode[];
|
||||
edges?: ElkLayoutEdge[];
|
||||
};
|
||||
type NodeSizingMode = "layered" | "compact";
|
||||
|
||||
type LayoutViewport = {
|
||||
width: number;
|
||||
|
|
@ -66,111 +39,6 @@ async function layoutGraph(
|
|||
links: EpiphanyGraphLink[],
|
||||
mode: EpiphanyGraphLayoutMode = "layered",
|
||||
viewport?: LayoutViewport,
|
||||
): Promise<GraphLayout> {
|
||||
try {
|
||||
return await layoutGraphWithRustSolver(graphKey, graph, links, mode, viewport);
|
||||
} catch (error) {
|
||||
console.warn("Epiphany Rust graph solver failed; falling back to ELK.", error);
|
||||
}
|
||||
|
||||
return layoutGraphWithElk(graphKey, graph, links, mode, viewport);
|
||||
}
|
||||
|
||||
async function layoutGraphWithElk(
|
||||
graphKey: GraphKey,
|
||||
graph: EpiphanyGraph,
|
||||
links: EpiphanyGraphLink[],
|
||||
mode: EpiphanyGraphLayoutMode = "layered",
|
||||
viewport?: LayoutViewport,
|
||||
): Promise<GraphLayout> {
|
||||
const algorithm = elkAlgorithmForMode(mode);
|
||||
const nodeDegrees = buildNodeDegrees(graph);
|
||||
const linkCounts = buildLinkCounts(graphKey, links);
|
||||
const nodes = graph.nodes.map((node) => {
|
||||
const size = estimateNodeSize(node, graphKey, algorithm, graph.nodes.length, viewport);
|
||||
return {
|
||||
id: node.id,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
labels: [{ text: node.title }],
|
||||
};
|
||||
});
|
||||
const edges = graph.edges.map((edge, index) => ({
|
||||
id: resolveEdgeId(edge, index),
|
||||
sources: [edge.source_id],
|
||||
targets: [edge.target_id],
|
||||
}));
|
||||
|
||||
const elkGraph = {
|
||||
id: graphKey,
|
||||
layoutOptions: {
|
||||
"elk.algorithm": algorithm,
|
||||
...layoutOptionsFor(graphKey, algorithm),
|
||||
},
|
||||
children: nodes,
|
||||
edges,
|
||||
};
|
||||
|
||||
const layout = (await elk.layout(elkGraph)) as ElkLayoutGraph;
|
||||
const childLookup = new Map((layout.children ?? []).map((child) => [child.id, child]));
|
||||
|
||||
const positionedNodes: PositionedNode[] = graph.nodes.map((node) => {
|
||||
const child = childLookup.get(node.id);
|
||||
const degree = nodeDegrees.get(node.id) ?? 0;
|
||||
const linkCount = linkCounts.get(node.id) ?? 0;
|
||||
return {
|
||||
...node,
|
||||
code_refs: node.code_refs ?? [],
|
||||
graphKey,
|
||||
x: child?.x ?? 0,
|
||||
y: child?.y ?? 0,
|
||||
width: child?.width ?? estimateNodeSize(node, graphKey, algorithm, graph.nodes.length, viewport).width,
|
||||
height: child?.height ?? estimateNodeSize(node, graphKey, algorithm, graph.nodes.length, viewport).height,
|
||||
degree,
|
||||
linkCount,
|
||||
badgeText: nodeBadgeText(node.title),
|
||||
fill: nodeFill(graphKey, node.status),
|
||||
stroke: nodeStroke(graphKey, node.status),
|
||||
};
|
||||
});
|
||||
const separatedNodes = normalizeNodeBounds(resizeNodesForSeparation(positionedNodes, algorithm));
|
||||
|
||||
const layoutEdgeLookup = new Map((layout.edges ?? []).map((edge, index) => [edge.id ?? `edge-${graphKey}-${index}`, edge]));
|
||||
const nodeLookup = new Map(separatedNodes.map((node) => [node.id, node]));
|
||||
const positionedEdges: PositionedEdge[] = graph.edges.map((source, index) => {
|
||||
const resolvedId = resolveEdgeId(source, index);
|
||||
const layoutEdge = layoutEdgeLookup.get(resolvedId);
|
||||
const points = isCompactAlgorithm(algorithm) ? [] : edgeSectionsToPoints(layoutEdge?.sections ?? []);
|
||||
const fallbackPoints = points.length > 0 ? points : straightEdgePoints(source, nodeLookup);
|
||||
return {
|
||||
...source,
|
||||
id: source.id ?? null,
|
||||
label: source.label ?? null,
|
||||
mechanism: source.mechanism ?? null,
|
||||
code_refs: source.code_refs ?? [],
|
||||
graphKey,
|
||||
resolvedId,
|
||||
points: fallbackPoints,
|
||||
path: pointsToPath(fallbackPoints),
|
||||
midpoint: edgeMidpoint(fallbackPoints),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
graphKey,
|
||||
width: Math.max(720, layoutBounds(separatedNodes).width, layout.width ?? 720),
|
||||
height: Math.max(520, layoutBounds(separatedNodes).height, layout.height ?? 520),
|
||||
nodes: separatedNodes,
|
||||
edges: positionedEdges,
|
||||
};
|
||||
}
|
||||
|
||||
async function layoutGraphWithRustSolver(
|
||||
graphKey: GraphKey,
|
||||
graph: EpiphanyGraph,
|
||||
links: EpiphanyGraphLink[],
|
||||
mode: EpiphanyGraphLayoutMode,
|
||||
viewport?: LayoutViewport,
|
||||
): Promise<GraphLayout> {
|
||||
const sizingAlgorithm = sizingAlgorithmForMode(mode);
|
||||
const nodeDegrees = buildNodeDegrees(graph);
|
||||
|
|
@ -261,20 +129,8 @@ function layoutModeFor(
|
|||
return mode[graphKey] ?? "layered";
|
||||
}
|
||||
|
||||
function elkAlgorithmForMode(mode: EpiphanyGraphLayoutMode) {
|
||||
if (mode === "stress" || mode === "combined-force") {
|
||||
return "org.eclipse.elk.stress";
|
||||
}
|
||||
|
||||
if (mode === "force") {
|
||||
return "org.eclipse.elk.force";
|
||||
}
|
||||
|
||||
return "org.eclipse.elk.layered";
|
||||
}
|
||||
|
||||
function sizingAlgorithmForMode(mode: EpiphanyGraphLayoutMode) {
|
||||
return mode === "layered" ? "org.eclipse.elk.layered" : "org.eclipse.elk.stress";
|
||||
function sizingAlgorithmForMode(mode: EpiphanyGraphLayoutMode): NodeSizingMode {
|
||||
return mode === "layered" ? "layered" : "compact";
|
||||
}
|
||||
|
||||
function rustSolverConfigFor(graphKey: GraphKey, mode: EpiphanyGraphLayoutMode) {
|
||||
|
|
@ -336,8 +192,8 @@ function layoutBounds(nodes: PositionedNode[]) {
|
|||
};
|
||||
}
|
||||
|
||||
function resizeNodesForSeparation(nodes: PositionedNode[], algorithm: string): PositionedNode[] {
|
||||
if (!isCompactAlgorithm(algorithm) || nodes.length < 2) {
|
||||
function resizeNodesForSeparation(nodes: PositionedNode[], sizingMode: NodeSizingMode): PositionedNode[] {
|
||||
if (!isCompactSizingMode(sizingMode) || nodes.length < 2) {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
|
|
@ -458,8 +314,8 @@ function relaxOverlappingNodes(nodes: PositionedNode[]): PositionedNode[] {
|
|||
return relaxedNodes;
|
||||
}
|
||||
|
||||
function isCompactAlgorithm(algorithm: string) {
|
||||
return algorithm === "org.eclipse.elk.stress" || algorithm === "org.eclipse.elk.force";
|
||||
function isCompactSizingMode(sizingMode: NodeSizingMode) {
|
||||
return sizingMode === "compact";
|
||||
}
|
||||
|
||||
function nodeCenter(node: { x: number; y: number; width: number; height: number }) {
|
||||
|
|
@ -469,51 +325,6 @@ function nodeCenter(node: { x: number; y: number; width: number; height: number
|
|||
};
|
||||
}
|
||||
|
||||
function layoutOptionsFor(graphKey: GraphKey, algorithm: string): Record<string, string> {
|
||||
if (algorithm === "org.eclipse.elk.stress") {
|
||||
return {
|
||||
"elk.padding": "[top=96,left=96,bottom=96,right=96]",
|
||||
"elk.spacing.nodeNode": graphKey === "architecture" ? "96" : "128",
|
||||
"elk.stress.desiredEdgeLength": graphKey === "architecture" ? "260" : "340",
|
||||
"elk.stress.iterationLimit": graphKey === "architecture" ? "240" : "180",
|
||||
"elk.stress.epsilon": "0.0001",
|
||||
};
|
||||
}
|
||||
|
||||
if (algorithm === "org.eclipse.elk.force") {
|
||||
return {
|
||||
"elk.padding": "[top=96,left=96,bottom=96,right=96]",
|
||||
"elk.spacing.nodeNode": graphKey === "architecture" ? "96" : "132",
|
||||
"elk.force.model": "FRUCHTERMAN_REINGOLD",
|
||||
"elk.force.iterations": graphKey === "architecture" ? "650" : "360",
|
||||
"elk.force.repulsion": graphKey === "architecture" ? "72" : "120",
|
||||
"elk.force.temperature": "0.1",
|
||||
"elk.randomSeed": "17",
|
||||
};
|
||||
}
|
||||
|
||||
return graphKey === "architecture"
|
||||
? {
|
||||
"elk.direction": "DOWN",
|
||||
"elk.edgeRouting": "SPLINES",
|
||||
"elk.spacing.nodeNode": "48",
|
||||
"elk.layered.spacing.nodeNodeBetweenLayers": "88",
|
||||
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
|
||||
"elk.padding": "[top=40,left=40,bottom=40,right=40]",
|
||||
}
|
||||
: {
|
||||
"elk.direction": "RIGHT",
|
||||
"elk.edgeRouting": "SPLINES",
|
||||
"elk.spacing.nodeNode": "88",
|
||||
"elk.layered.spacing.nodeNodeBetweenLayers": "176",
|
||||
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
|
||||
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
|
||||
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
||||
"elk.layered.unnecessaryBendpoints": "true",
|
||||
"elk.padding": "[top=72,left=84,bottom=72,right=96]",
|
||||
};
|
||||
}
|
||||
|
||||
function buildNodeDegrees(graph: EpiphanyGraph) {
|
||||
const degrees = new Map<string, number>();
|
||||
for (const node of graph.nodes ?? []) {
|
||||
|
|
@ -543,11 +354,11 @@ function resolveEdgeId(edge: EpiphanyGraphEdge, index: number) {
|
|||
function estimateNodeSize(
|
||||
node: { title: string; purpose: string; mechanism?: string | null; metaphor?: string | null },
|
||||
graphKey: GraphKey,
|
||||
algorithm: string,
|
||||
sizingMode: NodeSizingMode,
|
||||
nodeCount: number,
|
||||
viewport?: LayoutViewport,
|
||||
) {
|
||||
const compact = algorithm === "org.eclipse.elk.stress" || algorithm === "org.eclipse.elk.force";
|
||||
const compact = isCompactSizingMode(sizingMode);
|
||||
const densityScale = compact ? graphDensityScale(nodeCount, viewport) : 1;
|
||||
const titleWeight = Math.min(node.title.length, 36);
|
||||
const purposeWeight = Math.min(node.purpose.length, 110);
|
||||
|
|
@ -638,22 +449,6 @@ function estimateCharacterCapacity(pixelWidth: number, averageCharWidth: number)
|
|||
return Math.max(12, Math.floor(pixelWidth / averageCharWidth));
|
||||
}
|
||||
|
||||
function edgeSectionsToPoints(sections: ElkSection[]) {
|
||||
const points: PositionedPoint[] = [];
|
||||
for (const section of sections) {
|
||||
if (section.startPoint) {
|
||||
points.push(section.startPoint);
|
||||
}
|
||||
for (const bendPoint of section.bendPoints ?? []) {
|
||||
points.push(bendPoint);
|
||||
}
|
||||
if (section.endPoint) {
|
||||
points.push(section.endPoint);
|
||||
}
|
||||
}
|
||||
return dedupeSequentialPoints(points);
|
||||
}
|
||||
|
||||
function straightEdgePoints(
|
||||
edge: EpiphanyGraphEdge,
|
||||
nodes: Map<string, PositionedNode>,
|
||||
|
|
@ -695,17 +490,6 @@ function edgeMidpoint(points: PositionedPoint[]) {
|
|||
return points[midIndex];
|
||||
}
|
||||
|
||||
function dedupeSequentialPoints(points: PositionedPoint[]) {
|
||||
const deduped: PositionedPoint[] = [];
|
||||
for (const point of points) {
|
||||
const previous = deduped[deduped.length - 1];
|
||||
if (!previous || previous.x !== point.x || previous.y !== point.y) {
|
||||
deduped.push(point);
|
||||
}
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function nodeBadgeText(title: string) {
|
||||
const words = title.split(/[\s/_-]+/).filter(Boolean);
|
||||
if (words.length === 0) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export default defineConfig({
|
|||
output: {
|
||||
manualChunks: {
|
||||
react: ["react", "react-dom"],
|
||||
elk: ["elkjs/lib/elk.bundled.js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue