coz/viewer/ts/profile.ts
Charlie Curtsinger 917bc6de69 Organizing viewer
2019-08-18 10:52:41 -05:00

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;
}
}