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