UNPKG

@webcomponents/custom-elements

Version:
445 lines (402 loc) 15.7 kB
// Magnitude verifies time complexity of a function. // Magnitude.run can be called multiple times in a single script if desired, // optionally with different parameters each time. // // Usage: // <script src="../resources/magnitude-perf.js"></script> // <script> // function setup(magnitude) // { // ... // } // // function test(magnitude) // { // ... // } // ... // Magnitude.description('Test that foo is linear in number of nodes.'); // Magnitude.run(setup, test, expected); // </script> // // setup: function to run once before the test-loop. Takes a magnitude // argument that is the value of 'n'. // test: function whose runtime is being tested. Also takes a magnitude // argument. // expected: one of the run-time constants listed below, e.g.: // Magnitude.CONSTANT, Magnitude.LINEAR, etc. if (window.testRunner) testRunner.dumpAsText(); // Namespace var Magnitude = {}; // Description of test; prepended to the dumped markup Magnitude.description = function(description) { Magnitude._log(description); }; // Parameters (customize per test) // See "Time Complexity Tests" document for details: // http://dev.chromium.org/developers/testing/time-complexity-tests // Specify over which range the growth rate should hold: // 2^n ... 2^(n+k-1) where n = initialExponent, k = numPoints Magnitude.initialExponent = 0; Magnitude.numPoints = 8; // Number of trials to run // (Trial = compute times for all magnitudes, compute statistical test, // decide if for this dataset, the data fits the model) Magnitude.numTrials = 3; // Fraction of trials that must succeed for test overall to PASS // (Default 50% means 2 out of 3 must succeed) Magnitude.successThreshold = 0.50; // Run = for each magnitude, run test function until pass this time limit, // then stop and compute average time Magnitude.millisecondsPerRun = 25; // Strict tolerance, so noisy tests explicitly state noisiness // Ok to relax for noisy tests; // try tolerance = 0.10 or 0.20, and/or trim = 1 or 2 to relax slightly // tolerance is "max (trimmed) deviation from expected", // where "expected" is: // * expected slope (linear, polynomial), // * median value (constant), // * predicted value in linear regression (logarithmic). // For log this often needs to be very large (due to scaling), and 1.00 is ok. Magnitude.trim = 0; // trim = 1 useful if unusual value on initial run Magnitude.tolerance = 0.05; Magnitude.CONSTANT = 'O(1)'; Magnitude.LINEAR = 'O(n)'; Magnitude.POLYNOMIAL = '>=O(n^2)'; // Utility functions Magnitude._max = function(array) { return Math.max.apply(null, array); }; Magnitude._min = function(array) { return Math.min.apply(null, array); }; Magnitude._sortTrimArray = function(array, trim) { // sort array, then trim both ends // used for computing trimmed maximum absolute deviation var sorted_array = array.map(Math.abs).sort(function(a, b) { return a - b; }); if (!trim) return sorted_array; else return sorted_array.slice(trim, -trim); }; Magnitude._relativeDeviations = function(array, ref) { return array.map(function(x) { return (x - ref) / ref; }); }; Magnitude._absSortDownTrimTop = function(array, trim) { // only trim the top of array // used for computing trimmed maximum absolute deviation if (trim === undefined) trim = 0; return array.map(Math.abs).sort( function(a, b) { return b - a; }).slice(trim); }; Magnitude._median = function(array) { array.sort(function(a, b) { return a - b; }); var len = array.length; if (!len) return; var half = Math.floor(len / 2); if (len % 2) return array[half]; return (array[half - 1] + array[half]) / 2; }; // Logging functions Magnitude._log = function(msg) { if (!Magnitude._container) Magnitude._container = document.createElement('pre'); Magnitude._container.appendChild(document.createTextNode(msg + '\n')); }; Magnitude._debug = function(msg) { Magnitude._debugLog += msg + '\n'; }; Magnitude._logRunTimes = function() { // Print out the magnitudes and times in CSV // to afford easy copy-paste to a charting application. Magnitude._debug('magnitudes: ' + Magnitude._magnitudes.join(',')); Magnitude._debug('times: ' + Magnitude._times.join(',')); }; Magnitude._for = function(n, body, callback) { var i = 0; var results = []; function iteration(result) { results.push(result); if (++i === n) callback(results); else body(i, iteration); } if (Magnitude._async) { body(i, iteration); } else { for (; i < n; ++i) { body(i, function(result) { results.push(result); }); } callback(results); } }; Magnitude._while = function(condition, body, callback) { function iteration() { if (condition()) body(iteration); else callback(); } if (Magnitude._async) { iteration(); } else { while (condition()) { body(function() {}); } callback(); } }; // Main Magnitude.run = function(setup, test, expected) { function runTest(magnitude, callback) { test(magnitude); callback(); } Magnitude._run(setup, runTest, expected, false); }; Magnitude.runAsync = function(setup, test, expected) { if (window.testRunner) testRunner.waitUntilDone(); window.addEventListener('load', function() { Magnitude._run(setup, test, expected, true); }, false); }; Magnitude._run = function(setup, test, expected, async) { Magnitude._async = async; Magnitude._debugLog = '\nDEBUG LOG:\n'; Magnitude._debug('Expected complexity: ' + expected); Magnitude._exponents = []; Magnitude._magnitudes = []; var maxExponent = Magnitude.initialExponent + Magnitude.numPoints; for (var i = Magnitude.initialExponent; i < maxExponent; i++) { Magnitude._exponents.push(i); Magnitude._magnitudes.push(Math.pow(2, i)); } Magnitude._numSuccesses = 0; function runTrial(trialNumber, nextTrial) { Magnitude._trialNumber = trialNumber; Magnitude._debug('\nTrial #' + trialNumber); Magnitude._for(Magnitude.numPoints, Magnitude._runTime.bind(null, setup, test), completeTrial.bind(null, nextTrial)); } function completeTrial(nextTrial, times) { Magnitude._times = times; Magnitude._logRunTimes(); switch (expected) { case Magnitude.CONSTANT: passed = Magnitude._isConstant(); break; case Magnitude.LINEAR: passed = Magnitude._isLinear(); break; case Magnitude.POLYNOMIAL: passed = Magnitude._isPolynomial(); break; default: Magnitude._log('FAIL: unrecognized order of growth: ' + expected); passed = false; break; } Magnitude._debug('Trial #' + Magnitude._trialNumber + ': ' + (passed ? 'SUCCESS' : 'FAILURE')); if (passed) Magnitude._numSuccesses++; nextTrial(); } Magnitude._for(Magnitude.numTrials, runTrial, Magnitude._finish); }; Magnitude._finish = function() { var neededToPass = Magnitude.numTrials * Magnitude.successThreshold; Magnitude._debug('Successes: ' + Magnitude._numSuccesses + ', need ' + neededToPass + ' (' + (100 * Magnitude.successThreshold) + '%) ' + 'to pass'); var passedOverall = (Magnitude._numSuccesses >= neededToPass); Magnitude._log(passedOverall ? 'PASS' : 'FAIL'); // By default don't log detailed information to layout test results, // in order to keep expected results consistent from run to run. if (!window.testRunner || !passedOverall) Magnitude._log(Magnitude._debugLog); if (Magnitude._async && window.testRunner) testRunner.notifyDone(); }; Magnitude._runTime = function(setup, test, pointIndex, callback) { var magnitude = Magnitude._magnitudes[pointIndex]; setup(magnitude); var debugStr = 'run for magnitude ' + magnitude; if (window.GCController) { if (GCController.getJSObjectCount) debugStr += ' jsObjectCountBefore ' + GCController.getJSObjectCount(); GCController.collectAll(); if (GCController.getJSObjectCount) debugStr += ' jsObjectCountAfter ' + GCController.getJSObjectCount(); } Magnitude._debug(debugStr); var nowFunction = window.performance && window.performance.now ? window.performance.now.bind(window.performance) : Date.now; // Possibly run multiple times, to deal with delays at end var runOk = false; var attempt = 0; var maxAttempts = 5; var millisecondsPerIteration; var totalTimeMilliseconds; var iterations; function iteration(nextIteration) { var start = nowFunction(); iterations = 0; totalTimeMilliseconds = 0; function completeRun(nextRun) { iterations++; totalTimeMilliseconds = nowFunction() - start; nextRun(); } Magnitude._while(function() { return totalTimeMilliseconds < Magnitude.millisecondsPerRun; }, function(nextRun) { test(magnitude, completeRun.bind(null, nextRun)); }, completeIteration.bind(null, nextIteration)); } function completeIteration(nextIteration) { millisecondsPerIteration = totalTimeMilliseconds / iterations; Magnitude._debug(iterations + ' iterations in ' + totalTimeMilliseconds + ' milliseconds, ' + 'average ' + millisecondsPerIteration + ' milliseconds per iteration'); var overTime = totalTimeMilliseconds - Magnitude.millisecondsPerRun; var relativeOverTimeTolerance = 0.001; var overTimeTolerance = Magnitude.millisecondsPerRun * relativeOverTimeTolerance; // If overTime is more than the duration of a run, // there was some delay, which introduces noise. // Allow a small amount of tolerance. if (overTime < (millisecondsPerIteration + overTimeTolerance)) runOk = true; else { Magnitude._debug('over-time: ' + overTime + ' exceeds average run-time ' + millisecondsPerIteration + ' by more than tolerance of ' + overTimeTolerance + ' assuming delay'); attempt++; if (attempt < maxAttempts) Magnitude._debug('Retrying to reduce delay noise'); else { Magnitude._debug('Failed to reduce delay noise after ' + maxAttempts + ' attempts, proceeding with noisy data'); runOk = true; } } nextIteration(); } Magnitude._while(function() { return !runOk; }, iteration, function() { callback(millisecondsPerIteration); }); }; // Auxiliary computations Magnitude._log2Ratios = function() { // Compute base-2 logarithm of ratios of times, // equivalently the difference of the base-2 logarithms: // log_2(t[i+1]/t[i]) = log_2(t[i+1]) - log_2(t[i]) // Since times are exponentially spaced (base 2), // this is the slope of the step on the log-log scale, // and hence for O(n^k) growth should be k (the exponent). var ratioArray = []; var log2RatioArray = []; for (var i = 0; i < Magnitude.numPoints - 1; i++) { var ratio = Magnitude._times[i + 1] / Magnitude._times[i]; var log2Ratio = Math.log(ratio) / Math.log(2); ratioArray.push(ratio); log2RatioArray.push(log2Ratio); } Magnitude._debug('ratios: ' + ratioArray.join(',')); Magnitude._debug('log-ratios (base 2): ' + log2RatioArray.join(',')); return log2RatioArray; }; // Statistical tests Magnitude._isConstant = function() { // Time should be approximately constant. // To deal with noise, instead of constant, check that // (trimmed) abs relative deviation from median is within tolerance of 0. var times = Magnitude._times; var median = Magnitude._median(times); var relativeDeviations = Magnitude._relativeDeviations(times, median); Magnitude._debug('Median time: ' + median); Magnitude._debug('Relative deviations: ' + relativeDeviations.join(',')); var trimmedAbsRelativeDeviations = Magnitude._absSortDownTrimTop(relativeDeviations, Magnitude.trim); Magnitude._debug('Trimmed sorted absolute relative deviations: ' + trimmedAbsRelativeDeviations.join(',')); var maxAbsDeviation = trimmedAbsRelativeDeviations[0]; Magnitude._debug('Maximum Absolute Relative Deviation: ' + maxAbsDeviation); return maxAbsDeviation <= Magnitude.tolerance; }; Magnitude._isLinear = function() { // Exponent of a monomial is the slope on the log-log scale. // Magnitudes are exponentially stepped, so log ratio is slope // of secant on log-log scale. // In linear growth, (trimmed) log2Ratio should equal 1, to within tolerance // (Special case of polynomial; see below for why this is separate.) // // Can do better by removing outlying points (and then computing the // slope between the neighboring points) instead of trimming ratios, // so we retain the data of the slope of that double step, but since we are // only looking at the Maximum (Absolute) Deviation, in practice these // generally amount to the same: given an outlier, the slope coming into // the point and the slope going out will generally be high/low or low/high, // and thus both be trimmed, while the slope of the double step will // generally not be extreme, so not informative. var logRatios = Magnitude._log2Ratios(); var trimmedLogRatios = Magnitude._sortTrimArray(logRatios, Magnitude.trim); var minLogRatio = Magnitude._min(trimmedLogRatios); var maxLogRatio = Magnitude._max(trimmedLogRatios); var maxAbsDeviation = Math.max(Math.abs(1 - minLogRatio), Math.abs(maxLogRatio - 1)); Magnitude._debug('Maximum Absolute Deviation: ' + maxAbsDeviation); Magnitude._debug('Tolerance: ' + Magnitude.tolerance); return maxAbsDeviation <= Magnitude.tolerance; }; Magnitude._isPolynomial = function() { // Exponent of a monomial is the slope on the log-log scale. // Magnitudes are exponentially stepped, so log ratio is slope // of secant on log-log scale. // Polynomial growth is expected to be O(n^2) or greater, // so logRatio should be at least 2: check (trimmed) min with tolerance // // Linear is fundamentally a special case of polynomial, // but here not specifying a specific exponent, and instead testing // that it grows *at least* quadratically (>= 2, rather than = 2 or = 3). // Thus we have separate functions. var logRatios = Magnitude._log2Ratios(); var trimmedLogRatios = Magnitude._sortTrimArray(logRatios, Magnitude.trim); var minLogRatio = Magnitude._min(trimmedLogRatios); var absDeviationMin = Math.abs(2 - minLogRatio); Magnitude._debug('Absolute Deviation of Minimum: ' + absDeviationMin); Magnitude._debug('Tolerance: ' + Magnitude.tolerance); return absDeviationMin <= Magnitude.tolerance; }; // Register listener window.addEventListener('load', function() { // FIXME: Add Magnitude.waitUntilDone/notifyDone for tests that need to // operate after the load event has fired. if (window.testRunner) document.body.innerHTML = ''; document.body.appendChild(Magnitude._container); }, false);