From 4d7819802dbddc7120a5b268086a104fea6d44ea Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 1 Sep 2023 10:13:26 +0200 Subject: [PATCH 01/14] fix typo --- web/frontend/src/Analysis.root.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/frontend/src/Analysis.root.svelte b/web/frontend/src/Analysis.root.svelte index 67cc652..06219b9 100644 --- a/web/frontend/src/Analysis.root.svelte +++ b/web/frontend/src/Analysis.root.svelte @@ -293,7 +293,7 @@ {#each $topQuery.data.topList as te, i} - {#if groupSelection.key == 'User'} + {#if groupSelection.key == 'user'} {te.id} {:else} {te.id} From 69ee19bed09ad5ca9b79112175bfe18972fbb16a Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 1 Sep 2023 10:23:14 +0200 Subject: [PATCH 02/14] fix: include running jobs case in statsQueries --- internal/repository/stats.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/internal/repository/stats.go b/internal/repository/stats.go index 3ac3ffd..8084553 100644 --- a/internal/repository/stats.go +++ b/internal/repository/stats.go @@ -70,28 +70,30 @@ func (r *JobRepository) buildStatsQuery( var query sq.SelectBuilder 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 != "" { // Scan columns: id, totalJobs, totalWalltime, totalNodes, totalNodeHours, totalCores, totalCoreHours, totalAccs, totalAccHours 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(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(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(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)) / 3600) as %s) as totalWalltime`, time.Now().Unix(), castType), + fmt.Sprintf(`CAST(SUM(job.num_nodes) as %s) as totalNodes`, 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(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(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) } else { // Scan columns: totalJobs, totalWalltime, totalNodes, totalNodeHours, totalCores, totalCoreHours, totalAccs, totalAccHours query = sq.Select("COUNT(job.id)", - fmt.Sprintf("CAST(ROUND(SUM(job.duration) / 3600) 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(SUM(job.num_hwthreads) as %s)", castType), - fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_hwthreads) / 3600) 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)) / 3600) as %s)`, time.Now().Unix(), castType), + fmt.Sprintf(`CAST(SUM(job.num_nodes) 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(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(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 f5c43d60d37ddc1fff9a0a1636191793952a8651 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 1 Sep 2023 13:12:55 +0200 Subject: [PATCH 03/14] initial commit for rooflineuplot --- web/frontend/package-lock.json | 7 + web/frontend/package.json | 1 + web/frontend/src/Status.root.svelte | 6 + web/frontend/src/plots/Rooflineuplot.svelte | 187 ++++++ web/frontend/src/plots/Scatteruplot.svelte | 627 ++++++++++++++++++++ 5 files changed, 828 insertions(+) create mode 100644 web/frontend/src/plots/Rooflineuplot.svelte create mode 100644 web/frontend/src/plots/Scatteruplot.svelte diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json index eb80726..1ca3349 100644 --- a/web/frontend/package-lock.json +++ b/web/frontend/package-lock.json @@ -22,6 +22,7 @@ "@rollup/plugin-commonjs": "^24.1.0", "@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-terser": "^0.4.1", + "@timohausmann/quadtree-js": "^1.2.5", "rollup": "^3.21.0", "rollup-plugin-css-only": "^4.3.0", "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", diff --git a/web/frontend/package.json b/web/frontend/package.json index b0bb5af..3e77474 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -10,6 +10,7 @@ "@rollup/plugin-commonjs": "^24.1.0", "@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-terser": "^0.4.1", + "@timohausmann/quadtree-js": "^1.2.5", "rollup": "^3.21.0", "rollup-plugin-css-only": "^4.3.0", "rollup-plugin-svelte": "^7.1.4", diff --git a/web/frontend/src/Status.root.svelte b/web/frontend/src/Status.root.svelte index f1d8e05..27eb55e 100644 --- a/web/frontend/src/Status.root.svelte +++ b/web/frontend/src/Status.root.svelte @@ -2,6 +2,7 @@ import { getContext } from "svelte"; import Refresher from "./joblist/Refresher.svelte"; import Roofline, { transformPerNodeData } from "./plots/Roofline.svelte"; + import Rooflineuplot from "./plots/Rooflineuplot.svelte"; import Pie, { colors } from "./plots/Pie.svelte"; import Histogram from "./plots/Histogram.svelte"; import { @@ -665,4 +666,9 @@ {/key} + + + + + {/if} diff --git a/web/frontend/src/plots/Rooflineuplot.svelte b/web/frontend/src/plots/Rooflineuplot.svelte new file mode 100644 index 0000000..11c1a44 --- /dev/null +++ b/web/frontend/src/plots/Rooflineuplot.svelte @@ -0,0 +1,187 @@ + + +{#if data != null} +
+{:else} + Cannot render roofline: No data! +{/if} \ No newline at end of file diff --git a/web/frontend/src/plots/Scatteruplot.svelte b/web/frontend/src/plots/Scatteruplot.svelte new file mode 100644 index 0000000..0fac0b7 --- /dev/null +++ b/web/frontend/src/plots/Scatteruplot.svelte @@ -0,0 +1,627 @@ + + +{#if data != null} +
+{:else} + Cannot render scatter: No data! +{/if} \ No newline at end of file From c1b5134627d634e9e75cd675f3d7d3144e93c7c6 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Mon, 4 Sep 2023 10:37:20 +0200 Subject: [PATCH 04/14] Reduce uplot example code to common denominator --- web/frontend/src/Status.root.svelte | 8 +- web/frontend/src/plots/Rooflineuplot.svelte | 235 ++++++++++++-------- 2 files changed, 151 insertions(+), 92 deletions(-) diff --git a/web/frontend/src/Status.root.svelte b/web/frontend/src/Status.root.svelte index 27eb55e..1559121 100644 --- a/web/frontend/src/Status.root.svelte +++ b/web/frontend/src/Status.root.svelte @@ -33,7 +33,8 @@ let plotWidths = [], colWidth1 = 0, - colWidth2; + colWidth2 = 0, + colWidth3 = 0; let from = new Date(Date.now() - 5 * 60 * 1000), to = new Date(Date.now()); const topOptions = [ @@ -668,7 +669,10 @@ - +
+ {/if} diff --git a/web/frontend/src/plots/Rooflineuplot.svelte b/web/frontend/src/plots/Rooflineuplot.svelte index 11c1a44..e1f9c3c 100644 --- a/web/frontend/src/plots/Rooflineuplot.svelte +++ b/web/frontend/src/plots/Rooflineuplot.svelte @@ -4,10 +4,28 @@ import { onMount, onDestroy } from 'svelte' import { Card } from 'sveltestrap' + 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 + let plotWrapper = null let uplot = null let timeoutId = null + // Three Render-Cases: + // #1 Single-Job Roofline -> Has Time-Information: Use data, allow colorDots && showTime + // #2 MultiNode Roofline - > Has No Time-Information: Transform from nodeData, only "IST"-state of nodes, no timeInfo + // #3 Multi-Job Roofline -> No Time Information? -> Use Backend-Prepared "Tiles" with increasing occupancy for stronger values + + // Start Demo Data + function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } @@ -27,21 +45,52 @@ return arr; } - let points = 10000; - let series = 5; + let points = 100; + let series = 2; + let time = [] - console.time("prep"); + for (let i = 0; i < points; ++i) + time[i] = i; - let data = filledArr(series, v => [ - filledArr(points, i => randInt(0,500)), - filledArr(points, i => randInt(0,500)), + data = filledArr(series, v => [ + filledArr(points, i => randInt(0,200)), + filledArr(points, i => randInt(0,200)), ]); data[0] = null; - console.timeEnd("prep"); + console.log("Data: ", data); - console.log(data); + // End Demo Data + + // Helpers + + function getGradientR(x) { + if (x < 0.5) return 0 + if (x > 0.75) return 255 + x = (x - 0.5) * 4.0 + return Math.floor(x * 255.0) + } + + function getGradientG(x) { + if (x > 0.25 && x < 0.75) return 255 + if (x < 0.25) x = x * 4.0 + else x = 1.0 - (x - 0.75) * 4.0 + return Math.floor(x * 255.0) + } + + function getGradientB(x) { + if (x < 0.25) return 255 + if (x > 0.5) return 0 + x = 1.0 - (x - 0.25) * 4.0 + return Math.floor(x * 255.0) + } + + function getRGB(c) { + return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})` + } + + // End Helpers const drawPoints = (u, seriesIdx, idx0, idx1) => { const size = 5 * devicePixelRatio; @@ -53,8 +102,6 @@ let deg360 = 2 * Math.PI; - console.time("points"); - let p = new Path2D(); for (let i = 0; i < d[0].length; i++) { @@ -70,20 +117,12 @@ } } - console.timeEnd("points1"); - u.ctx.fill(p); }); return null; }; - let pxRatio; - - function setPxRatio() { - pxRatio = devicePixelRatio; - } - function guardedRange(u, min, max) { if (max == min) { if (min == null) { @@ -100,83 +139,99 @@ return [min, max]; } - setPxRatio(); - - window.addEventListener('dppxchange', setPxRatio); - - const opts = { - title: "Scatter Plot", - mode: 2, - width: 1920, - height: 600, - legend: { - live: false, - }, - hooks: { - drawClear: [ - u => { - u.series.forEach((s, i) => { - if (i > 0) - s._paths = null; - }); + function render() { + const opts = { + title: "", + mode: 2, + width: width, + height: height, + legend: { + live: false, + }, + hooks: { + drawClear: [ + u => { + u.series.forEach((s, i) => { + if (i > 0) + s._paths = null; + }); + }, + ], + }, + scales: { + x: { + time: false, + // auto: false, + // range: [0, 500], + // remove any scale padding, use raw data limits + range: guardedRange, }, + y: { + // auto: false, + // range: [0, 500], + // remove any scale padding, use raw data limits + range: guardedRange, + }, + }, + series: [ + { + /* + stroke: "red", + fill: "rgba(255,0,0,0.1)", + paths: (u, seriesIdx, idx0, idx1) => { + uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => { + let d = u.data[seriesIdx]; + + console.log(d); + }); + + return null; + }, + */ + }, + { + stroke: (u, seriesIdx) => { + for (let i = 0; i < points; ++i) { return getRGB(time[i]) } + }, + fill: (u, seriesIdx) => { + for (let i = 0; i < points; ++i) { return getRGB(time[i]) } + }, + paths: drawPoints, + } ], - }, - scales: { - x: { - time: false, - // auto: false, - // range: [0, 500], - // remove any scale padding, use raw data limits - range: guardedRange, - }, - y: { - // auto: false, - // range: [0, 500], - // remove any scale padding, use raw data limits - range: guardedRange, - }, - }, - series: [ - { - /* - stroke: "red", - fill: "rgba(255,0,0,0.1)", - paths: (u, seriesIdx, idx0, idx1) => { - uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => { - let d = u.data[seriesIdx]; + }; - console.log(d); - }); + uplot = new uPlot(opts, data, plotWrapper); + } - return null; - }, - */ - }, - { - stroke: "red", - fill: "rgba(255,0,0,0.1)", - paths: drawPoints, - }, - { - stroke: "green", - fill: "rgba(0,255,0,0.1)", - paths: drawPoints, - }, - { - stroke: "blue", - fill: "rgba(0,0,255,0.1)", - paths: drawPoints, - }, - { - stroke: "magenta", - fill: "rgba(0,0,255,0.1)", - paths: drawPoints, - }, - ], - }; + // Copied from Histogram + + onMount(() => { + render() + }) - let u = new uPlot(opts, data, document.body); + onDestroy(() => { + if (uplot) + uplot.destroy() + + if (timeoutId != null) + clearTimeout(timeoutId) + }) + + function sizeChanged() { + if (timeoutId != null) + clearTimeout(timeoutId) + + timeoutId = setTimeout(() => { + timeoutId = null + if (uplot) + uplot.destroy() + + render() + }, 200) + } + + $: sizeChanged(width, height) From 8d7f942de46a96facd9118e861a01c5ec61828f8 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Mon, 4 Sep 2023 12:53:38 +0200 Subject: [PATCH 05/14] Render loglog scatter, fix data format, start draw --- web/frontend/src/Status.root.svelte | 1 + web/frontend/src/plots/Rooflineuplot.svelte | 259 ++++++++++++++------ 2 files changed, 190 insertions(+), 70 deletions(-) diff --git a/web/frontend/src/Status.root.svelte b/web/frontend/src/Status.root.svelte index 1559121..df5eb71 100644 --- a/web/frontend/src/Status.root.svelte +++ b/web/frontend/src/Status.root.svelte @@ -672,6 +672,7 @@
c.name == cluster).subClusters[0]} /> diff --git a/web/frontend/src/plots/Rooflineuplot.svelte b/web/frontend/src/plots/Rooflineuplot.svelte index e1f9c3c..4defe68 100644 --- a/web/frontend/src/plots/Rooflineuplot.svelte +++ b/web/frontend/src/plots/Rooflineuplot.svelte @@ -7,7 +7,7 @@ export let flopsAny = null export let memBw = null export let cluster = null - export let maxY = null + export let defaultMaxY = null export let width = 500 export let height = 300 export let tiles = null @@ -19,6 +19,11 @@ let uplot = null let timeoutId = null + const paddingLeft = 40, + paddingRight = 10, + paddingTop = 10, + paddingBottom = 50 + // Three Render-Cases: // #1 Single-Job Roofline -> Has Time-Information: Use data, allow colorDots && showTime // #2 MultiNode Roofline - > Has No Time-Information: Transform from nodeData, only "IST"-state of nodes, no timeInfo @@ -30,6 +35,14 @@ return Math.floor(Math.random() * (max - min + 1)) + min; } + function randFloat(min, max) { + return roundTwo(((Math.random() * (max - min + 1)) + min) / randInt(1, 500)); + } + + function roundTwo(num) { + return Math.round((num + Number.EPSILON) * 100) / 100 + } + function filledArr(len, val) { let arr = Array(len); @@ -45,26 +58,42 @@ return arr; } - let points = 100; - let series = 2; - let time = [] + let points = 1000; - for (let i = 0; i < points; ++i) - time[i] = i; - - data = filledArr(series, v => [ - filledArr(points, i => randInt(0,200)), - filledArr(points, i => randInt(0,200)), - ]); - - data[0] = null; + data = [null, [], []] // Null-Axis required for scatter + data[1][0] = filledArr(points, i => randFloat(1,5000)) // Intensity + data[1][1] = filledArr(points, i => randFloat(1,5000)) // Performance + data[2] = filledArr(points, i => 0) // Time Information (Optional) + console.log("Subcluster: ", cluster); console.log("Data: ", data); // End Demo Data // Helpers + const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd?.value || defaultMaxY] + + const w = width - paddingLeft - paddingRight + + const h = height - paddingTop - paddingBottom + + const [log10minX, log10maxX, log10minY, log10maxY] = + [Math.log10(minX), Math.log10(maxX), Math.log10(minY), Math.log10(maxY)] + + 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) + } + function getGradientR(x) { if (x < 0.5) return 0 if (x > 0.75) return 255 @@ -90,6 +119,78 @@ return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})` } + 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 transformData(flopsAny, memBw, colorDots) { // Uses Metric Object + const nodes = flopsAny.series.length + const timesteps = flopsAny.series[0].data.length + + /* c will contain values from 0 to 1 representing the time */ + 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(colorDots ? j / timesteps : 0) + } + } + } else { + console.warn("transformData: metrics for 'mem_bw' and/or 'flops_any' missing!") + } + + return { + x, y, c, + xLabel: 'Intensity [FLOPS/byte]', + yLabel: 'Performance [GFLOPS]' + } + } + + // Return something to be plotted. The argument shall be the result of the + // `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]' + } + } + // End Helpers const drawPoints = (u, seriesIdx, idx0, idx1) => { @@ -123,22 +224,6 @@ return null; }; - function guardedRange(u, min, max) { - if (max == min) { - if (min == null) { - min = 0; - max = 100; - } - else { - let delta = Math.abs(max) || 100; - max += delta; - min -= delta; - } - } - - return [min, max]; - } - function render() { const opts = { title: "", @@ -146,8 +231,39 @@ width: width, height: height, legend: { - live: false, + show: false }, + axes: [ + { + label: 'Intensity [FLOPS/Byte]' + }, + { + label: 'Performace [GFLOPS]' + } + ], + scales: { + x: { + time: false, + distr: 3, + log: 10, + }, + y: { + distr: 3, + log: 10, + }, + }, + series: [ + {}, + { + stroke: (u, seriesIdx) => { + for (let i = 0; i < points; ++i) { return getRGB(data[2][i]) } + }, + fill: (u, seriesIdx) => { + for (let i = 0; i < points; ++i) { return getRGB(data[2][i]) } + }, + paths: drawPoints, + } + ], hooks: { drawClear: [ u => { @@ -157,48 +273,51 @@ }); }, ], - }, - scales: { - x: { - time: false, - // auto: false, - // range: [0, 500], - // remove any scale padding, use raw data limits - range: guardedRange, - }, - y: { - // auto: false, - // range: [0, 500], - // remove any scale padding, use raw data limits - range: guardedRange, - }, - }, - series: [ - { - /* - stroke: "red", - fill: "rgba(255,0,0,0.1)", - paths: (u, seriesIdx, idx0, idx1) => { - uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => { - let d = u.data[seriesIdx]; + draw: [ + u => { // draw roofs + u.ctx.strokeStyle = 'black' + u.ctx.lineWidth = 2 + u.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) - console.log(d); - }); + if (scalarKneeX < width - paddingRight) { + u.ctx.moveTo(scalarKneeX, flopRateScalarY) + u.ctx.lineTo(width - paddingRight, flopRateScalarY) + } - return null; - }, - */ - }, - { - stroke: (u, seriesIdx) => { - for (let i = 0; i < points; ++i) { return getRGB(time[i]) } - }, - fill: (u, seriesIdx) => { - for (let i = 0; i < points; ++i) { return getRGB(time[i]) } - }, - paths: drawPoints, - } - ], + if (simdKneeX < width - paddingRight) { + u.ctx.moveTo(simdKneeX, flopRateSimdY) + u.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 + } + + u.ctx.moveTo(x1, y1) + u.ctx.lineTo(x2, y2) + } + u.ctx.stroke() + } + ] + }, }; uplot = new uPlot(opts, data, plotWrapper); From b2b4beaeaa16d4059a4a6942059ad969b30b4c84 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Mon, 4 Sep 2023 16:31:47 +0200 Subject: [PATCH 06/14] Finish direct data render roofplot demo --- web/frontend/src/plots/Rooflineuplot.svelte | 92 +++++++++------------ 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/web/frontend/src/plots/Rooflineuplot.svelte b/web/frontend/src/plots/Rooflineuplot.svelte index 4defe68..6c1c54d 100644 --- a/web/frontend/src/plots/Rooflineuplot.svelte +++ b/web/frontend/src/plots/Rooflineuplot.svelte @@ -6,8 +6,8 @@ export let flopsAny = null export let memBw = null + export let maxY = null export let cluster = null - export let defaultMaxY = null export let width = 500 export let height = 300 export let tiles = null @@ -19,11 +19,6 @@ let uplot = null let timeoutId = null - const paddingLeft = 40, - paddingRight = 10, - paddingTop = 10, - paddingBottom = 50 - // Three Render-Cases: // #1 Single-Job Roofline -> Has Time-Information: Use data, allow colorDots && showTime // #2 MultiNode Roofline - > Has No Time-Information: Transform from nodeData, only "IST"-state of nodes, no timeInfo @@ -65,35 +60,10 @@ data[1][1] = filledArr(points, i => randFloat(1,5000)) // Performance data[2] = filledArr(points, i => 0) // Time Information (Optional) - console.log("Subcluster: ", cluster); - console.log("Data: ", data); - // End Demo Data // Helpers - const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd?.value || defaultMaxY] - - const w = width - paddingLeft - paddingRight - - const h = height - paddingTop - paddingBottom - - const [log10minX, log10maxX, log10minY, log10maxY] = - [Math.log10(minX), Math.log10(maxX), Math.log10(minY), Math.log10(maxY)] - - 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) - } - function getGradientR(x) { if (x < 0.5) return 0 if (x > 0.75) return 255 @@ -119,6 +89,10 @@ 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) { let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l @@ -233,6 +207,7 @@ legend: { show: false }, + cursor: { drag: { x: false, y: false } }, axes: [ { label: 'Intensity [FLOPS/Byte]' @@ -244,12 +219,14 @@ scales: { x: { time: false, - distr: 3, - log: 10, + range: [0.01, 1000], + distr: 3, // Render as log + log: 10, // log exp }, y: { - distr: 3, - log: 10, + range: [1.0, nearestThousand(cluster.flopRateSimd.value || maxY)], + distr: 3, // Render as log + log: 10, // log exp }, }, series: [ @@ -274,47 +251,58 @@ }, ], draw: [ - u => { // draw roofs - u.ctx.strokeStyle = 'black' - u.ctx.lineWidth = 2 - u.ctx.beginPath() + u => { // draw roofs when cluster set if (cluster != null) { + const padding = u._padding // [top, right, bottom, left] + + u.ctx.strokeStyle = 'black' + u.ctx.lineWidth = 2 + u.ctx.beginPath() + 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) + 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) - if (scalarKneeX < width - paddingRight) { + if (scalarKneeX < width - padding[1]) { // Top horizontal roofline u.ctx.moveTo(scalarKneeX, flopRateScalarY) - u.ctx.lineTo(width - paddingRight, flopRateScalarY) + u.ctx.lineTo(width - padding[1], flopRateScalarY) } - if (simdKneeX < width - paddingRight) { + if (simdKneeX < width - padding[1]) { // Lower horitontal roofline u.ctx.moveTo(simdKneeX, flopRateSimdY) - u.ctx.lineTo(width - paddingRight, flopRateSimdY) + u.ctx.lineTo(width - padding[1], flopRateSimdY) } - let x1 = getCanvasX(0.01), - y1 = getCanvasY(ycut), - x2 = getCanvasX(simdKnee), + let x1 = u.valToPos(0.01, 'x', true), + y1 = u.valToPos(ycut, 'y', true) + + let x2 = u.valToPos(simdKnee, 'x', true), y2 = flopRateSimdY let xAxisIntersect = lineIntersect( x1, y1, x2, y2, - 0, height - paddingBottom, width, height - paddingBottom) + u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), + u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis Coords + ) + if (xAxisIntersect.x > x1) { x1 = xAxisIntersect.x y1 = xAxisIntersect.y } + // Diagonal u.ctx.moveTo(x1, y1) u.ctx.lineTo(x2, y2) + + u.ctx.stroke() + // Reset lineWidth + u.ctx.lineWidth = 0.15 } - u.ctx.stroke() } ] }, From f235b1a99c0d03f856fb0aff983592fee8c09244 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Tue, 5 Sep 2023 09:19:43 +0200 Subject: [PATCH 07/14] Allow render of time information as color gradient --- web/frontend/src/plots/Rooflineuplot.svelte | 87 ++++++++++++--------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/web/frontend/src/plots/Rooflineuplot.svelte b/web/frontend/src/plots/Rooflineuplot.svelte index 6c1c54d..01ed489 100644 --- a/web/frontend/src/plots/Rooflineuplot.svelte +++ b/web/frontend/src/plots/Rooflineuplot.svelte @@ -10,9 +10,7 @@ export let cluster = null export let width = 500 export let height = 300 - export let tiles = null - export let colorDots = true - export let showTime = true + export let renderTime = false export let data = null let plotWrapper = null @@ -38,16 +36,20 @@ return Math.round((num + Number.EPSILON) * 100) / 100 } - function filledArr(len, val) { + function filledArr(len, val, time) { let arr = Array(len); if (typeof val == "function") { for (let i = 0; i < len; ++i) arr[i] = val(i); } + else if (time) { + for (let i = 0; i < len; ++i) + arr[i] = i / 1000; + } else { for (let i = 0; i < len; ++i) - arr[i] = val; + arr[i] = i; } return arr; @@ -55,10 +57,12 @@ let points = 1000; - data = [null, [], []] // Null-Axis required for scatter - data[1][0] = filledArr(points, i => randFloat(1,5000)) // Intensity - data[1][1] = filledArr(points, i => randFloat(1,5000)) // Performance - data[2] = filledArr(points, i => 0) // Time Information (Optional) + data = [null, []] // Null-Axis required for scatter + data[1][0] = filledArr(points, i => randFloat(1,5000), false) // Intensity + data[1][1] = filledArr(points, i => randFloat(1,5000), false) // Performance + // data[1][0] = filledArr(points, 0, false) // Intensity + // data[1][1] = filledArr(points, 0, false) // Performance + data[2] = filledArr(points, 0, true) // Time Information (Optional) // End Demo Data @@ -167,22 +171,18 @@ // End Helpers - const drawPoints = (u, seriesIdx, idx0, idx1) => { + const drawColorPoints = (u, seriesIdx, idx0, idx1) => { 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]; - - u.ctx.fillStyle = series.stroke(); - let deg360 = 2 * Math.PI; - - let p = new Path2D(); - 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); @@ -190,14 +190,37 @@ p.moveTo(cx + size/2, cy); arc(p, cx, cy, size/2, 0, deg360); } + u.ctx.fill(p); } - - u.ctx.fill(p); }); return null; }; + const drawPoints = (u, seriesIdx, idx0, idx1) => { + 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]; + u.ctx.strokeStyle = getRGB(0); + u.ctx.fillStyle = getRGB(0); + let deg360 = 2 * Math.PI; + let p = new Path2D(); + for (let i = 0; i < d[0].length; i++) { + let xVal = d[0][i]; + let yVal = d[1][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); + p.moveTo(cx + size/2, cy); + arc(p, cx, cy, size/2, 0, deg360); + } + } + u.ctx.fill(p); + }); + return null; + }; + function render() { const opts = { title: "", @@ -209,12 +232,8 @@ }, cursor: { drag: { x: false, y: false } }, axes: [ - { - label: 'Intensity [FLOPS/Byte]' - }, - { - label: 'Performace [GFLOPS]' - } + { label: 'Intensity [FLOPS/Byte]' }, + { label: 'Performace [GFLOPS]' } ], scales: { x: { @@ -231,15 +250,7 @@ }, series: [ {}, - { - stroke: (u, seriesIdx) => { - for (let i = 0; i < points; ++i) { return getRGB(data[2][i]) } - }, - fill: (u, seriesIdx) => { - for (let i = 0; i < points; ++i) { return getRGB(data[2][i]) } - }, - paths: drawPoints, - } + { paths: renderTime ? drawColorPoints : drawPoints } ], hooks: { drawClear: [ @@ -252,6 +263,7 @@ ], draw: [ u => { // draw roofs when cluster set + // console.log(u) if (cluster != null) { const padding = u._padding // [top, right, bottom, left] @@ -285,11 +297,10 @@ let xAxisIntersect = lineIntersect( x1, y1, x2, y2, - u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), - u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis Coords + u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), // X-Axis Start Coords + u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis End Coords ) - if (xAxisIntersect.x > x1) { x1 = xAxisIntersect.x y1 = xAxisIntersect.y @@ -300,7 +311,7 @@ u.ctx.lineTo(x2, y2) u.ctx.stroke() - // Reset lineWidth + // Reset grid lineWidth u.ctx.lineWidth = 0.15 } } From b449b77b959818de802d3e3d3ca5a8e16eb1c199 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Tue, 5 Sep 2023 10:01:34 +0200 Subject: [PATCH 08/14] Rename dev component, separate rooflineHeatmap - moved roofline helper functions to utils --- web/frontend/src/Analysis.root.svelte | 4 +- web/frontend/src/Status.root.svelte | 19 +- web/frontend/src/plots/Roofline.svelte | 519 ++++++++---------- web/frontend/src/plots/RooflineHeatmap.svelte | 234 ++++++++ web/frontend/src/plots/Rooflineuplot.svelte | 360 ------------ web/frontend/src/utils.js | 63 +++ 6 files changed, 532 insertions(+), 667 deletions(-) create mode 100644 web/frontend/src/plots/RooflineHeatmap.svelte delete mode 100644 web/frontend/src/plots/Rooflineuplot.svelte diff --git a/web/frontend/src/Analysis.root.svelte b/web/frontend/src/Analysis.root.svelte index 06219b9..aa4ae37 100644 --- a/web/frontend/src/Analysis.root.svelte +++ b/web/frontend/src/Analysis.root.svelte @@ -10,7 +10,7 @@ import { binsFromFootprint } from './utils.js' import ScatterPlot from './plots/Scatter.svelte' import PlotTable from './PlotTable.svelte' - import Roofline from './plots/Roofline.svelte' + import RooflineHeatmap from './plots/RooflineHeatmap.svelte' const { query: initq } = init() @@ -315,7 +315,7 @@ {:else if $rooflineQuery.data && cluster}
{#key $rooflineQuery.data} - import { getContext } from "svelte"; import Refresher from "./joblist/Refresher.svelte"; - import Roofline, { transformPerNodeData } from "./plots/Roofline.svelte"; - import Rooflineuplot from "./plots/Rooflineuplot.svelte"; + import Roofline from "./plots/Roofline.svelte"; import Pie, { colors } from "./plots/Pie.svelte"; import Histogram from "./plots/Histogram.svelte"; import { @@ -17,7 +16,7 @@ Progress, Icon, } from "sveltestrap"; - import { init, convert2uplot } from "./utils.js"; + import { init, convert2uplot, transformPerNodeDataForRoofline } from "./utils.js"; import { scaleNumbers } from "./units.js"; import { queryStore, @@ -33,8 +32,7 @@ let plotWidths = [], colWidth1 = 0, - colWidth2 = 0, - colWidth3 = 0; + colWidth2 = 0 let from = new Date(Date.now() - 5 * 60 * 1000), to = new Date(Date.now()); const topOptions = [ @@ -434,7 +432,7 @@ colorDots={true} showTime={false} cluster={subCluster} - data={transformPerNodeData( + data={transformPerNodeDataForRoofline( $mainQuery.data.nodeMetrics.filter( (data) => data.subCluster == subCluster.name ) @@ -667,13 +665,4 @@ {/key} - - -
- c.name == cluster).subClusters[0]} - /> - - {/if} diff --git a/web/frontend/src/plots/Roofline.svelte b/web/frontend/src/plots/Roofline.svelte index 57482ee..cc65f41 100644 --- a/web/frontend/src/plots/Roofline.svelte +++ b/web/frontend/src/plots/Roofline.svelte @@ -1,16 +1,72 @@ -
- -
+ - - + +{#if data != null} +
+{:else} + Cannot render roofline: No data! +{/if} \ No newline at end of file diff --git a/web/frontend/src/plots/RooflineHeatmap.svelte b/web/frontend/src/plots/RooflineHeatmap.svelte new file mode 100644 index 0000000..be7ec65 --- /dev/null +++ b/web/frontend/src/plots/RooflineHeatmap.svelte @@ -0,0 +1,234 @@ +
+ +
+ + + + diff --git a/web/frontend/src/plots/Rooflineuplot.svelte b/web/frontend/src/plots/Rooflineuplot.svelte deleted file mode 100644 index 01ed489..0000000 --- a/web/frontend/src/plots/Rooflineuplot.svelte +++ /dev/null @@ -1,360 +0,0 @@ - - -{#if data != null} -
-{:else} - Cannot render roofline: No data! -{/if} \ No newline at end of file diff --git a/web/frontend/src/utils.js b/web/frontend/src/utils.js index 0650916..c89afda 100644 --- a/web/frontend/src/utils.js +++ b/web/frontend/src/utils.js @@ -363,3 +363,66 @@ export function binsFromFootprint(weights, scope, values, numBins) { bins: bins } } + +export function transformDataForRoofline(flopsAny, memBw, renderTime) { // Uses Metric Object + const nodes = flopsAny.series.length + const timesteps = flopsAny.series[0].data.length + + /* c will contain values from 0 to 1 representing the time */ + 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(renderTime ? j / timesteps : 0) + } + } + } else { + console.warn("transformData: metrics for 'mem_bw' and/or 'flops_any' missing!") + } + + return { + x, y, c, + xLabel: 'Intensity [FLOPS/byte]', + yLabel: 'Performance [GFLOPS]' + } +} + +// Return something to be plotted. The argument shall be the result of the +// `nodeMetrics` GraphQL query. +export function transformPerNodeDataForRoofline(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]' + } +} From 1b8c4e293c3d1196569fee6425a46f16f8ef674d Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Tue, 5 Sep 2023 11:46:34 +0200 Subject: [PATCH 09/14] Change to prod data, allow and handle null data - fix errors regarding render timing - always collect time info in transFormData function - remove size from polar plot --- web/frontend/src/Job.root.svelte | 38 ++-- web/frontend/src/Status.root.svelte | 18 +- web/frontend/src/plots/Polar.svelte | 3 +- web/frontend/src/plots/Roofline.svelte | 285 +++++++++++-------------- web/frontend/src/units.js | 4 +- web/frontend/src/utils.js | 44 ++-- 6 files changed, 180 insertions(+), 212 deletions(-) diff --git a/web/frontend/src/Job.root.svelte b/web/frontend/src/Job.root.svelte index 153b8f9..b0db95e 100644 --- a/web/frontend/src/Job.root.svelte +++ b/web/frontend/src/Job.root.svelte @@ -4,6 +4,7 @@ groupByScope, fetchMetricsStore, checkMetricDisabled, + transformDataForRoofline } from "./utils.js"; import { Row, @@ -130,8 +131,8 @@ lazyFetchMoreMetrics(); let plots = {}, + roofWidth, jobTags, - fullWidth, statsTable; $: document.title = $initq.fetching ? "Loading..." @@ -190,7 +191,6 @@ })); -
{#if $initq.error} @@ -245,7 +245,6 @@ {/if} - c.name == $initq.data.job.cluster) - .subClusters.find( - (sc) => sc.name == $initq.data.job.subCluster - )} - flopsAny={$jobMetrics.data.jobMetrics.find( - (m) => m.name == "flops_any" && m.scope == "node" - )} - memBw={$jobMetrics.data.jobMetrics.find( - (m) => m.name == "mem_bw" && m.scope == "node" - )} - /> +
+ c.name == $initq.data.job.cluster) + .subClusters.find( + (sc) => sc.name == $initq.data.job.subCluster + )} + data={ + transformDataForRoofline ( + $jobMetrics.data.jobMetrics.find((m) => m.name == "flops_any" && m.scope == "node").metric, + $jobMetrics.data.jobMetrics.find((m) => m.name == "mem_bw" && m.scope == "node").metric + ) + } + /> +
{:else} diff --git a/web/frontend/src/Status.root.svelte b/web/frontend/src/Status.root.svelte index 8e162ab..73060c6 100644 --- a/web/frontend/src/Status.root.svelte +++ b/web/frontend/src/Status.root.svelte @@ -31,8 +31,8 @@ export let cluster; let plotWidths = [], - colWidth1 = 0, - colWidth2 = 0 + colWidth1, + colWidth2 let from = new Date(Date.now() - 5 * 60 * 1000), to = new Date(Date.now()); const topOptions = [ @@ -429,14 +429,14 @@ data.subCluster == subCluster.name + data={ + transformPerNodeDataForRoofline( + $mainQuery.data.nodeMetrics.filter( + (data) => data.subCluster == subCluster.name + ) ) - )} + } /> {/key}
@@ -444,7 +444,7 @@ {/each} -
+
diff --git a/web/frontend/src/plots/Polar.svelte b/web/frontend/src/plots/Polar.svelte index e556cc2..b5c488f 100644 --- a/web/frontend/src/plots/Polar.svelte +++ b/web/frontend/src/plots/Polar.svelte @@ -22,7 +22,6 @@ LineElement ); - export let size export let metrics export let cluster export let jobMetrics @@ -95,7 +94,7 @@
- +