1<!DOCTYPE html> 2<!-- 3Copyright (c) 2014 The Chromium Authors. All rights reserved. 4Use of this source code is governed by a BSD-style license that can be 5found in the LICENSE file. 6--> 7<link rel="import" href="/tracing/base/range.html"> 8<script> 9'use strict'; 10 11tr.exportTo('tr.b', function() { 12 13 function identity(d) { 14 return d; 15 } 16 17 function Statistics() { 18 } 19 20 /* Returns the quotient, or zero if the denominator is zero.*/ 21 Statistics.divideIfPossibleOrZero = function(numerator, denominator) { 22 if (denominator === 0) 23 return 0; 24 return numerator / denominator; 25 }; 26 27 Statistics.sum = function(ary, opt_func, opt_this) { 28 var func = opt_func || identity; 29 var ret = 0; 30 for (var i = 0; i < ary.length; i++) 31 ret += func.call(opt_this, ary[i], i); 32 return ret; 33 }; 34 35 Statistics.mean = function(ary, opt_func, opt_this) { 36 return Statistics.sum(ary, opt_func, opt_this) / ary.length; 37 }; 38 39 // Returns undefined if the sum of the weights is zero. 40 Statistics.weightedMean = function( 41 ary, weightCallback, opt_valueCallback, opt_this) { 42 var valueCallback = opt_valueCallback || identity; 43 var numerator = 0; 44 var denominator = 0; 45 46 for (var i = 0; i < ary.length; i++) { 47 var value = valueCallback.call(opt_this, ary[i], i); 48 if (value === undefined) 49 continue; 50 var weight = weightCallback.call(opt_this, ary[i], i, value); 51 numerator += weight * value; 52 denominator += weight; 53 } 54 55 if (denominator === 0) 56 return undefined; 57 58 return numerator / denominator; 59 }; 60 61 Statistics.variance = function(ary, opt_func, opt_this) { 62 var func = opt_func || identity; 63 var mean = Statistics.mean(ary, func, opt_this); 64 var sumOfSquaredDistances = Statistics.sum( 65 ary, 66 function(d, i) { 67 var v = func.call(this, d, i) - mean; 68 return v * v; 69 }, 70 opt_this); 71 return sumOfSquaredDistances / (ary.length - 1); 72 }; 73 74 Statistics.stddev = function(ary, opt_func, opt_this) { 75 return Math.sqrt( 76 Statistics.variance(ary, opt_func, opt_this)); 77 }; 78 79 Statistics.max = function(ary, opt_func, opt_this) { 80 var func = opt_func || identity; 81 var ret = -Infinity; 82 for (var i = 0; i < ary.length; i++) 83 ret = Math.max(ret, func.call(opt_this, ary[i], i)); 84 return ret; 85 }; 86 87 Statistics.min = function(ary, opt_func, opt_this) { 88 var func = opt_func || identity; 89 var ret = Infinity; 90 for (var i = 0; i < ary.length; i++) 91 ret = Math.min(ret, func.call(opt_this, ary[i], i)); 92 return ret; 93 }; 94 95 Statistics.range = function(ary, opt_func, opt_this) { 96 var func = opt_func || identity; 97 var ret = new tr.b.Range(); 98 for (var i = 0; i < ary.length; i++) 99 ret.addValue(func.call(opt_this, ary[i], i)); 100 return ret; 101 }; 102 103 Statistics.percentile = function(ary, percent, opt_func, opt_this) { 104 if (!(percent >= 0 && percent <= 1)) 105 throw new Error('percent must be [0,1]'); 106 107 var func = opt_func || identity; 108 var tmp = new Array(ary.length); 109 for (var i = 0; i < ary.length; i++) 110 tmp[i] = func.call(opt_this, ary[i], i); 111 tmp.sort(); 112 var idx = Math.floor((ary.length - 1) * percent); 113 return tmp[idx]; 114 }; 115 116 /* Clamp a value between some low and high value. */ 117 Statistics.clamp = function(value, opt_low, opt_high) { 118 opt_low = opt_low || 0.0; 119 opt_high = opt_high || 1.0; 120 return Math.min(Math.max(value, opt_low), opt_high); 121 }; 122 123 /** 124 * Sorts the samples, and map them linearly to the range [0,1]. 125 * 126 * They're mapped such that for the N samples, the first sample is 0.5/N and 127 * the last sample is (N-0.5)/N. 128 * 129 * Background: The discrepancy of the sample set i/(N-1); i=0, ..., N-1 is 130 * 2/N, twice the discrepancy of the sample set (i+1/2)/N; i=0, ..., N-1. In 131 * our case we don't want to distinguish between these two cases, as our 132 * original domain is not bounded (it is for Monte Carlo integration, where 133 * discrepancy was first used). 134 **/ 135 Statistics.normalizeSamples = function(samples) { 136 if (samples.length === 0) { 137 return { 138 normalized_samples: samples, 139 scale: 1.0 140 }; 141 } 142 // Create a copy to make sure that we don't mutate original |samples| input. 143 samples = samples.slice().sort( 144 function(a, b) { 145 return a - b; 146 } 147 ); 148 var low = Math.min.apply(null, samples); 149 var high = Math.max.apply(null, samples); 150 var new_low = 0.5 / samples.length; 151 var new_high = (samples.length - 0.5) / samples.length; 152 if (high - low === 0.0) { 153 // Samples is an array of 0.5 in this case. 154 samples = Array.apply(null, new Array(samples.length)).map( 155 function() { return 0.5;}); 156 return { 157 normalized_samples: samples, 158 scale: 1.0 159 }; 160 } 161 var scale = (new_high - new_low) / (high - low); 162 for (var i = 0; i < samples.length; i++) { 163 samples[i] = (samples[i] - low) * scale + new_low; 164 } 165 return { 166 normalized_samples: samples, 167 scale: scale 168 }; 169 }; 170 171 /** 172 * Computes the discrepancy of a set of 1D samples from the interval [0,1]. 173 * 174 * The samples must be sorted. We define the discrepancy of an empty set 175 * of samples to be zero. 176 * 177 * http://en.wikipedia.org/wiki/Low-discrepancy_sequence 178 * http://mathworld.wolfram.com/Discrepancy.html 179 */ 180 Statistics.discrepancy = function(samples, opt_location_count) { 181 if (samples.length === 0) 182 return 0.0; 183 184 var max_local_discrepancy = 0; 185 var inv_sample_count = 1.0 / samples.length; 186 var locations = []; 187 // For each location, stores the number of samples less than that location. 188 var count_less = []; 189 // For each location, stores the number of samples less than or equal to 190 // that location. 191 var count_less_equal = []; 192 193 if (opt_location_count !== undefined) { 194 // Generate list of equally spaced locations. 195 var sample_index = 0; 196 for (var i = 0; i < opt_location_count; i++) { 197 var location = i / (opt_location_count - 1); 198 locations.push(location); 199 while (sample_index < samples.length && 200 samples[sample_index] < location) { 201 sample_index += 1; 202 } 203 count_less.push(sample_index); 204 while (sample_index < samples.length && 205 samples[sample_index] <= location) { 206 sample_index += 1; 207 } 208 count_less_equal.push(sample_index); 209 } 210 } else { 211 // Populate locations with sample positions. Append 0 and 1 if necessary. 212 if (samples[0] > 0.0) { 213 locations.push(0.0); 214 count_less.push(0); 215 count_less_equal.push(0); 216 } 217 for (var i = 0; i < samples.length; i++) { 218 locations.push(samples[i]); 219 count_less.push(i); 220 count_less_equal.push(i + 1); 221 } 222 if (samples[-1] < 1.0) { 223 locations.push(1.0); 224 count_less.push(samples.length); 225 count_less_equal.push(samples.length); 226 } 227 } 228 // Iterate over the intervals defined by any pair of locations. 229 for (var i = 0; i < locations.length; i++) { 230 for (var j = i + 1; j < locations.length; j++) { 231 // Length of interval 232 var length = locations[j] - locations[i]; 233 234 // Local discrepancy for closed interval 235 var count_closed = count_less_equal[j] - count_less[i]; 236 var local_discrepancy_closed = Math.abs( 237 count_closed * inv_sample_count - length); 238 var max_local_discrepancy = Math.max( 239 local_discrepancy_closed, max_local_discrepancy); 240 241 // Local discrepancy for open interval 242 var count_open = count_less[j] - count_less_equal[i]; 243 var local_discrepancy_open = Math.abs( 244 count_open * inv_sample_count - length); 245 var max_local_discrepancy = Math.max( 246 local_discrepancy_open, max_local_discrepancy); 247 } 248 } 249 return max_local_discrepancy; 250 }; 251 252 /** 253 * A discrepancy based metric for measuring timestamp jank. 254 * 255 * timestampsDiscrepancy quantifies the largest area of jank observed in a 256 * series of timestamps. Note that this is different from metrics based on 257 * the max_time_interval. For example, the time stamp series A = [0,1,2,3,5,6] 258 * and B = [0,1,2,3,5,7] have the same max_time_interval = 2, but 259 * Discrepancy(B) > Discrepancy(A). 260 * 261 * Two variants of discrepancy can be computed: 262 * 263 * Relative discrepancy is following the original definition of 264 * discrepancy. It characterized the largest area of jank, relative to the 265 * duration of the entire time stamp series. We normalize the raw results, 266 * because the best case discrepancy for a set of N samples is 1/N (for 267 * equally spaced samples), and we want our metric to report 0.0 in that 268 * case. 269 * 270 * Absolute discrepancy also characterizes the largest area of jank, but its 271 * value wouldn't change (except for imprecisions due to a low 272 * |interval_multiplier|) if additional 'good' intervals were added to an 273 * exisiting list of time stamps. Its range is [0,inf] and the unit is 274 * milliseconds. 275 * 276 * The time stamp series C = [0,2,3,4] and D = [0,2,3,4,5] have the same 277 * absolute discrepancy, but D has lower relative discrepancy than C. 278 * 279 * |timestamps| may be a list of lists S = [S_1, S_2, ..., S_N], where each 280 * S_i is a time stamp series. In that case, the discrepancy D(S) is: 281 * D(S) = max(D(S_1), D(S_2), ..., D(S_N)) 282 **/ 283 Statistics.timestampsDiscrepancy = function(timestamps, opt_absolute, 284 opt_location_count) { 285 if (timestamps.length === 0) 286 return 0.0; 287 288 if (opt_absolute === undefined) 289 opt_absolute = true; 290 291 if (Array.isArray(timestamps[0])) { 292 var range_discrepancies = timestamps.map(function(r) { 293 return Statistics.timestampsDiscrepancy(r); 294 }); 295 return Math.max.apply(null, range_discrepancies); 296 } 297 298 var s = Statistics.normalizeSamples(timestamps); 299 var samples = s.normalized_samples; 300 var sample_scale = s.scale; 301 var discrepancy = Statistics.discrepancy(samples, opt_location_count); 302 var inv_sample_count = 1.0 / samples.length; 303 if (opt_absolute === true) { 304 // Compute absolute discrepancy 305 discrepancy /= sample_scale; 306 } else { 307 // Compute relative discrepancy 308 discrepancy = Statistics.clamp( 309 (discrepancy - inv_sample_count) / (1.0 - inv_sample_count)); 310 } 311 return discrepancy; 312 }; 313 314 /** 315 * A discrepancy based metric for measuring duration jank. 316 * 317 * DurationsDiscrepancy computes a jank metric which measures how irregular a 318 * given sequence of intervals is. In order to minimize jank, each duration 319 * should be equally long. This is similar to how timestamp jank works, 320 * and we therefore reuse the timestamp discrepancy function above to compute 321 * a similar duration discrepancy number. 322 * 323 * Because timestamp discrepancy is defined in terms of timestamps, we first 324 * convert the list of durations to monotonically increasing timestamps. 325 * 326 * Args: 327 * durations: List of interval lengths in milliseconds. 328 * absolute: See TimestampsDiscrepancy. 329 * opt_location_count: See TimestampsDiscrepancy. 330 **/ 331 Statistics.durationsDiscrepancy = function( 332 durations, opt_absolute, opt_location_count) { 333 if (durations.length === 0) 334 return 0.0; 335 336 var timestamps = durations.reduce(function(prev, curr, index, array) { 337 prev.push(prev[prev.length - 1] + curr); 338 return prev; 339 }, [0]); 340 return Statistics.timestampsDiscrepancy( 341 timestamps, opt_absolute, opt_location_count); 342 }; 343 344 345 /** 346 * A mechanism to uniformly sample elements from an arbitrary long stream. 347 * 348 * Call this method every time a new element is obtained from the stream, 349 * passing always the same |samples| array and the |numSamples| you desire. 350 * Also pass in the current |streamLength|, which is the same as the index of 351 * |newElement| within that stream. 352 * 353 * The |samples| array will possibly be updated, replacing one of its element 354 * with |newElements|. The length of |samples| will not be more than 355 * |numSamples|. 356 * 357 * This method guarantees that after |streamLength| elements have been 358 * processed each one has equal probability of being in |samples|. The order 359 * of samples is not preserved though. 360 * 361 * Args: 362 * samples: Array of elements that have already been selected. Start with []. 363 * streamLength: The current length of the stream, up to |newElement|. 364 * newElement: The element that was just extracted from the stream. 365 * numSamples: The total number of samples desired. 366 **/ 367 Statistics.uniformlySampleStream = function(samples, streamLength, newElement, 368 numSamples) { 369 if (streamLength <= numSamples) { 370 if (samples.length >= streamLength) 371 samples[streamLength - 1] = newElement; 372 else 373 samples.push(newElement); 374 return; 375 } 376 377 var probToKeep = numSamples / streamLength; 378 if (Math.random() > probToKeep) 379 return; // New sample was rejected. 380 381 // Keeping it, replace an alement randomly. 382 var index = Math.floor(Math.random() * numSamples); 383 samples[index] = newElement; 384 }; 385 386 /** 387 * A mechanism to merge two arrays of uniformly sampled elements in a way that 388 * ensures elements in the final array are still sampled uniformly. 389 * 390 * This works similarly to sampleStreamUniform. The |samplesA| array will be 391 * updated, some of its elements replaced by elements from |samplesB| in a 392 * way that ensure that elements will be sampled uniformly. 393 * 394 * Args: 395 * samplesA: Array of uniformly sampled elements, will be updated. 396 * streamLengthA: The length of the stream from which |samplesA| was sampled. 397 * samplesB: Other array of uniformly sampled elements, will NOT be updated. 398 * streamLengthB: The length of the stream from which |samplesB| was sampled. 399 * numSamples: The total number of samples desired, both in |samplesA| and 400 * |samplesB|. 401 **/ 402 Statistics.mergeSampledStreams = function( 403 samplesA, streamLengthA, 404 samplesB, streamLengthB, numSamples) { 405 if (streamLengthB < numSamples) { 406 // samplesB has not reached max capacity so every sample of stream B were 407 // chosen with certainty. Add them one by one into samplesA. 408 var nbElements = Math.min(streamLengthB, samplesB.length); 409 for (var i = 0; i < nbElements; ++i) { 410 Statistics.uniformlySampleStream(samplesA, streamLengthA + i + 1, 411 samplesB[i], numSamples); 412 } 413 return; 414 } 415 if (streamLengthA < numSamples) { 416 // samplesA has not reached max capacity so every sample of stream A were 417 // chosen with certainty. Add them one by one into samplesB. 418 var nbElements = Math.min(streamLengthA, samplesA.length); 419 var tempSamples = samplesB.slice(); 420 for (var i = 0; i < nbElements; ++i) { 421 Statistics.uniformlySampleStream(tempSamples, streamLengthB + i + 1, 422 samplesA[i], numSamples); 423 } 424 // Copy that back into the first vector. 425 for (var i = 0; i < tempSamples.length; ++i) { 426 samplesA[i] = tempSamples[i]; 427 } 428 return; 429 } 430 431 // Both sample arrays are at max capacity, use the power of maths! 432 // Elements in samplesA have been selected with probability 433 // numSamples / streamLengthA. Same for samplesB. For each index of the 434 // array we keep samplesA[i] with probability 435 // P = streamLengthA / (streamLengthA + streamLengthB) 436 // and replace it with samplesB[i] with probability 1-P. 437 // The total probability of keeping it is therefore 438 // numSamples / streamLengthA * 439 // streamLengthA / (streamLengthA + streamLengthB) 440 // = numSamples / (streamLengthA + streamLengthB) 441 // A similar computation shows we have the same probability of keeping any 442 // element in samplesB. Magic! 443 var nbElements = Math.min(numSamples, samplesB.length); 444 var probOfSwapping = streamLengthB / (streamLengthA + streamLengthB); 445 for (var i = 0; i < nbElements; ++i) { 446 if (Math.random() < probOfSwapping) { 447 samplesA[i] = samplesB[i]; 448 } 449 } 450 }; 451 452 return { 453 Statistics: Statistics 454 }; 455}); 456</script> 457