mirror of
				https://source.quilibrium.com/quilibrium/ceremonyclient.git
				synced 2025-11-04 15:27:27 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			696 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			696 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// TODO(peter)
 | 
						|
// - Save pan/zoom settings in query params
 | 
						|
//
 | 
						|
// TODO(travers): There exists an awkward ordering script loading issue where
 | 
						|
// write-throughput.js is loaded first, but contains references to functions
 | 
						|
// defined in this file. Work out a better way of modularizing this code.
 | 
						|
 | 
						|
const parseTime = d3.timeParse("%Y%m%d");
 | 
						|
const formatTime = d3.timeFormat("%b %d");
 | 
						|
const dateBisector = d3.bisector(d => d.date).left;
 | 
						|
 | 
						|
let minDate;
 | 
						|
let max = {
 | 
						|
    date: new Date(),
 | 
						|
    perChart: {},
 | 
						|
    opsSec: 0,
 | 
						|
    readBytes: 0,
 | 
						|
    writeBytes: 0,
 | 
						|
    readAmp: 0,
 | 
						|
    writeAmp: 0
 | 
						|
};
 | 
						|
let usePerChartMax = false;
 | 
						|
let detail;
 | 
						|
let detailName;
 | 
						|
let detailFormat;
 | 
						|
 | 
						|
let annotations = [];
 | 
						|
 | 
						|
function getMaxes(chartKey) {
 | 
						|
    return usePerChartMax ? max.perChart[chartKey] : max;
 | 
						|
}
 | 
						|
 | 
						|
function styleWidth(e) {
 | 
						|
    const width = +e.style("width").slice(0, -2);
 | 
						|
    return Math.round(Number(width));
 | 
						|
}
 | 
						|
 | 
						|
function styleHeight(e) {
 | 
						|
    const height = +e.style("height").slice(0, -2);
 | 
						|
    return Math.round(Number(height));
 | 
						|
}
 | 
						|
 | 
						|
