1 // Magnitude verifies time complexity of a function.
2 // Magnitude.run can be called multiple times in a single script if desired,
3 // optionally with different parameters each time.
6 // <script src="../resources/magnitude-perf.js"></script>
8 // function setup(magnitude)
13 // function test(magnitude)
18 // Magnitude.description('Test that foo is linear in number of nodes.');
19 // Magnitude.run(setup, test, expected);
22 // setup: function to run once before the test-loop. Takes a magnitude
23 // argument that is the value of 'n'.
24 // test: function whose runtime is being tested. Also takes a magnitude
26 // expected: one of the run-time constants listed below, e.g.:
27 // Magnitude.CONSTANT, Magnitude.LINEAR, etc.
29 if (window.testRunner)
30 testRunner.dumpAsText();
35 // Description of test; prepended to the dumped markup
36 Magnitude.description = function(description)
38 Magnitude._log(description);
41 // Parameters (customize per test)
42 // See "Time Complexity Tests" document for details:
43 // http://dev.chromium.org/developers/testing/time-complexity-tests
44 // Specify over which range the growth rate should hold:
45 // 2^n ... 2^(n+k-1) where n = initialExponent, k = numPoints
46 Magnitude.initialExponent = 0;
47 Magnitude.numPoints = 8;
48 // Number of trials to run
49 // (Trial = compute times for all magnitudes, compute statistical test,
50 // decide if for this dataset, the data fits the model)
51 Magnitude.numTrials = 3;
52 // Fraction of trials that must succeed for test overall to PASS
53 // (Default 50% means 2 out of 3 must succeed)
54 Magnitude.successThreshold = 0.50;
55 // Run = for each magnitude, run test function until pass this time limit,
56 // then stop and compute average time
57 Magnitude.millisecondsPerRun = 25;
58 // Strict tolerance, so noisy tests explicitly state noisiness
59 // Ok to relax for noisy tests;
60 // try tolerance = 0.10 or 0.20, and/or trim = 1 or 2 to relax slightly
61 // tolerance is "max (trimmed) deviation from expected",
62 // where "expected" is:
63 // * expected slope (linear, polynomial),
64 // * median value (constant),
65 // * predicted value in linear regression (logarithmic).
66 // For log this often needs to be very large (due to scaling), and 1.00 is ok.
67 Magnitude.trim = 0; // trim = 1 useful if unusual value on initial run
68 Magnitude.tolerance = 0.05;
70 Magnitude.CONSTANT = 'O(1)';
71 Magnitude.LINEAR = 'O(n)';
72 Magnitude.POLYNOMIAL = '>=O(n^2)';
75 Magnitude._max = function(array) {
76 return Math.max.apply(null, array);
79 Magnitude._min = function(array) {
80 return Math.min.apply(null, array);
83 Magnitude._sortTrimArray = function(array, trim) {
84 // sort array, then trim both ends
85 // used for computing trimmed maximum absolute deviation
86 var sorted_array = array.map(Math.abs).sort(function(a, b) { return a - b; });
90 return sorted_array.slice(trim, -trim);
93 Magnitude._relativeDeviations = function(array, ref)
95 return array.map(function(x) { return (x - ref) / ref; });
98 Magnitude._absSortDownTrimTop = function(array, trim) {
99 // only trim the top of array
100 // used for computing trimmed maximum absolute deviation
101 if (trim === undefined)
103 return array.map(Math.abs).sort(
104 function(a, b) { return b - a; }).slice(trim);
107 Magnitude._median = function(array) {
108 array.sort(function(a, b) { return a - b; });
109 var len = array.length;
112 var half = Math.floor(len / 2);
115 return (array[half - 1] + array[half]) / 2;
119 Magnitude._log = function(msg)
121 if (!Magnitude._container)
122 Magnitude._container = document.createElement('pre');
123 Magnitude._container.appendChild(document.createTextNode(msg + '\n'));
126 Magnitude._debug = function(msg)
128 Magnitude._debugLog += msg + '\n';
131 Magnitude._logRunTimes = function()
133 // Print out the magnitudes and times in CSV
134 // to afford easy copy-paste to a charting application.
135 Magnitude._debug('magnitudes: ' + Magnitude._magnitudes.join(','));
136 Magnitude._debug('times: ' + Magnitude._times.join(','));
139 Magnitude._for = function(n, body, callback)
144 function iteration(result) {
145 results.push(result);
152 if (Magnitude._async) {
156 body(i, function(result) {
157 results.push(result);
164 Magnitude._while = function(condition, body, callback)
166 function iteration() {
173 if (Magnitude._async) {
176 while (condition()) {
184 Magnitude.run = function(setup, test, expected)
186 function runTest(magnitude, callback) {
190 Magnitude._run(setup, runTest, expected, false);
193 Magnitude.runAsync = function(setup, test, expected)
195 if (window.testRunner)
196 testRunner.waitUntilDone();
197 window.addEventListener('load', function() {
198 Magnitude._run(setup, test, expected, true);
202 Magnitude._run = function(setup, test, expected, async)
204 Magnitude._async = async;
205 Magnitude._debugLog = '\nDEBUG LOG:\n';
206 Magnitude._debug('Expected complexity: ' + expected);
208 Magnitude._exponents = [];
209 Magnitude._magnitudes = [];
210 var maxExponent = Magnitude.initialExponent + Magnitude.numPoints;
211 for (var i = Magnitude.initialExponent; i < maxExponent; i++) {
212 Magnitude._exponents.push(i);
213 Magnitude._magnitudes.push(Math.pow(2, i));
216 Magnitude._numSuccesses = 0;
217 function runTrial(trialNumber, nextTrial)
219 Magnitude._trialNumber = trialNumber;
220 Magnitude._debug('\nTrial #' + trialNumber);
221 Magnitude._for(Magnitude.numPoints, Magnitude._runTime.bind(null, setup, test), completeTrial.bind(null, nextTrial));
223 function completeTrial(nextTrial, times)
225 Magnitude._times = times;
226 Magnitude._logRunTimes();
228 case Magnitude.CONSTANT:
229 passed = Magnitude._isConstant();
231 case Magnitude.LINEAR:
232 passed = Magnitude._isLinear();
234 case Magnitude.POLYNOMIAL:
235 passed = Magnitude._isPolynomial();
238 Magnitude._log('FAIL: unrecognized order of growth: ' + expected);
242 Magnitude._debug('Trial #' + Magnitude._trialNumber + ': ' +
243 (passed ? 'SUCCESS' : 'FAILURE'));
245 Magnitude._numSuccesses++;
248 Magnitude._for(Magnitude.numTrials, runTrial, Magnitude._finish);
251 Magnitude._finish = function()
253 var neededToPass = Magnitude.numTrials * Magnitude.successThreshold;
254 Magnitude._debug('Successes: ' + Magnitude._numSuccesses + ', need ' +
255 neededToPass + ' (' + (100 * Magnitude.successThreshold) + '%) ' +
257 var passedOverall = (Magnitude._numSuccesses >= neededToPass);
258 Magnitude._log(passedOverall ? 'PASS' : 'FAIL');
260 // By default don't log detailed information to layout test results,
261 // in order to keep expected results consistent from run to run.
262 if (!window.testRunner || !passedOverall)
263 Magnitude._log(Magnitude._debugLog);
265 if (Magnitude._async && window.testRunner)
266 testRunner.notifyDone();
269 Magnitude._runTime = function(setup, test, pointIndex, callback)
271 var magnitude = Magnitude._magnitudes[pointIndex];
274 var debugStr = 'run for magnitude ' + magnitude;
275 if (window.GCController) {
276 if (GCController.getJSObjectCount)
277 debugStr += ' jsObjectCountBefore ' + GCController.getJSObjectCount();
279 GCController.collectAll();
281 if (GCController.getJSObjectCount)
282 debugStr += ' jsObjectCountAfter ' + GCController.getJSObjectCount();
285 Magnitude._debug(debugStr);
287 var nowFunction = window.performance && window.performance.now ?
288 window.performance.now.bind(window.performance) : Date.now;
290 // Possibly run multiple times, to deal with delays at end
294 var millisecondsPerIteration;
295 var totalTimeMilliseconds;
298 function iteration(nextIteration)
300 var start = nowFunction();
302 totalTimeMilliseconds = 0;
303 function completeRun(nextRun)
306 totalTimeMilliseconds = nowFunction() - start;
309 Magnitude._while(function() { return totalTimeMilliseconds < Magnitude.millisecondsPerRun; }, function(nextRun) { test(magnitude, completeRun.bind(null, nextRun)); }, completeIteration.bind(null, nextIteration));
312 function completeIteration(nextIteration)
314 millisecondsPerIteration = totalTimeMilliseconds / iterations;
315 Magnitude._debug(iterations + ' iterations in ' +
316 totalTimeMilliseconds + ' milliseconds, ' +
317 'average ' + millisecondsPerIteration +
318 ' milliseconds per iteration');
320 var overTime = totalTimeMilliseconds - Magnitude.millisecondsPerRun;
321 var relativeOverTimeTolerance = 0.001;
322 var overTimeTolerance = Magnitude.millisecondsPerRun *
323 relativeOverTimeTolerance;
324 // If overTime is more than the duration of a run,
325 // there was some delay, which introduces noise.
326 // Allow a small amount of tolerance.
327 if (overTime < (millisecondsPerIteration + overTimeTolerance))
330 Magnitude._debug('over-time: ' + overTime +
331 ' exceeds average run-time ' + millisecondsPerIteration +
332 ' by more than tolerance of ' + overTimeTolerance +
335 if (attempt < maxAttempts)
336 Magnitude._debug('Retrying to reduce delay noise');
338 Magnitude._debug('Failed to reduce delay noise after ' +
339 maxAttempts + ' attempts, proceeding with noisy data');
345 Magnitude._while(function() { return !runOk; }, iteration, function() { callback(millisecondsPerIteration); });
348 // Auxiliary computations
349 Magnitude._log2Ratios = function()
351 // Compute base-2 logarithm of ratios of times,
352 // equivalently the difference of the base-2 logarithms:
353 // log_2(t[i+1]/t[i]) = log_2(t[i+1]) - log_2(t[i])
354 // Since times are exponentially spaced (base 2),
355 // this is the slope of the step on the log-log scale,
356 // and hence for O(n^k) growth should be k (the exponent).
358 var log2RatioArray = [];
359 for (var i = 0; i < Magnitude.numPoints - 1; i++) {
360 var ratio = Magnitude._times[i + 1] / Magnitude._times[i];
361 var log2Ratio = Math.log(ratio) / Math.log(2);
362 ratioArray.push(ratio);
363 log2RatioArray.push(log2Ratio);
365 Magnitude._debug('ratios: ' + ratioArray.join(','));
366 Magnitude._debug('log-ratios (base 2): ' + log2RatioArray.join(','));
367 return log2RatioArray;
371 Magnitude._isConstant = function()
373 // Time should be approximately constant.
374 // To deal with noise, instead of constant, check that
375 // (trimmed) abs relative deviation from median is within tolerance of 0.
376 var times = Magnitude._times;
377 var median = Magnitude._median(times);
378 var relativeDeviations = Magnitude._relativeDeviations(times, median);
379 Magnitude._debug('Median time: ' + median);
380 Magnitude._debug('Relative deviations: ' + relativeDeviations.join(','));
381 var trimmedAbsRelativeDeviations = Magnitude._absSortDownTrimTop(relativeDeviations, Magnitude.trim);
382 Magnitude._debug('Trimmed sorted absolute relative deviations: ' +
383 trimmedAbsRelativeDeviations.join(','));
384 var maxAbsDeviation = trimmedAbsRelativeDeviations[0];
385 Magnitude._debug('Maximum Absolute Relative Deviation: ' + maxAbsDeviation);
386 return maxAbsDeviation <= Magnitude.tolerance;
389 Magnitude._isLinear = function()
391 // Exponent of a monomial is the slope on the log-log scale.
392 // Magnitudes are exponentially stepped, so log ratio is slope
393 // of secant on log-log scale.
394 // In linear growth, (trimmed) log2Ratio should equal 1, to within tolerance
395 // (Special case of polynomial; see below for why this is separate.)
397 // Can do better by removing outlying points (and then computing the
398 // slope between the neighboring points) instead of trimming ratios,
399 // so we retain the data of the slope of that double step, but since we are
400 // only looking at the Maximum (Absolute) Deviation, in practice these
401 // generally amount to the same: given an outlier, the slope coming into
402 // the point and the slope going out will generally be high/low or low/high,
403 // and thus both be trimmed, while the slope of the double step will
404 // generally not be extreme, so not informative.
405 var logRatios = Magnitude._log2Ratios();
406 var trimmedLogRatios = Magnitude._sortTrimArray(logRatios, Magnitude.trim);
407 var minLogRatio = Magnitude._min(trimmedLogRatios);
408 var maxLogRatio = Magnitude._max(trimmedLogRatios);
409 var maxAbsDeviation = Math.max(Math.abs(1 - minLogRatio),
410 Math.abs(maxLogRatio - 1));
411 Magnitude._debug('Maximum Absolute Deviation: ' + maxAbsDeviation);
412 Magnitude._debug('Tolerance: ' + Magnitude.tolerance);
413 return maxAbsDeviation <= Magnitude.tolerance;
416 Magnitude._isPolynomial = function()
418 // Exponent of a monomial is the slope on the log-log scale.
419 // Magnitudes are exponentially stepped, so log ratio is slope
420 // of secant on log-log scale.
421 // Polynomial growth is expected to be O(n^2) or greater,
422 // so logRatio should be at least 2: check (trimmed) min with tolerance
424 // Linear is fundamentally a special case of polynomial,
425 // but here not specifying a specific exponent, and instead testing
426 // that it grows *at least* quadratically (>= 2, rather than = 2 or = 3).
427 // Thus we have separate functions.
428 var logRatios = Magnitude._log2Ratios();
429 var trimmedLogRatios = Magnitude._sortTrimArray(logRatios, Magnitude.trim);
430 var minLogRatio = Magnitude._min(trimmedLogRatios);
431 var absDeviationMin = Math.abs(2 - minLogRatio);
432 Magnitude._debug('Absolute Deviation of Minimum: ' + absDeviationMin);
433 Magnitude._debug('Tolerance: ' + Magnitude.tolerance);
434 return absDeviationMin <= Magnitude.tolerance;
438 window.addEventListener('load', function() {
439 // FIXME: Add Magnitude.waitUntilDone/notifyDone for tests that need to
440 // operate after the load event has fired.
441 if (window.testRunner)
442 document.body.innerHTML = '';
443 document.body.appendChild(Magnitude._container);