| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| <div class="aws-topology-wrapper"> |
| <div class="aws-topology-container"></div> |
| <div class="aws-topology-controls"></div> |
| <div class="aws-topology-legend"></div> |
| <div class="aws-topology-tooltip"></div> |
| <div class="aws-topology-bottleneck"> |
| <div class="bottleneck-label">Bandwidth Max</div> |
| <div class="bottleneck-path">for CPU → GPU</div> |
| <div class="bottleneck-value">-</div> |
| <div class="bottleneck-unit">GB/s</div> |
| <div class="bottleneck-efficiency" style="display: none;"> |
| <div class="efficiency-value">-</div> |
| <div class="efficiency-label">Efficiency</div> |
| </div> |
| <div class="real-bandwidths" style="display: none;"> |
| <div class="real-bandwidths-title">Real Bandwidths</div> |
| <div class="real-bandwidths-content"></div> |
| </div> |
| </div> |
| </div> |
|
|
| <style> |
| .aws-topology-wrapper { |
| width: 100%; |
| display: flex; |
| flex-direction: column; |
| position: relative; |
| } |
| |
| .aws-topology-container { |
| width: 100%; |
| height: auto; |
| min-height: 400px; |
| position: relative; |
| transition: opacity 0.15s ease; |
| } |
| |
| .aws-topology-container.fixed-height { |
| height: 850px; |
| } |
| |
| .aws-topology-container svg { |
| width: 100%; |
| height: 100%; |
| } |
| |
| .aws-topology-container svg g { |
| transition: opacity 0.2s ease; |
| } |
| |
| |
| .aws-topology-container svg g[data-link-type] line, |
| .aws-topology-container svg g[data-link-type] path { |
| filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)); |
| } |
| |
| .aws-topology-controls { |
| position: absolute; |
| bottom: 10px; |
| left: 10px; |
| display: flex; |
| flex-direction: column; |
| gap: 8px; |
| max-width: 800px; |
| } |
| |
| .aws-topology-controls-section { |
| display: flex; |
| flex-direction: column; |
| gap: 4px; |
| } |
| |
| .aws-topology-controls-row { |
| display: flex; |
| flex-direction: row; |
| gap: 16px; |
| align-items: flex-start; |
| } |
| |
| .aws-topology-controls-label { |
| font-size: 10px; |
| color: var(--text-secondary); |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| opacity: 0.7; |
| } |
| |
| .aws-topology-controls-buttons { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 5px; |
| } |
| |
| .aws-topology-controls-buttons .button { |
| flex-shrink: 0; |
| } |
| |
| |
| @media (max-width: 768px) { |
| .aws-topology-controls { |
| display: none; |
| } |
| } |
| |
| .aws-topology-legend { |
| position: absolute; |
| bottom: 10px; |
| right: 10px; |
| width: 264px; |
| |
| height: 120px; |
| |
| } |
| |
| .aws-topology-tooltip { |
| position: absolute; |
| padding: 8px 12px; |
| background: var(--surface-bg); |
| border: 1px solid var(--border-color); |
| border-radius: 4px; |
| font-size: 12px; |
| color: var(--text-color); |
| pointer-events: none; |
| opacity: 0; |
| transition: opacity 0.2s; |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| z-index: 1000; |
| display: flex; |
| flex-direction: column; |
| gap: 2px; |
| } |
| |
| .aws-topology-tooltip.visible { |
| opacity: 1; |
| } |
| |
| .tooltip-label { |
| font-size: 14px; |
| font-weight: 600; |
| color: var(--text-color); |
| } |
| |
| .tooltip-bandwidth { |
| font-size: 11px; |
| color: var(--text-secondary); |
| } |
| |
| .aws-topology-bottleneck { |
| position: absolute; |
| top: 10px; |
| right: 10px; |
| text-align: right; |
| opacity: 0; |
| transition: opacity 0.3s ease; |
| } |
| |
| .aws-topology-bottleneck.visible { |
| opacity: 1; |
| } |
| |
| .bottleneck-label { |
| font-size: 11px; |
| color: var(--text-secondary); |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| margin-bottom: 4px; |
| text-shadow: |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg); |
| } |
| |
| .bottleneck-value { |
| font-size: 24px; |
| font-weight: 700; |
| color: var(--primary-color); |
| line-height: 1; |
| margin-bottom: 2px; |
| text-shadow: |
| 0 0 10px var(--page-bg), |
| 0 0 10px var(--page-bg), |
| 0 0 10px var(--page-bg), |
| 0 0 10px var(--page-bg); |
| } |
| |
| .bottleneck-unit { |
| font-size: 12px; |
| color: var(--text-secondary); |
| font-weight: 500; |
| text-shadow: |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg); |
| } |
| |
| .bottleneck-path { |
| font-size: 10px; |
| color: var(--text-secondary); |
| font-style: italic; |
| margin-bottom: 6px; |
| opacity: 0.8; |
| text-shadow: |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg); |
| } |
| |
| .bottleneck-efficiency { |
| display: flex; |
| flex-direction: column; |
| align-items: flex-end; |
| margin-top: 12px; |
| margin-bottom: 4px; |
| } |
| |
| .efficiency-label { |
| font-size: 9px; |
| color: var(--text-secondary); |
| font-weight: 500; |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| margin-top: 2px; |
| text-shadow: |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg); |
| } |
| |
| .efficiency-value { |
| font-size: 20px; |
| font-weight: 700; |
| color: var(--primary-color); |
| line-height: 1; |
| text-shadow: |
| 0 0 10px var(--page-bg), |
| 0 0 10px var(--page-bg), |
| 0 0 10px var(--page-bg), |
| 0 0 10px var(--page-bg); |
| } |
| |
| .real-bandwidths { |
| margin-top: 12px; |
| padding-top: 12px; |
| border-top: 1px solid var(--border-color); |
| opacity: 0.9; |
| } |
| |
| .real-bandwidths-title { |
| font-size: 10px; |
| color: var(--text-secondary); |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| margin-bottom: 8px; |
| text-shadow: |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg); |
| } |
| |
| .real-bandwidths-content { |
| font-size: 11px; |
| color: var(--text-color); |
| line-height: 1.4; |
| text-shadow: |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg), |
| 0 0 8px var(--page-bg); |
| } |
| |
| .real-bandwidths-content .bandwidth-item { |
| margin-bottom: 4px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| .real-bandwidths-content .bandwidth-label { |
| font-size: 10px; |
| color: var(--text-secondary); |
| opacity: 0.8; |
| } |
| |
| .real-bandwidths-content .bandwidth-value { |
| font-size: 11px; |
| font-weight: 600; |
| color: var(--primary-color); |
| } |
| |
| |
| #real-bandwidth-toggle { |
| appearance: none; |
| width: 16px; |
| height: 16px; |
| border: 2px solid var(--border-color); |
| border-radius: 3px; |
| background-color: var(--page-bg); |
| cursor: pointer; |
| position: relative; |
| transition: all 0.2s ease; |
| margin-right: 8px; |
| } |
| |
| #real-bandwidth-toggle:hover { |
| border-color: var(--primary-color); |
| } |
| |
| #real-bandwidth-toggle:focus { |
| outline: none; |
| border-color: var(--primary-color); |
| box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1); |
| } |
| |
| #real-bandwidth-toggle:checked { |
| background-color: var(--primary-color); |
| border-color: var(--primary-color); |
| } |
| |
| #real-bandwidth-toggle:checked::before { |
| content: ''; |
| position: absolute; |
| top: 1px; |
| left: 4px; |
| width: 4px; |
| height: 8px; |
| border: solid var(--on-primary); |
| border-width: 0 2px 2px 0; |
| transform: rotate(45deg); |
| } |
| |
| #real-bandwidth-toggle:disabled { |
| opacity: 0.6; |
| cursor: not-allowed; |
| } |
| </style> |
|
|
| <script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/3.2.5/svg.min.js"></script> |
| <script> |
| (function () { |
| |
| const instanceId = 'aws-topology-' + Math.random().toString(36).substr(2, 9); |
| |
| |
| const scriptEl = document.currentScript; |
| |
| |
| let wrapperEl = null; |
| const findWrapper = () => { |
| if (!wrapperEl) { |
| wrapperEl = scriptEl.previousElementSibling; |
| if (!wrapperEl || !wrapperEl.classList.contains('aws-topology-wrapper')) { |
| |
| const allWrappers = document.querySelectorAll('.aws-topology-wrapper'); |
| for (const wrapper of allWrappers) { |
| if (!wrapper.dataset.mounted) { |
| wrapperEl = wrapper; |
| break; |
| } |
| } |
| } |
| if (wrapperEl && !wrapperEl.dataset.mounted) { |
| wrapperEl.dataset.mounted = 'true'; |
| } |
| } |
| return wrapperEl; |
| }; |
| |
| |
| const getElement = (className) => { |
| const wrapper = findWrapper(); |
| return wrapper ? wrapper.querySelector('.' + className) : null; |
| }; |
| |
| |
| |
| |
| const CONFIG = { |
| |
| viewbox: { |
| width: 2400, |
| get height() { |
| |
| const singleSystemHeight = this.parent.gaps.topMargin + |
| this.parent.sizes.cpu + |
| this.parent.gaps.cpuToPcie + |
| this.parent.sizes.pcie.height + |
| this.parent.gaps.pcieToGpu + |
| this.parent.sizes.gpu.height + |
| this.parent.gaps.gpuToNvswitch + |
| this.parent.sizes.nvswitch.height + |
| this.parent.gaps.bottomMargin; |
| |
| return singleSystemHeight * this.parent.systemCount + |
| this.parent.gaps.systemGap * (this.parent.systemCount - 1); |
| }, |
| parent: null |
| }, |
| |
| |
| systemCount: 2, |
| ensembleCount: 2, |
| groupCount: 4, |
| nvswitchCount: 2, |
| |
| |
| sizes: { |
| cpu: 80, |
| get pcie() { return { width: this.cpu * 0.75, height: this.cpu * 0.625 * 3 }; }, |
| get gpu() { return { width: this.cpu, height: this.cpu }; }, |
| get efa() { return { width: 60, height: 30 }; }, |
| get nvme() { return { width: 60, height: 30 }; }, |
| get nvswitch() { return { width: 100, height: 60 }; } |
| }, |
| |
| |
| gaps: { |
| topMargin: 0, |
| cpuToPcie: 80, |
| pcieToGpu: 80, |
| gpuToNvswitch: 200, |
| bottomMargin: 40, |
| horizontal: 230, |
| ensembleGap: 50, |
| systemGap: 30, |
| connectionOffset: 15 |
| }, |
| |
| |
| layout: { |
| pcieOffsetX: -40, |
| efaNvmeOffsetX: 60, |
| efaOffsetY: -30, |
| nvmeOffsetY: 30, |
| groupPadding: 20 |
| }, |
| |
| |
| debug: { |
| showPhantoms: false |
| }, |
| |
| |
| paths: { |
| 'cpu-gpu': { |
| label: 'CPU ⟷ GPU', |
| requiredEnsembles: 1, |
| requiredSystems: 1, |
| nodes: ['cpu', 'pcie', 'gpu'], |
| links: [ |
| { from: 'cpu', to: 'pcie' }, |
| { from: 'pcie', to: 'gpu' } |
| ] |
| }, |
| 'gpu-gpu-cpu': { |
| label: 'GPU ⟷ GPU via CPU', |
| requiredEnsembles: 1, |
| requiredSystems: 1, |
| nodes: ['gpu', 'pcie', 'cpu', 'pcie-1', 'gpu-1'], |
| links: [ |
| { from: 'pcie', to: 'gpu' }, |
| { from: 'pcie', to: 'cpu' }, |
| { from: 'cpu', to: 'pcie' }, |
| { from: 'cpu', to: 'pcie-1' }, |
| { from: 'pcie-1', to: 'gpu-1' } |
| ] |
| }, |
| 'gpu-gpu-nvswitch': { |
| label: 'GPU ⟷ GPU via NVSwitch', |
| requiredEnsembles: 1, |
| requiredSystems: 1, |
| nodes: ['gpu', 'nvswitch-0', 'gpu-1'], |
| links: [ |
| { from: 'gpu', to: 'nvswitch-0' }, |
| { from: 'gpu-1', to: 'nvswitch-0' } |
| ] |
| }, |
| 'gpu-gpu-efa': { |
| label: 'GPU ⟷ GPU via EFA', |
| requiredEnsembles: 1, |
| requiredSystems: 1, |
| nodes: ['gpu', 'pcie', 'efa', 'efa-1', 'pcie-1', 'gpu-1'], |
| links: [ |
| { from: 'pcie', to: 'gpu' }, |
| { from: 'efa', to: 'pcie-efa-phantom' }, |
| { from: 'efa', to: 'efa-external' }, |
| { from: 'efa-1', to: 'efa-external-1' }, |
| { from: 'efa-1', to: 'pcie-efa-phantom-1' }, |
| { from: 'pcie-1', to: 'gpu-1' } |
| ] |
| }, |
| 'gpu-gpu-efa-intranode': { |
| label: 'GPU ⟷ GPU via EFA', |
| requiredEnsembles: 2, |
| requiredSystems: 1, |
| nodes: ['gpu', 'pcie', 'efa', 'efa-4', 'pcie-4', 'gpu-4'], |
| links: [ |
| { from: 'pcie', to: 'gpu' }, |
| { from: 'efa', to: 'pcie-efa-phantom' }, |
| { from: 'efa', to: 'efa-external' }, |
| { from: 'efa-external', to: 'efa-external-4' }, |
| { from: 'efa-4', to: 'efa-external-4' }, |
| { from: 'efa-4', to: 'pcie-efa-phantom-4' }, |
| { from: 'pcie-4', to: 'gpu-4' } |
| ] |
| }, |
| 'gpu-gpu-efa-internode': { |
| label: 'GPU ⟷ GPU via EFA', |
| requiredEnsembles: 2, |
| requiredSystems: 2, |
| nodes: ['gpu', 'pcie', 'efa', 'efa-8', 'pcie-8', 'gpu-8'], |
| links: [ |
| { from: 'pcie', to: 'gpu' }, |
| { from: 'efa', to: 'pcie-efa-phantom' }, |
| { from: 'efa', to: 'efa-external' }, |
| { from: 'efa-external', to: 'efa-external-8' }, |
| { from: 'efa-8', to: 'efa-external-8' }, |
| { from: 'efa-8', to: 'pcie-efa-phantom-8' }, |
| { from: 'pcie-8', to: 'gpu-8' } |
| ] |
| }, |
| 'gpu-storage': { |
| label: 'GPU ⟷ Storage', |
| requiredEnsembles: 1, |
| requiredSystems: 1, |
| nodes: ['gpu', 'pcie', 'nvme'], |
| links: [ |
| { from: 'pcie', to: 'gpu' }, |
| { from: 'nvme', to: 'pcie-nvme-phantom' }, |
| { from: 'pcie', to: 'pcie-nvme-phantom' } |
| ] |
| }, |
| 'cpu-storage': { |
| label: 'CPU ⟷ Storage', |
| requiredEnsembles: 1, |
| requiredSystems: 1, |
| nodes: ['cpu', 'pcie', 'nvme'], |
| links: [ |
| { from: 'cpu', to: 'pcie' }, |
| { from: 'nvme', to: 'pcie-nvme-phantom' }, |
| { from: 'pcie', to: 'pcie-nvme-phantom' } |
| ] |
| }, |
| 'gpu-cpu-storage': { |
| label: 'GPU ⟷ Storage via CPU', |
| requiredEnsembles: 1, |
| requiredSystems: 1, |
| nodes: ['cpu', 'gpu', 'pcie', 'nvme'], |
| links: [ |
| { from: 'cpu', to: 'pcie' }, |
| { from: 'pcie', to: 'gpu' }, |
| { from: 'nvme', to: 'pcie-nvme-phantom' }, |
| { from: 'pcie', to: 'pcie-nvme-phantom' } |
| ] |
| } |
| }, |
| |
| |
| |
| bandwidths: [ |
| { speed: '900GB/s', label: 'NVLink 4.0', width: 9 }, |
| { speed: '64GB/s', label: 'PCIe Gen5', width: 6 }, |
| { speed: '16GB/s', label: 'PCIe Gen4', width: 3 }, |
| { speed: '12.5GB/s', label: 'EFA Link', width: 1.25 } |
| ], |
| |
| |
| groupLinks: [ |
| { from: 'cpu', to: 'pcie', bandwidth: '16GB/s', type: 'cpu', fromSide: 'bottom', toSide: 'top', multiLink: true }, |
| { from: 'efa', to: 'pcie-efa-phantom', bandwidth: '16GB/s', type: 'network', fromSide: 'left', toSide: 'right', stacked: 4 }, |
| { from: 'nvme', to: 'pcie-nvme-phantom', bandwidth: '16GB/s', type: 'storage', fromSide: 'left', toSide: 'right' }, |
| { from: 'pcie', to: 'gpu', bandwidth: '64GB/s', type: 'gpu', fromSide: 'bottom', toSide: 'top' }, |
| { from: 'efa', to: 'efa-external', bandwidth: '12.5GB/s', type: 'network', fromSide: 'right', toSide: 'left', stacked: 4 } |
| ], |
| |
| |
| gpuNvswitchLink: { |
| bandwidth: '900GB/s', |
| type: 'gpu', |
| fromSide: 'bottom', |
| toSide: 'top' |
| }, |
| |
| |
| colors: { |
| |
| nodeFill: 'var(--page-bg)', |
| nodeStroke: 'var(--muted-color)', |
| nodeText: 'var(--text-color)', |
| nodePins: 'var(--muted-color)', |
| nodeCoreFill: 'rgba(0, 0, 0, 0.05)', |
| nodeCoreStroke: 'rgba(0, 0, 0, 0.2)', |
| |
| |
| linkColor: 'var(--primary-color)', |
| linkCircleBorder: 'rgba(0, 0, 0, 0.1)', |
| |
| |
| groupBorder: 'var(--border-color)', |
| |
| |
| phantomFill: 'rgba(255, 0, 255, 0.2)', |
| phantomStroke: 'magenta' |
| }, |
| |
| |
| realBandwidths: { |
| 'cpu-gpu': '14.2GB/s', |
| 'gpu-gpu-same-node': '786GB/s', |
| 'gpu-gpu-efa-intranode': '40GB/s', |
| 'gpu-gpu-efa-internode': '40GB/s', |
| 'allreduce-same-node': '480GB/s', |
| 'all2all-same-node': '340GB/s', |
| 'allreduce-internode': '320GB/s', |
| 'alltoall-internode': '45GB/s', |
| 'gpu-storage': '14.2GB/s' |
| } |
| }; |
| |
| |
| CONFIG.viewbox.parent = CONFIG; |
| |
| |
| |
| |
| function getBandwidth(speed) { |
| const bw = CONFIG.bandwidths.find(b => b.speed === speed); |
| if (bw) { |
| |
| bw.numericValue = parseFloat(bw.speed.replace('GB/s', '')); |
| } |
| return bw; |
| } |
| |
| |
| function getLinkOffset(linkWidth) { |
| const minOffset = 5; |
| const maxOffset = 20; |
| |
| |
| const proportionalOffset = minOffset + (linkWidth / 10) * (maxOffset - minOffset); |
| return Math.min(Math.max(proportionalOffset, minOffset), maxOffset); |
| } |
| |
| |
| |
| |
| function drawEfaCrossLinks(renderer, fromId, toId, linkType = 'efa-crosslink') { |
| const phantom0 = renderer.nodes.get(fromId); |
| const phantom1 = renderer.nodes.get(toId); |
| |
| if (!phantom0 || !phantom1) return; |
| |
| const efaBw = getBandwidth('12.5GB/s'); |
| const efaColor = CONFIG.colors.linkColor; |
| |
| |
| const stackCount = 4; |
| const spacing = 6; |
| const verticalShift = -95; |
| const horizontalShift = -40; |
| const horizontalExtension = 20; |
| |
| |
| const linkIndices = Array.from({ length: stackCount }, (_, i) => i); |
| const shuffledIndices = linkIndices.sort(() => Math.random() - 0.5); |
| |
| |
| shuffledIndices.forEach(i => { |
| const offsetY = (i - (stackCount - 1) / 2) * spacing; |
| |
| const efaCrossGroup = renderer.crossLinksGroup.group(); |
| efaCrossGroup.attr('data-link-type', linkType); |
| efaCrossGroup.attr('data-from', fromId); |
| efaCrossGroup.attr('data-to', toId); |
| efaCrossGroup.attr('data-bandwidth', '12.5GB/s'); |
| efaCrossGroup.attr('data-label', 'EFA Link'); |
| |
| |
| const x0 = phantom0.x + horizontalShift; |
| const x1 = phantom1.x + horizontalShift + horizontalExtension; |
| const y0 = phantom0.y + offsetY + verticalShift; |
| const y1 = phantom1.y + offsetY + verticalShift; |
| |
| |
| efaCrossGroup.line(x0, y0, x1, y1) |
| .stroke({ color: 'rgba(0, 0, 0, 0.15)', width: efaBw.width + 1, opacity: 1 }); |
| |
| |
| efaCrossGroup.line(x0, y0, x1, y1) |
| .stroke({ color: efaColor, width: efaBw.width, opacity: 1 }); |
| |
| |
| const r = efaBw.width * 0.8; |
| const startCircle = efaCrossGroup.circle(r * 2).move(x0 - r, y0 - r); |
| startCircle.fill(efaColor); |
| startCircle.stroke({ color: CONFIG.colors.linkCircleBorder, width: 0.5 }); |
| startCircle.attr('data-link-circle', 'true'); |
| |
| const endCircle = efaCrossGroup.circle(r * 2).move(x1 - r, y1 - r); |
| endCircle.fill(efaColor); |
| endCircle.stroke({ color: CONFIG.colors.linkCircleBorder, width: 0.5 }); |
| endCircle.attr('data-link-circle', 'true'); |
| }); |
| |
| |
| const barColor = CONFIG.colors.linkColor; |
| |
| shuffledIndices.forEach(i => { |
| const offsetX = (i - (stackCount - 1) / 2) * spacing + 12; |
| const yHorizontal = phantom0.y + verticalShift + 10; |
| const yPhantom = phantom0.y + 8; |
| |
| |
| const leftBarGroup = renderer.crossLinksGroup.group(); |
| leftBarGroup.attr('data-link-type', linkType); |
| leftBarGroup.attr('data-from', fromId); |
| leftBarGroup.attr('data-to', toId); |
| leftBarGroup.attr('data-bandwidth', '12.5GB/s'); |
| leftBarGroup.attr('data-label', 'EFA Link'); |
| |
| const x0 = phantom0.x + horizontalShift + offsetX; |
| |
| |
| leftBarGroup.line(x0, yHorizontal, x0, yPhantom) |
| .stroke({ color: 'rgba(0, 0, 0, 0.15)', width: efaBw.width + 1, opacity: 1 }); |
| |
| |
| leftBarGroup.line(x0, yHorizontal, x0, yPhantom) |
| .stroke({ color: barColor, width: efaBw.width, opacity: 1 }); |
| |
| const r = efaBw.width * 0.8; |
| const bottomCircle0 = leftBarGroup.circle(r * 2).move(x0 - r, yPhantom - r); |
| bottomCircle0.fill(barColor); |
| bottomCircle0.stroke({ color: CONFIG.colors.linkCircleBorder, width: 0.5 }); |
| bottomCircle0.attr('data-link-circle', 'true'); |
| |
| |
| const rightBarGroup = renderer.crossLinksGroup.group(); |
| rightBarGroup.attr('data-link-type', linkType); |
| rightBarGroup.attr('data-from', fromId); |
| rightBarGroup.attr('data-to', toId); |
| rightBarGroup.attr('data-bandwidth', '12.5GB/s'); |
| rightBarGroup.attr('data-label', 'EFA Link'); |
| |
| const x1 = phantom1.x + horizontalShift + offsetX; |
| |
| |
| rightBarGroup.line(x1, yHorizontal, x1, yPhantom) |
| .stroke({ color: 'rgba(0, 0, 0, 0.15)', width: efaBw.width + 1, opacity: 1 }); |
| |
| |
| rightBarGroup.line(x1, yHorizontal, x1, yPhantom) |
| .stroke({ color: barColor, width: efaBw.width, opacity: 1 }); |
| |
| const bottomCircle1 = rightBarGroup.circle(r * 2).move(x1 - r, yPhantom - r); |
| bottomCircle1.fill(barColor); |
| bottomCircle1.stroke({ color: CONFIG.colors.linkCircleBorder, width: 0.5 }); |
| bottomCircle1.attr('data-link-circle', 'true'); |
| }); |
| } |
| |
| |
| |
| |
| class TopologyRenderer { |
| constructor(draw) { |
| this.draw = draw; |
| this.nodes = new Map(); |
| this.structuralGroup = draw.group(); |
| this.structuralGroup.attr('data-group', 'structural'); |
| this.baseLinksGroup = draw.group(); |
| this.baseLinksGroup.attr('data-group', 'base-links'); |
| this.linksGroup = this.baseLinksGroup; |
| this.nodesGroup = draw.group(); |
| this.phantomsGroup = draw.group(); |
| this.baseCrossLinksGroup = draw.group(); |
| this.baseCrossLinksGroup.attr('data-group', 'base-cross-links'); |
| this.crossLinksGroup = this.baseCrossLinksGroup; |
| this.activeLinksGroup = draw.group(); |
| this.activeLinksGroup.attr('data-group', 'active-links'); |
| } |
| |
| |
| |
| drawText(x, y, label, group = this.nodesGroup) { |
| if (!label) return; |
| |
| const lines = label.split('\n'); |
| lines.forEach((line, i) => { |
| group.text(line) |
| .font({ family: 'Inter, sans-serif', size: 12, weight: '600', anchor: 'middle' }) |
| .fill(CONFIG.colors.nodeText) |
| .cx(x) |
| .cy(y + (i - (lines.length - 1) / 2) * 14); |
| }); |
| } |
| |
| |
| drawPins(x, y, width, height, pinConfig = {}, targetGroup = null) { |
| const group = targetGroup || this.nodesGroup; |
| |
| const defaultConfig = { |
| pinsPerSide: width >= 80 ? 18 : 15, |
| pinLength: 3, |
| pinOffset: width / 2 + 1.5, |
| pinPadding: 12 |
| }; |
| |
| const config = { ...defaultConfig, ...pinConfig }; |
| const pinStartOffset = -width / 2 + config.pinPadding; |
| const pinEndOffset = width / 2 - config.pinPadding; |
| const pinSpacing = (pinEndOffset - pinStartOffset) / (config.pinsPerSide - 1); |
| |
| const sides = [ |
| { side: 'left', xOffset: -config.pinOffset, yOffset: 0 }, |
| { side: 'right', xOffset: config.pinOffset, yOffset: 0 }, |
| { side: 'top', xOffset: 0, yOffset: -config.pinOffset }, |
| { side: 'bottom', xOffset: 0, yOffset: config.pinOffset } |
| ]; |
| |
| sides.forEach(({ side, xOffset, yOffset }) => { |
| for (let i = 0; i < config.pinsPerSide; i++) { |
| const pinX = x + xOffset + (side === 'left' || side === 'right' ? 0 : pinStartOffset + (i * pinSpacing)); |
| const pinY = y + yOffset + (side === 'top' || side === 'bottom' ? 0 : pinStartOffset + (i * pinSpacing)); |
| |
| group.rect( |
| side === 'left' || side === 'right' ? config.pinLength : 1, |
| side === 'top' || side === 'bottom' ? config.pinLength : 1 |
| ) |
| .move( |
| pinX - (side === 'left' || side === 'right' ? config.pinLength : 1) / 2, |
| pinY - (side === 'top' || side === 'bottom' ? config.pinLength : 1) / 2 |
| ) |
| .fill(CONFIG.colors.nodePins) |
| .stroke('none'); |
| } |
| }); |
| } |
| |
| drawNode(id, x, y, width, height, label, nodeType = '') { |
| const group = this.nodesGroup.group(); |
| group.attr('id', id); |
| if (nodeType) group.attr('data-node-type', nodeType); |
| |
| group.rect(width, height) |
| .move(x - width / 2, y - height / 2) |
| .fill(CONFIG.colors.nodeFill) |
| .stroke({ color: CONFIG.colors.nodeStroke, width: 1 }) |
| .radius(4) |
| .opacity(1); |
| |
| this.drawText(x, y, label, group); |
| |
| this.nodes.set(id, { x, y, width, height }); |
| return group; |
| } |
| |
| drawNodeWithPins(id, x, y, width, height, label, pinConfig = {}, nodeType = '') { |
| const node = this.drawNode(id, x, y, width, height, label, nodeType); |
| this.drawPins(x, y, width, height, pinConfig); |
| return node; |
| } |
| |
| drawNodeWithCores(id, x, y, width, height, label, coreConfig = {}, nodeType = '') { |
| |
| const node = this.drawNode(id, x, y, width, height, label, nodeType); |
| |
| |
| const defaultCoreConfig = { |
| coresX: 2, |
| coresY: 2, |
| coreSpacing: 2, |
| coreMargin: 8 |
| }; |
| |
| const config = { ...defaultCoreConfig, ...coreConfig }; |
| |
| |
| const availableWidth = width - (config.coreMargin * 2); |
| const availableHeight = height - (config.coreMargin * 2); |
| const coreWidth = (availableWidth - (config.coreSpacing * (config.coresX - 1))) / config.coresX; |
| const coreHeight = (availableHeight - (config.coreSpacing * (config.coresY - 1))) / config.coresY; |
| |
| |
| for (let row = 0; row < config.coresY; row++) { |
| for (let col = 0; col < config.coresX; col++) { |
| const coreX = x - width / 2 + config.coreMargin + col * (coreWidth + config.coreSpacing); |
| const coreY = y - height / 2 + config.coreMargin + row * (coreHeight + config.coreSpacing); |
| |
| this.nodesGroup.rect(coreWidth, coreHeight) |
| .move(coreX, coreY) |
| .fill(CONFIG.colors.nodeCoreFill) |
| .stroke({ color: CONFIG.colors.nodeCoreStroke, width: 0.5 }) |
| .radius(1); |
| } |
| } |
| |
| return node; |
| } |
| |
| drawProcessorNode(id, x, y, width, height, label, type = 'cpu') { |
| |
| const nodeGroup = this.nodesGroup.group(); |
| nodeGroup.attr('id', id); |
| nodeGroup.attr('data-node-type', type); |
| |
| |
| nodeGroup.rect(width, height) |
| .move(x - width / 2, y - height / 2) |
| .fill(CONFIG.colors.nodeFill) |
| .stroke({ color: CONFIG.colors.nodeStroke, width: 1 }) |
| .radius(4) |
| .opacity(1); |
| |
| |
| const coreConfig = type === 'gpu' ? { coresX: 8, coresY: 8 } : { coresX: 2, coresY: 2 }; |
| const config = { coreSpacing: 2, coreMargin: 8, ...coreConfig }; |
| const availableWidth = width - (config.coreMargin * 2); |
| const availableHeight = height - (config.coreMargin * 2); |
| const coreWidth = (availableWidth - (config.coresX - 1) * config.coreSpacing) / config.coresX; |
| const coreHeight = (availableHeight - (config.coresY - 1) * config.coreSpacing) / config.coresY; |
| |
| for (let row = 0; row < config.coresY; row++) { |
| for (let col = 0; col < config.coresX; col++) { |
| const coreX = x - width / 2 + config.coreMargin + col * (coreWidth + config.coreSpacing); |
| const coreY = y - height / 2 + config.coreMargin + row * (coreHeight + config.coreSpacing); |
| |
| nodeGroup.rect(coreWidth, coreHeight) |
| .move(coreX, coreY) |
| .fill(CONFIG.colors.nodeCoreFill) |
| .stroke({ color: CONFIG.colors.nodeCoreStroke, width: 0.5 }) |
| .radius(1); |
| } |
| } |
| |
| |
| this.drawPins(x, y, width, height, {}, nodeGroup); |
| |
| |
| this.drawText(x, y, label, nodeGroup); |
| |
| |
| this.nodes.set(id, { x, y, width, height }); |
| |
| return nodeGroup; |
| } |
| |
| |
| addPinsToNode(nodeId, pinConfig = {}) { |
| const node = this.nodes.get(nodeId); |
| if (!node) return; |
| |
| |
| const svg = getElement('aws-topology-container').querySelector('svg'); |
| if (!svg) return; |
| const nodeGroup = svg.querySelector(`g[id="${nodeId}"]`); |
| if (!nodeGroup) return; |
| |
| |
| const group = this.draw.findOne(`#${nodeId}`); |
| if (!group) return; |
| |
| |
| const defaultPinConfig = { |
| pinsPerSide: node.width >= 80 ? 18 : 15, |
| sides: ['left', 'right', 'top', 'bottom'], |
| pinLength: 3, |
| pinPadding: 12 |
| }; |
| |
| const config = { ...defaultPinConfig, ...pinConfig }; |
| |
| |
| config.sides.forEach(side => { |
| let pinSpacing, pinStartOffset, pinEndOffset; |
| |
| if (side === 'left' || side === 'right') { |
| pinStartOffset = -node.height / 2 + config.pinPadding; |
| pinEndOffset = node.height / 2 - config.pinPadding; |
| pinSpacing = (pinEndOffset - pinStartOffset) / (config.pinsPerSide - 1); |
| } else { |
| pinStartOffset = -node.width / 2 + config.pinPadding; |
| pinEndOffset = node.width / 2 - config.pinPadding; |
| pinSpacing = (pinEndOffset - pinStartOffset) / (config.pinsPerSide - 1); |
| } |
| |
| for (let i = 0; i < config.pinsPerSide; i++) { |
| let pinX, pinY; |
| |
| if (side === 'left') { |
| pinX = node.x - node.width / 2 - 1.5; |
| pinY = node.y + pinStartOffset + (i * pinSpacing); |
| } else if (side === 'right') { |
| pinX = node.x + node.width / 2 + 1.5; |
| pinY = node.y + pinStartOffset + (i * pinSpacing); |
| } else if (side === 'top') { |
| pinX = node.x + pinStartOffset + (i * pinSpacing); |
| pinY = node.y - node.height / 2 - 1.5; |
| } else if (side === 'bottom') { |
| pinX = node.x + pinStartOffset + (i * pinSpacing); |
| pinY = node.y + node.height / 2 + 1.5; |
| } |
| |
| |
| group.rect( |
| side === 'left' || side === 'right' ? config.pinLength : 1, |
| side === 'top' || side === 'bottom' ? config.pinLength : 1 |
| ) |
| .move( |
| pinX - (side === 'left' || side === 'right' ? config.pinLength : 1) / 2, |
| pinY - (side === 'top' || side === 'bottom' ? config.pinLength : 1) / 2 |
| ) |
| .fill(CONFIG.colors.nodePins) |
| .stroke('none'); |
| } |
| }); |
| } |
| |
| |
| drawStackedNode(id, x, y, width, height, label, stackCount, nodeType = '') { |
| const group = this.nodesGroup.group(); |
| group.attr('id', id); |
| if (nodeType) group.attr('data-node-type', nodeType); |
| |
| for (let i = stackCount - 1; i >= 0; i--) { |
| const offsetY = i * 3; |
| group.rect(width, height) |
| .move(x - width / 2, y - height / 2 + offsetY) |
| .fill(CONFIG.colors.nodeFill) |
| .stroke({ color: CONFIG.colors.nodeStroke, width: 1 }) |
| .radius(4) |
| .opacity(1); |
| } |
| |
| this.drawText(x, y, label, group); |
| |
| this.nodes.set(id, { x, y, width, height }); |
| return group; |
| } |
| |
| |
| drawStorageNode(id, x, y, width, height, label, nodeType = 'storage') { |
| const group = this.nodesGroup.group(); |
| group.attr('id', id); |
| if (nodeType) group.attr('data-node-type', nodeType); |
| |
| |
| group.rect(width, height) |
| .move(x - width / 2, y - height / 2) |
| .fill(CONFIG.colors.nodeFill) |
| .stroke({ color: CONFIG.colors.nodeStroke, width: 1 }) |
| .radius(4) |
| .opacity(1); |
| |
| |
| const internalRectWidth = 5; |
| const internalRectHeight = height * .6; |
| const internalRectX = x - width / 2; |
| const internalRectY = y - height / 2 + height * .20; |
| |
| group.rect(internalRectWidth, internalRectHeight) |
| .move(internalRectX, internalRectY) |
| .fill(CONFIG.colors.nodeFill) |
| .stroke({ color: '#ffffff', width: 1 }) |
| .opacity(.4) |
| .radius(0); |
| |
| this.drawText(x, y, label, group); |
| |
| this.nodes.set(id, { x, y, width, height }); |
| return group; |
| } |
| |
| |
| drawBackground(id, x, y, width, height) { |
| const bg = this.linksGroup.rect(width, height) |
| .move(x - width / 2, y - height / 2) |
| .fill('none') |
| .stroke({ |
| color: CONFIG.colors.groupBorder, |
| width: 2, |
| dasharray: '5,5' |
| }) |
| .radius(8); |
| |
| this.nodes.set(id, { x, y, width, height }); |
| return bg; |
| } |
| |
| |
| getPoint(nodeId, side, offset = CONFIG.gaps.connectionOffset, linkIndex = 0, totalLinks = 1) { |
| const node = this.nodes.get(nodeId); |
| if (!node) return { x: 0, y: 0 }; |
| |
| const points = { |
| top: { x: node.x, y: node.y - node.height / 2 - offset }, |
| right: { x: node.x + node.width / 2 + offset, y: node.y }, |
| bottom: { x: node.x, y: node.y + node.height / 2 + offset }, |
| left: { x: node.x - node.width / 2 - offset, y: node.y } |
| }; |
| |
| let basePoint = points[side] || { x: node.x, y: node.y }; |
| |
| |
| if (totalLinks > 1) { |
| const isVerticalAnchor = (side === 'top' || side === 'bottom'); |
| |
| |
| const spacing = isVerticalAnchor ? node.width / (totalLinks + 1) : node.height / (totalLinks + 1); |
| const linkOffset = (linkIndex - (totalLinks - 1) / 2) * spacing; |
| |
| |
| if (isVerticalAnchor) { |
| basePoint.x += linkOffset; |
| } else { |
| basePoint.y += linkOffset; |
| } |
| } |
| |
| return basePoint; |
| } |
| |
| |
| drawLink(fromId, toId, color, width, fromSide = 'right', toSide = 'left', offset = CONFIG.gaps.connectionOffset, fromLinkIndex = 0, fromTotalLinks = 1, toLinkIndex = 0, toTotalLinks = 1, linkType = '', bandwidth = '') { |
| const start = this.getPoint(fromId, fromSide, offset, fromLinkIndex, fromTotalLinks); |
| const end = this.getPoint(toId, toSide, offset, toLinkIndex, toTotalLinks); |
| |
| const group = this.linksGroup.group(); |
| if (linkType) group.attr('data-link-type', linkType); |
| if (bandwidth) { |
| group.attr('data-bandwidth', bandwidth); |
| |
| const bwConfig = getBandwidth(bandwidth); |
| if (bwConfig && bwConfig.label) { |
| group.attr('data-label', bwConfig.label); |
| } |
| } |
| group.attr('data-from', fromId); |
| group.attr('data-to', toId); |
| |
| |
| const isVerticalConnection = fromSide === 'bottom' && toSide === 'top'; |
| |
| if (isVerticalConnection) { |
| |
| const curvature = 45; |
| |
| |
| const pathData = `M ${start.x} ${start.y} C ${start.x} ${start.y + curvature}, ${end.x} ${end.y - curvature}, ${end.x} ${end.y}`; |
| |
| |
| group.path(pathData) |
| .fill('none') |
| .stroke({ color: 'rgba(0, 0, 0, 0.15)', width: width + 1, opacity: 1 }); |
| |
| |
| group.path(pathData) |
| .fill('none') |
| .stroke({ color, width, opacity: 1 }); |
| } else { |
| |
| |
| group.line(start.x, start.y, end.x, end.y) |
| .stroke({ color: 'rgba(0, 0, 0, 0.15)', width: width + 1, opacity: 1 }); |
| |
| |
| group.line(start.x, start.y, end.x, end.y).stroke({ color, width, opacity: 1 }); |
| } |
| |
| const r = width * 0.8; |
| const startCircle = group.circle(r * 2).move(start.x - r, start.y - r); |
| startCircle.fill(color); |
| startCircle.stroke({ color: CONFIG.colors.linkCircleBorder, width: 0.5 }); |
| startCircle.attr('data-link-circle', 'true'); |
| |
| const endCircle = group.circle(r * 2).move(end.x - r, end.y - r); |
| endCircle.fill(color); |
| endCircle.stroke({ color: CONFIG.colors.linkCircleBorder, width: 0.5 }); |
| endCircle.attr('data-link-circle', 'true'); |
| |
| return group; |
| } |
| |
| |
| drawStackedLinks(fromId, toId, count, color, width, fromSide = 'right', toSide = 'left', offset = CONFIG.gaps.connectionOffset, linkType = '', bandwidth = '') { |
| const fromNode = this.nodes.get(fromId); |
| const toNode = this.nodes.get(toId); |
| if (!fromNode || !toNode) return []; |
| |
| const isHorizontal = (fromSide === 'left' || fromSide === 'right') && |
| (toSide === 'left' || toSide === 'right'); |
| const spacing = isHorizontal |
| ? Math.min(fromNode.height, toNode.height) / (count + 1) |
| : Math.min(fromNode.width, toNode.width) / (count + 1); |
| |
| const arrows = []; |
| for (let i = 0; i < count; i++) { |
| const stackOffset = (i - (count - 1) / 2) * spacing; |
| let start = this.getPoint(fromId, fromSide, offset); |
| let end = this.getPoint(toId, toSide, offset); |
| |
| if (isHorizontal) { |
| start.y += stackOffset; |
| end.y += stackOffset; |
| } else { |
| start.x += stackOffset; |
| end.x += stackOffset; |
| } |
| |
| const group = this.linksGroup.group(); |
| if (linkType) group.attr('data-link-type', linkType); |
| if (bandwidth) { |
| group.attr('data-bandwidth', bandwidth); |
| |
| const bwConfig = getBandwidth(bandwidth); |
| if (bwConfig && bwConfig.label) { |
| group.attr('data-label', bwConfig.label); |
| } |
| } |
| group.attr('data-from', fromId); |
| group.attr('data-to', toId); |
| |
| |
| group.line(start.x, start.y, end.x, end.y) |
| .stroke({ color: 'rgba(0, 0, 0, 0.15)', width: width + 1, opacity: 1 }); |
| |
| |
| group.line(start.x, start.y, end.x, end.y).stroke({ color, width, opacity: 1 }); |
| |
| const r = width * 0.8; |
| const startCircle = group.circle(r * 2).move(start.x - r, start.y - r); |
| startCircle.fill(color); |
| startCircle.stroke({ color: CONFIG.colors.linkCircleBorder, width: 1 }); |
| startCircle.attr('data-link-circle', 'true'); |
| |
| const endCircle = group.circle(r * 2).move(end.x - r, end.y - r); |
| endCircle.fill(color); |
| endCircle.stroke({ color: CONFIG.colors.linkCircleBorder, width: 1 }); |
| endCircle.attr('data-link-circle', 'true'); |
| |
| arrows.push(group); |
| } |
| return arrows; |
| } |
| |
| |
| addPhantom(id, x, y, width = 30, height = 30) { |
| this.nodes.set(id, { x, y, width, height }); |
| |
| |
| if (CONFIG.debug.showPhantoms) { |
| this.phantomsGroup.rect(width, height) |
| .move(x - width / 2, y - height / 2) |
| .fill('rgba(255, 0, 255, 0.2)') |
| .stroke({ color: 'magenta', width: 2, dasharray: '4,4' }) |
| .radius(4); |
| |
| |
| this.phantomsGroup.text(id) |
| .move(x, y) |
| .font({ |
| family: 'system-ui, -apple-system, sans-serif', |
| size: 10, |
| anchor: 'middle', |
| fill: 'magenta', |
| weight: 'bold' |
| }) |
| .dy(-height / 2 - 5); |
| } |
| } |
| } |
| |
| |
| |
| |
| function drawGroup(renderer, globalIndex, localIndex, x, cpuY, nvswitchY, cpuId = 'cpu') { |
| const s = CONFIG.sizes; |
| const g = CONFIG.gaps; |
| const l = CONFIG.layout; |
| const suffix = globalIndex > 0 ? `-${globalIndex}` : ''; |
| |
| |
| const pcieY = cpuY + s.cpu / 2 + g.cpuToPcie + s.pcie.height / 2; |
| const gpuY = pcieY + s.pcie.height / 2 + g.pcieToGpu + s.gpu.height / 2; |
| |
| |
| const pcieX = x + l.pcieOffsetX; |
| const efaX = x + l.efaNvmeOffsetX; |
| const nvmeX = x + l.efaNvmeOffsetX; |
| const efaY = pcieY + l.efaOffsetY; |
| const nvmeY = pcieY + l.nvmeOffsetY; |
| |
| |
| const nodeBounds = [ |
| { x: pcieX, y: pcieY, width: s.pcie.width, height: s.pcie.height }, |
| { x: efaX, y: efaY, width: s.efa.width, height: s.efa.height }, |
| { x: nvmeX, y: nvmeY, width: s.nvme.width, height: s.nvme.height }, |
| { x: pcieX, y: gpuY, width: s.gpu.width, height: s.gpu.height } |
| ]; |
| |
| let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; |
| nodeBounds.forEach(node => { |
| minX = Math.min(minX, node.x - node.width / 2); |
| maxX = Math.max(maxX, node.x + node.width / 2); |
| minY = Math.min(minY, node.y - node.height / 2); |
| maxY = Math.max(maxY, node.y + node.height / 2); |
| }); |
| |
| const bgWidth = maxX - minX + l.groupPadding * 2; |
| const bgHeight = maxY - minY + l.groupPadding * 2; |
| const bgCenterX = (minX + maxX) / 2; |
| const bgCenterY = (minY + maxY) / 2; |
| |
| |
| renderer.drawBackground(`group${suffix}`, bgCenterX, bgCenterY, bgWidth, bgHeight); |
| |
| |
| renderer.drawNode(`pcie${suffix}`, pcieX, pcieY, s.pcie.width, s.pcie.height, 'PCIe\nSwitch', 'network'); |
| renderer.addPinsToNode(`pcie${suffix}`, { |
| pinsPerSide: 6, |
| sides: ['left', 'right'], |
| pinLength: 3, |
| pinPadding: 8 |
| }); |
| renderer.drawStackedNode(`efa${suffix}`, efaX, efaY, s.efa.width, s.efa.height, 'EFA', 4, 'network'); |
| renderer.drawStorageNode(`nvme${suffix}`, nvmeX, nvmeY, s.nvme.width, s.nvme.height, 'NVMe', 'storage'); |
| renderer.drawProcessorNode(`gpu${suffix}`, pcieX, gpuY, s.gpu.width, s.gpu.height, 'GPU', 'gpu'); |
| |
| |
| |
| const pcieLeftEdge = pcieX; |
| const pciePhantomX = pcieLeftEdge; |
| renderer.addPhantom(`pcie-efa-phantom${suffix}`, pciePhantomX, efaY, s.pcie.width, s.efa.height); |
| renderer.addPhantom(`pcie-nvme-phantom${suffix}`, pciePhantomX, nvmeY, s.pcie.width, s.nvme.height); |
| |
| |
| const efaRightEdge = efaX + s.efa.width / 2; |
| const efaPhantomX = efaRightEdge + g.connectionOffset + s.efa.width / 2 + 20; |
| renderer.addPhantom(`efa-external${suffix}`, efaPhantomX, efaY, s.efa.width, s.efa.height); |
| |
| |
| CONFIG.groupLinks.forEach(linkDef => { |
| const bw = getBandwidth(linkDef.bandwidth); |
| const fromId = linkDef.from === 'cpu' ? cpuId : `${linkDef.from}${suffix}`; |
| const toId = `${linkDef.to}${suffix}`; |
| const color = CONFIG.colors.linkColor; |
| const offset = getLinkOffset(bw.width); |
| |
| if (linkDef.stacked) { |
| renderer.drawStackedLinks(fromId, toId, linkDef.stacked, color, bw.width, linkDef.fromSide, linkDef.toSide, offset, linkDef.type, linkDef.bandwidth); |
| } else if (linkDef.multiLink) { |
| |
| renderer.drawLink(fromId, toId, color, bw.width, linkDef.fromSide, linkDef.toSide, offset, localIndex, CONFIG.groupCount, 0, 1, linkDef.type, linkDef.bandwidth); |
| } else { |
| renderer.drawLink(fromId, toId, color, bw.width, linkDef.fromSide, linkDef.toSide, offset, 0, 1, 0, 1, linkDef.type, linkDef.bandwidth); |
| } |
| }); |
| } |
| |
| |
| |
| |
| function drawEnsemble(renderer, ensembleGlobalIndex, centerX, cpuY, nvswitchY) { |
| const cpuId = ensembleGlobalIndex === 0 ? 'cpu' : `cpu-${ensembleGlobalIndex}`; |
| |
| |
| renderer.drawProcessorNode(cpuId, centerX, cpuY, CONFIG.sizes.cpu, CONFIG.sizes.cpu, 'CPU', 'cpu'); |
| |
| |
| for (let i = 0; i < CONFIG.groupCount; i++) { |
| const groupGlobalIndex = ensembleGlobalIndex * CONFIG.groupCount + i; |
| const offsetX = (i - (CONFIG.groupCount - 1) / 2) * CONFIG.gaps.horizontal; |
| drawGroup(renderer, groupGlobalIndex, i, centerX + offsetX, cpuY, nvswitchY, cpuId); |
| } |
| |
| |
| for (let i = 0; i < CONFIG.nvswitchCount; i++) { |
| const nvswitchGlobalIndex = ensembleGlobalIndex * CONFIG.nvswitchCount + i; |
| const groupPairStart = i * 2; |
| const groupPairEnd = groupPairStart + 1; |
| |
| const group1X = centerX + ((groupPairStart - (CONFIG.groupCount - 1) / 2) * CONFIG.gaps.horizontal); |
| const group2X = centerX + ((groupPairEnd - (CONFIG.groupCount - 1) / 2) * CONFIG.gaps.horizontal); |
| const nvswitchX = (group1X + group2X) / 2; |
| |
| renderer.drawNode(`nvswitch-${nvswitchGlobalIndex}`, nvswitchX, nvswitchY, |
| CONFIG.sizes.nvswitch.width, CONFIG.sizes.nvswitch.height, 'NVSwitch', 'network'); |
| |
| |
| renderer.addPinsToNode(`nvswitch-${nvswitchGlobalIndex}`, { |
| pinsPerSide: 6, |
| sides: ['left', 'right'], |
| pinLength: 3, |
| pinPadding: 8 |
| }); |
| } |
| |
| } |
| |
| |
| |
| |
| |
| function drawSystem(renderer, systemIndex, systemCenterX, cpuY, nvswitchY, ensembleCount = 1, systemCount = 1, shouldDrawEfaCrossLinks = false) { |
| |
| const ensemblesToShow = (systemCount === 2) ? 1 : ensembleCount; |
| |
| |
| const ensembleWidth = CONFIG.groupCount * CONFIG.gaps.horizontal; |
| |
| |
| const systemWidth = ensemblesToShow * ensembleWidth + (ensemblesToShow - 1) * CONFIG.gaps.ensembleGap; |
| const systemHeight = CONFIG.sizes.cpu + CONFIG.gaps.cpuToPcie + CONFIG.sizes.pcie.height + |
| CONFIG.gaps.pcieToGpu + CONFIG.sizes.gpu.height + |
| CONFIG.gaps.gpuToNvswitch + CONFIG.sizes.nvswitch.height; |
| |
| const systemBgCenterY = cpuY + (nvswitchY - cpuY) / 2; |
| const systemPadding = 30; |
| |
| |
| const isInternode = systemCount === 2; |
| const extraWidth = isInternode && ensembleCount === 2 ? 210 : 0; |
| |
| |
| if (ensembleCount === 2) { |
| |
| const totalWidth = systemWidth + systemPadding * 2 + extraWidth; |
| const rectX = systemCenterX - totalWidth / 2; |
| const rectY = systemBgCenterY - (systemHeight + systemPadding * 2) / 2; |
| const rectWidth = totalWidth; |
| const rectHeight = systemHeight + systemPadding * 2; |
| |
| renderer.structuralGroup.rect(rectWidth, rectHeight) |
| .move(rectX, rectY) |
| .fill('none') |
| .stroke({ |
| color: CONFIG.colors.groupBorder, |
| width: 3, |
| opacity: 1 |
| }) |
| .radius(12) |
| .attr('data-group-border', 'node'); |
| |
| |
| renderer.nodesGroup.text('Node') |
| .move(rectX + 35, rectY + 40) |
| .font({ |
| size: 32, |
| family: 'var(--font-family)', |
| weight: 900, |
| fill: CONFIG.colors.textSecondary |
| }) |
| .opacity(0.8); |
| |
| |
| if (isInternode) { |
| const numaPadding = 15; |
| const numaWidth = extraWidth - 15; |
| const numaHeight = systemHeight; |
| |
| const numaX = rectX + rectWidth - extraWidth; |
| const numaY = systemBgCenterY - (numaHeight + numaPadding * 2) / 2; |
| const numaRectWidth = numaWidth; |
| const numaRectHeight = numaHeight + numaPadding * 2; |
| |
| |
| renderer.structuralGroup.rect(numaRectWidth, numaRectHeight) |
| .move(numaX, numaY) |
| .fill('none') |
| .stroke({ |
| color: CONFIG.colors.groupBorder, |
| width: 3, |
| opacity: 1 |
| }) |
| .radius(8) |
| .attr('data-group-border', 'numa'); |
| |
| |
| renderer.nodesGroup.text('NUMA 1') |
| .move(numaX + numaRectWidth - 15, numaY + 15) |
| .font({ |
| size: 14, |
| family: 'var(--font-family)', |
| weight: 600, |
| fill: CONFIG.colors.textSecondary, |
| anchor: 'end' |
| }) |
| .opacity(0.7); |
| |
| |
| renderer.nodesGroup.text('...') |
| .move(numaX + numaRectWidth / 2, numaY + numaRectHeight / 2) |
| .font({ |
| size: 40, |
| family: 'var(--font-family)', |
| weight: 900, |
| fill: CONFIG.colors.textSecondary, |
| anchor: 'middle' |
| }) |
| .opacity(0.6); |
| } |
| } |
| |
| |
| for (let ensIndex = 0; ensIndex < ensemblesToShow; ensIndex++) { |
| const ensembleGlobalIndex = systemIndex * CONFIG.ensembleCount + ensIndex; |
| |
| |
| let ensembleCenterX; |
| if (isInternode) { |
| |
| const rectX = systemCenterX - (systemWidth + systemPadding * 2 + extraWidth) / 2; |
| ensembleCenterX = rectX + systemPadding + ensembleWidth / 2; |
| } else { |
| |
| const ensembleOffsetX = (ensIndex - (ensemblesToShow - 1) / 2) * (ensembleWidth + CONFIG.gaps.ensembleGap); |
| ensembleCenterX = systemCenterX + ensembleOffsetX; |
| } |
| |
| drawEnsemble(renderer, ensembleGlobalIndex, ensembleCenterX, cpuY, nvswitchY); |
| } |
| |
| |
| if (ensembleCount === 2) { |
| for (let ensIndex = 0; ensIndex < ensemblesToShow; ensIndex++) { |
| let ensembleCenterX; |
| if (isInternode) { |
| |
| const rectX = systemCenterX - (systemWidth + systemPadding * 2 + extraWidth) / 2; |
| ensembleCenterX = rectX + systemPadding + ensembleWidth / 2; |
| } else { |
| |
| const ensembleOffsetX = (ensIndex - (ensemblesToShow - 1) / 2) * (ensembleWidth + CONFIG.gaps.ensembleGap); |
| ensembleCenterX = systemCenterX + ensembleOffsetX; |
| } |
| |
| |
| const numaWidth = ensembleWidth; |
| const numaHeight = systemHeight; |
| const numaPadding = 15; |
| |
| const numaX = ensembleCenterX - (numaWidth + numaPadding * 2) / 2; |
| const numaY = systemBgCenterY - (numaHeight + numaPadding * 2) / 2; |
| const numaRectWidth = numaWidth + numaPadding * 2; |
| const numaRectHeight = numaHeight + numaPadding * 2; |
| |
| renderer.structuralGroup.rect(numaRectWidth, numaRectHeight) |
| .move(numaX, numaY) |
| .fill('none') |
| .stroke({ |
| color: CONFIG.colors.groupBorder, |
| width: 3, |
| opacity: 1 |
| }) |
| .radius(8) |
| .attr('data-group-border', 'numa'); |
| |
| |
| renderer.nodesGroup.text(`NUMA ${ensIndex}`) |
| .move(numaX + numaRectWidth - 15, numaY + 15) |
| .font({ |
| size: 14, |
| family: 'var(--font-family)', |
| weight: 600, |
| fill: CONFIG.colors.textSecondary, |
| anchor: 'end' |
| }) |
| .opacity(0.7); |
| } |
| } |
| |
| |
| const linkDef = CONFIG.gpuNvswitchLink; |
| const bw = getBandwidth(linkDef.bandwidth); |
| const color = CONFIG.colors.linkColor; |
| const offset = getLinkOffset(bw.width); |
| |
| |
| const totalGpusInSystem = ensemblesToShow * CONFIG.groupCount; |
| const totalNVSwitchesInSystem = ensemblesToShow * CONFIG.nvswitchCount; |
| |
| |
| const gpuNvswitchConnections = []; |
| for (let gpuIndex = 0; gpuIndex < totalGpusInSystem; gpuIndex++) { |
| const gpuGlobalIndex = systemIndex * CONFIG.ensembleCount * CONFIG.groupCount + gpuIndex; |
| const gpuId = gpuGlobalIndex === 0 ? 'gpu' : `gpu-${gpuGlobalIndex}`; |
| |
| for (let nvIndex = 0; nvIndex < totalNVSwitchesInSystem; nvIndex++) { |
| const nvswitchGlobalIndex = systemIndex * CONFIG.ensembleCount * CONFIG.nvswitchCount + nvIndex; |
| gpuNvswitchConnections.push({ |
| fromId: gpuId, |
| toId: `nvswitch-${nvswitchGlobalIndex}`, |
| color: color, |
| width: bw.width, |
| fromSide: linkDef.fromSide, |
| toSide: linkDef.toSide, |
| offset: offset, |
| type: linkDef.type, |
| bandwidth: linkDef.bandwidth |
| }); |
| } |
| } |
| |
| |
| const shuffledGpuNvswitchConnections = gpuNvswitchConnections.sort(() => Math.random() - 0.5); |
| shuffledGpuNvswitchConnections.forEach(conn => { |
| renderer.drawLink(conn.fromId, conn.toId, conn.color, conn.width, conn.fromSide, conn.toSide, conn.offset, 0, 1, 0, 1, conn.type, conn.bandwidth); |
| }); |
| |
| |
| if (ensemblesToShow === 2) { |
| const cpu0Id = systemIndex * CONFIG.ensembleCount === 0 ? 'cpu' : `cpu-${systemIndex * CONFIG.ensembleCount}`; |
| const cpu1Id = `cpu-${systemIndex * CONFIG.ensembleCount + 1}`; |
| |
| |
| const bw = getBandwidth('16GB/s'); |
| const color = CONFIG.colors.linkColor; |
| const offset = getLinkOffset(bw.width); |
| |
| renderer.drawLink(cpu0Id, cpu1Id, color, bw.width, 'right', 'left', offset, 0, 1, 0, 1, 'cpu', '16GB/s'); |
| |
| |
| if (ensemblesToShow === 2 && shouldDrawEfaCrossLinks) { |
| const efaExt0Id = systemIndex * CONFIG.ensembleCount * CONFIG.groupCount === 0 ? 'efa-external' : `efa-external-${systemIndex * CONFIG.ensembleCount * CONFIG.groupCount}`; |
| const efaExt1Id = `efa-external-${systemIndex * CONFIG.ensembleCount * CONFIG.groupCount + CONFIG.groupCount}`; |
| |
| |
| drawEfaCrossLinks(renderer, efaExt0Id, efaExt1Id, 'efa-crosslink'); |
| } |
| } |
| } |
| |
| |
| |
| |
| let currentEnsembleCount = 2; |
| let currentSystemCount = 1; |
| let currentActivePathId = null; |
| let showRealBandwidthsOverride = null; |
| |
| function drawTopology(ensembleCount = currentEnsembleCount, systemCount = currentSystemCount) { |
| |
| const container = getElement('aws-topology-container'); |
| container.innerHTML = ''; |
| |
| |
| const ensembleWidth = CONFIG.groupCount * CONFIG.gaps.horizontal; |
| const isInternodeLayout = systemCount === 2 && currentActivePathId === 'gpu-gpu-efa-internode'; |
| |
| |
| const singleSystemHeight = CONFIG.gaps.topMargin + CONFIG.sizes.cpu + |
| CONFIG.gaps.cpuToPcie + CONFIG.sizes.pcie.height + |
| CONFIG.gaps.pcieToGpu + CONFIG.sizes.gpu.height + |
| CONFIG.gaps.gpuToNvswitch + CONFIG.sizes.nvswitch.height + |
| CONFIG.gaps.bottomMargin; |
| |
| |
| const maxEnsembleWidthForViewbox = 2 * ensembleWidth; |
| let viewboxWidth = maxEnsembleWidthForViewbox + CONFIG.gaps.ensembleGap + 200; |
| let viewboxHeight = singleSystemHeight; |
| |
| |
| if (ensembleCount === 1 && systemCount === 1) { |
| viewboxWidth *= 0.65; |
| viewboxHeight *= 0.65; |
| } |
| |
| |
| if (systemCount === 2 && !isInternodeLayout) { |
| viewboxHeight = singleSystemHeight * 2 + CONFIG.gaps.systemGap; |
| viewboxWidth *= 1.15; |
| viewboxHeight *= 1.15; |
| } |
| |
| |
| if (isInternodeLayout) { |
| const extraWidth = 210; |
| const systemPadding = 30; |
| const singleEnsembleWidth = ensembleWidth; |
| const totalSystemWidth = singleEnsembleWidth + systemPadding * 2 + extraWidth; |
| const gap = 80; |
| viewboxWidth = totalSystemWidth * 2 + gap + 200; |
| } |
| |
| |
| const embedConfig = readEmbedConfig(); |
| const containerWidth = container.clientWidth || 800; |
| |
| |
| |
| const maxEnsembleWidth = 2 * (CONFIG.groupCount * CONFIG.gaps.horizontal); |
| const maxSingleSystemHeight = CONFIG.gaps.topMargin + CONFIG.sizes.cpu + |
| CONFIG.gaps.cpuToPcie + CONFIG.sizes.pcie.height + |
| CONFIG.gaps.pcieToGpu + CONFIG.sizes.gpu.height + |
| CONFIG.gaps.gpuToNvswitch + CONFIG.sizes.nvswitch.height + |
| CONFIG.gaps.bottomMargin; |
| const maxViewboxHeight = maxSingleSystemHeight * 1.15; |
| const maxViewboxWidth = maxEnsembleWidth + 200; |
| |
| const maxAspectRatio = maxViewboxHeight / maxViewboxWidth; |
| const baseHeight = containerWidth * maxAspectRatio; |
| const legendHeight = embedConfig.initialFilter ? 150 : 200; |
| |
| |
| if (!container.dataset.heightSet) { |
| container.style.height = `${baseHeight + legendHeight}px`; |
| container.dataset.heightSet = 'true'; |
| } |
| |
| |
| let viewboxY = 0; |
| let paddedViewboxHeight = viewboxHeight; |
| const verticalPadding = 0; |
| viewboxY = -verticalPadding / 2 + 100; |
| paddedViewboxHeight = viewboxHeight + verticalPadding; |
| |
| |
| if (ensembleCount === 1 && systemCount === 1) { |
| viewboxY += 150; |
| } |
| |
| |
| |
| const isEfaFilter = currentActivePathId === 'gpu-gpu-efa-intranode' || currentActivePathId === 'gpu-gpu-efa-internode'; |
| const hasActiveFilter = currentActivePathId && currentActivePathId !== ''; |
| if (!isInternodeLayout && hasActiveFilter && !isEfaFilter) { |
| viewboxY -= 80; |
| } |
| |
| const draw = SVG().addTo(container).size('100%', '100%').viewbox(0, viewboxY, viewboxWidth, paddedViewboxHeight); |
| const renderer = new TopologyRenderer(draw); |
| |
| const centerX = viewboxWidth / 2; |
| |
| |
| const isInternodeHorizontalLayout = isInternodeLayout; |
| |
| |
| const initialFilterYOffset = embedConfig.initialFilter ? 20 : 0; |
| |
| for (let sysIndex = 0; sysIndex < systemCount; sysIndex++) { |
| let systemCenterX, cpuY, nvswitchY; |
| |
| if (isInternodeHorizontalLayout) { |
| |
| |
| const fullSystemWidth = ensembleCount * ensembleWidth + (ensembleCount - 1) * CONFIG.gaps.ensembleGap; |
| const actualSystemWidth = fullSystemWidth * 0.5; |
| const extraWidth = 210; |
| const systemPadding = 10; |
| const totalSystemWidth = actualSystemWidth + systemPadding * 2 + extraWidth; |
| const gap = 80; |
| |
| |
| if (sysIndex === 0) { |
| systemCenterX = centerX - (totalSystemWidth / 2 + gap / 2); |
| } else { |
| systemCenterX = centerX + (totalSystemWidth / 2 + gap / 2); |
| } |
| cpuY = CONFIG.gaps.topMargin + CONFIG.sizes.cpu / 2 + initialFilterYOffset; |
| nvswitchY = cpuY + CONFIG.sizes.cpu / 2 + CONFIG.gaps.cpuToPcie + CONFIG.sizes.pcie.height + |
| CONFIG.gaps.pcieToGpu + CONFIG.sizes.gpu.height / 2 + CONFIG.gaps.gpuToNvswitch + |
| CONFIG.sizes.nvswitch.height / 2; |
| } else { |
| |
| const systemOffsetY = sysIndex * (singleSystemHeight + CONFIG.gaps.systemGap); |
| systemCenterX = centerX; |
| cpuY = systemOffsetY + CONFIG.gaps.topMargin + CONFIG.sizes.cpu / 2 + initialFilterYOffset; |
| nvswitchY = cpuY + CONFIG.sizes.cpu / 2 + CONFIG.gaps.cpuToPcie + CONFIG.sizes.pcie.height + |
| CONFIG.gaps.pcieToGpu + CONFIG.sizes.gpu.height / 2 + CONFIG.gaps.gpuToNvswitch + |
| CONFIG.sizes.nvswitch.height / 2; |
| } |
| |
| |
| const shouldDrawEfaCrossLinks = currentActivePathId === 'gpu-gpu-efa-intranode'; |
| drawSystem(renderer, sysIndex, systemCenterX, cpuY, nvswitchY, ensembleCount, systemCount, shouldDrawEfaCrossLinks); |
| } |
| |
| |
| if (systemCount === 2 && ensembleCount === 2 && currentActivePathId === 'gpu-gpu-efa-internode') { |
| const efaExt0Id = 'efa-external'; |
| const efaExt1Id = 'efa-external-8'; |
| |
| |
| drawEfaCrossLinks(renderer, efaExt0Id, efaExt1Id, 'efa-crosslink'); |
| } |
| |
| currentEnsembleCount = ensembleCount; |
| currentSystemCount = systemCount; |
| } |
| |
| |
| |
| |
| function getRealBandwidthForPath(pathId) { |
| const pathToRealBandwidth = { |
| 'cpu-gpu': { value: '14.2', unit: 'GB/s' }, |
| 'gpu-gpu-cpu': { value: '14.2', unit: 'GB/s' }, |
| 'gpu-gpu-nvswitch': { value: '786', unit: 'GB/s' }, |
| 'gpu-gpu-efa-intranode': { value: '40', unit: 'GB/s' }, |
| 'gpu-gpu-efa-internode': { value: '40', unit: 'GB/s' }, |
| 'gpu-storage': { value: '14.2', unit: 'GB/s' }, |
| 'cpu-storage': { value: '14.2', unit: 'GB/s' }, |
| 'gpu-cpu-storage': { value: '14.2', unit: 'GB/s' } |
| }; |
| |
| const bandwidth = pathToRealBandwidth[pathId]; |
| if (bandwidth) { |
| return { |
| value: bandwidth.value, |
| unit: bandwidth.unit |
| }; |
| } |
| return null; |
| } |
| |
| |
| |
| |
| function highlightPath(path, pathLabel = '', pathId = '') { |
| const svg = getElement('aws-topology-container').querySelector('svg'); |
| if (!svg) return; |
| |
| |
| const activeLinksGroup = svg.querySelector('g[data-group="active-links"]'); |
| if (activeLinksGroup) { |
| activeLinksGroup.innerHTML = ''; |
| } |
| |
| |
| const baseLinksGroup = svg.querySelector('g[data-group="base-links"]'); |
| const baseCrossLinksGroup = svg.querySelector('g[data-group="base-cross-links"]'); |
| |
| if (baseLinksGroup) { |
| baseLinksGroup.style.opacity = '0.35'; |
| |
| |
| baseLinksGroup.querySelectorAll('[data-group-border]').forEach(border => { |
| border.setAttribute('stroke-width', '5'); |
| |
| }); |
| } |
| if (baseCrossLinksGroup) { |
| baseCrossLinksGroup.style.opacity = '0.25'; |
| } |
| |
| |
| svg.querySelectorAll('g[data-node-type]').forEach(el => { |
| el.style.opacity = '0.6'; |
| |
| el.querySelectorAll('text').forEach(text => { |
| text.style.opacity = '0.25'; |
| }); |
| }); |
| |
| |
| path.nodes.forEach(nodeId => { |
| let nodeEl = svg.querySelector(`g[id="${nodeId}"]`); |
| if (!nodeEl) { |
| const candidates = svg.querySelectorAll(`g[id^="${nodeId}"]`); |
| nodeEl = candidates[0]; |
| } |
| if (nodeEl) { |
| nodeEl.style.opacity = '1'; |
| |
| nodeEl.querySelectorAll('text').forEach(text => { |
| text.style.opacity = '1'; |
| }); |
| } |
| }); |
| |
| |
| if (activeLinksGroup) { |
| path.links.forEach(linkSpec => { |
| |
| const allBaseLinks = [ |
| ...svg.querySelectorAll('g[data-group="base-links"] > g[data-link-type]'), |
| ...svg.querySelectorAll('g[data-group="base-cross-links"] > g[data-link-type]') |
| ]; |
| |
| allBaseLinks.forEach(linkGroup => { |
| const linkFrom = linkGroup.getAttribute('data-from'); |
| const linkTo = linkGroup.getAttribute('data-to'); |
| |
| const matchesFrom = linkFrom === linkSpec.from || linkFrom === `${linkSpec.from}`; |
| const matchesTo = linkTo === linkSpec.to || linkTo === `${linkSpec.to}`; |
| |
| if (matchesFrom && matchesTo) { |
| |
| const clonedLink = linkGroup.cloneNode(true); |
| activeLinksGroup.appendChild(clonedLink); |
| } |
| }); |
| }); |
| |
| |
| const currentPathId = currentActivePathId; |
| if (currentPathId === 'gpu-gpu-efa-intranode' || currentPathId === 'gpu-gpu-efa-internode') { |
| const efaCrossLinks = svg.querySelectorAll('g[data-group="base-cross-links"] > g[data-link-type="efa-crosslink"]'); |
| efaCrossLinks.forEach(linkGroup => { |
| const clonedLink = linkGroup.cloneNode(true); |
| activeLinksGroup.appendChild(clonedLink); |
| }); |
| } |
| } |
| |
| |
| let minBandwidth = Infinity; |
| let minBandwidthValue = null; |
| const usedBandwidths = new Set(); |
| |
| path.links.forEach(linkSpec => { |
| svg.querySelectorAll('g[data-link-type]').forEach(linkGroup => { |
| const linkFrom = linkGroup.getAttribute('data-from'); |
| const linkTo = linkGroup.getAttribute('data-to'); |
| |
| const matchesFrom = linkFrom === linkSpec.from || linkFrom === `${linkSpec.from}`; |
| const matchesTo = linkTo === linkSpec.to || linkTo === `${linkSpec.to}`; |
| |
| if (matchesFrom && matchesTo) { |
| const bandwidth = linkGroup.getAttribute('data-bandwidth'); |
| if (bandwidth) { |
| const bw = getBandwidth(bandwidth); |
| if (bw && bw.numericValue < minBandwidth) { |
| minBandwidth = bw.numericValue; |
| minBandwidthValue = bw.speed; |
| } |
| if (bw) { |
| usedBandwidths.add(bw.speed); |
| } |
| } |
| } |
| }); |
| }); |
| |
| |
| const legendContainer = getElement('aws-topology-legend'); |
| if (legendContainer) { |
| legendContainer.querySelectorAll('g[data-legend-bandwidth]').forEach(legendItem => { |
| const bandwidth = legendItem.getAttribute('data-legend-bandwidth'); |
| if (usedBandwidths.has(bandwidth)) { |
| |
| legendItem.style.opacity = '1'; |
| } else { |
| |
| legendItem.style.opacity = '0.4'; |
| } |
| }); |
| } |
| |
| |
| const bottleneckEl = getElement('aws-topology-bottleneck'); |
| const bottleneckValueEl = bottleneckEl.querySelector('.bottleneck-value'); |
| const bottleneckPathEl = bottleneckEl.querySelector('.bottleneck-path'); |
| const bottleneckLabelEl = bottleneckEl.querySelector('.bottleneck-label'); |
| const realBandwidthsEl = bottleneckEl.querySelector('.real-bandwidths'); |
| const efficiencyEl = bottleneckEl.querySelector('.bottleneck-efficiency'); |
| const efficiencyValueEl = bottleneckEl.querySelector('.efficiency-value'); |
| |
| |
| const embedConfig = readEmbedConfig(); |
| const showRealBandwidths = embedConfig.showRealBandwidths; |
| |
| if (showRealBandwidths) { |
| |
| const realBandwidth = getRealBandwidthForPath(pathId); |
| if (realBandwidth) { |
| bottleneckValueEl.textContent = realBandwidth.value; |
| bottleneckPathEl.textContent = `for ${pathLabel}`; |
| bottleneckLabelEl.textContent = 'Real Bandwidth'; |
| bottleneckEl.classList.add('visible'); |
| |
| |
| if (minBandwidthValue && efficiencyEl && efficiencyValueEl) { |
| const theoreticalBandwidth = parseFloat(minBandwidthValue.replace('GB/s', '')); |
| const realBandwidthNum = parseFloat(realBandwidth.value); |
| |
| |
| const adjustedTheoretical = minBandwidthValue === '12.5GB/s' ? theoreticalBandwidth * 4 : theoreticalBandwidth; |
| |
| const efficiency = (realBandwidthNum / adjustedTheoretical) * 100; |
| efficiencyValueEl.textContent = `${efficiency.toFixed(1)}%`; |
| efficiencyEl.style.display = 'block'; |
| } |
| } else { |
| |
| bottleneckValueEl.textContent = '?'; |
| bottleneckPathEl.textContent = `for ${pathLabel}`; |
| bottleneckLabelEl.textContent = 'Real Bandwidth'; |
| bottleneckEl.classList.add('visible'); |
| if (efficiencyEl) { |
| efficiencyEl.style.display = 'none'; |
| } |
| } |
| |
| const realBandwidthsEl = bottleneckEl.querySelector('.real-bandwidths'); |
| if (realBandwidthsEl) { |
| realBandwidthsEl.style.display = 'none'; |
| } |
| } else { |
| |
| bottleneckLabelEl.textContent = 'Bandwidth Max'; |
| if (minBandwidthValue) { |
| const value = minBandwidthValue.replace('GB/s', ''); |
| |
| const displayValue = value === '12.5' ? '50' : value; |
| bottleneckValueEl.textContent = displayValue; |
| bottleneckPathEl.textContent = `for ${pathLabel}`; |
| bottleneckEl.classList.add('visible'); |
| } else { |
| |
| bottleneckValueEl.textContent = '?'; |
| bottleneckPathEl.textContent = `for ${pathLabel}`; |
| bottleneckEl.classList.add('visible'); |
| } |
| |
| if (efficiencyEl) { |
| efficiencyEl.style.display = 'none'; |
| } |
| } |
| } |
| |
| function resetHighlight() { |
| const svg = getElement('aws-topology-container').querySelector('svg'); |
| if (!svg) return; |
| |
| |
| const activeLinksGroup = svg.querySelector('g[data-group="active-links"]'); |
| if (activeLinksGroup) { |
| activeLinksGroup.innerHTML = ''; |
| } |
| |
| |
| const baseLinksGroup = svg.querySelector('g[data-group="base-links"]'); |
| const baseCrossLinksGroup = svg.querySelector('g[data-group="base-cross-links"]'); |
| |
| if (baseLinksGroup) { |
| baseLinksGroup.style.opacity = '1'; |
| |
| |
| baseLinksGroup.querySelectorAll('[data-group-border]').forEach(border => { |
| border.setAttribute('stroke-width', '3'); |
| border.setAttribute('stroke-opacity', '1'); |
| }); |
| } |
| if (baseCrossLinksGroup) { |
| baseCrossLinksGroup.style.opacity = '1'; |
| } |
| |
| |
| svg.querySelectorAll('g[data-node-type]').forEach(el => { |
| el.style.opacity = '1'; |
| |
| el.querySelectorAll('text').forEach(text => { |
| text.style.opacity = '1'; |
| }); |
| }); |
| |
| |
| currentActivePathId = null; |
| |
| |
| const legendContainer = getElement('aws-topology-legend'); |
| if (legendContainer) { |
| legendContainer.querySelectorAll('g[data-legend-bandwidth]').forEach(legendItem => { |
| legendItem.style.opacity = '1'; |
| }); |
| } |
| |
| |
| const bottleneckEl = getElement('aws-topology-bottleneck'); |
| const bottleneckLabelEl = bottleneckEl.querySelector('.bottleneck-label'); |
| const embedConfig = readEmbedConfig(); |
| const showRealBandwidths = embedConfig.showRealBandwidths; |
| |
| if (!showRealBandwidths) { |
| bottleneckLabelEl.textContent = 'Bandwidth Max'; |
| bottleneckEl.classList.remove('visible'); |
| } else { |
| |
| bottleneckLabelEl.textContent = 'Real Bandwidth'; |
| bottleneckEl.classList.remove('visible'); |
| |
| const realBandwidthsEl = bottleneckEl.querySelector('.real-bandwidths'); |
| if (realBandwidthsEl) { |
| realBandwidthsEl.style.display = 'none'; |
| } |
| } |
| |
| |
| const efficiencyEl = bottleneckEl.querySelector('.bottleneck-efficiency'); |
| if (efficiencyEl) { |
| efficiencyEl.style.display = 'none'; |
| } |
| } |
| |
| |
| |
| |
| function readEmbedConfig() { |
| |
| let mountEl = getElement('aws-topology-container'); |
| while (mountEl && !mountEl.getAttribute?.('data-config')) { |
| mountEl = mountEl.parentElement; |
| } |
| |
| let providedConfig = null; |
| try { |
| const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null; |
| if (cfg && cfg.trim()) { |
| providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg; |
| } |
| } catch (e) { |
| console.error('Error parsing embed config:', e); |
| } |
| |
| const config = providedConfig || {}; |
| |
| |
| if (showRealBandwidthsOverride !== null) { |
| config.showRealBandwidths = showRealBandwidthsOverride; |
| } |
| |
| return config; |
| } |
| |
| |
| |
| |
| function displayContextualRealBandwidths(pathId, pathLabel) { |
| const bottleneckEl = getElement('aws-topology-bottleneck'); |
| const bottleneckValueEl = bottleneckEl.querySelector('.bottleneck-value'); |
| const bottleneckPathEl = bottleneckEl.querySelector('.bottleneck-path'); |
| const realBandwidthsEl = bottleneckEl.querySelector('.real-bandwidths'); |
| const realBandwidthsContentEl = bottleneckEl.querySelector('.real-bandwidths-content'); |
| |
| if (!realBandwidthsEl || !realBandwidthsContentEl) return; |
| |
| |
| realBandwidthsEl.style.display = 'block'; |
| |
| |
| const pathToBandwidths = { |
| 'cpu-gpu': [ |
| { label: 'CPU-GPU', value: CONFIG.realBandwidths['cpu-gpu'] } |
| ], |
| 'gpu-gpu-cpu': [ |
| { label: 'CPU-GPU', value: CONFIG.realBandwidths['cpu-gpu'] }, |
| { label: 'GPU-GPU (via CPU)', value: CONFIG.realBandwidths['cpu-gpu'] } |
| ], |
| 'gpu-gpu-nvswitch': [ |
| { label: 'GPU-GPU (NVSwitch)', value: CONFIG.realBandwidths['gpu-gpu-same-node'] } |
| ], |
| 'gpu-gpu-efa-intranode': [ |
| { label: 'GPU-GPU (EFA)', value: CONFIG.realBandwidths['gpu-gpu-efa-intranode'] }, |
| { label: 'AllReduce', value: CONFIG.realBandwidths['allreduce-same-node'] }, |
| { label: 'All2All', value: CONFIG.realBandwidths['all2all-same-node'] } |
| ], |
| 'gpu-gpu-efa-internode': [ |
| { label: 'GPU-GPU (EFA)', value: CONFIG.realBandwidths['gpu-gpu-efa-internode'] }, |
| { label: 'AllReduce', value: CONFIG.realBandwidths['allreduce-internode'] }, |
| { label: 'All2All', value: CONFIG.realBandwidths['alltoall-internode'] } |
| ], |
| 'gpu-storage': [ |
| { label: 'GPU-Storage', value: CONFIG.realBandwidths['gpu-storage'] } |
| ], |
| 'cpu-storage': [ |
| { label: 'CPU-Storage', value: CONFIG.realBandwidths['gpu-storage'] } |
| ], |
| 'gpu-cpu-storage': [ |
| { label: 'CPU-GPU', value: CONFIG.realBandwidths['cpu-gpu'] }, |
| { label: 'GPU-Storage', value: CONFIG.realBandwidths['gpu-storage'] } |
| ] |
| }; |
| |
| const relevantBandwidths = pathToBandwidths[pathId] || []; |
| |
| if (relevantBandwidths.length > 0) { |
| |
| bottleneckValueEl.textContent = ''; |
| bottleneckPathEl.textContent = ''; |
| |
| |
| const html = relevantBandwidths.map(item => ` |
| <div class="bandwidth-item"> |
| <span class="bandwidth-label">${item.label}</span> |
| <span class="bandwidth-value">${item.value}</span> |
| </div> |
| `).join(''); |
| |
| realBandwidthsContentEl.innerHTML = html; |
| } else { |
| |
| setupRealBandwidthsDisplay(); |
| } |
| } |
| |
| function setupRealBandwidthsDisplay() { |
| const bottleneckEl = getElement('aws-topology-bottleneck'); |
| const realBandwidthsEl = bottleneckEl.querySelector('.real-bandwidths'); |
| const realBandwidthsContentEl = bottleneckEl.querySelector('.real-bandwidths-content'); |
| |
| if (!realBandwidthsEl || !realBandwidthsContentEl) return; |
| |
| |
| realBandwidthsEl.style.display = 'block'; |
| |
| |
| const bandwidthItems = [ |
| { label: 'CPU-GPU', value: CONFIG.realBandwidths['cpu-gpu'] }, |
| { label: 'Same Node', value: '' }, |
| { label: ' GPU-GPU', value: CONFIG.realBandwidths['gpu-gpu-same-node'] }, |
| { label: ' AllReduce', value: CONFIG.realBandwidths['allreduce-same-node'] }, |
| { label: ' All2All', value: CONFIG.realBandwidths['all2all-same-node'] }, |
| { label: 'Internode', value: '' }, |
| { label: ' GPU-GPU', value: CONFIG.realBandwidths['gpu-gpu-internode'] }, |
| { label: ' AllReduce', value: CONFIG.realBandwidths['allreduce-internode'] }, |
| { label: ' All2All', value: CONFIG.realBandwidths['alltoall-internode'] }, |
| { label: 'GPU-Storage', value: CONFIG.realBandwidths['gpu-storage'] } |
| ]; |
| |
| const html = bandwidthItems.map(item => { |
| if (item.value === '') { |
| |
| return `<div class="bandwidth-section-header" style="font-weight: 600; margin: 6px 0 4px 0; color: var(--text-secondary);">${item.label}</div>`; |
| } else { |
| |
| return ` |
| <div class="bandwidth-item"> |
| <span class="bandwidth-label">${item.label}</span> |
| <span class="bandwidth-value">${item.value}</span> |
| </div> |
| `; |
| } |
| }).join(''); |
| |
| realBandwidthsContentEl.innerHTML = html; |
| } |
| |
| |
| |
| |
| function initialize() { |
| const container = getElement('aws-topology-container'); |
| const tooltip = getElement('aws-topology-tooltip'); |
| |
| |
| let mountEl = getElement('aws-topology-container'); |
| while (mountEl && !mountEl.getAttribute?.('data-config')) { |
| mountEl = mountEl.parentElement; |
| } |
| |
| let initialConfig = null; |
| try { |
| const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null; |
| if (cfg && cfg.trim()) { |
| initialConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg; |
| } |
| } catch (e) { |
| console.error('Error parsing embed config:', e); |
| } |
| |
| |
| if (initialConfig && initialConfig.showRealBandwidths !== undefined) { |
| showRealBandwidthsOverride = initialConfig.showRealBandwidths; |
| } |
| |
| |
| const embedConfig = readEmbedConfig(); |
| |
| |
| if (embedConfig.showRealBandwidths) { |
| const bottleneckEl = getElement('aws-topology-bottleneck'); |
| const bottleneckLabelEl = bottleneckEl.querySelector('.bottleneck-label'); |
| const realBandwidthsEl = bottleneckEl.querySelector('.real-bandwidths'); |
| |
| |
| bottleneckLabelEl.textContent = 'Real Bandwidth'; |
| |
| |
| if (realBandwidthsEl) { |
| realBandwidthsEl.style.display = 'none'; |
| } |
| |
| bottleneckEl.classList.remove('visible'); |
| } |
| |
| drawTopology(); |
| |
| |
| requestAnimationFrame(() => { |
| container.style.opacity = '1'; |
| }); |
| |
| |
| |
| container.addEventListener('mouseover', (e) => { |
| const target = e.target.closest('[data-bandwidth]'); |
| if (target) { |
| const bandwidth = target.getAttribute('data-bandwidth'); |
| const label = target.getAttribute('data-label'); |
| |
| |
| tooltip.innerHTML = ''; |
| |
| if (label) { |
| const labelDiv = document.createElement('div'); |
| labelDiv.className = 'tooltip-label'; |
| labelDiv.textContent = label; |
| tooltip.appendChild(labelDiv); |
| } |
| |
| const bandwidthDiv = document.createElement('div'); |
| bandwidthDiv.className = 'tooltip-bandwidth'; |
| bandwidthDiv.textContent = bandwidth; |
| tooltip.appendChild(bandwidthDiv); |
| |
| tooltip.classList.add('visible'); |
| } |
| }); |
| |
| container.addEventListener('mousemove', (e) => { |
| if (tooltip.classList.contains('visible')) { |
| const rect = container.getBoundingClientRect(); |
| tooltip.style.left = (e.clientX - rect.left + 10) + 'px'; |
| tooltip.style.top = (e.clientY - rect.top + 10) + 'px'; |
| } |
| }); |
| |
| container.addEventListener('mouseout', (e) => { |
| const target = e.target.closest('[data-bandwidth]'); |
| if (target) { |
| tooltip.classList.remove('visible'); |
| } |
| }); |
| |
| |
| const controlsContainer = getElement('aws-topology-controls'); |
| |
| |
| const pathsByCategory = { |
| intranode: [ |
| { id: 'cpu-gpu', label: CONFIG.paths['cpu-gpu'].label }, |
| { id: 'gpu-gpu-cpu', label: CONFIG.paths['gpu-gpu-cpu'].label }, |
| { id: 'gpu-gpu-nvswitch', label: CONFIG.paths['gpu-gpu-nvswitch'].label }, |
| { id: 'gpu-gpu-efa-intranode', label: CONFIG.paths['gpu-gpu-efa-intranode'].label } |
| ], |
| internode: [ |
| { id: 'gpu-gpu-efa-internode', label: CONFIG.paths['gpu-gpu-efa-internode'].label } |
| ], |
| storage: [ |
| { id: 'gpu-storage', label: CONFIG.paths['gpu-storage'].label }, |
| { id: 'cpu-storage', label: CONFIG.paths['cpu-storage'].label }, |
| { id: 'gpu-cpu-storage', label: CONFIG.paths['gpu-cpu-storage'].label } |
| ] |
| }; |
| |
| |
| const isChecked = showRealBandwidthsOverride === true || (showRealBandwidthsOverride === null && embedConfig.showRealBandwidths); |
| const controlsHTML = ` |
| <div> |
| <label id="real-bandwidth-label" style="display: flex; align-items: center; gap: 0px; font-size: 14px; color: var(--text-color); cursor: pointer; opacity: ${embedConfig.initialFilter ? '1' : '0.3'}; transition: opacity 0.2s;"> |
| <input type="checkbox" id="real-bandwidth-toggle" ${isChecked ? 'checked' : ''} ${embedConfig.initialFilter ? '' : 'disabled'}> |
| <span>Show Real Bandwidths</span> |
| </label> |
| </div> |
| |
| <div style="display: flex; gap: 12px; align-items: center; margin-top: 8px;"> |
| <select id="path-select" style="min-width: 250px;"> |
| <option value="">Select path...</option> |
| </select> |
| </div> |
| `; |
| controlsContainer.innerHTML = controlsHTML; |
| |
| const pathSelect = controlsContainer.querySelector('#path-select'); |
| const realBandwidthToggle = controlsContainer.querySelector('#real-bandwidth-toggle'); |
| const realBandwidthLabel = controlsContainer.querySelector('#real-bandwidth-label'); |
| |
| |
| Object.entries(pathsByCategory).forEach(([category, paths]) => { |
| const categoryLabel = category.charAt(0).toUpperCase() + category.slice(1); |
| |
| paths.forEach(path => { |
| const option = document.createElement('option'); |
| option.value = path.id; |
| option.textContent = `${categoryLabel}: ${path.label}`; |
| pathSelect.appendChild(option); |
| }); |
| }); |
| |
| |
| if (embedConfig.initialFilter) { |
| pathSelect.value = embedConfig.initialFilter; |
| } |
| |
| |
| const updateRealBandwidthToggleState = (enabled) => { |
| if (enabled) { |
| realBandwidthToggle.disabled = false; |
| realBandwidthLabel.style.opacity = '1'; |
| realBandwidthLabel.style.cursor = 'pointer'; |
| } else { |
| realBandwidthToggle.disabled = true; |
| realBandwidthLabel.style.opacity = '0.3'; |
| realBandwidthLabel.style.cursor = 'default'; |
| } |
| }; |
| |
| |
| realBandwidthToggle.addEventListener('change', (e) => { |
| showRealBandwidthsOverride = e.target.checked; |
| |
| |
| if (currentActivePathId) { |
| const path = CONFIG.paths[currentActivePathId]; |
| if (path) { |
| highlightPath(path, path.label, currentActivePathId); |
| } |
| } else { |
| |
| resetHighlight(); |
| } |
| }); |
| |
| |
| const activatePath = (pathId) => { |
| if (!pathId) { |
| |
| currentActivePathId = null; |
| updateRealBandwidthToggleState(false); |
| const needsRedraw = currentEnsembleCount !== 2 || currentSystemCount !== 1; |
| |
| if (needsRedraw) { |
| const container = getElement('aws-topology-container'); |
| container.style.opacity = '0'; |
| |
| setTimeout(() => { |
| drawTopology(2, 1); |
| resetHighlight(); |
| |
| requestAnimationFrame(() => { |
| container.style.opacity = '1'; |
| }); |
| }, 150); |
| } else { |
| resetHighlight(); |
| } |
| return; |
| } |
| |
| const path = CONFIG.paths[pathId]; |
| if (!path) return; |
| |
| currentActivePathId = pathId; |
| updateRealBandwidthToggleState(true); |
| |
| |
| const needsRedraw = path.requiredEnsembles !== currentEnsembleCount || |
| path.requiredSystems !== currentSystemCount || |
| pathId === 'gpu-gpu-efa-intranode' || |
| pathId === 'gpu-gpu-efa-internode'; |
| |
| if (needsRedraw) { |
| const container = getElement('aws-topology-container'); |
| container.style.opacity = '0'; |
| |
| setTimeout(() => { |
| currentActivePathId = pathId; |
| drawTopology(path.requiredEnsembles, path.requiredSystems); |
| highlightPath(path, path.label, pathId); |
| |
| requestAnimationFrame(() => { |
| container.style.opacity = '1'; |
| }); |
| }, 150); |
| } else { |
| currentActivePathId = pathId; |
| highlightPath(path, path.label, pathId); |
| } |
| }; |
| |
| |
| pathSelect.addEventListener('change', (e) => { |
| const pathId = e.target.value; |
| activatePath(pathId); |
| }); |
| |
| |
| if (embedConfig.initialFilter) { |
| const initialPathId = embedConfig.initialFilter; |
| const path = CONFIG.paths[initialPathId]; |
| |
| if (path) { |
| currentActivePathId = initialPathId; |
| |
| |
| updateRealBandwidthToggleState(true); |
| |
| |
| const needsRedraw = path.requiredEnsembles !== currentEnsembleCount || |
| path.requiredSystems !== currentSystemCount || |
| initialPathId === 'gpu-gpu-efa-intranode' || |
| initialPathId === 'gpu-gpu-efa-internode'; |
| |
| if (needsRedraw) { |
| drawTopology(path.requiredEnsembles, path.requiredSystems); |
| } |
| } |
| } |
| |
| |
| const legendContainer = getElement('aws-topology-legend'); |
| const legendSvg = SVG().addTo(legendContainer).size('100%', '100%'); |
| |
| const lineLength = 36; |
| const itemSpacing = 30; |
| const startX = 96; |
| const startY = 42; |
| const textOffset = 24; |
| |
| |
| const legendWidth = startX + lineLength + textOffset + 168; |
| const legendHeight = 144; |
| legendSvg.viewbox(0, 0, legendWidth, legendHeight); |
| |
| [...CONFIG.bandwidths].reverse().forEach((bw, index) => { |
| const y = startY + (index * itemSpacing); |
| const color = CONFIG.colors.linkColor; |
| const width = bw.width; |
| |
| |
| const legendItemGroup = legendSvg.group(); |
| legendItemGroup.attr('data-legend-bandwidth', bw.speed); |
| |
| |
| legendItemGroup.line(startX, y, startX + lineLength, y) |
| .stroke({ color, width }); |
| |
| |
| const r = width * 0.8; |
| const startCircle = legendItemGroup.circle(r * 2).move(startX - r, y - r); |
| startCircle.fill(color); |
| startCircle.stroke({ color: CONFIG.colors.linkCircleBorder, width: 0.5 }); |
| |
| const endCircle = legendItemGroup.circle(r * 2).move(startX + lineLength - r, y - r); |
| endCircle.fill(color); |
| endCircle.stroke({ color: CONFIG.colors.linkCircleBorder, width: 0.5 }); |
| |
| |
| const textX = legendWidth - 5; |
| const value = bw.speed.replace('GB/s', '').trim(); |
| |
| |
| const textEl = legendItemGroup.text(function (add) { |
| add.tspan(bw.label + ' - ').font({ weight: 'normal' }); |
| add.tspan(value).font({ weight: 'bold' }); |
| add.tspan(' GB/s').font({ weight: 'normal' }); |
| }) |
| .move(textX, y) |
| .font({ |
| family: 'system-ui, -apple-system, sans-serif', |
| size: 14, |
| anchor: 'end', |
| fill: CONFIG.colors.nodeText |
| }) |
| .dy(-7); |
| }); |
| } |
| |
| |
| if (findWrapper()) { |
| initialize(); |
| |
| |
| const embedConfig = readEmbedConfig(); |
| if (embedConfig.initialFilter) { |
| const initialPathId = embedConfig.initialFilter; |
| const path = CONFIG.paths[initialPathId]; |
| |
| if (path) { |
| |
| highlightPath(path, path.label, initialPathId); |
| } |
| } |
| |
| |
| const handleResize = () => { |
| const container = getElement('aws-topology-container'); |
| if (!container) return; |
| |
| |
| const containerWidth = container.clientWidth || 800; |
| |
| |
| const maxEnsembleWidth = 2 * (CONFIG.groupCount * CONFIG.gaps.horizontal); |
| const maxSingleSystemHeight = CONFIG.gaps.topMargin + CONFIG.sizes.cpu + |
| CONFIG.gaps.cpuToPcie + CONFIG.sizes.pcie.height + |
| CONFIG.gaps.pcieToGpu + CONFIG.sizes.gpu.height + |
| CONFIG.gaps.gpuToNvswitch + CONFIG.sizes.nvswitch.height + |
| CONFIG.gaps.bottomMargin; |
| const maxViewboxHeight = maxSingleSystemHeight * 1.15; |
| const maxViewboxWidth = maxEnsembleWidth + 200; |
| |
| const maxAspectRatio = maxViewboxHeight / maxViewboxWidth; |
| const baseHeight = containerWidth * maxAspectRatio; |
| const legendHeight = embedConfig.initialFilter ? 150 : 200; |
| |
| |
| container.style.height = `${baseHeight + legendHeight}px`; |
| }; |
| |
| |
| if (window.ResizeObserver) { |
| const ro = new ResizeObserver(() => handleResize()); |
| ro.observe(findWrapper()); |
| } else { |
| window.addEventListener('resize', handleResize); |
| } |
| } else { |
| console.warn('AWS topology: wrapper not found, skipping initialization'); |
| } |
| })(); |
| </script> |