function pathGetY(path, x) {
 | 
						|
    // Walk along the path using binary search to locate the point
 | 
						|
    // with the supplied x value.
 | 
						|
    let start = 0;
 | 
						|
    let end = path.getTotalLength();
 | 
						|
    while (start < end) {
 | 
						|
        const target = (start + end) / 2;
 | 
						|
        const pos = path.getPointAtLength(target);
 | 
						|
        if (Math.abs(pos.x - x) < 0.01) {
 | 
						|
            // Close enough.
 | 
						|
            return pos.y;
 | 
						|
        } else if (pos.x > x) {
 | 
						|
            end = target;
 | 
						|
        } else {
 | 
						|
            start = target;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return path.getPointAtLength(start).y;
 | 
						|
}
 | 
						|
 | 
						|
// Pretty formatting of a number in human readable units.
 | 
						|
function humanize(s) {
 | 
						|
    const iecSuffixes = [" B", " KB", " MB", " GB", " TB", " PB", " EB"];
 | 
						|
    if (s < 10) {
 | 
						|
        return "" + s;
 | 
						|
    }
 | 
						|
    let e = Math.floor(Math.log(s) / Math.log(1024));
 | 
						|
    let suffix = iecSuffixes[Math.floor(e)];
 | 
						|
    let val = Math.floor(s / Math.pow(1024, e) * 10 + 0.5) / 10;
 | 
						|
    return val.toFixed(val < 10 ? 1 : 0) + suffix;
 | 
						|
}
 | 
						|
 | 
						|
function dirname(path) {
 | 
						|
    return path.match(/.*\//)[0];
 | 
						|
}
 | 
						|
 | 
						|
function equalDay(d1, d2) {
 | 
						|
    return (
 | 
						|
        d1.getYear() == d2.getYear() &&
 | 
						|
        d1.getMonth() == d2.getMonth() &&
 | 
						|
        d1.getDate() == d2.getDate()
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
function computeSegments(data) {
 | 
						|
    return data.reduce(function(segments, d) {
 | 
						|
        if (segments.length == 0) {
 | 
						|
            segments.push([d]);
 | 
						|
            return segments;
 | 
						|
        }
 | 
						|
 | 
						|
        const lastSegment = segments[segments.length - 1];
 | 
						|
        const lastDatum = lastSegment[lastSegment.length - 1];
 | 
						|
        const days = Math.round(
 | 
						|
            (d.date.getTime() - lastDatum.date.getTime()) /
 | 
						|
                (24 * 60 * 60 * 1000)
 | 
						|
        );
 | 
						|
        if (days == 1) {
 | 
						|
            lastSegment.push(d);
 | 
						|
        } else {
 | 
						|
            segments.push([d]);
 | 
						|
        }
 | 
						|
        return segments;
 | 
						|
    }, []);
 | 
						|
}
 | 
						|
 | 
						|
function computeGaps(segments) {
 | 
						|
    let gaps = [];
 | 
						|
    for (let i = 1; i < segments.length; ++i) {
 | 
						|
        const last = segments[i - 1];
 | 
						|
        const cur = segments[i];
 | 
						|
        gaps.push([last[last.length - 1], cur[0]]);
 | 
						|
    }
 | 
						|
 | 
						|
    // If the last day is not equal to the current day, add a gap that
 | 
						|
    // spans to the current day.
 | 
						|
    const last = segments[segments.length - 1];
 | 
						|
    const lastDay = last[last.length - 1];
 | 
						|
    if (!equalDay(lastDay.date, max.date)) {
 | 
						|
        const maxDay = Object.assign({}, lastDay);
 | 
						|
        maxDay.date = max.date;
 | 
						|
        gaps.push([lastDay, maxDay]);
 | 
						|
    }
 | 
						|
    return gaps;
 | 
						|
}
 | 
						|
 | 
						|
function renderChart(chart) {
 | 
						|
    const chartKey = chart.attr("data-key");
 | 
						|
    const vals = data[chartKey];
 | 
						|
 | 
						|
    const svg = chart.html("");
 | 
						|
 | 
						|
    const margin = { top: 25, right: 60, bottom: 25, left: 60 };
 | 
						|
 | 
						|
    const width = styleWidth(svg) - margin.left - margin.right,
 | 
						|
        height = styleHeight(svg) - margin.top - margin.bottom;
 | 
						|
 | 
						|
    const defs = svg.append("defs");
 | 
						|
    const filter = defs
 | 
						|
        .append("filter")
 | 
						|
        .attr("id", "textBackground")
 | 
						|
        .attr("x", 0)
 | 
						|
        .attr("y", 0)
 | 
						|
        .attr("width", 1)
 | 
						|
        .attr("height", 1);
 | 
						|
    filter.append("feFlood").attr("flood-color", "white");
 | 
						|
    filter.append("feComposite").attr("in", "SourceGraphic");
 | 
						|
 | 
						|
    defs
 | 
						|
        .append("clipPath")
 | 
						|
        .attr("id", chartKey)
 | 
						|
        .append("rect")
 | 
						|
        .attr("x", 0)
 | 
						|
        .attr("y", -margin.top)
 | 
						|
        .attr("width", width)
 | 
						|
        .attr("height", margin.top + height + 10);
 | 
						|
 | 
						|
    const title = svg
 | 
						|
        .append("text")
 | 
						|
        .attr("class", "chart-title")
 | 
						|
        .attr("x", margin.left + width / 2)
 | 
						|
        .attr("y", 15)
 | 
						|
        .style("text-anchor", "middle")
 | 
						|
        .style("font", "8pt sans-serif")
 | 
						|
        .text(chartKey);
 | 
						|
 | 
						|
    const g = svg
 | 
						|
        .append("g")
 | 
						|
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
 | 
						|
 | 
						|
    const x = d3.scaleTime().range([0, width]);
 | 
						|
    const x2 = d3.scaleTime().range([0, width]);
 | 
						|
    const y1 = d3.scaleLinear().range([height, 0]);
 | 
						|
    const z = d3.scaleOrdinal(d3.schemeCategory10);
 | 
						|
    const xFormat = formatTime;
 | 
						|
 | 
						|
    x.domain([minDate, max.date]);
 | 
						|
    x2.domain([minDate, max.date]);
 | 
						|
 | 
						|
    y1.domain([0, getMaxes(chartKey).opsSec]);
 | 
						|
 | 
						|
    const xAxis = d3.axisBottom(x).ticks(5);
 | 
						|
 | 
						|
    g
 | 
						|
        .append("g")
 | 
						|
        .attr("class", "axis axis--x")
 | 
						|
        .attr("transform", "translate(0," + height + ")")
 | 
						|
        .call(xAxis);
 | 
						|
    g
 | 
						|
        .append("g")
 | 
						|
        .attr("class", "axis axis--y")
 | 
						|
        .call(d3.axisLeft(y1).ticks(5));
 | 
						|
 | 
						|
    if (!vals) {
 | 
						|
        // That's all we can draw for an empty chart.
 | 
						|
        svg
 | 
						|
            .append("text")
 | 
						|
            .attr("x", margin.left + width / 2)
 | 
						|
            .attr("y", margin.top + height / 2)
 | 
						|
            .style("text-anchor", "middle")
 | 
						|
            .style("font", "8pt sans-serif")
 | 
						|
            .text("No data");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const view = g
 | 
						|
        .append("g")
 | 
						|
        .attr("class", "view")
 | 
						|
        .attr("clip-path", "url(#" + chartKey + ")");
 | 
						|
 | 
						|
    const triangle = d3
 | 
						|
        .symbol()
 | 
						|
        .type(d3.symbolTriangle)
 | 
						|
        .size(12);
 | 
						|
    view
 | 
						|
        .selectAll("path.annotation")
 | 
						|
        .data(annotations)
 | 
						|
        .enter()
 | 
						|
        .append("path")
 | 
						|
        .attr("class", "annotation")
 | 
						|
        .attr("d", triangle)
 | 
						|
        .attr("stroke", "#2b2")
 | 
						|
        .attr("fill", "#2b2")
 | 
						|
        .attr(
 | 
						|
            "transform",
 | 
						|
            d => "translate(" + (x(d.date) + "," + (height + 5) + ")")
 | 
						|
        );
 | 
						|
 | 
						|
    view
 | 
						|
        .selectAll("line.annotation")
 | 
						|
        .data(annotations)
 | 
						|
        .enter()
 | 
						|
        .append("line")
 | 
						|
        .attr("class", "annotation")
 | 
						|
        .attr("fill", "none")
 | 
						|
        .attr("stroke", "#2b2")
 | 
						|
        .attr("stroke-width", "1px")
 | 
						|
        .attr("stroke-dasharray", "1 2")
 | 
						|
        .attr("x1", d => x(d.date))
 | 
						|
        .attr("x2", d => x(d.date))
 | 
						|
        .attr("y1", 0)
 | 
						|
        .attr("y2", height);
 | 
						|
 | 
						|
    // Divide the data into contiguous days so that we can avoid
 | 
						|
    // interpolating days where there is missing data.
 | 
						|
    const segments = computeSegments(vals);
 | 
						|
    const gaps = computeGaps(segments);
 | 
						|
 | 
						|
    const line1 = d3
 | 
						|
        .line()
 | 
						|
        .x(d => x(d.date))
 | 
						|
        .y(d => y1(d.opsSec));
 | 
						|
    const path1 = view
 | 
						|
        .selectAll(".line1")
 | 
						|
        .data(segments)
 | 
						|
        .enter()
 | 
						|
        .append("path")
 | 
						|
        .attr("class", "line1")
 | 
						|
        .attr("d", line1)
 | 
						|
        .style("stroke", d => z(0));
 | 
						|
 | 
						|
    view
 | 
						|
        .selectAll(".line1-gaps")
 | 
						|
        .data(gaps)
 | 
						|
        .enter()
 | 
						|
        .append("path")
 | 
						|
        .attr("class", "line1-gaps")
 | 
						|
        .attr("d", line1)
 | 
						|
        .attr("opacity", 0.8)
 | 
						|
        .style("stroke", d => z(0))
 | 
						|
        .style("stroke-dasharray", "1 2");
 | 
						|
 | 
						|
    let y2 = d3.scaleLinear().range([height, 0]);
 | 
						|
    let line2;
 | 
						|
    let path2;
 | 
						|
    if (detail) {
 | 
						|
        y2 = d3.scaleLinear().range([height, 0]);
 | 
						|
        y2.domain([0, detail(getMaxes(chartKey))]);
 | 
						|
        g
 | 
						|
            .append("g")
 | 
						|
            .attr("class", "axis axis--y")
 | 
						|
            .attr("transform", "translate(" + width + ",0)")
 | 
						|
            .call(
 | 
						|
                d3
 | 
						|
                    .axisRight(y2)
 | 
						|
                    .ticks(5)
 | 
						|
                    .tickFormat(detailFormat)
 | 
						|
            );
 | 
						|
 | 
						|
        line2 = d3
 | 
						|
            .line()
 | 
						|
            .x(d => x(d.date))
 | 
						|
            .y(d => y2(detail(d)));
 | 
						|
        path2 = view
 | 
						|
            .selectAll(".line2")
 | 
						|
            .data(segments)
 | 
						|
            .enter()
 | 
						|
            .append("path")
 | 
						|
            .attr("class", "line2")
 | 
						|
            .attr("d", line2)
 | 
						|
            .style("stroke", d => z(1));
 | 
						|
        view
 | 
						|
            .selectAll(".line2-gaps")
 | 
						|
            .data(gaps)
 | 
						|
            .enter()
 | 
						|
            .append("path")
 | 
						|
            .attr("class", "line2-gaps")
 | 
						|
            .attr("d", line2)
 | 
						|
            .attr("opacity", 0.8)
 | 
						|
            .style("stroke", d => z(1))
 | 
						|
            .style("stroke-dasharray", "1 2");
 | 
						|
    }
 | 
						|
 | 
						|
    const updateZoom = function(t) {
 | 
						|
        x.domain(t.rescaleX(x2).domain());
 | 
						|
        g.select(".axis--x").call(xAxis);
 | 
						|
        g.selectAll(".line1").attr("d", line1);
 | 
						|
        g.selectAll(".line1-gaps").attr("d", line1);
 | 
						|
        if (detail) {
 | 
						|
            g.selectAll(".line2").attr("d", line2);
 | 
						|
            g.selectAll(".line2-gaps").attr("d", line2);
 | 
						|
        }
 | 
						|
        g
 | 
						|
            .selectAll("path.annotation")
 | 
						|
            .attr(
 | 
						|
                "transform",
 | 
						|
                d => "translate(" + (x(d.date) + "," + (height + 5) + ")")
 | 
						|
            );
 | 
						|
        g
 | 
						|
            .selectAll("line.annotation")
 | 
						|
            .attr("x1", d => x(d.date))
 | 
						|
            .attr("x2", d => x(d.date));
 | 
						|
    };
 | 
						|
    svg.node().updateZoom = updateZoom;
 | 
						|
 | 
						|
    const hoverSeries = function(mouse) {
 | 
						|
        if (!detail) {
 | 
						|
            return 1;
 | 
						|
        }
 | 
						|
        const mousex = mouse[0];
 | 
						|
        const mousey = mouse[1] - margin.top;
 | 
						|
        const path1Y = pathGetY(path1.node(), mousex);
 | 
						|
        const path2Y = pathGetY(path2.node(), mousex);
 | 
						|
        return Math.abs(mousey - path1Y) < Math.abs(mousey - path2Y) ? 1 : 2;
 | 
						|
    };
 | 
						|
 | 
						|
    // This is a bit funky: initDate() initializes the date range to
 | 
						|
    // [today-90,today]. We then allow zooming out by 4x which will
 | 
						|
    // give a maximum range of 360 days. We limit translation to the
 | 
						|
    // 360 day period. The funkiness is that it would be more natural
 | 
						|
    // to start at the maximum zoomed amount and then initialize the
 | 
						|
    // zoom. But that doesn't work because we want to maintain the
 | 
						|
    // existing zoom settings whenever we have to (re-)render().
 | 
						|
    const zoom = d3
 | 
						|
        .zoom()
 | 
						|
        .scaleExtent([0.25, 2])
 | 
						|
        .translateExtent([[-width * 3, 0], [width, 1]])
 | 
						|
        .extent([[0, 0], [width, 1]])
 | 
						|
        .on("zoom", function() {
 | 
						|
            const t = d3.event.transform;
 | 
						|
            if (!d3.event.sourceEvent) {
 | 
						|
                updateZoom(t);
 | 
						|
                return;
 | 
						|
            }
 | 
						|
 | 
						|
            d3.selectAll(".chart").each(function() {
 | 
						|
                if (this.updateZoom != null) {
 | 
						|
                    this.updateZoom(t);
 | 
						|
                }
 | 
						|
            });
 | 
						|
 | 
						|
            d3.selectAll(".chart").each(function() {
 | 
						|
                this.__zoom = t.translate(0, 0);
 | 
						|
            });
 | 
						|
 | 
						|
            const mouse = d3.mouse(this);
 | 
						|
            if (mouse) {
 | 
						|
                mouse[0] -= margin.left; // adjust for rect.mouse position
 | 
						|
                const date = x.invert(mouse[0]);
 | 
						|
                const hover = hoverSeries(mouse);
 | 
						|
                d3.selectAll(".chart.ycsb").each(function() {
 | 
						|
                    this.updateMouse(mouse, date, hover);
 | 
						|
                });
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
    svg.call(zoom);
 | 
						|
    svg.call(zoom.transform, d3.zoomTransform(svg.node()));
 | 
						|
 | 
						|
    const lineHover = g
 | 
						|
        .append("line")
 | 
						|
        .attr("class", "hover")
 | 
						|
        .style("fill", "none")
 | 
						|
        .style("stroke", "#f99")
 | 
						|
        .style("stroke-width", "1px");
 | 
						|
 | 
						|
    const dateHover = g
 | 
						|
        .append("text")
 | 
						|
        .attr("class", "hover")
 | 
						|
        .attr("fill", "#f22")
 | 
						|
        .attr("text-anchor", "middle")
 | 
						|
        .attr("alignment-baseline", "hanging")
 | 
						|
        .attr("transform", "translate(0, 0)");
 | 
						|
 | 
						|
    const opsHover = g
 | 
						|
        .append("text")
 | 
						|
        .attr("class", "hover")
 | 
						|
        .attr("fill", "#f22")
 | 
						|
        .attr("text-anchor", "middle")
 | 
						|
        .attr("transform", "translate(0, 0)");
 | 
						|
 | 
						|
    const marker = g
 | 
						|
        .append("circle")
 | 
						|
        .attr("class", "hover")
 | 
						|
        .attr("r", 3)
 | 
						|
        .style("opacity", "0")
 | 
						|
        .style("stroke", "#f22")
 | 
						|
        .style("fill", "#f22");
 | 
						|
 | 
						|
    svg.node().updateMouse = function(mouse, date, hover) {
 | 
						|
        const mousex = mouse[0];
 | 
						|
        const mousey = mouse[1];
 | 
						|
        const i = dateBisector(vals, date, 1);
 | 
						|
        const v =
 | 
						|
            i == vals.length
 | 
						|
                ? vals[i - 1]
 | 
						|
                : mousex - x(vals[i - 1].date) < x(vals[i].date) - mousex
 | 
						|
                    ? vals[i - 1]
 | 
						|
                    : vals[i];
 | 
						|
        const noData = mousex < x(vals[0].date);
 | 
						|
 | 
						|
        let lineY = height;
 | 
						|
        if (!noData) {
 | 
						|
            if (hover == 1) {
 | 
						|
                lineY = pathGetY(path1.node(), mousex);
 | 
						|
            } else {
 | 
						|
                lineY = pathGetY(path2.node(), mousex);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        let val, valY, valFormat;
 | 
						|
        if (hover == 1) {
 | 
						|
            val = v.opsSec;
 | 
						|
            valY = y1(val);
 | 
						|
            valFormat = d3.format(",.0f");
 | 
						|
        } else {
 | 
						|
            val = detail(v);
 | 
						|
            valY = y2(val);
 | 
						|
            valFormat = detailFormat;
 | 
						|
        }
 | 
						|
 | 
						|
        lineHover
 | 
						|
            .attr("x1", mousex)
 | 
						|
            .attr("x2", mousex)
 | 
						|
            .attr("y1", lineY)
 | 
						|
            .attr("y2", height);
 | 
						|
        marker.attr("transform", "translate(" + x(v.date) + "," + valY + ")");
 | 
						|
        dateHover
 | 
						|
            .attr("transform", "translate(" + mousex + "," + (height + 8) + ")")
 | 
						|
            .text(xFormat(date));
 | 
						|
        opsHover
 | 
						|
            .attr(
 | 
						|
                "transform",
 | 
						|
                "translate(" + x(v.date) + "," + (valY - 7) + ")"
 | 
						|
            )
 | 
						|
            .text(valFormat(val));
 | 
						|
    };
 | 
						|
 | 
						|
    const rect = svg
 | 
						|
        .append("rect")
 | 
						|
        .attr("class", "mouse")
 | 
						|
        .attr("cursor", "move")
 | 
						|
        .attr("fill", "none")
 | 
						|
        .attr("pointer-events", "all")
 | 
						|
        .attr("width", width)
 | 
						|
        .attr("height", height + margin.top + margin.bottom)
 | 
						|
        .attr("transform", "translate(" + margin.left + "," + 0 + ")")
 | 
						|
        .on("mousemove", function() {
 | 
						|
            const mouse = d3.mouse(this);
 | 
						|
            const date = x.invert(mouse[0]);
 | 
						|
            const hover = hoverSeries(mouse);
 | 
						|
 | 
						|
            let resetTitle = true;
 | 
						|
            for (let i in annotations) {
 | 
						|
                if (Math.abs(mouse[0] - x(annotations[i].date)) <= 5) {
 | 
						|
                    title
 | 
						|
                        .style("font-size", "9pt")
 | 
						|
                        .text(annotations[i].message);
 | 
						|
                    resetTitle = false;
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
            }
 | 
						|
            if (resetTitle) {
 | 
						|
                title.style("font-size", "8pt").text(chartKey);
 | 
						|
            }
 | 
						|
 | 
						|
            d3.selectAll(".chart").each(function() {
 | 
						|
                if (this.updateMouse != null) {
 | 
						|
                    this.updateMouse(mouse, date, hover);
 | 
						|
                }
 | 
						|
            });
 | 
						|
        })
 | 
						|
        .on("mouseover", function() {
 | 
						|
            d3
 | 
						|
                .selectAll(".chart")
 | 
						|
                .selectAll(".hover")
 | 
						|
                .style("opacity", 1.0);
 | 
						|
        })
 | 
						|
        .on("mouseout", function() {
 | 
						|
            d3
 | 
						|
                .selectAll(".chart")
 | 
						|
                .selectAll(".hover")
 | 
						|
                .style("opacity", 0);
 | 
						|
        });
 | 
						|
}
 | 
						|
 | 
						|
function renderYCSB() {
 | 
						|
    d3.selectAll(".chart.ycsb").each(function(d, i) {
 | 
						|
        renderChart(d3.select(this));
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
function initData() {
 | 
						|
    for (key in data) {
 | 
						|
        data[key] = d3.csvParseRows(data[key], function(d, i) {
 | 
						|
            return {
 | 
						|
                date: parseTime(d[0]),
 | 
						|
                opsSec: +d[1],
 | 
						|
                readBytes: +d[2],
 | 
						|
                writeBytes: +d[3],
 | 
						|
                readAmp: +d[4],
 | 
						|
                writeAmp: +d[5]
 | 
						|
            };
 | 
						|
        });
 | 
						|
 | 
						|
        const vals = data[key];
 | 
						|
        max.perChart[key] = {
 | 
						|
            opsSec: d3.max(vals, d => d.opsSec),
 | 
						|
            readBytes: d3.max(vals, d => d.readBytes),
 | 
						|
            writeBytes: d3.max(vals, d => d.writeBytes),
 | 
						|
            readAmp: d3.max(vals, d => d.readAmp),
 | 
						|
            writeAmp: d3.max(vals, d => d.writeAmp),
 | 
						|
        }
 | 
						|
        max.opsSec = Math.max(max.opsSec, max.perChart[key].opsSec);
 | 
						|
        max.readBytes = Math.max(max.readBytes, max.perChart[key].readBytes);
 | 
						|
        max.writeBytes = Math.max(
 | 
						|
            max.writeBytes,
 | 
						|
            max.perChart[key].writeBytes,
 | 
						|
        );
 | 
						|
        max.readAmp = Math.max(max.readAmp, max.perChart[key].readAmp);
 | 
						|
        max.writeAmp = Math.max(max.writeAmp, max.perChart[key].writeAmp);
 | 
						|
    }
 | 
						|
 | 
						|
    // Load the write-throughput data and merge with the existing data. We
 | 
						|
    // return a promise here to allow us to continue to make progress elsewhere.
 | 
						|
    return fetch(writeThroughputSummaryURL())
 | 
						|
      .then(response => response.json())
 | 
						|
      .then(wtData => {
 | 
						|
            for (let key in wtData) {
 | 
						|
                data[key] = wtData[key];
 | 
						|
            }
 | 
						|
      });
 | 
						|
}
 | 
						|
 | 
						|
function initDateRange() {
 | 
						|
    max.date.setHours(0, 0, 0, 0);
 | 
						|
    minDate = new Date(new Date().setDate(max.date.getDate() - 90));
 | 
						|
}
 | 
						|
 | 
						|
function initAnnotations() {
 | 
						|
    d3.selectAll(".annotation").each(function() {
 | 
						|
        const annotation = d3.select(this);
 | 
						|
        const date = parseTime(annotation.attr("data-date"));
 | 
						|
        annotations.push({ date: date, message: annotation.text() });
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
function setQueryParams() {
 | 
						|
    var params = new URLSearchParams();
 | 
						|
    if (detailName) {
 | 
						|
        params.set("detail", detailName);
 | 
						|
    }
 | 
						|
    if (usePerChartMax) {
 | 
						|
        params.set("max", "local");
 | 
						|
    }
 | 
						|
    var search = "?" + params;
 | 
						|
    if (window.location.search != search) {
 | 
						|
        window.history.pushState(null, null, search);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function setDetail(name) {
 | 
						|
    detail = undefined;
 | 
						|
    detailFormat = undefined;
 | 
						|
    detailName = name;
 | 
						|
 | 
						|
    switch (detailName) {
 | 
						|
        case "readBytes":
 | 
						|
            detail = d => d.readBytes;
 | 
						|
            detailFormat = humanize;
 | 
						|
            break;
 | 
						|
        case "writeBytes":
 | 
						|
            detail = d => d.writeBytes;
 | 
						|
            detailFormat = humanize;
 | 
						|
            break;
 | 
						|
        case "readAmp":
 | 
						|
            detail = d => d.readAmp;
 | 
						|
            detailFormat = d3.format(",.1f");
 | 
						|
            break;
 | 
						|
        case "writeAmp":
 | 
						|
            detail = d => d.writeAmp;
 | 
						|
            detailFormat = d3.format(",.1f");
 | 
						|
            break;
 | 
						|
    }
 | 
						|
 | 
						|
    d3.selectAll(".toggle").classed("selected", false);
 | 
						|
    d3.select("#" + detailName).classed("selected", detail != null);
 | 
						|
}
 | 
						|
 | 
						|
function initQueryParams() {
 | 
						|
    var params = new URLSearchParams(window.location.search.substring(1));
 | 
						|
    setDetail(params.get("detail"));
 | 
						|
    usePerChartMax = params.get("max") == "local";
 | 
						|
    d3.select("#localMax").classed("selected", usePerChartMax);
 | 
						|
}
 | 
						|
 | 
						|
function toggleDetail(name) {
 | 
						|
    const link = d3.select("#" + name);
 | 
						|
    const selected = !link.classed("selected");
 | 
						|
    link.classed("selected", selected);
 | 
						|
    if (selected) {
 | 
						|
        setDetail(name);
 | 
						|
    } else {
 | 
						|
        setDetail(null);
 | 
						|
    }
 | 
						|
    setQueryParams();
 | 
						|
    renderYCSB();
 | 
						|
}
 | 
						|
 | 
						|
function toggleLocalMax() {
 | 
						|
    const link = d3.select("#localMax");
 | 
						|
    const selected = !link.classed("selected");
 | 
						|
    link.classed("selected", selected);
 | 
						|
    usePerChartMax = selected;
 | 
						|
    setQueryParams();
 | 
						|
    renderYCSB();
 | 
						|
}
 | 
						|
 | 
						|
window.onload = function init() {
 | 
						|
    d3.selectAll(".toggle").each(function() {
 | 
						|
        const link = d3.select(this);
 | 
						|
        link.attr("href", 'javascript:toggleDetail("' + link.attr("id") + '")');
 | 
						|
    });
 | 
						|
    d3.selectAll("#localMax").each(function() {
 | 
						|
        const link = d3.select(this);
 | 
						|
        link.attr("href", 'javascript:toggleLocalMax()');
 | 
						|
    });
 | 
						|
 | 
						|
    initData().then(_ => {
 | 
						|
        initDateRange();
 | 
						|
        initAnnotations();
 | 
						|
        initQueryParams();
 | 
						|
 | 
						|
        renderYCSB();
 | 
						|
        renderWriteThroughputSummary(data);
 | 
						|
 | 
						|
        // Use the max date to bisect into the workload data to pluck out the
 | 
						|
        // correct datapoint.
 | 
						|
        let workloadData = data[writeThroughputWorkload];
 | 
						|
        bisectAndRenderWriteThroughputDetail(workloadData, max.date);
 | 
						|
 | 
						|
        let lastUpdate;
 | 
						|
        for (let key in data) {
 | 
						|
            const max = d3.max(data[key], d => d.date);
 | 
						|
            if (!lastUpdate || lastUpdate < max) {
 | 
						|
                lastUpdate = max;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        d3.selectAll(".updated")
 | 
						|
            .text("Last updated: " + d3.timeFormat("%b %e, %Y")(lastUpdate));
 | 
						|
    })
 | 
						|
 | 
						|
    // By default, display each panel with its local max, which makes spotting
 | 
						|
    // regressions simpler.
 | 
						|
    toggleLocalMax();
 | 
						|
};
 | 
						|
 | 
						|
window.onpopstate = function() {
 | 
						|
    initQueryParams();
 | 
						|
    renderYCSB();
 | 
						|
};
 | 
						|
 | 
						|
window.addEventListener("resize", renderYCSB);
 |