Skip to content

Instantly share code, notes, and snippets.

@zookzook
Created September 5, 2024 06:48
Show Gist options
  • Select an option

  • Save zookzook/c84317a3827d7d24c0017a3aee62f596 to your computer and use it in GitHub Desktop.

Select an option

Save zookzook/c84317a3827d7d24c0017a3aee62f596 to your computer and use it in GitHub Desktop.
Scheduler utilization with D3.js
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