| '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); |