open-nomad/ui/app/components/topo-viz/node.js
Michael Lange 0ab6b31cab Refactor topo viz to do as much computation upfront & use faster data structures
Now all data loading happens in the TopoViz component as well as
computation of resource proportions.

Allocation selection state is also managed centrally uses a dedicated
structure indexed by group key (job id and task group name). This way
allocations don't need to be scanned at the node level, which is O(n) at
the best (assuming no ember overhead on recomputes).
2020-10-15 02:54:14 -07:00

176 lines
4.6 KiB
JavaScript

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { guidFor } from '@ember/object/internals';
export default class TopoVizNode extends Component {
@tracked data = { cpu: [], memory: [] };
@tracked dimensionsWidth = 0;
@tracked padding = 5;
@tracked activeAllocation = null;
get height() {
return this.args.heightScale ? this.args.heightScale(this.args.node.memory) : 15;
}
get labelHeight() {
return this.height / 2;
}
get paddingLeft() {
const labelWidth = 20;
return this.padding + labelWidth;
}
// Since strokes are placed centered on the perimeter of fills, The width of the stroke needs to be removed from
// the height of the fill to match unstroked height and avoid clipping.
get selectedHeight() {
return this.height - 1;
}
// Since strokes are placed centered on the perimeter of fills, half the width of the stroke needs to be added to
// the yOffset to match heights with unstroked shapes.
get selectedYOffset() {
return this.height + 2.5;
}
get yOffset() {
return this.height + 2;
}
get maskHeight() {
return this.height + this.yOffset;
}
get totalHeight() {
return this.maskHeight + this.padding * 2;
}
get maskId() {
return `topo-viz-node-mask-${guidFor(this)}`;
}
get count() {
return this.args.node.allocations.length;
}
get allocations() {
// Sort by the delta between memory and cpu percent. This creates the least amount of
// drift between the positional alignment of an alloc's cpu and memory representations.
return this.args.node.allocations.filterBy('allocation.isScheduled').sort((a, b) => {
const deltaA = Math.abs(a.memoryPercent - a.cpuPercent);
const deltaB = Math.abs(b.memoryPercent - b.cpuPercent);
return deltaA - deltaB;
});
}
@action
async reloadNode() {
if (this.args.node.isPartial) {
await this.args.node.reload();
this.data = this.computeData(this.dimensionsWidth);
}
}
@action
render(svg) {
this.dimensionsWidth = svg.clientWidth - this.padding - this.paddingLeft;
this.data = this.computeData(this.dimensionsWidth);
}
@action
updateRender(svg) {
// Only update all data when the width changes
const newWidth = svg.clientWidth - this.padding - this.paddingLeft;
if (newWidth !== this.dimensionsWidth) {
this.dimensionsWidth = newWidth;
this.data = this.computeData(this.dimensionsWidth);
}
}
@action
highlightAllocation(allocation) {
this.activeAllocation = allocation;
}
@action
clearHighlight() {
this.activeAllocation = null;
}
@action
selectAllocation(allocation) {
if (this.args.onAllocationSelect) this.args.onAllocationSelect(allocation);
}
containsActiveTaskGroup() {
return this.args.node.allocations.some(
allocation =>
allocation.taskGroupName === this.args.activeTaskGroup &&
allocation.belongsTo('job').id() === this.args.activeJobId
);
}
computeData(width) {
const allocations = this.allocations;
let cpuOffset = 0;
let memoryOffset = 0;
const cpu = [];
const memory = [];
for (const allocation of allocations) {
const { cpuPercent, memoryPercent, isSelected } = allocation;
const isFirst = allocation === allocations[0];
let cpuWidth = cpuPercent * width - 1;
let memoryWidth = memoryPercent * width - 1;
if (isFirst) {
cpuWidth += 0.5;
memoryWidth += 0.5;
}
if (isSelected) {
cpuWidth--;
memoryWidth--;
}
cpu.push({
allocation,
offset: cpuOffset * 100,
percent: cpuPercent * 100,
width: cpuWidth,
x: cpuOffset * width + (isFirst ? 0 : 0.5) + (isSelected ? 0.5 : 0),
className: allocation.allocation.clientStatus,
});
memory.push({
allocation,
offset: memoryOffset * 100,
percent: memoryPercent * 100,
width: memoryWidth,
x: memoryOffset * width + (isFirst ? 0 : 0.5) + (isSelected ? 0.5 : 0),
className: allocation.allocation.clientStatus,
});
cpuOffset += cpuPercent;
memoryOffset += memoryPercent;
}
const cpuRemainder = {
x: cpuOffset * width + 0.5,
width: width - cpuOffset * width,
};
const memoryRemainder = {
x: memoryOffset * width + 0.5,
width: width - memoryOffset * width,
};
return {
cpu,
memory,
cpuRemainder,
memoryRemainder,
cpuLabel: { x: -this.paddingLeft / 2, y: this.height / 2 + this.yOffset },
memoryLabel: { x: -this.paddingLeft / 2, y: this.height / 2 },
};
}
}