1<!DOCTYPE html>
2<!--
3Copyright (c) 2013 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/model/helpers/chrome_model_helper.html">
8<link rel="import" href="/tracing/model/async_slice.html">
9<link rel="import" href="/tracing/model/event_set.html">
10
11<script>
12'use strict';
13
14tr.exportTo('tr.e.cc', function() {
15  var AsyncSlice = tr.model.AsyncSlice;
16  var EventSet = tr.model.EventSet;
17
18  var UI_COMP_NAME = 'INPUT_EVENT_LATENCY_UI_COMPONENT';
19  var ORIGINAL_COMP_NAME = 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT';
20  var BEGIN_COMP_NAME = 'INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT';
21  var END_COMP_NAME = 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT';
22
23  var MAIN_RENDERER_THREAD_NAME = 'CrRendererMain';
24  var COMPOSITOR_THREAD_NAME = 'Compositor';
25
26  var POSTTASK_FLOW_EVENT = 'disabled-by-default-toplevel.flow';
27  var IPC_FLOW_EVENT = 'disabled-by-default-ipc.flow';
28
29  var INPUT_EVENT_TYPE_NAMES = {
30    CHAR: 'Char',
31    CLICK: 'GestureClick',
32    CONTEXT_MENU: 'ContextMenu',
33    FLING_CANCEL: 'GestureFlingCancel',
34    FLING_START: 'GestureFlingStart',
35    KEY_DOWN: 'KeyDown',
36    KEY_DOWN_RAW: 'RawKeyDown',
37    KEY_UP: 'KeyUp',
38    LATENCY_SCROLL_UPDATE: 'ScrollUpdate',
39    MOUSE_DOWN: 'MouseDown',
40    MOUSE_ENTER: 'MouseEnter',
41    MOUSE_LEAVE: 'MouseLeave',
42    MOUSE_MOVE: 'MouseMove',
43    MOUSE_UP: 'MouseUp',
44    MOUSE_WHEEL: 'MouseWheel',
45    PINCH_BEGIN: 'GesturePinchBegin',
46    PINCH_END: 'GesturePinchEnd',
47    PINCH_UPDATE: 'GesturePinchUpdate',
48    SCROLL_BEGIN: 'GestureScrollBegin',
49    SCROLL_END: 'GestureScrollEnd',
50    SCROLL_UPDATE: 'GestureScrollUpdate',
51    SCROLL_UPDATE_RENDERER: 'ScrollUpdate',
52    SHOW_PRESS: 'GestureShowPress',
53    TAP: 'GestureTap',
54    TAP_CANCEL: 'GestureTapCancel',
55    TAP_DOWN: 'GestureTapDown',
56    TOUCH_CANCEL: 'TouchCancel',
57    TOUCH_END: 'TouchEnd',
58    TOUCH_MOVE: 'TouchMove',
59    TOUCH_START: 'TouchStart',
60    UNKNOWN: 'UNKNOWN'
61  };
62
63  function InputLatencyAsyncSlice() {
64    AsyncSlice.apply(this, arguments);
65    this.associatedEvents_ = new EventSet();
66    this.typeName_ = undefined;
67    if (!this.isLegacyEvent)
68      this.determineModernTypeName_();
69  }
70
71  InputLatencyAsyncSlice.prototype = {
72    __proto__: AsyncSlice.prototype,
73
74    // Legacy InputLatencyAsyncSlices involve a top-level slice titled
75    // "InputLatency" containing a subSlice whose title starts with
76    // "InputLatency:". Modern InputLatencyAsyncSlices involve a single
77    // top-level slice whose title starts with "InputLatency::".
78    // Legacy subSlices are not available at construction time, so
79    // determineLegacyTypeName_() must be called at get time.
80    // So this returns false for the legacy subSlice events titled like
81    // "InputLatency:Foo" even though they are technically legacy events.
82    get isLegacyEvent() {
83      return this.title === 'InputLatency';
84    },
85
86    get typeName() {
87      if (!this.typeName_)
88        this.determineLegacyTypeName_();
89      return this.typeName_;
90    },
91
92    checkTypeName_: function() {
93      if (!this.typeName_)
94        throw 'Unable to determine typeName';
95      var found = false;
96      for (var type_name in INPUT_EVENT_TYPE_NAMES) {
97        if (this.typeName === INPUT_EVENT_TYPE_NAMES[type_name]) {
98          found = true;
99          break;
100        }
101      }
102      if (!found)
103        this.typeName_ = INPUT_EVENT_TYPE_NAMES.UNKNOWN;
104    },
105
106    determineModernTypeName_: function() {
107      // This method works both on modern events titled like
108      // "InputLatency::Foo" and also on the legacy subSlices titled like
109      // "InputLatency:Foo". Modern events' titles contain 2 colons, whereas the
110      // legacy subSlices events contain 1 colon.
111
112      var lastColonIndex = this.title.lastIndexOf(':');
113      if (lastColonIndex < 0)
114        return;
115
116      var characterAfterLastColonIndex = lastColonIndex + 1;
117      this.typeName_ = this.title.slice(characterAfterLastColonIndex);
118
119      // Check that the determined typeName is known.
120      this.checkTypeName_();
121    },
122
123    determineLegacyTypeName_: function() {
124      // Iterate over all descendent subSlices.
125      this.iterateAllDescendents(function(subSlice) {
126
127        // If |subSlice| is not an InputLatencyAsyncSlice, then ignore it.
128        var subSliceIsAInputLatencyAsyncSlice = (
129            subSlice instanceof InputLatencyAsyncSlice);
130        if (!subSliceIsAInputLatencyAsyncSlice)
131          return;
132
133        // If |subSlice| does not have a typeName, then ignore it.
134        if (!subSlice.typeName)
135          return;
136
137        // If |this| already has a typeName and |subSlice| has a different
138        // typeName, then explode!
139        if (this.typeName_ && subSlice.typeName_) {
140          var subSliceHasDifferentTypeName = (
141              this.typeName_ !== subSlice.typeName_);
142          if (subSliceHasDifferentTypeName) {
143            throw 'InputLatencyAsyncSlice.determineLegacyTypeName_() ' +
144              ' found multiple typeNames';
145          }
146        }
147
148        // The typeName of |this| top-level event is whatever the typeName of
149        // |subSlice| is. Set |this.typeName_| to the subSlice's typeName.
150        this.typeName_ = subSlice.typeName_;
151      }, this);
152
153      // If typeName could not be determined, then explode!
154      if (!this.typeName_)
155        throw 'InputLatencyAsyncSlice.determineLegacyTypeName_() failed';
156
157      // Check that the determined typeName is known.
158      this.checkTypeName_();
159    },
160
161    getRendererHelper: function(sourceSlices) {
162      var traceModel = this.startThread.parent.model;
163      var modelHelper = traceModel.getOrCreateHelper(
164          tr.model.helpers.ChromeModelHelper);
165      if (!modelHelper)
166        return undefined;
167
168      var mainThread = undefined;
169      var compositorThread = undefined;
170
171      for (var i in sourceSlices) {
172        if (sourceSlices[i].parentContainer.name ===
173            MAIN_RENDERER_THREAD_NAME)
174          mainThread = sourceSlices[i].parentContainer;
175        else if (sourceSlices[i].parentContainer.name ===
176            COMPOSITOR_THREAD_NAME)
177          compositorThread = sourceSlices[i].parentContainer;
178
179        if (mainThread && compositorThread)
180          break;
181      }
182
183      var rendererHelpers = modelHelper.rendererHelpers;
184
185      var pids = Object.keys(rendererHelpers);
186      for (var i = 0; i < pids.length; i++) {
187        var pid = pids[i];
188        var rendererHelper = rendererHelpers[pid];
189        if (rendererHelper.mainThread === mainThread ||
190            rendererHelper.compositorThread === compositorThread)
191          return rendererHelper;
192      }
193
194      return undefined;
195    },
196
197    addEntireSliceHierarchy: function(slice) {
198      this.associatedEvents_.push(slice);
199      slice.iterateAllSubsequentSlices(function(subsequentSlice) {
200        this.associatedEvents_.push(subsequentSlice);
201      }, this);
202    },
203
204    addDirectlyAssociatedEvents: function(flowEvents) {
205      var slices = [];
206
207      flowEvents.forEach(function(flowEvent) {
208        this.associatedEvents_.push(flowEvent);
209        var newSource = flowEvent.startSlice.mostTopLevelSlice;
210        if (slices.indexOf(newSource) === -1)
211          slices.push(newSource);
212      }, this);
213
214      var lastFlowEvent = flowEvents[flowEvents.length - 1];
215      var lastSource = lastFlowEvent.endSlice.mostTopLevelSlice;
216      if (slices.indexOf(lastSource) === -1)
217        slices.push(lastSource);
218
219      return slices;
220    },
221
222    // Find the Latency::ScrollUpdate slice that corresponds to the
223    // InputLatency::GestureScrollUpdate slice.
224    // The C++ CL that makes this connection is at:
225    // https://codereview.chromium.org/1178963003
226    addScrollUpdateEvents: function(rendererHelper) {
227      if (!rendererHelper || !rendererHelper.compositorThread)
228        return;
229
230      var compositorThread = rendererHelper.compositorThread;
231      var gestureScrollUpdateStart = this.start;
232      var gestureScrollUpdateEnd = this.end;
233
234      var allCompositorAsyncSlices =
235        compositorThread.asyncSliceGroup.slices;
236
237      for (var i in allCompositorAsyncSlices) {
238        var slice = allCompositorAsyncSlices[i];
239
240        if (slice.title !== 'Latency::ScrollUpdate')
241          continue;
242
243        var parentId = slice.args.data.
244            INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT.
245            sequence_number;
246
247        if (parentId === undefined) {
248          // Old trace, we can only rely on the timestamp to find the slice
249          if (slice.start < gestureScrollUpdateStart ||
250              slice.start >= gestureScrollUpdateEnd)
251            continue;
252        } else {
253          // New trace, we can definitively find the latency slice by comparing
254          // its sequence number with gesture id
255          if (parseInt(parentId) !== parseInt(this.id))
256            continue;
257        }
258
259        slice.associatedEvents.forEach(function(event) {
260          this.associatedEvents_.push(event);
261        }, this);
262        break;
263      }
264    },
265
266    // Return true if the slice hierarchy is tracked by LatencyInfo of other
267    // input latency events. If the slice hierarchy is tracked by both, this
268    // function still returns true.
269    belongToOtherInputs: function(slice, flowEvents) {
270      var fromOtherInputs = false;
271
272      slice.iterateEntireHierarchy(function(subsequentSlice) {
273        if (fromOtherInputs)
274          return;
275
276        subsequentSlice.inFlowEvents.forEach(function(inflow) {
277          if (fromOtherInputs)
278            return;
279
280          if (inflow.category.indexOf('input') > -1) {
281            if (flowEvents.indexOf(inflow) === -1)
282              fromOtherInputs = true;
283          }
284        }, this);
285      }, this);
286
287      return fromOtherInputs;
288    },
289
290    // Return true if |event| triggers slices of other inputs.
291    triggerOtherInputs: function(event, flowEvents) {
292      if (event.outFlowEvents === undefined ||
293          event.outFlowEvents.length === 0)
294        return false;
295
296      // Once we fix the bug of flow event binding, there should exist one and
297      // only one outgoing flow (PostTask) from ScheduleBeginImplFrameDeadline
298      // and PostComposite.
299      var flow = event.outFlowEvents[0];
300
301      if (flow.category !== POSTTASK_FLOW_EVENT ||
302          !flow.endSlice)
303        return false;
304
305      var endSlice = flow.endSlice;
306      if (this.belongToOtherInputs(endSlice.mostTopLevelSlice, flowEvents))
307        return true;
308
309      return false;
310    },
311
312    // Follow outgoing flow of subsequentSlices in the current hierarchy.
313    // We also handle cases where different inputs interfere with each other.
314    followSubsequentSlices: function(event, queue, visited, flowEvents) {
315      var stopFollowing = false;
316      var inputAck = false;
317
318      event.iterateAllSubsequentSlices(function(slice) {
319        if (stopFollowing)
320          return;
321
322        // Do not follow TaskQueueManager::RunTask because it causes
323        // many false events to be included.
324        if (slice.title === 'TaskQueueManager::RunTask')
325          return;
326
327        // Do not follow ScheduledActionSendBeginMainFrame because the real
328        // main thread BeginMainFrame is already traced by LatencyInfo flow.
329        if (slice.title === 'ThreadProxy::ScheduledActionSendBeginMainFrame')
330          return;
331
332        // Do not follow ScheduleBeginImplFrameDeadline that triggers an
333        // OnBeginImplFrameDeadline that is tracked by another LatencyInfo.
334        if (slice.title === 'Scheduler::ScheduleBeginImplFrameDeadline') {
335          if (this.triggerOtherInputs(slice, flowEvents))
336            return;
337        }
338
339        // Do not follow PostComposite that triggers CompositeImmediately
340        // that is tracked by another LatencyInfo.
341        if (slice.title === 'CompositorImpl::PostComposite') {
342          if (this.triggerOtherInputs(slice, flowEvents))
343            return;
344        }
345
346        // Stop following the rest of the current slice hierarchy if
347        // FilterAndSendWebInputEvent occurs after ProcessInputEventAck.
348        if (slice.title === 'InputRouterImpl::ProcessInputEventAck')
349          inputAck = true;
350        if (inputAck &&
351            slice.title === 'InputRouterImpl::FilterAndSendWebInputEvent')
352          stopFollowing = true;
353
354        this.followCurrentSlice(slice, queue, visited);
355      }, this);
356    },
357
358    // Follow outgoing flow events of the current slice.
359    followCurrentSlice: function(event, queue, visited) {
360      event.outFlowEvents.forEach(function(outflow) {
361        if ((outflow.category === POSTTASK_FLOW_EVENT ||
362            outflow.category === IPC_FLOW_EVENT) &&
363            outflow.endSlice) {
364          this.associatedEvents_.push(outflow);
365
366          var nextEvent = outflow.endSlice.mostTopLevelSlice;
367          if (!visited.contains(nextEvent)) {
368            visited.push(nextEvent);
369            queue.push(nextEvent);
370          }
371        }
372      }, this);
373    },
374
375    backtraceFromDraw: function(beginImplFrame, visited) {
376      var pendingEventQueue = [];
377      pendingEventQueue.push(beginImplFrame.mostTopLevelSlice);
378
379      while (pendingEventQueue.length !== 0) {
380        var event = pendingEventQueue.pop();
381
382        this.addEntireSliceHierarchy(event);
383
384        // TODO(yuhao): For now, we backtrace all the way to the source input.
385        // But is this really needed? I will have an entry in the design
386        // doc to discuss this.
387        event.inFlowEvents.forEach(function(inflow) {
388          if (inflow.category === POSTTASK_FLOW_EVENT && inflow.startSlice) {
389            var nextEvent = inflow.startSlice.mostTopLevelSlice;
390            if (!visited.contains(nextEvent)) {
391              visited.push(nextEvent);
392              pendingEventQueue.push(nextEvent);
393            }
394          }
395        }, this);
396      }
397    },
398
399    sortRasterizerSlices: function(rasterWorkerThreads,
400        sortedRasterizerSlices) {
401      rasterWorkerThreads.forEach(function(rasterizer) {
402        Array.prototype.push.apply(sortedRasterizerSlices,
403            rasterizer.sliceGroup.slices);
404      }, this);
405
406      sortedRasterizerSlices.sort(function(a, b) {
407        if (a.start !== b.start)
408          return a.start - b.start;
409        return a.guid - b.guid;
410      });
411    },
412
413    // Find rasterization slices that have the source_prepare_tiles_id
414    // same as the prepare_tiles_id of TileManager::PrepareTiles
415    // The C++ CL that makes this connection is at:
416    // https://codereview.chromium.org/1208683002/
417    addRasterizationEvents: function(prepareTiles, rendererHelper,
418        visited, flowEvents, sortedRasterizerSlices) {
419      if (!prepareTiles.args.prepare_tiles_id)
420        return;
421
422      if (!rendererHelper || !rendererHelper.rasterWorkerThreads)
423        return;
424
425      var rasterWorkerThreads = rendererHelper.rasterWorkerThreads;
426      var prepare_tile_id = prepareTiles.args.prepare_tiles_id;
427      var pendingEventQueue = [];
428
429      // Collect all the rasterizer tasks. Return the cached copy if possible.
430      if (sortedRasterizerSlices.length === 0)
431        this.sortRasterizerSlices(rasterWorkerThreads, sortedRasterizerSlices);
432
433      // TODO(yuhao): Once TaskSetFinishedTaskImpl also get the prepare_tile_id
434      // we can simply track by checking id rather than counting.
435      var numFinishedTasks = 0;
436      var RASTER_TASK_TITLE = 'RasterizerTaskImpl::RunOnWorkerThread';
437      var IMAGEDECODE_TASK_TITLE = 'ImageDecodeTaskImpl::RunOnWorkerThread';
438      var FINISHED_TASK_TITLE = 'TaskSetFinishedTaskImpl::RunOnWorkerThread';
439
440      for (var i = 0; i < sortedRasterizerSlices.length; i++) {
441        var task = sortedRasterizerSlices[i];
442
443        if (task.title === RASTER_TASK_TITLE ||
444            task.title === IMAGEDECODE_TASK_TITLE) {
445          if (task.args.source_prepare_tiles_id === prepare_tile_id)
446            this.addEntireSliceHierarchy(task.mostTopLevelSlice);
447        } else if (task.title === FINISHED_TASK_TITLE) {
448          if (task.start > prepareTiles.start) {
449            pendingEventQueue.push(task.mostTopLevelSlice);
450            if (++numFinishedTasks === 3)
451              break;
452          }
453        }
454      }
455
456      // Trace PostTask from rasterizer tasks.
457      while (pendingEventQueue.length != 0) {
458        var event = pendingEventQueue.pop();
459
460        this.addEntireSliceHierarchy(event);
461        this.followSubsequentSlices(event, pendingEventQueue, visited,
462            flowEvents);
463      }
464    },
465
466    addOtherCausallyRelatedEvents: function(rendererHelper, sourceSlices,
467        flowEvents, sortedRasterizerSlices) {
468      var pendingEventQueue = [];
469      // Keep track of visited nodes when traversing a DAG
470      var visitedEvents = new EventSet();
471      var beginImplFrame = undefined;
472      var prepareTiles = undefined;
473      var sortedRasterizerSlices = [];
474
475      sourceSlices.forEach(function(sourceSlice) {
476        if (!visitedEvents.contains(sourceSlice)) {
477          visitedEvents.push(sourceSlice);
478          pendingEventQueue.push(sourceSlice);
479        }
480      }, this);
481
482      while (pendingEventQueue.length != 0) {
483        var event = pendingEventQueue.pop();
484
485        // Push the current event chunk into associatedEvents.
486        this.addEntireSliceHierarchy(event);
487
488        this.followCurrentSlice(event, pendingEventQueue, visitedEvents);
489
490        this.followSubsequentSlices(event, pendingEventQueue, visitedEvents,
491            flowEvents);
492
493        // The rasterization work (CompositorTileWorker thread) and the
494        // Compositor tile manager are connect by the prepare_tiles_id
495        // instead of flow events.
496        var COMPOSITOR_PREPARE_TILES = 'TileManager::PrepareTiles';
497        prepareTiles = event.findDescendentSlice(COMPOSITOR_PREPARE_TILES);
498        if (prepareTiles)
499           this.addRasterizationEvents(prepareTiles, rendererHelper,
500               visitedEvents, flowEvents, sortedRasterizerSlices);
501
502        // OnBeginImplFrameDeadline could be triggered by other inputs.
503        // For now, we backtrace from it.
504        // TODO(yuhao): There are more such slices that we need to backtrace
505        var COMPOSITOR_ON_BIFD = 'Scheduler::OnBeginImplFrameDeadline';
506        beginImplFrame = event.findDescendentSlice(COMPOSITOR_ON_BIFD);
507        if (beginImplFrame)
508          this.backtraceFromDraw(beginImplFrame, visitedEvents);
509      }
510
511      // A separate pass on GestureScrollUpdate.
512      // Scroll update doesn't go through the main thread, but the compositor
513      // may go back to the main thread if there is an onscroll event handler.
514      // This is captured by a different flow event, which does not have the
515      // same ID as the Input Latency Event, but it is technically causally
516      // related to the GestureScrollUpdate input. Add them manually for now.
517      var INPUT_GSU = 'InputLatency::GestureScrollUpdate';
518      if (this.title === INPUT_GSU)
519        this.addScrollUpdateEvents(rendererHelper);
520    },
521
522    get associatedEvents() {
523      if (this.associatedEvents_.length !== 0)
524        return this.associatedEvents_;
525
526      var modelIndices = this.startThread.parent.model.modelIndices;
527      var flowEvents = modelIndices.getFlowEventsWithId(this.id);
528
529      if (flowEvents.length === 0)
530        return this.associatedEvents_;
531
532      // Step 1: Get events that are directly connected by the LatencyInfo
533      // flow events. This gives us a small set of events that are guaranteed
534      // to be associated with the input, but are almost certain incomplete.
535      // We call this set "source" event set.
536      // This step returns the "source" event set (sourceSlices), which is then
537      // used in the second step.
538      var sourceSlices = this.addDirectlyAssociatedEvents(flowEvents);
539
540      // Step 2: Start from the previously constructed "source" event set, we
541      // follow the toplevel (i.e., PostTask) and IPC flow events. Any slices
542      // that are reachable from the "source" event set via PostTasks or IPCs
543      // are conservatively considered associated with the input event.
544      // We then deal with specific cases where flow events either over include
545      // or miss capturing slices.
546      var rendererHelper = this.getRendererHelper(sourceSlices);
547      this.addOtherCausallyRelatedEvents(rendererHelper, sourceSlices,
548          flowEvents);
549
550      return this.associatedEvents_;
551    },
552
553    get inputLatency() {
554      if (!('data' in this.args))
555        return undefined;
556
557      var data = this.args.data;
558      if (!(END_COMP_NAME in data))
559        return undefined;
560
561      var latency = 0;
562      var endTime = data[END_COMP_NAME].time;
563      if (ORIGINAL_COMP_NAME in data) {
564        latency = endTime - data[ORIGINAL_COMP_NAME].time;
565      } else if (UI_COMP_NAME in data) {
566        latency = endTime - data[UI_COMP_NAME].time;
567      } else if (BEGIN_COMP_NAME in data) {
568        latency = endTime - data[BEGIN_COMP_NAME].time;
569      } else {
570        throw new Error('No valid begin latency component');
571      }
572      return latency;
573    }
574  };
575
576  var eventTypeNames = [
577    'Char',
578    'ContextMenu',
579    'GestureClick',
580    'GestureFlingCancel',
581    'GestureFlingStart',
582    'GestureScrollBegin',
583    'GestureScrollEnd',
584    'GestureScrollUpdate',
585    'GestureShowPress',
586    'GestureTap',
587    'GestureTapCancel',
588    'GestureTapDown',
589    'GesturePinchBegin',
590    'GesturePinchEnd',
591    'GesturePinchUpdate',
592    'KeyDown',
593    'KeyUp',
594    'MouseDown',
595    'MouseEnter',
596    'MouseLeave',
597    'MouseMove',
598    'MouseUp',
599    'MouseWheel',
600    'RawKeyDown',
601    'ScrollUpdate',
602    'TouchCancel',
603    'TouchEnd',
604    'TouchMove',
605    'TouchStart'
606  ];
607  var allTypeNames = ['InputLatency'];
608  eventTypeNames.forEach(function(eventTypeName) {
609    // Old style.
610    allTypeNames.push('InputLatency:' + eventTypeName);
611
612    // New style.
613    allTypeNames.push('InputLatency::' + eventTypeName);
614  });
615
616  AsyncSlice.register(
617    InputLatencyAsyncSlice,
618    {
619      typeNames: allTypeNames,
620      categoryParts: ['latencyInfo']
621    });
622
623  return {
624    InputLatencyAsyncSlice: InputLatencyAsyncSlice,
625    INPUT_EVENT_TYPE_NAMES: INPUT_EVENT_TYPE_NAMES
626  };
627});
628</script>
629