From b734a80cb12dd0462f310eb87a4cc005eeb86d29 Mon Sep 17 00:00:00 2001 From: Ben Kalman Date: Thu, 1 Sep 2016 13:36:53 -0700 Subject: [PATCH] Improvements to the perf viewer (#2499) * Increase number of results to 20. * See newer results at the end of the graph, not start. * Make point radius show standard deviation to scale. * Try not to draw datasets over each other. * Invert light colours to make them darker. --- js/perf/viewer/src/main.js | 52 +++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/js/perf/viewer/src/main.js b/js/perf/viewer/src/main.js index 1006b5b365..d85d4b11f8 100644 --- a/js/perf/viewer/src/main.js +++ b/js/perf/viewer/src/main.js @@ -28,10 +28,11 @@ window.onpopstate = load; window.onresize = render; // The maximum number of git revisions to show in the perf history. +// // The larger this number, the more screen real estate needed to render the graph - and the slower // it will take to render, since the entire parent commit chain must be walked to form the graph. // TODO: Implement paging mechanism. -const MAX_PERF_HISTORY = 15; +const MAX_PERF_HISTORY = 20; let chartDatasets: Map; let chartLabels: string[]; @@ -97,8 +98,8 @@ async function getPerfHistory(ds: Dataset): Promise<[Struct[], string[]]> { for (let head = await ds.head(), i = 0; head && i < MAX_PERF_HISTORY; i++) { const val = head.value; invariant(val instanceof Struct); - perfData.push(val); - gitRevs.push(val.nomsRevision); + perfData.unshift(val); + gitRevs.unshift(val.nomsRevision); const parentRef = await head.parents.first(); // TODO: how to deal with multiple parents? head = parentRef ? await parentRef.targetValue(ds.database) : null; @@ -126,19 +127,38 @@ async function render() { return; } + // We use the point radius to indicate the standard deviation, for lack of any better option. + // Unfortunately chart.js doesn't provide any way to scale this relative to large the Y axis + // values are with respect to the graph pixel height. + // + // So, try to approximate it by taking into account: (a) the expected magnitude of the Y axis (the + // maximum value), and (b) and how much space the graph will take up on the screen (half of screen + // *width* - this does appear to be what chart.js does). + const maxElapsedTime = Array.from(chartDatasets.values()).reduce((max, dataPoints) => { + const medians = dataPoints.map(dp => dp !== null ? dp.median : 0); + return Math.max(max, ...medians); + }, 0); + const graphHeight = document.body.scrollWidth / 2; + const getStddevPointRadius = stddev => Math.ceil(stddev / maxElapsedTime * graphHeight); + const datasets = []; for (const [testName, dataPoints] of chartDatasets) { - const [borderColor, backgroundColor] = await getSolidAndAlphaColors(testName); + const [borderColor, backgroundColor] = getSolidAndAlphaColors(testName); datasets.push({ backgroundColor, borderColor, borderWidth: 1, - pointRadius: dataPoints.map(dp => dp !== null ? 1 + dp.stddev : 0), + pointRadius: dataPoints.map(dp => dp !== null ? getStddevPointRadius(dp.stddev) : 0), data: dataPoints.map(dp => dp !== null ? dp.median : null), label: testName, + _maxMedian: Math.max(...dataPoints.map(dp => dp !== null ? dp.median : 0)), // for our sorting }); } + // Draw the datasets in order of largest to smallest, so that we (try not to) draw over the top of + // entire datasets. + datasets.sort((a, b) => a._maxMedian - b._maxMedian); + new Chart(document.getElementById('chart'), { type: 'line', data: { @@ -150,7 +170,7 @@ async function render() { yAxes: [{ scaleLabel: { display: true, - labelString: 'elapsed (seconds)', + labelString: 'elapsed seconds (point radius is standard deviation to scale)', }, ticks: { beginAtZero: true, @@ -178,19 +198,22 @@ function makeDataPoint(nums: number[]): DataPoint { median /= 2; } - const calcMean = ns => ns.reduce((t, n) => t + n, 0) / ns.length; - const mean = calcMean(nums); - const stddev = Math.sqrt(calcMean(nums.map(n => Math.pow(n - mean, 2)))); + const mean = getMean(nums); + const stddev = Math.sqrt(getMean(nums.map(n => Math.pow(n - mean, 2)))); return {median, stddev}; } // Generates a light and dark version of some color randomly (but stable) derived from `str`. -async function getSolidAndAlphaColors(str: string): Promise<[string, string]> { +function getSolidAndAlphaColors(str: string): [string, string] { // getHashOfValue() returns a Uint8Array, so pull out the first 3 8-bit numbers - which will be in // the range [0, 255] - to generate a full RGB colour. - const [r, g, b] = getHashOfValue(str).digest; - return [`rgb(${r}, ${g}, ${b})`, `rgba(${r}, ${g}, ${b}, 0.25)`]; + let [r, g, b] = getHashOfValue(str).digest; + // Invert if it's too light. + if (getMean([r, g, b]) > 127) { + [r, g, b] = [r, g, b].map(c => 255 - c); + } + return [`rgb(${r}, ${g}, ${b})`, `rgba(${r}, ${g}, ${b}, 0.2)`]; } // Returns the keys of `map`. @@ -200,3 +223,8 @@ function keys(map: NomsMap): Promise { keys.push(key); }).then(() => keys); } + +// Returns the mean of `nums`. +function getMean(nums: number[]): number { + return nums.reduce((t, n) => t + n, 0) / nums.length; +}