Created
September 5, 2024 06:48
-
-
Save zookzook/c84317a3827d7d24c0017a3aee62f596 to your computer and use it in GitHub Desktop.
Scheduler utilization with D3.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as d3 from "d3"; | |
| function extentTime(ref) { | |
| let to = ref || new Date(); | |
| let totalMilliSeconds = to.getTime(); | |
| let millisecondsToSubtract = 60 * 1000; | |
| let from = new Date(totalMilliSeconds - millisecondsToSubtract); | |
| return d3.extent([from, to]); | |
| } | |
| function filterDomain(values, minTS, maxTS) { | |
| return values.filter((el) => minTS <= el.ts && el.ts <= maxTS); | |
| } | |
| function init(self) { | |
| let svg = d3.select("#utilization-graph"); | |
| const width = 900; | |
| const height = 400; | |
| const marginRight = 30; | |
| const marginLeft = 60; | |
| const marginTop = 20; | |
| const marginBottom = 80; | |
| const normColors = ["#A5B4FC", "#818CF8", "#6366F1", "#4F46E5", "#4338CA", "#3730A3", "#312E81", | |
| "#93C5FD", "#60A5FA", "#3B82F6", "#2563EB", "#1D4ED8", "#1E40AF", "#1E3A8A"]; | |
| const dirtyColors = ["#FBD5D5", "#F8B4B4", "#F98080", "#F05252", "#E02424", "#9B1C1C", "#771D1D", | |
| "#FAD1E8", "#F8B4D9", "#F17EB8", "#E74694", "#D61F69", "#BF125D", "#99154"]; | |
| const maxColors = normColors.length; | |
| let totalUtilization = []; | |
| const x = d3.scaleTime(extentTime(), [marginLeft, width - marginRight]); | |
| const y = d3.scaleLinear([0, 100], [height - marginBottom, marginTop]); | |
| // utilization values for the normal schedulers | |
| let normals = {}; | |
| // utilization values for the dirty schedulers | |
| let dirties = {} | |
| // Declare the line generator. | |
| const line = d3.line() | |
| .x(d => x(d.ts)) | |
| .y(d => y(d.utilization)) | |
| .curve(d3.curveCatmullRom.alpha(0.5)); | |
| // Create the SVG container. | |
| svg = svg | |
| .attr("width", width) | |
| .attr("height", height) | |
| .attr("viewBox", [0, 0, width, height]) | |
| .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); | |
| // Add the x-axis. | |
| svg.append("g") | |
| .attr("class", "x-axis") | |
| .attr("transform", `translate(0, ${height - marginBottom})`) | |
| .call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0)); | |
| // Add the y-axis, remove the domain line, add grid lines and a label. | |
| svg.append("g") | |
| .attr("transform", `translate(${marginLeft},0)`) | |
| .call(d3.axisLeft(y).ticks(height / 40)) | |
| .call(g => g.select(".domain").remove()) | |
| .call(g => g.selectAll(".tick line").clone() | |
| .attr("x2", width - marginLeft - marginRight) | |
| .attr("stroke-opacity", 0.1)) | |
| .call(g => g.append("text") | |
| .attr("x", -marginLeft) | |
| .attr("y", 10) | |
| .attr("fill", "currentColor") | |
| .attr("text-anchor", "start") | |
| .text("Utilization in %")); | |
| // add the group for the legend for dirty and normal schedulers | |
| let normalLegend = svg.append("g").attr("transform", `translate(${marginLeft}, ${height - 50})`); | |
| let dirtyLegend = svg.append("g").attr("transform", `translate(${marginLeft}, ${height - 20})`); | |
| // Append a path for the utilization lines. | |
| let graphView = svg.append("g").attr("class", "graph-view"); | |
| graphView.append("path") | |
| .attr("class", "graph-total") | |
| .attr("fill", "none") | |
| .attr("stroke", "#6B7280") | |
| .attr("stroke-width", 1) | |
| .attr("d", line([])); | |
| function addUtilization(data, ts, values, domain) { | |
| if(values != null) { | |
| for (const [key, value] of Object.entries(values)) { | |
| let e = {ts: ts, utilization: value}; | |
| if(data[key] === undefined) { | |
| data[key] = [e] | |
| } | |
| else { | |
| let normalValues = data[key]; | |
| normalValues.push(e); | |
| data[key] = filterDomain(normalValues, domain[0], domain[1]); | |
| } // else | |
| } | |
| } | |
| } | |
| function update(values) { | |
| let ts = new Date(values.ts); | |
| let domain = extentTime(new Date(values.ts)); | |
| addUtilization(normals, ts, values.normals, domain); | |
| addUtilization(dirties, ts, values.dirties, domain); | |
| totalUtilization.push({ts: ts, utilization: values.total}); | |
| totalUtilization = filterDomain(totalUtilization, domain[0], domain[1]); | |
| x.domain(domain); | |
| svg.select(".x-axis").call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0)); | |
| let i = 0; | |
| let normalSchedulerValues = []; | |
| for (const [key, values] of Object.entries(normals)) { | |
| normalSchedulerValues.push({index: i, id: key, values: values}) | |
| i += 1; | |
| } | |
| let dirtySchedulerValues = []; | |
| i = 0; | |
| for (const [key, values] of Object.entries(dirties)) { | |
| dirtySchedulerValues.push({index: i, id: key, values: values}) | |
| i += 1; | |
| } | |
| graphView.selectAll('.normal-scheduler').data(normalSchedulerValues, function (d) { return d.id; }) | |
| .join( | |
| enter => enter.append('path') | |
| .attr('class', 'normal-scheduler') | |
| .attr("fill", "none") | |
| .attr("stroke", (d) => normColors[d.id % maxColors]) | |
| .attr("stroke-width", 0.5) | |
| .attr('d', (d) => {return line(d.values);}), | |
| update => update.attr('d', (d) => {return line(d.values);}), | |
| exit => exit.remove() | |
| ); | |
| normalLegend.selectAll('rect').data(normalSchedulerValues, function (d) { return d.id; }) | |
| .join( | |
| enter => { | |
| enter.append('rect') | |
| .attr("fill", (d) => normColors[d.index % maxColors]) | |
| .attr("x", (d) => d.index * 40) | |
| .attr("y", 0) | |
| .attr("width", 15) | |
| .attr("height", 15); | |
| enter.append("text") | |
| .attr("x", (d) => d.index * 40 + 18) | |
| .attr("y", 12) | |
| .attr("class", "text-xs") | |
| .text(function(d) { return d.id; }); | |
| }, | |
| update => update, | |
| exit => exit.remove() | |
| ); | |
| // dirty schedulers | |
| graphView.selectAll('.dirty-scheduler').data(dirtySchedulerValues, function (d) { return d.id; }) | |
| .join( | |
| enter => enter.append('path') | |
| .attr('class', 'dirty-scheduler') | |
| .attr("fill", "none") | |
| .attr("stroke", (d) => dirtyColors[d.index % maxColors]) | |
| .attr("stroke-width", 0.5) | |
| .attr("stroke-dasharray", "2,2") | |
| .attr('d', (d) => {return line(d.values);}), | |
| update => update.attr('d', (d) => {return line(d.values);}), | |
| exit => exit.remove() | |
| ); | |
| dirtyLegend.selectAll('rect').data(dirtySchedulerValues, function (d) { return d.id; }) | |
| .join( | |
| enter => { | |
| enter.append('rect') | |
| .attr("fill", (d) => dirtyColors[d.index % maxColors]) | |
| .attr("x", (d) => d.index * 40) | |
| .attr("y", 0) | |
| .attr("width", 15) | |
| .attr("height", 15); | |
| enter.append("text") | |
| .attr("x", (d) => d.index * 40 + 18) | |
| .attr("y", 12) | |
| .attr("class", "text-xs") | |
| .text(function(d) { return d.id; }); | |
| }, | |
| update => update, | |
| exit => exit.remove() | |
| ); | |
| // total usages | |
| svg.select(".graph-total").attr("d", line(totalUtilization)); | |
| } | |
| return function(data) {return update(data)}; | |
| } | |
| function initIO(self) { | |
| let svg = d3.select("#io-graph"); | |
| const width = 900; | |
| const height = 400; | |
| const marginRight = 30; | |
| const marginLeft = 60; | |
| const marginTop = 20; | |
| const marginBottom = 40; | |
| let io = []; | |
| const x = d3.scaleTime(extentTime(), [marginLeft, width - marginRight]); | |
| const y = d3.scaleLinear([0, 1024*1024], [height - marginBottom, marginTop]); | |
| // Declare the line generator. | |
| const inputLine = d3.line() | |
| .x(d => x(d.ts)) | |
| .y(d => y(d.input)) | |
| .curve(d3.curveCatmullRom.alpha(0.5)); | |
| const outputLine = d3.line() | |
| .x(d => x(d.ts)) | |
| .y(d => y(d.output)) | |
| .curve(d3.curveCatmullRom.alpha(0.5)); | |
| // Create the SVG container. | |
| svg = svg | |
| .attr("width", width) | |
| .attr("height", height) | |
| .attr("viewBox", [0, 0, width, height]) | |
| .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); | |
| // Add the x-axis. | |
| svg.append("g") | |
| .attr("class", "x-axis") | |
| .attr("transform", `translate(0, ${height - marginBottom})`) | |
| .call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0)); | |
| // Add the y-axis, remove the domain line, add grid lines and a label. | |
| svg.append("g") | |
| .attr("class", "y-axis") | |
| .attr("transform", `translate(${marginLeft},0)`) | |
| .call(d3.axisLeft(y).ticks(height / 40, "~s")) | |
| .call(g => g.append("text") | |
| .attr("x", -marginLeft) | |
| .attr("y", 10) | |
| .attr("fill", "currentColor") | |
| .attr("text-anchor", "start") | |
| .text("Input/Output Bytes")); | |
| const yGrid = d3.axisLeft() | |
| .scale(y) | |
| .tickFormat('') | |
| .ticks(height / 40) | |
| .tickSizeInner(-width + marginLeft + marginRight) | |
| svg.append('g') | |
| .attr('class', 'y-grid') | |
| .attr('transform', `translate(${marginLeft},0)`) | |
| .call(yGrid) | |
| .call(g => g.selectAll(".tick line") | |
| .attr("stroke-opacity", 0.1)); | |
| // Append a path for the utilization lines. | |
| let graphView = svg.append("g").attr("class", "graph-view"); | |
| graphView.append("path") | |
| .attr("class", "graph-input") | |
| .attr("fill", "none") | |
| .attr("stroke", "green") | |
| .attr("stroke-width", 1) | |
| .attr("d", inputLine([])); | |
| graphView.append("path") | |
| .attr("class", "graph-output") | |
| .attr("fill", "none") | |
| .attr("stroke", "red") | |
| .attr("stroke-width", 1) | |
| .attr("d", outputLine([])); | |
| function update(values) { | |
| let ts = new Date(values.ts); | |
| let domain = extentTime(new Date(values.ts)); | |
| if(values.io) { | |
| io.push({ts: ts, input: values.io.input, output: values.io.output}); | |
| io = filterDomain(io, domain[0], domain[1]); | |
| y.domain([0, d3.max(io, (d) => Math.max(d.input, d.output) + 1000)]); | |
| svg.select(".y-axis").call(d3.axisLeft(y).ticks(height / 40, "~s")); | |
| svg.select(".y-grid").call(yGrid) | |
| .call(g => g.selectAll(".tick line") | |
| .attr("stroke-opacity", 0.1)); | |
| } | |
| x.domain(domain); | |
| svg.select(".x-axis").call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0)); | |
| // total usages | |
| svg.select(".graph-input").attr("d", inputLine(io)); | |
| svg.select(".graph-output").attr("d", outputLine(io)); | |
| } | |
| return function(data) {return update(data)}; | |
| } | |
| const Utilization = { | |
| mounted() { | |
| this.utilization = init(this); | |
| this.io = initIO(this); | |
| this.handleEvent("add-utilization", (values) => { | |
| this.io(values); | |
| this.utilization(values); | |
| }) | |
| }, | |
| updated() { | |
| this.utilization = init(this); | |
| this.io = initIO(this); | |
| this.handleEvent("add-utilization", (values) => { | |
| this.io(values); | |
| this.utilization(values); | |
| }) | |
| } | |
| }; | |
| export default Utilization; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment