blob: 49d741f8dc1f953ff0ff810b82a8636707983b98 [file] [log] [blame]
'use strict';
/* Add this to the skia namespace */
var skia = skia || {};
(function (ns) {
// c contains all constants. Primarily relating to backend resources.
// They need to match the definitions in go/skiacorrectness/main.go
ns.c = {
// PREFIX_URL is the prefix to all backend request to JSON resources.
PREFIX_URL: '/rest',
// URLs exposed by the backend to retrieve JSON data.
URL_COUNTS: '/counts',
URL_TRIAGE: '/triage',
URL_STATUS: '/status',
URL_LOGIN_STATUS: '/loginstatus',
URL_LOGOUT: '/logout',
// The triage labels need to match the values in golden/go/types/types.go
UNTRIAGED: 0,
POSITIVE: 1,
NEGATIVE: 2,
// Key in parameters that identifies a test.
PRIMARY_KEY_FIELD: 'name',
// Param fields to filter.
PARAMS_FILTER: {
'name': true,
'source_type': true,
}
};
// List of states - used to cycle through via nextState.
ns.c.ALL_STATES = [ns.c.UNTRIAGED, ns.c.POSITIVE, ns.c.NEGATIVE];
/**
* Plot is a class that wraps the flot object and exposes draw functions.
*
* @param {jQueryElement} element This a jquery element to which the
flot instance to attach to.
*
* @return {Plot} instance of Plot class.
**/
ns.Plot = function (element) {
this.element = element;
// initialize the flot element with empty data.
this.flotObj = element.plot([], {
legend: {
show: true
},
xaxis: {
show: true
}
}).data('plot');
};
/**
* setData sets the data that the plot needs to draw and forces a redraw.
* If ticks is not null it will also set the ticks and reset the x-axis.
*
* @param {array} data Array of series understood by Flot. See
* https://github.com/flot/flot/blob/master/API.md#data-format
*
* @param {array} ticks Array or function that defines the ticks for the
* x-axis.
*/
ns.Plot.prototype.setData = function(data, ticks) {
this.flotObj.setData(data);
// Set the ticks on the x axis if necessary.
if (ticks) {
var opt = this.flotObj.getOptions();
opt.xaxes.forEach(function(axis) {
axis.ticks = ticks;
});
}
this.redraw();
};
/**
* redraw forces a resize and redraw of the canvas.
*/
ns.Plot.prototype.redraw = function () {
// redraw the graph
this.flotObj.resize();
this.flotObj.setupGrid();
this.flotObj.draw();
}
/**
* PlotData is a class that used as the return value of processAllCounts and
* contains the processed data.
* */
ns.PlotData = function (data, ticks, allAggregates, testDetails) {
this.plotData = data;
this.ticks = ticks;
this.testDetails = testDetails;
this.allAggregates = allAggregates;
};
/**
* getTicks returns the ticks for the PlotData object at hand.
*/
ns.PlotData.prototype.getTicks = function (axis) {
return this.ticks;
};
/**
* TestDetails is a class that contails the aggregated information about
* a single tests. It is derived from the data returned by the server.
*/
ns.TestDetails = function (name, counts) {
this.name = name;
this.counts = counts;
this.aggregates = aggregateCounts(counts);
};
/**
* DiffDigestInfo is a helper class to store information about a
* digest (usually positive) and how it differs from the a given
* untriaged digest.
*/
ns.DiffDigestInfo = function (digest, imgUrl, count, paramCounts, diff) {
this.digest = digest;
this.imgUrl = imgUrl;
this.count = count;
this.paramCounts = paramCounts;
this.diff = diff;
};
/**
* isIdentical returns true if the current digest is identical
* to the untriaged digest.
*/
ns.DiffDigestInfo.prototype.isIdentical = function () {
return this.diff.numDiffPixels === 0;
}
/**
* addIndexAsX adds takes an array of numbers and returns an array of
* datapoints (x,y) where x is the index of the input element y.
*/
function addIndexAsX(arr) {
var result = [];
for(var i=0, len=arr.length; i<len; i++) {
result.push([i, arr[i]])
}
return result;
}
/**
* aggregateCounts sums over the counts contained in an object.
* Each member in the object is assumed to be an array of numbers.
*
* @param { object } countsObj contains attributes where each attribute
* is an array of numbers.
* @return {object} an array with the same attributes as the input object.
* Each attribute contains the sum of the corresponding
* array.
*/
function aggregateCounts(countsObj) {
var result = {};
var arr;
for(var k in countsObj) {
if (countsObj.hasOwnProperty(k)) {
result[k] = 0;
arr = countsObj[k];
for(var i=0,len=arr.length; i < len; i++) {
result[k] += arr[i];
}
}
}
return result;
}
/**
* processAllCounts converts the data returned by the server to
* an instance of PlotData that can then be used to render the UI
* and also serve as input to the Plot class.
*
* @param {object} serverData returned from the server containing the
* aggregated values over all tests.
*
* @param {string} testName specifies whether we want to get the data
* for a specific test. If null all data are returned.
*
* @return {object} instance of PlotData.
*
*/
ns.processCounts = function (serverData, testName) {
// get the counts from the tests.
var testCounts = [];
if (testName && serverData.counts.hasOwnProperty(testName)) {
testCounts.push(new ns.TestDetails(testName, serverData.counts[testName]))
}
else {
for(var tName in serverData.counts) {
if (serverData.counts.hasOwnProperty(tName)) {
testCounts.push(new ns.TestDetails(tName, serverData.counts[tName]));
}
}
}
// assemble the plot data.
var targetData = testName ? serverData.counts[testName] : serverData.aggregated;
var data = [];
for(var k in targetData) {
if (targetData.hasOwnProperty(k)) {
data.push({
label: k,
lines: {
show: true,
steps: true
},
data: addIndexAsX(targetData[k])
});
}
}
return new ns.PlotData(data,
serverData.ticks,
aggregateCounts(serverData.aggregated),
testCounts);
};
/**
getAutoCommitRanges returns a list of decreasing commit ranges
that can be used to render a simple selection on the screen.
TODO: Make this adaptive on the backend so that the minimum is not
5 but whatever number of commits cover all traces aka constitute
out current knowledge of HEAD.
*/
var COMMIT_INTERVALS = [100, 50, 20, 15, 10, 5];
ns.getAutoCommitRanges = function(serverData) {
var commits = serverData.commits;
var result = [{ start: commits[0].hash, name: commits.length }];
for(var i=0, len=COMMIT_INTERVALS.length; i < len; i++) {
if (commits.length > COMMIT_INTERVALS[i]) {
result.push({ start: commits[COMMIT_INTERVALS[i]-1], name: COMMIT_INTERVALS[i] });
}
}
return result;
};
/**
* extractTriageData is the central function to pre-process data coming
* from the server.
*/
ns.extractTriageData = function (serverData, testName) {
var untStats = new Stats();
var posStats = new Stats();
var negStats = new Stats();
var positive = ns.getSortedDigests(serverData, testName, 'positive', posStats);
var negative = ns.getSortedDigests(serverData, testName, 'negative', negStats);
var untriaged = ns.getUntriagedSorted(serverData, testName, untStats);
// Set the triage state for each digest.
var add = function(arr, state) {
for (var i=0, len=arr.length; i<len; i++) {
triageState[arr[i].digest] = state;
}
};
var triageState = {};
add(positive, ns.c.POSITIVE);
add(negative, ns.c.NEGATIVE);
add(untriaged, ns.c.UNTRIAGED);
return {
untriaged: untriaged,
positive: positive,
negative: negative,
untStats: untStats,
posStats: posStats,
negStats: negStats,
allParams: ns.getSortedParams(serverData, true),
triageState: triageState
};
};
/**
* Stats is a helper class to hold counts about a set of digests.
*/
function Stats(total, unique) {
this.total = total || 0;
this.unique = unique || 0;
}
Stats.prototype.set = function (total, unique) {
this.total = total;
this.unique = unique;
};
/**
* getUntriagedSorted returns the untriaged digests sorted by largest
* deviation from a positively labeled digest. It processes the data
* directly returned by the backend.
* It also resolves the references to the positive digests contained in
* the diff metrics.
*/
ns.getUntriagedSorted = function(serverData, testName, stats) {
var unt = robust_get(serverData, ['tests', testName, 'untriaged']);
if (!unt) {
return [];
}
var posd, d;
var result = [];
var positive = serverData.tests[testName].positive;
var hasPos = false;
var total = 0;
for (var digest in unt) {
if (unt.hasOwnProperty(digest)) {
total += unt[digest].count;
var posDiffs = [];
for(var i=0, len=unt[digest].diffs.length; i < len; i++) {
// TODO (stephana): Fill in expanding the diff information.
// This will be done once triaging works. So we can test it
// with real data.
hasPos = true;
d = unt[digest].diffs[i];
posd = positive[d.posDigest];
posDiffs.push(new ns.DiffDigestInfo(d.posDigest, posd.imgUrl,
posd.count, posd.paramCounts, d));
}
// Inject the digest and the augmented positive diffs.
unt[digest].digest = digest;
unt[digest].positiveDiffs = posDiffs;
unt[digest].paramCounts = ns.filterObject(unt[digest].paramCounts, ns.c.PARAMS_FILTER);
result.push(unt[digest]);
}
}
stats.set(total, result.length);
// Sort the result increasing by pixel difference or
// decreasing by counts if there are no positives.
var sortFn;
if (hasPos) {
sortFn = function (a,b) {
return a.positiveDiffs[0].diff.numDiffPixels - b.positiveDiffs[0].diff.numDiffPixels;
};
} else {
sortFn = function (a,b) { return b.count - a.count; };
}
result.sort(sortFn);
return result;
};
/**
* getSortedPositivesFromUntriaged returns the list of positively labeled
* digests. It assumes that 'untriagedRec' was generated by a call to
* getUntriagedSorted(...).
*/
ns.getSortedPositivesFromUntriaged = function (untriagedRec) {
if (untriagedRec && untriagedRec.positiveDiffs && untriagedRec.positiveDiffs.length > 0) {
return untriagedRec.positiveDiffs;
}
return [];
};
/**
* getSortedPositives returns a list of positive digests from the
* data returnded by the backend. This is to be used when there are no
* untriaged digests.
*/
ns.getSortedDigests= function (serverData, testName, digestClass, stats) {
var targetDigests = robust_get(serverData, ['tests', testName, digestClass]);
if (!targetDigests) {
return [];
}
var result = [];
var total = 0;
for (var digest in targetDigests) {
if (targetDigests.hasOwnProperty(digest)) {
total += targetDigests[digest].count;
// Inject the digest into the object.
targetDigests[digest].digest = digest;
targetDigests[digest].paramCounts = ns.filterObject(
targetDigests[digest].paramCounts, ns.c.PARAMS_FILTER);
result.push(targetDigests[digest]);
}
}
stats.set(total, result.length);
// Sort the result in decreasing order of their occurences.
result.sort(function (a,b) {
a.count - b.count;
});
return result;
};
/**
* getSortedParams returns all parameters and the union of their values as a
* (nested) sorted Array in the format:
* [[param1, [val1, val2, ...],
[param2, [val3, val4, ...], ... ]]]
*/
ns.getSortedParams = function (serverData, filter) {
var result = [];
for(var k in serverData.allParams) {
if (serverData.allParams.hasOwnProperty(k) && (!filter || !ns.c.PARAMS_FILTER[k])) {
serverData.allParams[k].sort();
result.push([k, serverData.allParams[k]]);
}
}
result.sort(function(a,b){
return (a[0] < b[0]) ? -1 : (a[0] > b[0]) ? 1 : 0;
});
return result;
};
// sortedKeys returns the keys of the object in sorted order.
function sortedKeys(obj) {
var result = [];
for(var k in obj) {
if (obj.hasOwnProperty(k)) {
result.push(k);
}
}
result.sort();
return result;
}
/**
* getCombinedParamsTable takes a variable number of paramCounts and
* combines them into a multi dimensional array to be displayed as a
* table. The output format is:
* [
* { p: 'paramName1', c: [['val1','val2'],['val3', 'val4']] },
* { p: 'paramName2', c: [['valx','valy'],['val3', 'val4']] },
* { p: 'paramName3', c: [['val1','val2'],['val3', 'val4']] },
* { p: 'paramName4', c: [['val1','val2'],['val3', 'val4']] }
* ]
* The array is sorted by values of 'p'.
*/
ns.getCombinedParamsTable = function ( _ ) {
var combined = {};
for(var i=0, len=arguments.length; (i<len) && arguments[i]; i++) {
var params = arguments[i];
for(var k in params) {
if (params.hasOwnProperty(k)) {
if (!combined[k]) {
combined[k] = [];
}
combined[k][i] = sortedKeys(params[k]);
}
}
}
var result = [];
for(var k in combined) {
if (combined.hasOwnProperty(k)) {
result.push({p: k, c: combined[k]});
}
}
result.sort(function(a,b) {
return (a.p < b.p) ? -1 : (a.p > b.p) ? 1 : 0;
});
return result;
};
/**
* nextState returns the next state in the order defined by ALL_STATES.
*/
ns.nextState = function(state) {
var idx = (ns.c.ALL_STATES.indexOf(state) + 1) % ns.c.ALL_STATES.length;
return ns.c.ALL_STATES[idx];
};
/**
* getNumArray returns an array of numbers of the given length and each
* element is initialized with initVal. If initVal is omitted, 0 (zero)
* is used instead.
*/
ns.getNumArray = function(len, initVal) {
if (!initVal) {
initVal = 0;
}
var result = [];
for(var i =0; i < len; i++) {
result.push(initVal);
}
return result;
};
/**
* getDelta returns an object that contains all the key/value pairs
* from changed that where the values are differnt in changed and original.
*/
ns.getDelta = function (changed, original) {
var delta = {};
var count = 0;
for (var k in changed) {
if (changed.hasOwnProperty(k) &&
original.hasOwnProperty(k) &&
(changed[k] !== original[k])) {
delta[k] = changed[k];
count++;
}
}
return {
delta: delta,
count: count
};
};
/**
* filterObject returns a copy of 'obj' without the members that
* are also members in 'exclude'.
*/
ns.filterObject = function(obj, exclude) {
var result = {};
for(var k in obj) {
if (obj.hasOwnProperty(k) && !exclude[k]) {
result[k] = obj[k];
}
}
return result;
}
/**
* TriageDigestReq is a container type for sending labeled digests to the
* backend. It matches the input parameters of the triageDigestsHandler in
* 'go/skiacorrectness/main.go'.
*/
ns.TriageDigestReq = function () {
};
/**
* addDigestLabel is a convenience method to add digests and their label to the
* the instance.
*/
ns.TriageDigestReq.prototype.add = function (testName, digest, label) {
this[testName] = this[testName] || {};
this[testName][digest] = label;
};
/////////////////////////////////////////////////////////////////
// Generic utility functions.
/////////////////////////////////////////////////////////////////
/*
* isEmpty returns true if the provided object is empty and false
* otherwise.
*/
ns.isEmpty = function (obj) {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
};
/*
*/
ns.extractQueryString = function (url) {
var idx = url.indexOf('?');
return (idx === -1) ? '' : url.substring(idx);
};
/////////////////////////////////////////////////////////////////
// Utility functions that are not exposed in the namespace.
/////////////////////////////////////////////////////////////////
/**
* robust_get finds a sub object within 'obj' by following the path
* in 'idx'. It will not throw an error if any sub object is missing
* but instead return 'undefined'.
**/
function robust_get(obj, idx) {
if (!idx) {
return;
}
for(var i=0, len=idx.length; i<len; i++) {
if ((typeof obj === 'undefined') || (!idx[i])) {
return; // returns 'undefined'
}
obj = obj[idx[i]];
}
return obj;
}
})(skia);