mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 05:19:05 +01:00
Merge pull request #211 from ClusterCockpit/uplot_roofline_scatter
Uplot roofline scatter
This commit is contained in:
commit
84b63af080
@ -70,28 +70,30 @@ func (r *JobRepository) buildStatsQuery(
|
|||||||
var query sq.SelectBuilder
|
var query sq.SelectBuilder
|
||||||
castType := r.getCastType()
|
castType := r.getCastType()
|
||||||
|
|
||||||
|
// fmt.Sprintf(`CAST(ROUND((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) / 3600) as %s) as value`, time.Now().Unix(), castType)
|
||||||
|
|
||||||
if col != "" {
|
if col != "" {
|
||||||
// Scan columns: id, totalJobs, totalWalltime, totalNodes, totalNodeHours, totalCores, totalCoreHours, totalAccs, totalAccHours
|
// Scan columns: id, totalJobs, totalWalltime, totalNodes, totalNodeHours, totalCores, totalCoreHours, totalAccs, totalAccHours
|
||||||
query = sq.Select(col, "COUNT(job.id) as totalJobs",
|
query = sq.Select(col, "COUNT(job.id) as totalJobs",
|
||||||
fmt.Sprintf("CAST(ROUND(SUM(job.duration) / 3600) as %s) as totalWalltime", castType),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END)) / 3600) as %s) as totalWalltime`, time.Now().Unix(), castType),
|
||||||
fmt.Sprintf("CAST(SUM(job.num_nodes) as %s) as totalNodes", castType),
|
fmt.Sprintf(`CAST(SUM(job.num_nodes) as %s) as totalNodes`, castType),
|
||||||
fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_nodes) / 3600) as %s) as totalNodeHours", castType),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_nodes) / 3600) as %s) as totalNodeHours`, time.Now().Unix(), castType),
|
||||||
fmt.Sprintf("CAST(SUM(job.num_hwthreads) as %s) as totalCores", castType),
|
fmt.Sprintf(`CAST(SUM(job.num_hwthreads) as %s) as totalCores`, castType),
|
||||||
fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_hwthreads) / 3600) as %s) as totalCoreHours", castType),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_hwthreads) / 3600) as %s) as totalCoreHours`, time.Now().Unix(), castType),
|
||||||
fmt.Sprintf("CAST(SUM(job.num_acc) as %s) as totalAccs", castType),
|
fmt.Sprintf(`CAST(SUM(job.num_acc) as %s) as totalAccs`, castType),
|
||||||
fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_acc) / 3600) as %s) as totalAccHours", castType),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_acc) / 3600) as %s) as totalAccHours`, time.Now().Unix(), castType),
|
||||||
).From("job").GroupBy(col)
|
).From("job").GroupBy(col)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Scan columns: totalJobs, totalWalltime, totalNodes, totalNodeHours, totalCores, totalCoreHours, totalAccs, totalAccHours
|
// Scan columns: totalJobs, totalWalltime, totalNodes, totalNodeHours, totalCores, totalCoreHours, totalAccs, totalAccHours
|
||||||
query = sq.Select("COUNT(job.id)",
|
query = sq.Select("COUNT(job.id)",
|
||||||
fmt.Sprintf("CAST(ROUND(SUM(job.duration) / 3600) as %s)", castType),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END)) / 3600) as %s)`, time.Now().Unix(), castType),
|
||||||
fmt.Sprintf("CAST(SUM(job.num_nodes) as %s)", castType),
|
fmt.Sprintf(`CAST(SUM(job.num_nodes) as %s)`, castType),
|
||||||
fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_nodes) / 3600) as %s)", castType),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_nodes) / 3600) as %s)`, time.Now().Unix(), castType),
|
||||||
fmt.Sprintf("CAST(SUM(job.num_hwthreads) as %s)", castType),
|
fmt.Sprintf(`CAST(SUM(job.num_hwthreads) as %s)`, castType),
|
||||||
fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_hwthreads) / 3600) as %s)", castType),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_hwthreads) / 3600) as %s)`, time.Now().Unix(), castType),
|
||||||
fmt.Sprintf("CAST(SUM(job.num_acc) as %s)", castType),
|
fmt.Sprintf(`CAST(SUM(job.num_acc) as %s)`, castType),
|
||||||
fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_acc) / 3600) as %s)", castType),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_acc) / 3600) as %s)`, time.Now().Unix(), castType),
|
||||||
).From("job")
|
).From("job")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
web/frontend/package-lock.json
generated
7
web/frontend/package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"@rollup/plugin-commonjs": "^24.1.0",
|
"@rollup/plugin-commonjs": "^24.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
"@rollup/plugin-terser": "^0.4.1",
|
"@rollup/plugin-terser": "^0.4.1",
|
||||||
|
"@timohausmann/quadtree-js": "^1.2.5",
|
||||||
"rollup": "^3.21.0",
|
"rollup": "^3.21.0",
|
||||||
"rollup-plugin-css-only": "^4.3.0",
|
"rollup-plugin-css-only": "^4.3.0",
|
||||||
"rollup-plugin-svelte": "^7.1.4",
|
"rollup-plugin-svelte": "^7.1.4",
|
||||||
@ -225,6 +226,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@timohausmann/quadtree-js": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-WcH3pouYtpyLjTCRvNP0WuSV4m7mRyYhLzW44egveFryT7pJhpDsdIJASEe37iCFNA0vmEpqTYGoG0siyXEthA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"@rollup/plugin-commonjs": "^24.1.0",
|
"@rollup/plugin-commonjs": "^24.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
"@rollup/plugin-terser": "^0.4.1",
|
"@rollup/plugin-terser": "^0.4.1",
|
||||||
|
"@timohausmann/quadtree-js": "^1.2.5",
|
||||||
"rollup": "^3.21.0",
|
"rollup": "^3.21.0",
|
||||||
"rollup-plugin-css-only": "^4.3.0",
|
"rollup-plugin-css-only": "^4.3.0",
|
||||||
"rollup-plugin-svelte": "^7.1.4",
|
"rollup-plugin-svelte": "^7.1.4",
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { binsFromFootprint } from './utils.js'
|
import { binsFromFootprint } from './utils.js'
|
||||||
import ScatterPlot from './plots/Scatter.svelte'
|
import ScatterPlot from './plots/Scatter.svelte'
|
||||||
import PlotTable from './PlotTable.svelte'
|
import PlotTable from './PlotTable.svelte'
|
||||||
import Roofline from './plots/Roofline.svelte'
|
import RooflineHeatmap from './plots/RooflineHeatmap.svelte'
|
||||||
|
|
||||||
const { query: initq } = init()
|
const { query: initq } = init()
|
||||||
|
|
||||||
@ -293,7 +293,7 @@
|
|||||||
{#each $topQuery.data.topList as te, i}
|
{#each $topQuery.data.topList as te, i}
|
||||||
<tr>
|
<tr>
|
||||||
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
|
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
|
||||||
{#if groupSelection.key == 'User'}
|
{#if groupSelection.key == 'user'}
|
||||||
<th scope="col"><a href="/monitoring/user/{te.id}?cluster={cluster.name}">{te.id}</a></th>
|
<th scope="col"><a href="/monitoring/user/{te.id}?cluster={cluster.name}">{te.id}</a></th>
|
||||||
{:else}
|
{:else}
|
||||||
<th scope="col"><a href="/monitoring/jobs/?cluster={cluster.name}&project={te.id}&projectMatch=eq">{te.id}</a></th>
|
<th scope="col"><a href="/monitoring/jobs/?cluster={cluster.name}&project={te.id}&projectMatch=eq">{te.id}</a></th>
|
||||||
@ -315,7 +315,7 @@
|
|||||||
{:else if $rooflineQuery.data && cluster}
|
{:else if $rooflineQuery.data && cluster}
|
||||||
<div bind:clientWidth={colWidth2}>
|
<div bind:clientWidth={colWidth2}>
|
||||||
{#key $rooflineQuery.data}
|
{#key $rooflineQuery.data}
|
||||||
<Roofline
|
<RooflineHeatmap
|
||||||
width={colWidth2} height={300}
|
width={colWidth2} height={300}
|
||||||
tiles={$rooflineQuery.data.rooflineHeatmap}
|
tiles={$rooflineQuery.data.rooflineHeatmap}
|
||||||
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
|
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
groupByScope,
|
groupByScope,
|
||||||
fetchMetricsStore,
|
fetchMetricsStore,
|
||||||
checkMetricDisabled,
|
checkMetricDisabled,
|
||||||
|
transformDataForRoofline
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
@ -131,7 +132,6 @@
|
|||||||
|
|
||||||
let plots = {},
|
let plots = {},
|
||||||
jobTags,
|
jobTags,
|
||||||
fullWidth,
|
|
||||||
statsTable;
|
statsTable;
|
||||||
$: document.title = $initq.fetching
|
$: document.title = $initq.fetching
|
||||||
? "Loading..."
|
? "Loading..."
|
||||||
@ -190,7 +190,6 @@
|
|||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="row" bind:clientWidth={fullWidth} />
|
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
{#if $initq.error}
|
{#if $initq.error}
|
||||||
@ -245,7 +244,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<Col>
|
<Col>
|
||||||
<Polar
|
<Polar
|
||||||
size={fullWidth / 4.1}
|
|
||||||
metrics={ccconfig[
|
metrics={ccconfig[
|
||||||
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
|
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
|
||||||
] || ccconfig[`job_view_polarPlotMetrics`]}
|
] || ccconfig[`job_view_polarPlotMetrics`]}
|
||||||
@ -255,19 +253,18 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Roofline
|
<Roofline
|
||||||
width={fullWidth / 3 - 10}
|
renderTime={true}
|
||||||
height={fullWidth / 5}
|
|
||||||
cluster={clusters
|
cluster={clusters
|
||||||
.find((c) => c.name == $initq.data.job.cluster)
|
.find((c) => c.name == $initq.data.job.cluster)
|
||||||
.subClusters.find(
|
.subClusters.find(
|
||||||
(sc) => sc.name == $initq.data.job.subCluster
|
(sc) => sc.name == $initq.data.job.subCluster
|
||||||
)}
|
)}
|
||||||
flopsAny={$jobMetrics.data.jobMetrics.find(
|
data={
|
||||||
(m) => m.name == "flops_any" && m.scope == "node"
|
transformDataForRoofline (
|
||||||
)}
|
$jobMetrics.data.jobMetrics.find((m) => m.name == "flops_any" && m.scope == "node").metric,
|
||||||
memBw={$jobMetrics.data.jobMetrics.find(
|
$jobMetrics.data.jobMetrics.find((m) => m.name == "mem_bw" && m.scope == "node").metric
|
||||||
(m) => m.name == "mem_bw" && m.scope == "node"
|
)
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
{:else}
|
{:else}
|
||||||
@ -275,8 +272,7 @@
|
|||||||
<Col />
|
<Col />
|
||||||
{/if}
|
{/if}
|
||||||
</Row>
|
</Row>
|
||||||
<br />
|
<Row class="mb-3">
|
||||||
<Row>
|
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
{#if $initq.data}
|
{#if $initq.data}
|
||||||
<TagManagement job={$initq.data.job} bind:jobTags />
|
<TagManagement job={$initq.data.job} bind:jobTags />
|
||||||
@ -293,7 +289,6 @@
|
|||||||
<Zoom timeseriesPlots={plots} />
|
<Zoom timeseriesPlots={plots} />
|
||||||
</Col> -->
|
</Col> -->
|
||||||
</Row>
|
</Row>
|
||||||
<br />
|
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
{#if $jobMetrics.error}
|
{#if $jobMetrics.error}
|
||||||
@ -340,8 +335,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<br />
|
<Row class="mt-2">
|
||||||
<Row>
|
|
||||||
<Col>
|
<Col>
|
||||||
{#if $initq.data}
|
{#if $initq.data}
|
||||||
<TabContent>
|
<TabContent>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import Refresher from "./joblist/Refresher.svelte";
|
import Refresher from "./joblist/Refresher.svelte";
|
||||||
import Roofline, { transformPerNodeData } from "./plots/Roofline.svelte";
|
import Roofline from "./plots/Roofline.svelte";
|
||||||
import Pie, { colors } from "./plots/Pie.svelte";
|
import Pie, { colors } from "./plots/Pie.svelte";
|
||||||
import Histogram from "./plots/Histogram.svelte";
|
import Histogram from "./plots/Histogram.svelte";
|
||||||
import {
|
import {
|
||||||
@ -16,7 +16,7 @@
|
|||||||
Progress,
|
Progress,
|
||||||
Icon,
|
Icon,
|
||||||
} from "sveltestrap";
|
} from "sveltestrap";
|
||||||
import { init, convert2uplot } from "./utils.js";
|
import { init, convert2uplot, transformPerNodeDataForRoofline } from "./utils.js";
|
||||||
import { scaleNumbers } from "./units.js";
|
import { scaleNumbers } from "./units.js";
|
||||||
import {
|
import {
|
||||||
queryStore,
|
queryStore,
|
||||||
@ -31,8 +31,8 @@
|
|||||||
export let cluster;
|
export let cluster;
|
||||||
|
|
||||||
let plotWidths = [],
|
let plotWidths = [],
|
||||||
colWidth1 = 0,
|
colWidth1,
|
||||||
colWidth2;
|
colWidth2
|
||||||
let from = new Date(Date.now() - 5 * 60 * 1000),
|
let from = new Date(Date.now() - 5 * 60 * 1000),
|
||||||
to = new Date(Date.now());
|
to = new Date(Date.now());
|
||||||
const topOptions = [
|
const topOptions = [
|
||||||
@ -427,16 +427,17 @@
|
|||||||
<div bind:clientWidth={plotWidths[i]}>
|
<div bind:clientWidth={plotWidths[i]}>
|
||||||
{#key $mainQuery.data.nodeMetrics}
|
{#key $mainQuery.data.nodeMetrics}
|
||||||
<Roofline
|
<Roofline
|
||||||
|
allowSizeChange={true}
|
||||||
width={plotWidths[i] - 10}
|
width={plotWidths[i] - 10}
|
||||||
height={300}
|
height={300}
|
||||||
colorDots={true}
|
|
||||||
showTime={false}
|
|
||||||
cluster={subCluster}
|
cluster={subCluster}
|
||||||
data={transformPerNodeData(
|
data={
|
||||||
$mainQuery.data.nodeMetrics.filter(
|
transformPerNodeDataForRoofline(
|
||||||
(data) => data.subCluster == subCluster.name
|
$mainQuery.data.nodeMetrics.filter(
|
||||||
|
(data) => data.subCluster == subCluster.name
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
@ -444,7 +445,7 @@
|
|||||||
</Row>
|
</Row>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<hr style="margin-top: -1em;" />
|
<hr/>
|
||||||
|
|
||||||
<!-- Usage Stats as Histograms -->
|
<!-- Usage Stats as Histograms -->
|
||||||
|
|
||||||
|
@ -22,10 +22,10 @@
|
|||||||
LineElement
|
LineElement
|
||||||
);
|
);
|
||||||
|
|
||||||
export let size
|
|
||||||
export let metrics
|
export let metrics
|
||||||
export let cluster
|
export let cluster
|
||||||
export let jobMetrics
|
export let jobMetrics
|
||||||
|
export let height = 365
|
||||||
|
|
||||||
const metricConfig = getContext('metrics')
|
const metricConfig = getContext('metrics')
|
||||||
|
|
||||||
@ -89,13 +89,19 @@
|
|||||||
// No custom defined options but keep for clarity
|
// No custom defined options but keep for clarity
|
||||||
const options = {
|
const options = {
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
animation: false
|
animation: false,
|
||||||
|
scales: { // fix scale
|
||||||
|
r: {
|
||||||
|
suggestedMin: 0.0,
|
||||||
|
suggestedMax: 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<Radar {data} {options} width={size} height={size}/>
|
<Radar {data} {options} {height}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,42 +1,52 @@
|
|||||||
<div class="cc-plot">
|
<script>
|
||||||
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
|
import uPlot from 'uplot'
|
||||||
</div>
|
import { formatNumber } from '../units.js'
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
import { Card } from 'sveltestrap'
|
||||||
|
|
||||||
<script context="module">
|
export let data = null
|
||||||
const axesColor = '#aaaaaa'
|
export let renderTime = false
|
||||||
const tickFontSize = 10
|
export let allowSizeChange = false
|
||||||
const labelFontSize = 12
|
export let cluster = null
|
||||||
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
|
export let width = 600
|
||||||
const paddingLeft = 40,
|
export let height = 350
|
||||||
paddingRight = 10,
|
|
||||||
paddingTop = 10,
|
|
||||||
paddingBottom = 50
|
|
||||||
|
|
||||||
|
let plotWrapper = null
|
||||||
|
let uplot = null
|
||||||
|
let timeoutId = null
|
||||||
|
|
||||||
|
/* Data Format
|
||||||
|
* data = [null, [], []] // 0: null-axis required for scatter, 1: Array of XY-Array for Scatter, 2: Optional Time Info
|
||||||
|
* data[1][0] = [100, 200, 500, ...] // X Axis -> Intensity (Vals up to clusters' flopRateScalar value)
|
||||||
|
* data[1][1] = [1000, 2000, 1500, ...] // Y Axis -> Performance (Vals up to clusters' flopRateSimd value)
|
||||||
|
* data[2] = [0.1, 0.15, 0.2, ...] // Color Code -> Time Information (Floats from 0 to 1) (Optional)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Helpers
|
||||||
function getGradientR(x) {
|
function getGradientR(x) {
|
||||||
if (x < 0.5) return 0
|
if (x < 0.5) return 0
|
||||||
if (x > 0.75) return 255
|
if (x > 0.75) return 255
|
||||||
x = (x - 0.5) * 4.0
|
x = (x - 0.5) * 4.0
|
||||||
return Math.floor(x * 255.0)
|
return Math.floor(x * 255.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGradientG(x) {
|
function getGradientG(x) {
|
||||||
if (x > 0.25 && x < 0.75) return 255
|
if (x > 0.25 && x < 0.75) return 255
|
||||||
if (x < 0.25) x = x * 4.0
|
if (x < 0.25) x = x * 4.0
|
||||||
else x = 1.0 - (x - 0.75) * 4.0
|
else x = 1.0 - (x - 0.75) * 4.0
|
||||||
return Math.floor(x * 255.0)
|
return Math.floor(x * 255.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGradientB(x) {
|
function getGradientB(x) {
|
||||||
if (x < 0.25) return 255
|
if (x < 0.25) return 255
|
||||||
if (x > 0.5) return 0
|
if (x > 0.5) return 0
|
||||||
x = 1.0 - (x - 0.25) * 4.0
|
x = 1.0 - (x - 0.25) * 4.0
|
||||||
return Math.floor(x * 255.0)
|
return Math.floor(x * 255.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRGB(c) {
|
function getRGB(c) {
|
||||||
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`
|
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`
|
||||||
}
|
}
|
||||||
|
function nearestThousand (num) {
|
||||||
|
return Math.ceil(num/1000) * 1000
|
||||||
|
}
|
||||||
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||||
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
||||||
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
|
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
|
||||||
@ -45,314 +55,194 @@
|
|||||||
y: y1 + a * (y2 - y1)
|
y: y1 + a * (y2 - y1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// End Helpers
|
||||||
|
|
||||||
function axisStepFactor(i, size) {
|
// Dot Renderers
|
||||||
if (size && size < 500)
|
const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
|
||||||
return 10
|
const size = 5 * devicePixelRatio;
|
||||||
|
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||||
|
let d = u.data[seriesIdx];
|
||||||
|
let deg360 = 2 * Math.PI;
|
||||||
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
|
let p = new Path2D();
|
||||||
|
let xVal = d[0][i];
|
||||||
|
let yVal = d[1][i];
|
||||||
|
u.ctx.strokeStyle = getRGB(u.data[2][i])
|
||||||
|
u.ctx.fillStyle = getRGB(u.data[2][i])
|
||||||
|
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
|
||||||
|
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||||
|
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||||
|
|
||||||
if (i % 3 == 0)
|
p.moveTo(cx + size/2, cy);
|
||||||
return 2
|
arc(p, cx, cy, size/2, 0, deg360);
|
||||||
else if (i % 3 == 1)
|
}
|
||||||
return 2.5
|
u.ctx.fill(p);
|
||||||
else
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(ctx, data, cluster, width, height, colorDots, showTime, defaultMaxY) {
|
|
||||||
if (width <= 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd?.value || defaultMaxY]
|
|
||||||
const w = width - paddingLeft - paddingRight
|
|
||||||
const h = height - paddingTop - paddingBottom
|
|
||||||
|
|
||||||
// Helpers:
|
|
||||||
const [log10minX, log10maxX, log10minY, log10maxY] =
|
|
||||||
[Math.log10(minX), Math.log10(maxX), Math.log10(minY), Math.log10(maxY)]
|
|
||||||
|
|
||||||
/* Value -> Pixel-Coordinate */
|
|
||||||
const getCanvasX = (x) => {
|
|
||||||
x = Math.log10(x)
|
|
||||||
x -= log10minX; x /= (log10maxX - log10minX)
|
|
||||||
return Math.round((x * w) + paddingLeft)
|
|
||||||
}
|
|
||||||
const getCanvasY = (y) => {
|
|
||||||
y = Math.log10(y)
|
|
||||||
y -= log10minY
|
|
||||||
y /= (log10maxY - log10minY)
|
|
||||||
return Math.round((h - y * h) + paddingTop)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axes
|
|
||||||
ctx.fillStyle = 'black'
|
|
||||||
ctx.strokeStyle = axesColor
|
|
||||||
ctx.font = `${tickFontSize}px ${fontFamily}`
|
|
||||||
ctx.beginPath()
|
|
||||||
for (let x = minX, i = 0; x <= maxX; i++) {
|
|
||||||
let px = getCanvasX(x)
|
|
||||||
let text = formatNumber(x)
|
|
||||||
let textWidth = ctx.measureText(text).width
|
|
||||||
ctx.fillText(text,
|
|
||||||
Math.floor(px - (textWidth / 2)),
|
|
||||||
height - paddingBottom + tickFontSize + 5)
|
|
||||||
ctx.moveTo(px, paddingTop - 5)
|
|
||||||
ctx.lineTo(px, height - paddingBottom + 5)
|
|
||||||
|
|
||||||
x *= axisStepFactor(i, w)
|
|
||||||
}
|
|
||||||
if (data.xLabel) {
|
|
||||||
ctx.font = `${labelFontSize}px ${fontFamily}`
|
|
||||||
let textWidth = ctx.measureText(data.xLabel).width
|
|
||||||
ctx.fillText(data.xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.textAlign = 'center'
|
|
||||||
ctx.font = `${tickFontSize}px ${fontFamily}`
|
|
||||||
for (let y = minY, i = 0; y <= maxY; i++) {
|
|
||||||
let py = getCanvasY(y)
|
|
||||||
ctx.moveTo(paddingLeft - 5, py)
|
|
||||||
ctx.lineTo(width - paddingRight + 5, py)
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
ctx.translate(paddingLeft - 10, py)
|
|
||||||
ctx.rotate(-Math.PI / 2)
|
|
||||||
ctx.fillText(formatNumber(y), 0, 0)
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
y *= axisStepFactor(i)
|
|
||||||
}
|
|
||||||
if (data.yLabel) {
|
|
||||||
ctx.font = `${labelFontSize}px ${fontFamily}`
|
|
||||||
ctx.save()
|
|
||||||
ctx.translate(15, Math.floor(height / 2))
|
|
||||||
ctx.rotate(-Math.PI / 2)
|
|
||||||
ctx.fillText(data.yLabel, 0, 0)
|
|
||||||
ctx.restore()
|
|
||||||
}
|
|
||||||
ctx.stroke()
|
|
||||||
|
|
||||||
// Draw Data
|
|
||||||
if (data.x && data.y) {
|
|
||||||
for (let i = 0; i < data.x.length; i++) {
|
|
||||||
let x = data.x[i], y = data.y[i], c = data.c[i]
|
|
||||||
if (x == null || y == null || Number.isNaN(x) || Number.isNaN(y))
|
|
||||||
continue
|
|
||||||
|
|
||||||
const s = 3
|
|
||||||
const px = getCanvasX(x)
|
|
||||||
const py = getCanvasY(y)
|
|
||||||
|
|
||||||
ctx.fillStyle = getRGB(c)
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(px, py, s, 0, Math.PI * 2, false)
|
|
||||||
ctx.fill()
|
|
||||||
}
|
}
|
||||||
} else if (data.tiles) {
|
});
|
||||||
const rows = data.tiles.length
|
return null;
|
||||||
const cols = data.tiles[0].length
|
};
|
||||||
|
|
||||||
const tileWidth = Math.ceil(w / cols)
|
const drawPoints = (u, seriesIdx, idx0, idx1) => {
|
||||||
const tileHeight = Math.ceil(h / rows)
|
const size = 5 * devicePixelRatio;
|
||||||
|
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||||
let max = data.tiles.reduce((max, row) =>
|
let d = u.data[seriesIdx];
|
||||||
Math.max(max, row.reduce((max, val) =>
|
u.ctx.strokeStyle = getRGB(0);
|
||||||
Math.max(max, val)), 0), 0)
|
u.ctx.fillStyle = getRGB(0);
|
||||||
|
let deg360 = 2 * Math.PI;
|
||||||
if (max == 0)
|
let p = new Path2D();
|
||||||
max = 1
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
|
let xVal = d[0][i];
|
||||||
const tileColor = val => `rgba(255, 0, 0, ${(val / max)})`
|
let yVal = d[1][i];
|
||||||
|
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
|
||||||
for (let i = 0; i < rows; i++) {
|
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||||
for (let j = 0; j < cols; j++) {
|
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||||
let px = paddingLeft + (j / cols) * w
|
p.moveTo(cx + size/2, cy);
|
||||||
let py = paddingTop + (h - (i / rows) * h) - tileHeight
|
arc(p, cx, cy, size/2, 0, deg360);
|
||||||
|
|
||||||
ctx.fillStyle = tileColor(data.tiles[i][j])
|
|
||||||
ctx.fillRect(px, py, tileWidth, tileHeight)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
u.ctx.fill(p);
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// Draw roofs
|
// Main Function
|
||||||
ctx.strokeStyle = 'black'
|
function render(plotData) {
|
||||||
ctx.lineWidth = 2
|
if (plotData) {
|
||||||
ctx.beginPath()
|
const opts = {
|
||||||
if (cluster != null) {
|
title: "",
|
||||||
const ycut = 0.01 * cluster.memoryBandwidth.value
|
mode: 2,
|
||||||
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
|
width: width,
|
||||||
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
|
height: height,
|
||||||
const scalarKneeX = getCanvasX(scalarKnee),
|
legend: {
|
||||||
simdKneeX = getCanvasX(simdKnee),
|
show: false
|
||||||
flopRateScalarY = getCanvasY(cluster.flopRateScalar.value),
|
},
|
||||||
flopRateSimdY = getCanvasY(cluster.flopRateSimd.value)
|
cursor: { drag: { x: false, y: false } },
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
label: 'Intensity [FLOPS/Byte]',
|
||||||
|
values: (u, vals) => vals.map(v => formatNumber(v))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Performace [GFLOPS]',
|
||||||
|
values: (u, vals) => vals.map(v => formatNumber(v))
|
||||||
|
}
|
||||||
|
],
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
time: false,
|
||||||
|
range: [0.01, 1000],
|
||||||
|
distr: 3, // Render as log
|
||||||
|
log: 10, // log exp
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
range: [1.0, cluster?.flopRateSimd?.value ? nearestThousand(cluster.flopRateSimd.value) : 10000],
|
||||||
|
distr: 3, // Render as log
|
||||||
|
log: 10, // log exp
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{},
|
||||||
|
{ paths: renderTime ? drawColorPoints : drawPoints }
|
||||||
|
],
|
||||||
|
hooks: {
|
||||||
|
drawClear: [
|
||||||
|
u => {
|
||||||
|
u.series.forEach((s, i) => {
|
||||||
|
if (i > 0)
|
||||||
|
s._paths = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
draw: [
|
||||||
|
u => { // draw roofs when cluster set
|
||||||
|
// console.log(u)
|
||||||
|
if (cluster != null) {
|
||||||
|
const padding = u._padding // [top, right, bottom, left]
|
||||||
|
|
||||||
if (scalarKneeX < width - paddingRight) {
|
u.ctx.strokeStyle = 'black'
|
||||||
ctx.moveTo(scalarKneeX, flopRateScalarY)
|
u.ctx.lineWidth = 2
|
||||||
ctx.lineTo(width - paddingRight, flopRateScalarY)
|
u.ctx.beginPath()
|
||||||
}
|
|
||||||
|
|
||||||
if (simdKneeX < width - paddingRight) {
|
const ycut = 0.01 * cluster.memoryBandwidth.value
|
||||||
ctx.moveTo(simdKneeX, flopRateSimdY)
|
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
|
||||||
ctx.lineTo(width - paddingRight, flopRateSimdY)
|
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
|
||||||
}
|
const scalarKneeX = u.valToPos(scalarKnee, 'x', true), // Value, axis, toCanvasPixels
|
||||||
|
simdKneeX = u.valToPos(simdKnee, 'x', true),
|
||||||
|
flopRateScalarY = u.valToPos(cluster.flopRateScalar.value, 'y', true),
|
||||||
|
flopRateSimdY = u.valToPos(cluster.flopRateSimd.value, 'y', true)
|
||||||
|
|
||||||
let x1 = getCanvasX(0.01),
|
if (scalarKneeX < width - padding[1]) { // Top horizontal roofline
|
||||||
y1 = getCanvasY(ycut),
|
u.ctx.moveTo(scalarKneeX, flopRateScalarY)
|
||||||
x2 = getCanvasX(simdKnee),
|
u.ctx.lineTo(width - padding[1], flopRateScalarY)
|
||||||
y2 = flopRateSimdY
|
}
|
||||||
|
|
||||||
let xAxisIntersect = lineIntersect(
|
if (simdKneeX < width - padding[1]) { // Lower horitontal roofline
|
||||||
x1, y1, x2, y2,
|
u.ctx.moveTo(simdKneeX, flopRateSimdY)
|
||||||
0, height - paddingBottom, width, height - paddingBottom)
|
u.ctx.lineTo(width - padding[1], flopRateSimdY)
|
||||||
|
}
|
||||||
|
|
||||||
if (xAxisIntersect.x > x1) {
|
let x1 = u.valToPos(0.01, 'x', true),
|
||||||
x1 = xAxisIntersect.x
|
y1 = u.valToPos(ycut, 'y', true)
|
||||||
y1 = xAxisIntersect.y
|
|
||||||
}
|
let x2 = u.valToPos(simdKnee, 'x', true),
|
||||||
|
y2 = flopRateSimdY
|
||||||
|
|
||||||
ctx.moveTo(x1, y1)
|
let xAxisIntersect = lineIntersect(
|
||||||
ctx.lineTo(x2, y2)
|
x1, y1, x2, y2,
|
||||||
}
|
u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), // X-Axis Start Coords
|
||||||
ctx.stroke()
|
u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis End Coords
|
||||||
|
)
|
||||||
|
|
||||||
if (colorDots && showTime && data.x && data.y) {
|
if (xAxisIntersect.x > x1) {
|
||||||
// The Color Scale For Time Information
|
x1 = xAxisIntersect.x
|
||||||
ctx.fillStyle = 'black'
|
y1 = xAxisIntersect.y
|
||||||
ctx.fillText('Time:', 17, height - 5)
|
}
|
||||||
const start = paddingLeft + 5
|
|
||||||
for (let x = start; x < width - paddingRight; x += 15) {
|
|
||||||
let c = (x - start) / (width - start - paddingRight)
|
|
||||||
ctx.fillStyle = getRGB(c)
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(x, height - 10, 5, 0, Math.PI * 2, false)
|
|
||||||
ctx.fill()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function transformData(flopsAny, memBw, colorDots) { // Uses Metric Object
|
// Diagonal
|
||||||
const nodes = flopsAny.series.length
|
u.ctx.moveTo(x1, y1)
|
||||||
const timesteps = flopsAny.series[0].data.length
|
u.ctx.lineTo(x2, y2)
|
||||||
|
|
||||||
/* c will contain values from 0 to 1 representing the time */
|
u.ctx.stroke()
|
||||||
const x = [], y = [], c = []
|
// Reset grid lineWidth
|
||||||
|
u.ctx.lineWidth = 0.15
|
||||||
if (flopsAny && memBw) {
|
}
|
||||||
for (let i = 0; i < nodes; i++) {
|
}
|
||||||
const flopsData = flopsAny.series[i].data
|
]
|
||||||
const memBwData = memBw.series[i].data
|
},
|
||||||
for (let j = 0; j < timesteps; j++) {
|
};
|
||||||
const f = flopsData[j], m = memBwData[j]
|
uplot = new uPlot(opts, plotData, plotWrapper);
|
||||||
const intensity = f / m
|
|
||||||
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
|
|
||||||
continue
|
|
||||||
|
|
||||||
x.push(intensity)
|
|
||||||
y.push(f)
|
|
||||||
c.push(colorDots ? j / timesteps : 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn("transformData: metrics for 'mem_bw' and/or 'flops_any' missing!")
|
console.log('No data for roofline!')
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x, y, c,
|
|
||||||
xLabel: 'Intensity [FLOPS/byte]',
|
|
||||||
yLabel: 'Performance [GFLOPS]'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return something to be plotted. The argument shall be the result of the
|
// Svelte and Sizechange
|
||||||
// `nodeMetrics` GraphQL query.
|
|
||||||
export function transformPerNodeData(nodes) {
|
|
||||||
const x = [], y = [], c = []
|
|
||||||
for (let node of nodes) {
|
|
||||||
let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.scope == 'node')?.metric
|
|
||||||
let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.scope == 'node')?.metric
|
|
||||||
if (!flopsAny || !memBw) {
|
|
||||||
console.warn("transformPerNodeData: metrics for 'mem_bw' and/or 'flops_any' missing!")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let flopsData = flopsAny.series[0].data, memBwData = memBw.series[0].data
|
|
||||||
const f = flopsData[flopsData.length - 1], m = memBwData[flopsData.length - 1]
|
|
||||||
const intensity = f / m
|
|
||||||
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
|
|
||||||
continue
|
|
||||||
|
|
||||||
x.push(intensity)
|
|
||||||
y.push(f)
|
|
||||||
c.push(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x, y, c,
|
|
||||||
xLabel: 'Intensity [FLOPS/byte]',
|
|
||||||
yLabel: 'Performance [GFLOPS]'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { onMount, tick } from 'svelte'
|
|
||||||
import { formatNumber } from '../units.js'
|
|
||||||
|
|
||||||
export let flopsAny = null
|
|
||||||
export let memBw = null
|
|
||||||
export let cluster = null
|
|
||||||
export let maxY = null
|
|
||||||
export let width = 500
|
|
||||||
export let height = 300
|
|
||||||
export let tiles = null
|
|
||||||
export let colorDots = true
|
|
||||||
export let showTime = true
|
|
||||||
export let data = null
|
|
||||||
|
|
||||||
console.assert(data || tiles || (flopsAny && memBw), "you must provide flopsAny and memBw or tiles!")
|
|
||||||
|
|
||||||
let ctx, canvasElement, prevWidth = width, prevHeight = height
|
|
||||||
data = data != null ? data : (flopsAny && memBw
|
|
||||||
? transformData(flopsAny.metric, memBw.metric, colorDots) // Use Metric Object from Parent
|
|
||||||
: {
|
|
||||||
tiles: tiles,
|
|
||||||
xLabel: 'Intensity [FLOPS/byte]',
|
|
||||||
yLabel: 'Performance [GFLOPS]'
|
|
||||||
})
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
ctx = canvasElement.getContext('2d')
|
render(data)
|
||||||
if (prevWidth != width || prevHeight != height) {
|
|
||||||
sizeChanged()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
canvasElement.width = width
|
|
||||||
canvasElement.height = height
|
|
||||||
render(ctx, data, cluster, width, height, colorDots, showTime, maxY)
|
|
||||||
})
|
})
|
||||||
|
onDestroy(() => {
|
||||||
let timeoutId = null
|
if (uplot)
|
||||||
function sizeChanged() {
|
uplot.destroy()
|
||||||
if (!ctx)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (timeoutId != null)
|
if (timeoutId != null)
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
})
|
||||||
|
function sizeChanged() {
|
||||||
|
if (timeoutId != null)
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
prevWidth = width
|
|
||||||
prevHeight = height
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
if (!canvasElement)
|
|
||||||
return
|
|
||||||
|
|
||||||
timeoutId = null
|
timeoutId = null
|
||||||
canvasElement.width = width
|
if (uplot)
|
||||||
canvasElement.height = height
|
uplot.destroy()
|
||||||
render(ctx, data, cluster, width, height, colorDots, showTime, maxY)
|
render(data)
|
||||||
}, 250)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
$: if (allowSizeChange) sizeChanged(width, height)
|
||||||
$: sizeChanged(width, height)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if data != null}
|
||||||
|
<div bind:this={plotWrapper}/>
|
||||||
|
{:else}
|
||||||
|
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card>
|
||||||
|
{/if}
|
234
web/frontend/src/plots/RooflineHeatmap.svelte
Normal file
234
web/frontend/src/plots/RooflineHeatmap.svelte
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
<div class="cc-plot">
|
||||||
|
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script context="module">
|
||||||
|
const axesColor = '#aaaaaa'
|
||||||
|
const tickFontSize = 10
|
||||||
|
const labelFontSize = 12
|
||||||
|
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
|
||||||
|
const paddingLeft = 40,
|
||||||
|
paddingRight = 10,
|
||||||
|
paddingTop = 10,
|
||||||
|
paddingBottom = 50
|
||||||
|
|
||||||
|
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||||
|
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
||||||
|
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
|
||||||
|
return {
|
||||||
|
x: x1 + a * (x2 - x1),
|
||||||
|
y: y1 + a * (y2 - y1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function axisStepFactor(i, size) {
|
||||||
|
if (size && size < 500)
|
||||||
|
return 10
|
||||||
|
|
||||||
|
if (i % 3 == 0)
|
||||||
|
return 2
|
||||||
|
else if (i % 3 == 1)
|
||||||
|
return 2.5
|
||||||
|
else
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(ctx, data, cluster, width, height, defaultMaxY) {
|
||||||
|
if (width <= 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd?.value || defaultMaxY]
|
||||||
|
const w = width - paddingLeft - paddingRight
|
||||||
|
const h = height - paddingTop - paddingBottom
|
||||||
|
|
||||||
|
// Helpers:
|
||||||
|
const [log10minX, log10maxX, log10minY, log10maxY] =
|
||||||
|
[Math.log10(minX), Math.log10(maxX), Math.log10(minY), Math.log10(maxY)]
|
||||||
|
|
||||||
|
/* Value -> Pixel-Coordinate */
|
||||||
|
const getCanvasX = (x) => {
|
||||||
|
x = Math.log10(x)
|
||||||
|
x -= log10minX; x /= (log10maxX - log10minX)
|
||||||
|
return Math.round((x * w) + paddingLeft)
|
||||||
|
}
|
||||||
|
const getCanvasY = (y) => {
|
||||||
|
y = Math.log10(y)
|
||||||
|
y -= log10minY
|
||||||
|
y /= (log10maxY - log10minY)
|
||||||
|
return Math.round((h - y * h) + paddingTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axes
|
||||||
|
ctx.fillStyle = 'black'
|
||||||
|
ctx.strokeStyle = axesColor
|
||||||
|
ctx.font = `${tickFontSize}px ${fontFamily}`
|
||||||
|
ctx.beginPath()
|
||||||
|
for (let x = minX, i = 0; x <= maxX; i++) {
|
||||||
|
let px = getCanvasX(x)
|
||||||
|
let text = formatNumber(x)
|
||||||
|
let textWidth = ctx.measureText(text).width
|
||||||
|
ctx.fillText(text,
|
||||||
|
Math.floor(px - (textWidth / 2)),
|
||||||
|
height - paddingBottom + tickFontSize + 5)
|
||||||
|
ctx.moveTo(px, paddingTop - 5)
|
||||||
|
ctx.lineTo(px, height - paddingBottom + 5)
|
||||||
|
|
||||||
|
x *= axisStepFactor(i, w)
|
||||||
|
}
|
||||||
|
if (data.xLabel) {
|
||||||
|
ctx.font = `${labelFontSize}px ${fontFamily}`
|
||||||
|
let textWidth = ctx.measureText(data.xLabel).width
|
||||||
|
ctx.fillText(data.xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.font = `${tickFontSize}px ${fontFamily}`
|
||||||
|
for (let y = minY, i = 0; y <= maxY; i++) {
|
||||||
|
let py = getCanvasY(y)
|
||||||
|
ctx.moveTo(paddingLeft - 5, py)
|
||||||
|
ctx.lineTo(width - paddingRight + 5, py)
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(paddingLeft - 10, py)
|
||||||
|
ctx.rotate(-Math.PI / 2)
|
||||||
|
ctx.fillText(formatNumber(y), 0, 0)
|
||||||
|
ctx.restore()
|
||||||
|
|
||||||
|
y *= axisStepFactor(i)
|
||||||
|
}
|
||||||
|
if (data.yLabel) {
|
||||||
|
ctx.font = `${labelFontSize}px ${fontFamily}`
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(15, Math.floor(height / 2))
|
||||||
|
ctx.rotate(-Math.PI / 2)
|
||||||
|
ctx.fillText(data.yLabel, 0, 0)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Draw Data
|
||||||
|
if (data.tiles) {
|
||||||
|
const rows = data.tiles.length
|
||||||
|
const cols = data.tiles[0].length
|
||||||
|
|
||||||
|
const tileWidth = Math.ceil(w / cols)
|
||||||
|
const tileHeight = Math.ceil(h / rows)
|
||||||
|
|
||||||
|
let max = data.tiles.reduce((max, row) =>
|
||||||
|
Math.max(max, row.reduce((max, val) =>
|
||||||
|
Math.max(max, val)), 0), 0)
|
||||||
|
|
||||||
|
if (max == 0)
|
||||||
|
max = 1
|
||||||
|
|
||||||
|
const tileColor = val => `rgba(255, 0, 0, ${(val / max)})`
|
||||||
|
|
||||||
|
for (let i = 0; i < rows; i++) {
|
||||||
|
for (let j = 0; j < cols; j++) {
|
||||||
|
let px = paddingLeft + (j / cols) * w
|
||||||
|
let py = paddingTop + (h - (i / rows) * h) - tileHeight
|
||||||
|
|
||||||
|
ctx.fillStyle = tileColor(data.tiles[i][j])
|
||||||
|
ctx.fillRect(px, py, tileWidth, tileHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw roofs
|
||||||
|
ctx.strokeStyle = 'black'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.beginPath()
|
||||||
|
if (cluster != null) {
|
||||||
|
const ycut = 0.01 * cluster.memoryBandwidth.value
|
||||||
|
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
|
||||||
|
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
|
||||||
|
const scalarKneeX = getCanvasX(scalarKnee),
|
||||||
|
simdKneeX = getCanvasX(simdKnee),
|
||||||
|
flopRateScalarY = getCanvasY(cluster.flopRateScalar.value),
|
||||||
|
flopRateSimdY = getCanvasY(cluster.flopRateSimd.value)
|
||||||
|
|
||||||
|
if (scalarKneeX < width - paddingRight) {
|
||||||
|
ctx.moveTo(scalarKneeX, flopRateScalarY)
|
||||||
|
ctx.lineTo(width - paddingRight, flopRateScalarY)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simdKneeX < width - paddingRight) {
|
||||||
|
ctx.moveTo(simdKneeX, flopRateSimdY)
|
||||||
|
ctx.lineTo(width - paddingRight, flopRateSimdY)
|
||||||
|
}
|
||||||
|
|
||||||
|
let x1 = getCanvasX(0.01),
|
||||||
|
y1 = getCanvasY(ycut),
|
||||||
|
x2 = getCanvasX(simdKnee),
|
||||||
|
y2 = flopRateSimdY
|
||||||
|
|
||||||
|
let xAxisIntersect = lineIntersect(
|
||||||
|
x1, y1, x2, y2,
|
||||||
|
0, height - paddingBottom, width, height - paddingBottom)
|
||||||
|
|
||||||
|
if (xAxisIntersect.x > x1) {
|
||||||
|
x1 = xAxisIntersect.x
|
||||||
|
y1 = xAxisIntersect.y
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.moveTo(x1, y1)
|
||||||
|
ctx.lineTo(x2, y2)
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { formatNumber } from '../units.js'
|
||||||
|
|
||||||
|
export let cluster = null
|
||||||
|
export let tiles = null
|
||||||
|
export let maxY = null
|
||||||
|
export let width = 500
|
||||||
|
export let height = 300
|
||||||
|
|
||||||
|
console.assert(tiles, "you must provide tiles!")
|
||||||
|
|
||||||
|
let ctx, canvasElement, prevWidth = width, prevHeight = height
|
||||||
|
const data = {
|
||||||
|
tiles: tiles,
|
||||||
|
xLabel: 'Intensity [FLOPS/byte]',
|
||||||
|
yLabel: 'Performance [GFLOPS]'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ctx = canvasElement.getContext('2d')
|
||||||
|
if (prevWidth != width || prevHeight != height) {
|
||||||
|
sizeChanged()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasElement.width = width
|
||||||
|
canvasElement.height = height
|
||||||
|
render(ctx, data, cluster, width, height, maxY)
|
||||||
|
})
|
||||||
|
|
||||||
|
let timeoutId = null
|
||||||
|
function sizeChanged() {
|
||||||
|
if (!ctx)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (timeoutId != null)
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
|
prevWidth = width
|
||||||
|
prevHeight = height
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
if (!canvasElement)
|
||||||
|
return
|
||||||
|
|
||||||
|
timeoutId = null
|
||||||
|
canvasElement.width = width
|
||||||
|
canvasElement.height = height
|
||||||
|
render(ctx, data, cluster, width, height, maxY)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: sizeChanged(width, height)
|
||||||
|
</script>
|
@ -6,8 +6,8 @@ const power = [1, 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21]
|
|||||||
const prefix = ['', 'K', 'M', 'G', 'T', 'P', 'E']
|
const prefix = ['', 'K', 'M', 'G', 'T', 'P', 'E']
|
||||||
|
|
||||||
export function formatNumber(x) {
|
export function formatNumber(x) {
|
||||||
if ( isNaN(x) ) {
|
if ( isNaN(x) || x == null) {
|
||||||
return x // Return if String , used in Histograms
|
return x // Return if String or Null
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < prefix.length; i++)
|
for (let i = 0; i < prefix.length; i++)
|
||||||
if (power[i] <= x && x < power[i+1])
|
if (power[i] <= x && x < power[i+1])
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
} from "@urql/svelte";
|
} from "@urql/svelte";
|
||||||
import { setContext, getContext, hasContext, onDestroy, tick } from "svelte";
|
import { setContext, getContext, hasContext, onDestroy, tick } from "svelte";
|
||||||
import { readable } from "svelte/store";
|
import { readable } from "svelte/store";
|
||||||
import { formatNumber } from './units.js'
|
// import { formatNumber } from './units.js'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Call this function only at component initialization time!
|
* Call this function only at component initialization time!
|
||||||
@ -326,8 +326,11 @@ export function convert2uplot(canvasData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function binsFromFootprint(weights, scope, values, numBins) {
|
export function binsFromFootprint(weights, scope, values, numBins) {
|
||||||
let min = 0, max = 0
|
let min = 0, max = 0 //, median = 0
|
||||||
if (values.length != 0) {
|
if (values.length != 0) {
|
||||||
|
// Extreme, wrong peak vlaues: Filter here or backend?
|
||||||
|
// median = median(values)
|
||||||
|
|
||||||
for (let x of values) {
|
for (let x of values) {
|
||||||
min = Math.min(min, x)
|
min = Math.min(min, x)
|
||||||
max = Math.max(max, x)
|
max = Math.max(max, x)
|
||||||
@ -363,3 +366,75 @@ export function binsFromFootprint(weights, scope, values, numBins) {
|
|||||||
bins: bins
|
bins: bins
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function transformDataForRoofline(flopsAny, memBw) { // Uses Metric Objects: {series:[{},{},...], timestep:60, name:$NAME}
|
||||||
|
const nodes = flopsAny.series.length
|
||||||
|
const timesteps = flopsAny.series[0].data.length
|
||||||
|
|
||||||
|
/* c will contain values from 0 to 1 representing the time */
|
||||||
|
let data = null
|
||||||
|
const x = [], y = [], c = []
|
||||||
|
|
||||||
|
if (flopsAny && memBw) {
|
||||||
|
for (let i = 0; i < nodes; i++) {
|
||||||
|
const flopsData = flopsAny.series[i].data
|
||||||
|
const memBwData = memBw.series[i].data
|
||||||
|
for (let j = 0; j < timesteps; j++) {
|
||||||
|
const f = flopsData[j], m = memBwData[j]
|
||||||
|
const intensity = f / m
|
||||||
|
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
|
||||||
|
continue
|
||||||
|
|
||||||
|
x.push(intensity)
|
||||||
|
y.push(f)
|
||||||
|
c.push(j / timesteps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("transformData: metrics for 'mem_bw' and/or 'flops_any' missing!")
|
||||||
|
}
|
||||||
|
if (x.length > 0 && y.length > 0 && c.length > 0) {
|
||||||
|
data = [null, [x, y], c] // for dataformat see roofline.svelte
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return something to be plotted. The argument shall be the result of the
|
||||||
|
// `nodeMetrics` GraphQL query.
|
||||||
|
export function transformPerNodeDataForRoofline(nodes) {
|
||||||
|
let data = null
|
||||||
|
const x = [], y = []
|
||||||
|
for (let node of nodes) {
|
||||||
|
let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.scope == 'node')?.metric
|
||||||
|
let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.scope == 'node')?.metric
|
||||||
|
if (!flopsAny || !memBw) {
|
||||||
|
console.warn("transformPerNodeData: metrics for 'mem_bw' and/or 'flops_any' missing!")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let flopsData = flopsAny.series[0].data, memBwData = memBw.series[0].data
|
||||||
|
const f = flopsData[flopsData.length - 1], m = memBwData[flopsData.length - 1]
|
||||||
|
const intensity = f / m
|
||||||
|
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
|
||||||
|
continue
|
||||||
|
|
||||||
|
x.push(intensity)
|
||||||
|
y.push(f)
|
||||||
|
}
|
||||||
|
if (x.length > 0 && y.length > 0) {
|
||||||
|
data = [null, [x, y], []] // for dataformat see roofline.svelte
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/45309447/calculating-median-javascript
|
||||||
|
// function median(numbers) {
|
||||||
|
// const sorted = Array.from(numbers).sort((a, b) => a - b);
|
||||||
|
// const middle = Math.floor(sorted.length / 2);
|
||||||
|
|
||||||
|
// if (sorted.length % 2 === 0) {
|
||||||
|
// return (sorted[middle - 1] + sorted[middle]) / 2;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return sorted[middle];
|
||||||
|
// }
|
||||||
|
Loading…
Reference in New Issue
Block a user