mirror of
https://github.com/plasma-umass/coz.git
synced 2024-11-05 07:27:53 +03:00
708 lines
25 KiB
TypeScript
708 lines
25 KiB
TypeScript
type ExperimentData = ThroughputData | LatencyData;
|
|
declare let science: any;
|
|
|
|
interface ExperimentResult {
|
|
name: string;
|
|
progress_points: Point[];
|
|
}
|
|
|
|
interface Point {
|
|
name: string;
|
|
measurements: Measurement[]
|
|
}
|
|
|
|
interface Measurement {
|
|
speedup: number;
|
|
progress_speedup: number;
|
|
}
|
|
|
|
interface ThroughputData {
|
|
type: 'throughput';
|
|
delta: number;
|
|
duration: number;
|
|
}
|
|
|
|
interface LatencyData {
|
|
type: 'latency';
|
|
arrivals: number;
|
|
departures: number;
|
|
difference: number;
|
|
duration: number;
|
|
}
|
|
|
|
type Line = Experiment | ThroughputPoint | LatencyPoint | IgnoredRecord;
|
|
|
|
interface Experiment {
|
|
type: 'experiment';
|
|
selected: string;
|
|
speedup: number;
|
|
duration: number;
|
|
}
|
|
|
|
interface ThroughputPoint {
|
|
type: 'throughput-point' | 'progress-point';
|
|
name: string;
|
|
delta: number;
|
|
}
|
|
|
|
interface LatencyPoint {
|
|
type: 'latency-point';
|
|
name: string;
|
|
arrivals: number;
|
|
departures: number;
|
|
difference: number;
|
|
}
|
|
|
|
interface IgnoredRecord {
|
|
type: 'startup' | 'shutdown' | 'samples' | 'runtime';
|
|
}
|
|
|
|
/**
|
|
* Returns if this data point is valid.
|
|
* Infinity / -Infinity occurs when dividing by 0.
|
|
* NaN happens when the data is messed up and we get a NaN into a computation somewhere.
|
|
*/
|
|
function isValidDataPoint(data: number): boolean {
|
|
return !isNaN(data) && data !== Infinity && data !== -Infinity;
|
|
}
|
|
|
|
/**
|
|
* Get the applicable data point from this particular experiment.
|
|
* Returns -Infinity or Infinity in undefined cases.
|
|
*/
|
|
function getDataPoint(data: ExperimentData): number {
|
|
// Discard obviously incorrect data points.
|
|
if (data.duration < 0) {
|
|
return NaN;
|
|
}
|
|
switch (data.type) {
|
|
case 'throughput':
|
|
return data.duration / data.delta;
|
|
case 'latency':
|
|
const arrivalRate = data.arrivals / data.duration;
|
|
// Average latency, according to Little's Law.
|
|
return data.difference / arrivalRate;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Are we trying to maximize or minimize this progress point?
|
|
*/
|
|
function shouldMaximize(data: ExperimentData) {
|
|
switch (data.type) {
|
|
case 'latency':
|
|
return false;
|
|
case 'throughput':
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function parseLine(s: string): Line {
|
|
if(s[0] == '{') {
|
|
return JSON.parse(s);
|
|
} else {
|
|
let parts = s.split('\t');
|
|
let obj: {
|
|
type: string;
|
|
[key: string]: string | number
|
|
} = { type: parts[0] };
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const equals_index = parts[i].indexOf('=');
|
|
if (equals_index === -1) continue;
|
|
let key = parts[i].substring(0, equals_index);
|
|
let value: string | number = parts[i].substring(equals_index + 1);
|
|
|
|
switch (key) {
|
|
case 'type':
|
|
if (obj.type === 'progress-point') {
|
|
key = 'point-type';
|
|
}
|
|
break;
|
|
case 'delta':
|
|
case 'time':
|
|
case 'duration':
|
|
case 'arrivals':
|
|
case 'departures':
|
|
case 'difference':
|
|
value = parseInt(value, 10);
|
|
break;
|
|
case 'speedup':
|
|
value = parseFloat(value);
|
|
break;
|
|
}
|
|
|
|
obj[key] = value;
|
|
}
|
|
return <Line> <any> obj;
|
|
}
|
|
}
|
|
|
|
function max_normalized_area(d: ExperimentResult): number {
|
|
let max_normalized_area = 0;
|
|
for (let i = 0; i < d.progress_points.length; i++) {
|
|
let area = 0;
|
|
for(let j = 1; j < d.progress_points[i].measurements.length; j++) {
|
|
let prev_data = d.progress_points[i].measurements[j-1];
|
|
let current_data = d.progress_points[i].measurements[j];
|
|
let avg_progress_speedup = (prev_data.progress_speedup + current_data.progress_speedup) / 2;
|
|
area += avg_progress_speedup * (current_data.speedup - prev_data.speedup);
|
|
let normalized_area = area / current_data.speedup;
|
|
if (normalized_area > max_normalized_area) max_normalized_area = normalized_area;
|
|
}
|
|
}
|
|
return max_normalized_area;
|
|
}
|
|
|
|
function max_progress_speedup(d: ExperimentResult): number {
|
|
let max_progress_speedup = 0;
|
|
for (let i in d.progress_points) {
|
|
for (let j in d.progress_points[i].measurements) {
|
|
let progress_speedup = d.progress_points[i].measurements[j].progress_speedup;
|
|
if (progress_speedup > max_progress_speedup) max_progress_speedup = progress_speedup;
|
|
}
|
|
}
|
|
return max_progress_speedup;
|
|
}
|
|
|
|
function min_progress_speedup(d: ExperimentResult): number {
|
|
let min_progress_speedup = 0;
|
|
for (let i in d.progress_points) {
|
|
for (let j in d.progress_points[i].measurements) {
|
|
let progress_speedup = d.progress_points[i].measurements[j].progress_speedup;
|
|
if (progress_speedup < min_progress_speedup) min_progress_speedup = progress_speedup;
|
|
}
|
|
}
|
|
return min_progress_speedup;
|
|
}
|
|
|
|
const sort_functions: {[name: string]: (a: ExperimentResult, b: ExperimentResult) => number} = {
|
|
alphabetical: function(a: ExperimentResult, b: ExperimentResult): number {
|
|
if(a.name > b.name) return 1;
|
|
else return -1;
|
|
},
|
|
|
|
impact: function(a: ExperimentResult, b: ExperimentResult): number {
|
|
if (max_normalized_area(b) > max_normalized_area(a)) return 1;
|
|
else return -1;
|
|
},
|
|
|
|
max_speedup: function(a: ExperimentResult, b: ExperimentResult): number {
|
|
if (max_progress_speedup(b) > max_progress_speedup(a)) return 1;
|
|
else return -1;
|
|
},
|
|
|
|
min_speedup: function(a: ExperimentResult, b: ExperimentResult): number {
|
|
if (min_progress_speedup(a) > min_progress_speedup(b)) return 1;
|
|
else return -1;
|
|
}
|
|
};
|
|
|
|
class Profile {
|
|
private _data: {[location: string]: {
|
|
[progressPoint: string]: {
|
|
[speedupAmount: number]: ExperimentData
|
|
}
|
|
}}= {};
|
|
private _disabled_progress_points: string[] = [];
|
|
private _plot_container: d3.Selection<any>;
|
|
private _plot_legend: d3.Selection<any>;
|
|
private _get_min_points: () => number;
|
|
private _display_warning: (title: string, msg: string) => void;
|
|
private _progress_points: string[] = null;
|
|
constructor(profile_text: string, container: d3.Selection<any>, legend: d3.Selection<any>, get_min_points: () => number, display_warning: (title: string, msg: string) => void) {
|
|
this._plot_container = container;
|
|
this._plot_legend = legend;
|
|
this._get_min_points = get_min_points;
|
|
this._display_warning = display_warning;
|
|
let lines = profile_text.split('\n');
|
|
let experiment: Experiment = null;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].length == 0) continue;
|
|
let entry = parseLine(lines[i]);
|
|
|
|
if (entry.type === 'startup') {
|
|
// Do nothing
|
|
} else if (entry.type === 'shutdown') {
|
|
// Do nothing
|
|
} else if (entry.type === 'samples') {
|
|
// Do nothing
|
|
} else if (entry.type === 'runtime') {
|
|
// Do nothing
|
|
} else if (entry.type === 'experiment') {
|
|
experiment = entry;
|
|
} else if (entry.type === 'throughput-point' || entry.type === 'progress-point') {
|
|
this.addThroughputMeasurement(experiment, entry);
|
|
} else if (entry.type === 'latency-point') {
|
|
this.addLatencyMeasurement(experiment, entry);
|
|
} else {
|
|
display_warning('Invalid Profile', 'The profile you loaded contains an invalid line: <pre>' + lines[i] + '</pre>');
|
|
}
|
|
}
|
|
|
|
if (experiment == null) {
|
|
display_warning('Empty Profile', 'The profile you loaded does not contain result from any performance experiments. Make sure you specified a progress point, built your program with debug information, and ran your program on an input that took at least a few seconds.')
|
|
}
|
|
}
|
|
|
|
public ensureDataEntry<T extends ExperimentData>(selected: string, point: string, speedup: number, initial_value: T): T {
|
|
if (!this._data[selected]) this._data[selected] = {};
|
|
if (!this._data[selected][point]) this._data[selected][point] = {};
|
|
if (!this._data[selected][point][speedup]) this._data[selected][point][speedup] = initial_value;
|
|
return <T> this._data[selected][point][speedup];
|
|
}
|
|
|
|
public addThroughputMeasurement(experiment: Experiment, point: ThroughputPoint) {
|
|
let entry = this.ensureDataEntry(experiment.selected, point.name, experiment.speedup, {
|
|
delta: 0,
|
|
duration: 0,
|
|
type: 'throughput'
|
|
});
|
|
|
|
// Add new delta and duration to data
|
|
entry.delta += point.delta;
|
|
entry.duration += experiment.duration;
|
|
}
|
|
|
|
public addLatencyMeasurement(experiment: Experiment, point: LatencyPoint) {
|
|
let entry = this.ensureDataEntry(experiment.selected, point.name, experiment.speedup, {
|
|
arrivals: 0,
|
|
departures: 0,
|
|
difference: 0,
|
|
duration: 0,
|
|
type: 'latency'
|
|
});
|
|
|
|
entry.arrivals += point.arrivals;
|
|
entry.departures += point.departures;
|
|
|
|
// Compute a running weighted average of the difference between arrivals and departures
|
|
if (entry.duration === 0) {
|
|
entry.difference = point.difference;
|
|
} else {
|
|
// Compute the new total duration of all experiments combined (including the new one)
|
|
let total_duration = entry.duration + experiment.duration;
|
|
// Scale the difference down by the ratio of the prior and total durations. This scale factor will be closer to 1 than 0, so divide first for better numerical stability
|
|
entry.difference *= entry.duration / total_duration;
|
|
// Add the contribution to average difference from the current experiment. The scale factor will be close to zero, so multiply first for better numerical stability.
|
|
entry.difference += (point.difference * experiment.duration) / total_duration;
|
|
}
|
|
|
|
// Update the total duration
|
|
entry.duration += experiment.duration;
|
|
}
|
|
|
|
public getProgressPoints(): string[] {
|
|
if (this._progress_points) {
|
|
return this._progress_points;
|
|
}
|
|
let points: string[] = [];
|
|
for (var selected in this._data) {
|
|
for (var point in this._data[selected]) {
|
|
if (points.indexOf(point) === -1) points.push(point);
|
|
}
|
|
}
|
|
// Stable order.
|
|
return this._progress_points = points.sort();
|
|
}
|
|
|
|
public getSpeedupData(min_points: number): ExperimentResult[] {
|
|
const progress_points = this.getProgressPoints().filter((pp) => this._disabled_progress_points.indexOf(pp) === -1);
|
|
let result: ExperimentResult[] = [];
|
|
for (let selected in this._data) {
|
|
let points: Point[] = [];
|
|
let points_with_enough = 0;
|
|
progress_point_loop:
|
|
for (let i = 0; i < progress_points.length; i++) {
|
|
// Set up an empty record for this progress point
|
|
const point = {
|
|
name: progress_points[i],
|
|
measurements: new Array<Measurement>()
|
|
};
|
|
points.push(point);
|
|
|
|
// Get the data for this progress point, if any
|
|
let point_data = this._data[selected][progress_points[i]];
|
|
|
|
// Check to be sure the point was observed and we have baseline (zero speedup) data
|
|
if (point_data !== undefined && point_data[0] !== undefined) {
|
|
// Compute the baseline data point
|
|
let baseline_data_point = getDataPoint(point_data[0]);
|
|
let maximize = shouldMaximize(point_data[0]);
|
|
if (!isValidDataPoint(baseline_data_point)) {
|
|
// Baseline data point is invalid (divide by zero, or a NaN)
|
|
continue progress_point_loop;
|
|
}
|
|
|
|
// Loop over measurements and compute progress speedups in D3-friendly format
|
|
let measurements: Measurement[] = [];
|
|
for (let speedup in point_data) {
|
|
let data_point = getDataPoint(point_data[speedup]);
|
|
// Skip invalid data.
|
|
if (!isValidDataPoint(data_point)) {
|
|
continue;
|
|
}
|
|
|
|
let progress_speedup = (baseline_data_point - data_point) / baseline_data_point;
|
|
if (!maximize) {
|
|
// We are trying to *minimize* this progress point, so negate the speedup.
|
|
progress_speedup = -progress_speedup;
|
|
}
|
|
|
|
// Skip really large negative and positive values
|
|
if (progress_speedup >= -1 && progress_speedup <= 2) {
|
|
// Add entry to measurements
|
|
measurements.push({
|
|
speedup: +speedup,
|
|
progress_speedup: progress_speedup
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort measurements by speedup
|
|
measurements.sort(function(a, b) { return a.speedup - b.speedup; });
|
|
|
|
// Use these measurements if we have enough different points
|
|
if (measurements.length >= min_points) {
|
|
points_with_enough++;
|
|
point.measurements = measurements;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (points_with_enough > 0) {
|
|
result.push({
|
|
name: selected,
|
|
progress_points: points
|
|
});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public drawLegend() {
|
|
let container = this._plot_legend;
|
|
const progress_points = this.getProgressPoints();
|
|
let legend_entries_sel = container.selectAll('p.legend-entry').data(progress_points);
|
|
legend_entries_sel.enter().append('p').attr('class', 'legend-entry');
|
|
|
|
// Remove the noseries class from legend entries
|
|
legend_entries_sel.classed('noseries', false).text('');
|
|
legend_entries_sel.append('i')
|
|
.attr('class', (d, i) => { return `fa fa-circle${this._disabled_progress_points.indexOf(d) !== -1 ? '-o' : ''} series${i % 4}`; })
|
|
.on('click', (d, i) => {
|
|
const ind = this._disabled_progress_points.indexOf(d);
|
|
if (ind !== -1) {
|
|
// Re-enable.
|
|
this._disabled_progress_points.splice(ind, 1);
|
|
} else if (this._disabled_progress_points.length + 1 < progress_points.length) {
|
|
// Disable.
|
|
this._disabled_progress_points.push(d);
|
|
} else {
|
|
// This is the last enabled progress point. Forbid disabling it.
|
|
this._display_warning("Warning", `At least one progress point must be enabled.`);
|
|
}
|
|
this.drawPlots(true);
|
|
this.drawLegend();
|
|
});
|
|
legend_entries_sel.append('span')
|
|
.attr('class', 'path')
|
|
.text(function(d) { return d; });
|
|
|
|
// Remove defunct legend entries
|
|
legend_entries_sel.exit().remove();
|
|
}
|
|
|
|
public drawPlots(no_animate: boolean): number {
|
|
const container = this._plot_container;
|
|
const min_points = this._get_min_points();
|
|
const speedup_data = this.getSpeedupData(min_points);
|
|
|
|
let min_speedup = Infinity;
|
|
let max_speedup = -Infinity;
|
|
for (let i = 0; i < speedup_data.length; i++) {
|
|
const result = speedup_data[i];
|
|
const result_min = min_progress_speedup(result);
|
|
const result_max = max_progress_speedup(result);
|
|
if (result_min < min_speedup) {
|
|
min_speedup = result_min;
|
|
}
|
|
if (result_max > max_speedup) {
|
|
max_speedup = result_max;
|
|
}
|
|
}
|
|
// Give some wiggle room to display points.
|
|
min_speedup *= 1.05;
|
|
max_speedup *= 1.05;
|
|
|
|
/****** Compute dimensions ******/
|
|
const container_width = parseInt(container.style('width'));
|
|
|
|
// Add columns while maintaining a target width
|
|
let cols = 1;
|
|
while (container_width / (cols + 1) >= 300) cols++;
|
|
|
|
const div_width = container_width / cols;
|
|
const div_height = 190;
|
|
const svg_width = div_width - 10;
|
|
const svg_height = div_height - 40;
|
|
const margins = {left: 55, right: 20, top: 10, bottom: 35};
|
|
const plot_width = svg_width - margins.left - margins.right;
|
|
const plot_height = svg_height - margins.top - margins.bottom;
|
|
const radius = 3;
|
|
const tick_size = 6;
|
|
|
|
// Formatters
|
|
const axisFormat = d3.format('.0%');
|
|
const percentFormat = d3.format('+.1%');
|
|
|
|
// Scales
|
|
let xscale = d3.scale.linear().domain([0, 1]);
|
|
let yscale = d3.scale.linear().domain([min_speedup, max_speedup]);
|
|
|
|
// Axes
|
|
let xaxis = d3.svg.axis()
|
|
.scale(xscale)
|
|
.orient('bottom')
|
|
.ticks(5)
|
|
.tickSize(tick_size)
|
|
.tickFormat(axisFormat);
|
|
//.tickSize(tick_size, 0, 0)
|
|
//.tickFormat(axisFormat);
|
|
|
|
let yaxis = (<d3.svg.Axis> (<any> d3.svg.axis()
|
|
.scale(yscale)
|
|
.orient('left')
|
|
.ticks(5))
|
|
.tickSize(tick_size, 0, 0))
|
|
.tickFormat(axisFormat);
|
|
|
|
// Gridlines
|
|
let xgrid = d3.svg.axis().scale(xscale).orient('bottom').tickFormat('');
|
|
let ygrid = d3.svg.axis().scale(yscale).orient('left').tickFormat('').ticks(5);
|
|
|
|
// Tooltip
|
|
let tip = (<any> d3).tip()
|
|
.attr('class', 'd3-tip')
|
|
.offset([-5, 0])
|
|
.html(function (d: Measurement) {
|
|
return '<strong>Line Speedup:</strong> ' + percentFormat(d.speedup) + '<br>' +
|
|
'<strong>Progress Speedup:</strong> ' + percentFormat(d.progress_speedup);
|
|
})
|
|
.direction(function (d: Measurement) {
|
|
if (d.speedup > 0.8) return 'w';
|
|
else return 'n';
|
|
});
|
|
|
|
/****** Add or update divs to hold each plot ******/
|
|
let plot_div_sel = container.selectAll('div.plot')
|
|
.data(speedup_data, function(d) { return d.name; });
|
|
|
|
let plot_x_pos = function(d: any, i: number) {
|
|
let col = i % cols;
|
|
return (col * div_width) + 'px';
|
|
}
|
|
|
|
let plot_y_pos = function(d: any, i: number) {
|
|
let row = (i - (i % cols)) / cols;
|
|
return (row * div_height) + 'px';
|
|
}
|
|
|
|
// First, remove divs that are disappearing
|
|
plot_div_sel.exit().transition().duration(200)
|
|
.style('opacity', 0).remove();
|
|
|
|
// Insert new divs with zero opacity
|
|
plot_div_sel.enter().append('div')
|
|
.attr('class', 'plot')
|
|
.style('margin-bottom', -div_height+'px')
|
|
.style('opacity', 0)
|
|
.style('width', div_width);
|
|
|
|
// Sort plots by the chosen sorting function
|
|
plot_div_sel.sort(sort_functions[(<any> d3.select('#sortby_field').node()).value]);
|
|
|
|
// Move divs into place. Only animate if we are not on a resizing redraw
|
|
if (!no_animate) {
|
|
plot_div_sel.transition().duration(400).delay(200)
|
|
.style('top', plot_y_pos)
|
|
.style('left', plot_x_pos)
|
|
.style('opacity', 1);
|
|
} else {
|
|
plot_div_sel.style('left', plot_x_pos)
|
|
.style('top', plot_y_pos);
|
|
}
|
|
|
|
/****** Insert, remove, and update plot titles ******/
|
|
let plot_title_sel = plot_div_sel.selectAll('div.plot-title').data(function(d) { return [d.name]; });
|
|
plot_title_sel.enter().append('div').attr('class', 'plot-title');
|
|
plot_title_sel.text(function(d) { return d; })
|
|
.classed('path', true)
|
|
.style('width', div_width+'px');
|
|
plot_title_sel.exit().remove();
|
|
|
|
/****** Update scales ******/
|
|
xscale.domain([0, 1]).range([0, plot_width]);
|
|
yscale.domain([min_speedup, max_speedup]).range([plot_height, 0]);
|
|
|
|
/****** Update gridlines ******/
|
|
(<any> xgrid).tickSize(-plot_height, 0, 0);
|
|
(<any> ygrid).tickSize(-plot_width, 0, 0);
|
|
|
|
/****** Insert and update plot svgs ******/
|
|
let plot_svg_sel = plot_div_sel.selectAll('svg').data([1]);
|
|
plot_svg_sel.enter().append('svg');
|
|
plot_svg_sel.attr('width', svg_width)
|
|
.attr('height', svg_height)
|
|
.call(tip);
|
|
plot_svg_sel.exit().remove();
|
|
|
|
/****** Add or update plot areas ******/
|
|
let plot_area_sel = <d3.selection.Update<ExperimentResult>> <any> plot_svg_sel.selectAll('g.plot_area').data([0]);
|
|
plot_area_sel.enter().append('g').attr('class', 'plot_area');
|
|
plot_area_sel.attr('transform', `translate(${margins.left}, ${margins.top})`);
|
|
plot_area_sel.exit().remove();
|
|
|
|
/****** Add or update clip paths ******/
|
|
let clippath_sel = plot_area_sel.selectAll('#clip').data([0]);
|
|
clippath_sel.enter().append('clipPath').attr('id', 'clip');
|
|
clippath_sel.exit().remove();
|
|
|
|
/****** Add or update clipping rectangles to clip paths ******/
|
|
let clip_rect_sel = clippath_sel.selectAll('rect').data([0]);
|
|
clip_rect_sel.enter().append('rect');
|
|
clip_rect_sel.attr('x', -radius - 1)
|
|
.attr('y', 0)
|
|
.attr('width', plot_width + 2 * radius + 2)
|
|
.attr('height', plot_height);
|
|
clip_rect_sel.exit().remove();
|
|
|
|
/****** Select plots areas, but preserve the real speedup data ******/
|
|
plot_area_sel = plot_div_sel.select('svg').select('g.plot_area');
|
|
|
|
/****** Add or update x-grids ******/
|
|
let xgrid_sel = plot_area_sel.selectAll('g.xgrid').data([0]);
|
|
xgrid_sel.enter().append('g').attr('class', 'xgrid');
|
|
xgrid_sel.attr('transform', `translate(0, ${plot_height})`).call(xgrid);
|
|
xgrid_sel.exit().remove();
|
|
|
|
/****** Add or update y-grids ******/
|
|
let ygrid_sel = plot_area_sel.selectAll('g.ygrid').data([0]);
|
|
ygrid_sel.enter().append('g').attr('class', 'ygrid');
|
|
ygrid_sel.call(ygrid);
|
|
ygrid_sel.exit().remove();
|
|
|
|
/****** Add or update x-axes ******/
|
|
let xaxis_sel = plot_area_sel.selectAll('g.xaxis').data([0]);
|
|
xaxis_sel.enter().append('g').attr('class', 'xaxis');
|
|
xaxis_sel.attr('transform', `translate(0, ${plot_height})`).call(xaxis);
|
|
xaxis_sel.exit().remove();
|
|
|
|
/****** Add or update x-axis titles ******/
|
|
let xtitle_sel = plot_area_sel.selectAll('text.xtitle').data([0]);
|
|
xtitle_sel.enter().append('text').attr('class', 'xtitle');
|
|
xtitle_sel.attr('x', xscale(0.5))
|
|
.attr('y', 32) // Approximate height of the x-axis
|
|
.attr('transform', `translate(0, ${plot_height})`)
|
|
.style('text-anchor', 'middle')
|
|
.text('Line speedup');
|
|
xtitle_sel.exit().remove();
|
|
|
|
/****** Add or update y-axes ******/
|
|
let yaxis_sel = plot_area_sel.selectAll('g.yaxis').data([0]);
|
|
yaxis_sel.enter().append('g').attr('class', 'yaxis');
|
|
yaxis_sel.call(yaxis);
|
|
yaxis_sel.exit().remove();
|
|
|
|
/****** Add or update y-axis title ******/
|
|
let ytitle_sel = plot_area_sel.selectAll('text.ytitle').data([0]);
|
|
ytitle_sel.enter().append('text').attr('class', 'ytitle');
|
|
ytitle_sel.attr('x', -yscale(0)) // x and y are flipped because of rotation
|
|
.attr('y', -50) // Approximate width of y-axis
|
|
.attr('transform', 'rotate(-90)')
|
|
.style('text-anchor', 'middle')
|
|
.style('alignment-baseline', 'central')
|
|
.text('Program Speedup');
|
|
ytitle_sel.exit().remove();
|
|
|
|
/****** Add or update x-zero line ******/
|
|
let xzero_sel = plot_area_sel.selectAll('line.xzero').data([0]);
|
|
xzero_sel.enter().append('line').attr('class', 'xzero');
|
|
xzero_sel.attr('x1', xscale(0))
|
|
.attr('y1', 0)
|
|
.attr('x2', xscale(0))
|
|
.attr('y2', plot_height + tick_size);
|
|
xzero_sel.exit().remove();
|
|
|
|
/****** Add or update y-zero line ******/
|
|
let yzero_sel = plot_area_sel.selectAll('line.yzero').data([0]);
|
|
yzero_sel.enter().append('line').attr('class', 'yzero');
|
|
yzero_sel.attr('x1', -tick_size)
|
|
.attr('y1', yscale(0))
|
|
.attr('x2', plot_width)
|
|
.attr('y2', yscale(0));
|
|
yzero_sel.exit().remove();
|
|
|
|
/****** Add or update series ******/
|
|
let progress_points = this.getProgressPoints();
|
|
let series_sel = plot_area_sel.selectAll('g.series')
|
|
.data(function(d) { return d.progress_points; }, function(d) { return d.name; });
|
|
series_sel.enter().append('g');
|
|
series_sel.attr('class', function(d, k) {
|
|
// Use progress point's position in array to assign it a stable color, no matter
|
|
// which points are enabled for display.
|
|
return `series series${(progress_points.indexOf(d.name)) % 5}`; })
|
|
.attr('style', 'clip-path: url(#clip);');
|
|
series_sel.exit().remove();
|
|
|
|
/****** Add or update trendlines ******/
|
|
// Configure a loess smoother
|
|
let loess = science.stats.loess()
|
|
.bandwidth(0.4)
|
|
.robustnessIterations(5);
|
|
|
|
// Create an svg line to draw the loess curve
|
|
let line = d3.svg.line().x(function(d) { return xscale(d[0]); })
|
|
.y(function(d) { return yscale(d[1]); })
|
|
.interpolate('basis');
|
|
|
|
// Apply the loess smoothing to each series, then draw the lines
|
|
let lines_sel = series_sel.selectAll('path').data(function(d) {
|
|
let xvals = d.measurements.map(function(e) { return e.speedup; });
|
|
let yvals = d.measurements.map(function(e) { return e.progress_speedup; });
|
|
let smoothed_y: number[] = [];
|
|
try {
|
|
smoothed_y = loess(xvals, yvals);
|
|
} catch (e) {
|
|
// Bandwidth too small error. Ignore and proceed with empty smoothed line.
|
|
}
|
|
// Speedup is always zero for a line speedup of zero
|
|
smoothed_y[0] = 0;
|
|
if (xvals.length > 5) return [d3.zip(xvals, smoothed_y)];
|
|
else return [d3.zip(xvals, yvals)];
|
|
});
|
|
lines_sel.enter().append('path');
|
|
lines_sel.attr('d', line);
|
|
lines_sel.exit().remove();
|
|
|
|
/****** Add or update points ******/
|
|
let points_sel = series_sel.selectAll('circle').data(function(d) { return d.measurements; });
|
|
points_sel.enter().append('circle').attr('r', radius);
|
|
points_sel.attr('cx', function(d) { return xscale(d.speedup); })
|
|
.attr('cy', function(d) { return yscale(d.progress_speedup); })
|
|
.on('mouseover', function(d, i) {
|
|
d3.select(this).classed('highlight', true);
|
|
tip.show(d, i);
|
|
})
|
|
.on('mouseout', function(d, i) {
|
|
d3.select(this).classed('highlight', false);
|
|
tip.hide(d, i);
|
|
});
|
|
points_sel.exit().remove();
|
|
|
|
// Return the number of plots
|
|
return speedup_data.length;
|
|
}
|
|
}
|