| Index: tracing/tracing/metrics/system_health/first_paint_metric.html | 
| diff --git a/tracing/tracing/metrics/system_health/first_paint_metric.html b/tracing/tracing/metrics/system_health/first_paint_metric.html | 
| index f4cb399cc63bfa8501ad87b2997635e8ed2c83a7..d3e154929496d15e51b0e50b4e4953b8689f74d4 100644 | 
| --- a/tracing/tracing/metrics/system_health/first_paint_metric.html | 
| +++ b/tracing/tracing/metrics/system_health/first_paint_metric.html | 
| @@ -9,6 +9,7 @@ found in the LICENSE file. | 
| <link rel="import" href="/tracing/metrics/metric_registry.html"> | 
| <link rel="import" href="/tracing/metrics/system_health/utils.html"> | 
| <link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> | 
| +<link rel="import" href="/tracing/model/timed_event.html"> | 
| <link rel="import" href="/tracing/value/numeric.html"> | 
| <link rel="import" href="/tracing/value/value.html"> | 
|  | 
| @@ -19,10 +20,7 @@ tr.exportTo('tr.metrics.sh', function() { | 
| var timeDurationInMs_smallerIsBetter = | 
| tr.v.Unit.byName.timeDurationInMs_smallerIsBetter; | 
|  | 
| -  function findTargetRendererHelper(model) { | 
| -    var chromeHelper = model.getOrCreateHelper( | 
| -        tr.model.helpers.ChromeModelHelper); | 
| - | 
| +  function findTargetRendererHelper(chromeHelper) { | 
| var largestPid = -1; | 
| for (var pid in chromeHelper.rendererHelpers) { | 
| var rendererHelper = chromeHelper.rendererHelpers[pid]; | 
| @@ -38,63 +36,155 @@ tr.exportTo('tr.metrics.sh', function() { | 
| return chromeHelper.rendererHelpers[largestPid]; | 
| } | 
|  | 
| -  function findNavigationStartEvent(rendererHelper) { | 
| -    var navigationStartEvent = undefined; | 
| - | 
| +  function navigationStartFinder(rendererHelper) { | 
| +    var navigationStartsForFrameId = {}; | 
| rendererHelper.mainThread.sliceGroup.iterateAllEventsInThisContainer( | 
| () => true, function(ev) { | 
| -          if (navigationStartEvent !== undefined || | 
| -              ev.category !== 'blink.user_timing') | 
| +          if (ev.category !== 'blink.user_timing' || | 
| +              ev.title !== 'navigationStart') | 
| return; | 
| -          if (ev.title === 'navigationStart') | 
| -            navigationStartEvent = ev; | 
| + | 
| +          var frameIdRef = ev.args['frame']; | 
| +          var list = navigationStartsForFrameId[frameIdRef]; | 
| +          if (list === undefined) { | 
| +              navigationStartsForFrameId[frameIdRef] = list = []; | 
| +          } | 
| +          list.unshift(ev); | 
| }, | 
| this); | 
|  | 
| -    return navigationStartEvent; | 
| +    return function findNavigationStartEventForFrameBeforeTimestamp(frameIdRef, | 
| +        ts) { | 
| +      var list = navigationStartsForFrameId[frameIdRef]; | 
| +      if (list === undefined) | 
| +        throw new Error('No navigationStartEvent found for frame id "' + | 
| +            frameIdRef + '"'); | 
| + | 
| +      var eventBeforeTimestamp; | 
| +      list.forEach(function(ev) { | 
| +        if (ev.start > ts) | 
| +          return; | 
| + | 
| +        if (eventBeforeTimestamp === undefined) | 
| +          eventBeforeTimestamp = ev; | 
| +      }, this); | 
| +      if (eventBeforeTimestamp === undefined) | 
| +        throw new Error('Failed to find navigationStartEvent.'); | 
| +      return eventBeforeTimestamp; | 
| +    } | 
| } | 
|  | 
| -  function findFirstPaintEvent(rendererHelper, title, frame) { | 
| -    var firstPaintEvent = undefined; | 
| +  function findUrlOfFrameAt(rendererHelper, frameIdRef, ts) { | 
| +    var url; | 
| + | 
| +    var objects = rendererHelper.process.objects; | 
| +    var frameLoaderInstances = objects.instancesByTypeName_['FrameLoader']; | 
| +    if (frameLoaderInstances === undefined) { | 
| +      throw new Error( | 
| +          'Couldn\'t find any FrameLoader instances to query frame url.'); | 
| +    } | 
| +    frameLoaderInstances.forEach(function(instance) { | 
| +      if (!instance.isAliveAt(ts)) | 
| +        return; | 
| +      var snapshot = instance.getSnapshotAt(ts); | 
| +      if (frameIdRef !== snapshot.args['frame']['id_ref']) | 
| +        return; | 
| + | 
| +      url = snapshot.args['documentLoaderURL']; | 
| +    }, this); | 
| + | 
| +    return url; | 
| +  } | 
| + | 
| +  function findFirstPaintEvents(rendererHelper, title) { | 
| +    var firstPaintEvents = []; | 
|  | 
| rendererHelper.process.iterateAllEvents( | 
| function(ev) { | 
| -          if (firstPaintEvent !== undefined || | 
| -              ev.category !== 'blink.user_timing' || | 
| -              ev.title !== title || | 
| -              ev.args === undefined || ev.args['frame'] !== frame) | 
| +          if (ev.category !== 'blink.user_timing' || | 
| +              ev.title !== title) | 
| return; | 
|  | 
| -          firstPaintEvent = ev; | 
| +          firstPaintEvents.push(ev); | 
| }, this); | 
|  | 
| -    return firstPaintEvent; | 
| +    return firstPaintEvents; | 
| } | 
|  | 
| -  function firstPaintMetric(values, model) { | 
| -    var rendererHelper = findTargetRendererHelper(model); | 
| -    var navigationStartEvent = findNavigationStartEvent(rendererHelper); | 
| +  function prepareTelemetryInternalEventPredicate(rendererHelper) { | 
| +    var ignoreRegions = []; | 
|  | 
| -    if (navigationStartEvent === undefined) | 
| -      throw new Error('Failed to find navigationStartEvent.'); | 
| +    var warmCacheStart; | 
| +    rendererHelper.mainThread.asyncSliceGroup.iterateAllEventsInThisContainer( | 
| +        () => true, function(slice) { | 
| +          if (slice.title === 'telemetry.internal.warmCache.start') | 
| +            warmCacheStart = slice.start; | 
| +          if (slice.title === 'telemetry.internal.warmCache.end') { | 
| +            var warmCacheTimedEvent = new tr.model.TimedEvent(warmCacheStart); | 
| +            warmCacheTimedEvent.duration = slice.end - warmCacheStart; | 
|  | 
| -    var frame = navigationStartEvent.args['frame']; | 
| -    var firstContentfulPaintEvent = findFirstPaintEvent(rendererHelper, | 
| -        'firstContentfulPaint', frame); | 
| -    if (firstContentfulPaintEvent === undefined) | 
| -      throw new Error( | 
| -          'Failed to find firstContentfulPaintEvent for frame ' + frame); | 
| +            ignoreRegions.push(warmCacheTimedEvent); | 
| +          } | 
| +        }, this); | 
|  | 
| -    var grouping_keys = {}; | 
| +    return function isTelemetryInternalEvent(slice) { | 
| +      for (var i = 0; i < ignoreRegions.length; ++ i) { | 
| +        if (ignoreRegions[i].bounds(slice)) | 
| +          return true; | 
| +      } | 
| +      return false; | 
| +    } | 
| +  } | 
|  | 
| -    var timeToFirstContentfulPaint = | 
| -        firstContentfulPaintEvent.start - navigationStartEvent.start; | 
| +  var URL_BLACKLIST = ['about:blank']; | 
| +  function shouldIgnoreURL(url) { | 
| +    return URL_BLACKLIST.indexOf(url) >= 0; | 
| +  } | 
| + | 
| +  function firstPaintMetric(values, model) { | 
| +    var chromeHelper = model.getOrCreateHelper( | 
| +        tr.model.helpers.ChromeModelHelper); | 
| +    var rendererHelper = findTargetRendererHelper(chromeHelper); | 
| +    var isTelemetryInternalEvent = | 
| +        prepareTelemetryInternalEventPredicate(rendererHelper); | 
| +    var findNavigationStartEventForFrameBeforeTimestamp = | 
| +        navigationStartFinder(rendererHelper); | 
| + | 
| +    var numericBuilder = | 
| +      new tr.v.NumericBuilder(timeDurationInMs_smallerIsBetter, 0) | 
| +      .addLinearBins(1000, 20) // 50ms step to 1s | 
| +      .addLinearBins(3000, 20) // 100ms step to 3s | 
| +      .addExponentialBins(20000, 20); | 
| +    var firstContentfulPaintHistogram = numericBuilder.build(); | 
| +    firstContentfulPaintHistogram.customizeSummaryOptions({ | 
| +      avg: true, | 
| +      count: true, | 
| +      max: true, | 
| +      min: true, | 
| +      std: true, | 
| +      sum: false, | 
| +      percentile: [0.90, 0.95, 0.99], | 
| +    }); | 
| + | 
| +    var firstPaintEvents = findFirstPaintEvents(rendererHelper, | 
| +        'firstContentfulPaint'); | 
| +    firstPaintEvents = firstPaintEvents.filter( | 
| +        (ev) => !isTelemetryInternalEvent(ev)); | 
| +    firstPaintEvents.forEach(function(ev) { | 
| +      var frameIdRef = ev.args['frame']; | 
| +      var url = findUrlOfFrameAt(rendererHelper, frameIdRef, ev.start); | 
| +      if (shouldIgnoreURL(url)) | 
| +        return; | 
| +      var navigationStartEvent = | 
| +        findNavigationStartEventForFrameBeforeTimestamp(frameIdRef, ev.start); | 
| + | 
| +      var timeToFirstContentfulPaint = ev.start - navigationStartEvent.start; | 
| +      firstContentfulPaintHistogram.add(timeToFirstContentfulPaint, {url: url}); | 
| +    }, this); | 
| values.addValue(new tr.v.NumericValue( | 
| -        model.canonicalUrlThatCreatedThisTrace, 'firstContentfulPaint', | 
| -        new tr.v.ScalarNumeric(timeDurationInMs_smallerIsBetter, | 
| -            timeToFirstContentfulPaint), | 
| -        { description: 'time to first contentful paint' }, | 
| -        grouping_keys)); | 
| +        model.canonicalUrl, 'firstContentfulPaint', | 
| +        firstContentfulPaintHistogram, | 
| +        { description: 'time to first contentful paint' })); | 
| } | 
|  | 
| firstPaintMetric.prototype = { | 
|  |