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
8<link rel="import" href="/tracing/base/iteration_helpers.html">
9<link rel="import" href="/tracing/base/statistics.html">
10<link rel="import" href="/tracing/model/event_set.html">
11<link rel="import" href="/tracing/ui/base/dom_helpers.html">
12<link rel="import" href="/tracing/ui/base/pie_chart.html">
13<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
14<link rel="import" href="/tracing/value/ui/scalar_span.html">
15<link rel="import" href="/tracing/value/unit.html">
16
17<polymer-element name="tr-ui-e-s-time-summary-side-panel"
18    extends="tr-ui-side-panel">
19  <template>
20    <style>
21    :host {
22      flex-direction: column;
23      display: flex;
24    }
25    toolbar {
26      flex: 0 0 auto;
27      border-bottom: 1px solid black;
28      display: flex;
29    }
30    result-area {
31      flex: 1 1 auto;
32      display: block;
33      min-height: 0;
34      overflow-y: auto;
35    }
36    </style>
37
38    <toolbar id='toolbar'></toolbar>
39    <result-area id='result_area'></result-area>
40  </template>
41
42  <script>
43  'use strict';
44  (function() {
45    var GROUP_BY_PROCESS_NAME = 'process';
46    var GROUP_BY_THREAD_NAME = 'thread';
47
48    var WALL_TIME_GROUPING_UNIT = 'Wall time';
49    var CPU_TIME_GROUPING_UNIT = 'CPU time';
50
51    /**
52     * @constructor
53     */
54    function ResultsForGroup(model, name) {
55      this.model = model;
56      this.name = name;
57      this.topLevelSlices = [];
58      this.allSlices = [];
59    }
60
61    ResultsForGroup.prototype = {
62      get wallTime() {
63        var wallSum = tr.b.Statistics.sum(
64            this.topLevelSlices, function(x) { return x.duration; });
65        return wallSum;
66      },
67
68      get cpuTime() {
69        var cpuDuration = 0;
70        for (var i = 0; i < this.topLevelSlices.length; i++) {
71          var x = this.topLevelSlices[i];
72          // Only report thread-duration if we have it for all events.
73          //
74          // A thread_duration of 0 is valid, so this only returns 0 if it is
75          // None.
76          if (x.cpuDuration === undefined) {
77            if (x.duration === undefined)
78              continue;
79            return 0;
80          }
81          cpuDuration += x.cpuDuration;
82        }
83        return cpuDuration;
84      },
85
86      appendGroupContents: function(group) {
87        if (group.model != this.model)
88          throw new Error('Models must be the same');
89
90        group.allSlices.forEach(function(slice) {
91          this.allSlices.push(slice);
92        }, this);
93        group.topLevelSlices.forEach(function(slice) {
94          this.topLevelSlices.push(slice);
95        }, this);
96      },
97
98      appendThreadSlices: function(rangeOfInterest, thread) {
99        var tmp = this.getSlicesIntersectingRange(
100            rangeOfInterest, thread.sliceGroup.slices);
101        tmp.forEach(function(slice) {
102          this.allSlices.push(slice);
103        }, this);
104        tmp = this.getSlicesIntersectingRange(
105            rangeOfInterest, thread.sliceGroup.topLevelSlices);
106        tmp.forEach(function(slice) {
107          this.topLevelSlices.push(slice);
108        }, this);
109      },
110
111      getSlicesIntersectingRange: function(rangeOfInterest, slices) {
112        var slicesInFilterRange = [];
113        for (var i = 0; i < slices.length; i++) {
114          var slice = slices[i];
115          if (rangeOfInterest.intersectsExplicitRangeInclusive(
116                slice.start, slice.end))
117            slicesInFilterRange.push(slice);
118        }
119        return slicesInFilterRange;
120      }
121    };
122
123    Polymer({
124      ready: function() {
125        this.rangeOfInterest_ = new tr.b.Range();
126        this.selection_ = undefined;
127        this.groupBy_ = GROUP_BY_PROCESS_NAME;
128        this.groupingUnit_ = CPU_TIME_GROUPING_UNIT;
129        this.showCpuIdleTime_ = true;
130        this.chart_ = undefined;
131
132        var toolbarEl = this.$.toolbar;
133        this.groupBySelector_ = tr.ui.b.createSelector(
134            this, 'groupBy',
135            'timeSummarySidePanel.groupBy', this.groupBy_,
136            [{label: 'Group by process', value: GROUP_BY_PROCESS_NAME},
137             {label: 'Group by thread', value: GROUP_BY_THREAD_NAME}
138            ]);
139        toolbarEl.appendChild(this.groupBySelector_);
140
141        this.groupingUnitSelector_ = tr.ui.b.createSelector(
142            this, 'groupingUnit',
143            'timeSummarySidePanel.groupingUnit', this.groupingUnit_,
144            [{label: 'Wall time', value: WALL_TIME_GROUPING_UNIT},
145             {label: 'CPU time', value: CPU_TIME_GROUPING_UNIT}
146            ]);
147        toolbarEl.appendChild(this.groupingUnitSelector_);
148
149        this.showCpuIdleTimeCheckbox_ = tr.ui.b.createCheckBox(
150            this, 'showCpuIdleTime',
151            'timeSummarySidePanel.showCpuIdleTime', this.showCpuIdleTime_,
152            'Show CPU idle time');
153        toolbarEl.appendChild(this.showCpuIdleTimeCheckbox_);
154        this.updateShowCpuIdleTimeCheckboxVisibility_();
155      },
156
157      /**
158       * This function takes an array of groups and merges smaller groups into
159       * the provided 'Other' group item such that the remaining items are ready
160       * for pie-chart consumption. Otherwise, the pie chart gets overwhelmed
161       * with tons of little slices.
162       */
163      trimPieChartData: function(groups, otherGroup, getValue, opt_extraValue) {
164        // Copy the array so it can be mutated.
165        groups = groups.filter(function(d) {
166          return getValue(d) != 0;
167        });
168
169        // Figure out total array range.
170        var sum = tr.b.Statistics.sum(groups, getValue);
171        if (opt_extraValue !== undefined)
172          sum += opt_extraValue;
173
174        // Sort by value.
175        function compareByValue(a, b) {
176          return getValue(a) - getValue(b);
177        }
178        groups.sort(compareByValue);
179
180        // Now start fusing elements until none are less than threshold in size.
181        var thresshold = 0.1 * sum;
182        while (groups.length > 1) {
183          var group = groups[0];
184          if (getValue(group) >= thresshold)
185            break;
186
187          var v = getValue(group);
188          if (v + getValue(otherGroup) > thresshold)
189            break;
190
191          // Remove the group from the list and add it to the 'Other' group.
192          groups.splice(0, 1);
193          otherGroup.appendGroupContents(group);
194        }
195
196        // Final return.
197        if (getValue(otherGroup) > 0)
198          groups.push(otherGroup);
199
200        groups.sort(compareByValue);
201
202        return groups;
203      },
204
205      generateResultsForGroup: function(model, name) {
206        return new ResultsForGroup(model, name);
207      },
208
209      createPieChartFromResultGroups: function(
210          groups, title, getValue, opt_extraData) {
211        var chart = new tr.ui.b.PieChart();
212
213        function pushDataForGroup(data, resultsForGroup, value) {
214          data.push({
215            label: resultsForGroup.name,
216            value: value,
217            valueText: tr.v.Unit.byName.timeDurationInMs.format(value),
218            resultsForGroup: resultsForGroup
219          });
220        }
221        chart.addEventListener('item-click', function(clickEvent) {
222          var resultsForGroup = clickEvent.data.resultsForGroup;
223          if (resultsForGroup === undefined)
224            return;
225
226          var event = new tr.model.RequestSelectionChangeEvent();
227          event.selection = new tr.model.EventSet(resultsForGroup.allSlices);
228          event.selection.timeSummaryGroupName = resultsForGroup.name;
229          chart.dispatchEvent(event);
230        });
231
232
233        // Build chart data.
234        var data = [];
235        groups.forEach(function(resultsForGroup) {
236          var value = getValue(resultsForGroup);
237          if (value === 0)
238            return;
239          pushDataForGroup(data, resultsForGroup, value);
240        });
241        if (opt_extraData)
242          data.push.apply(data, opt_extraData);
243
244        chart.chartTitle = title;
245        chart.data = data;
246        return chart;
247      },
248
249      get model() {
250        return this.model_;
251      },
252
253      set model(model) {
254        this.model_ = model;
255        this.updateContents_();
256      },
257
258      get groupBy() {
259        return groupBy_;
260      },
261
262      set groupBy(groupBy) {
263        this.groupBy_ = groupBy;
264        if (this.groupBySelector_)
265          this.groupBySelector_.selectedValue = groupBy;
266        this.updateContents_();
267      },
268
269      get groupingUnit() {
270        return groupingUnit_;
271      },
272
273      set groupingUnit(groupingUnit) {
274        this.groupingUnit_ = groupingUnit;
275        if (this.groupingUnitSelector_)
276          this.groupingUnitSelector_.selectedValue = groupingUnit;
277        this.updateShowCpuIdleTimeCheckboxVisibility_();
278        this.updateContents_();
279      },
280
281      get showCpuIdleTime() {
282        return this.showCpuIdleTime_;
283      },
284
285      set showCpuIdleTime(showCpuIdleTime) {
286        this.showCpuIdleTime_ = showCpuIdleTime;
287        if (this.showCpuIdleTimeCheckbox_)
288          this.showCpuIdleTimeCheckbox_.checked = showCpuIdleTime;
289        this.updateContents_();
290      },
291
292      updateShowCpuIdleTimeCheckboxVisibility_: function() {
293        if (!this.showCpuIdleTimeCheckbox_)
294          return;
295        var visible = this.groupingUnit_ == CPU_TIME_GROUPING_UNIT;
296        if (visible)
297          this.showCpuIdleTimeCheckbox_.style.display = '';
298        else
299          this.showCpuIdleTimeCheckbox_.style.display = 'none';
300      },
301
302      getGroupNameForThread_: function(thread) {
303        if (this.groupBy_ == GROUP_BY_THREAD_NAME)
304          return thread.name ? thread.name : thread.userFriendlyName;
305
306        if (this.groupBy_ == GROUP_BY_PROCESS_NAME)
307          return thread.parent.userFriendlyName;
308      },
309
310      updateContents_: function() {
311        var resultArea = this.$.result_area;
312        this.chart_ = undefined;
313        resultArea.textContent = '';
314
315        if (this.model_ === undefined)
316          return;
317
318        var rangeOfInterest;
319        if (this.rangeOfInterest_.isEmpty)
320          rangeOfInterest = this.model_.bounds;
321        else
322          rangeOfInterest = this.rangeOfInterest_;
323
324        var allGroup = this.generateResultsForGroup(this.model_, 'all');
325        var resultsByGroupName = {};
326        this.model_.getAllThreads().forEach(function(thread) {
327          var groupName = this.getGroupNameForThread_(thread);
328          if (resultsByGroupName[groupName] === undefined) {
329            resultsByGroupName[groupName] = this.generateResultsForGroup(
330                this.model_, groupName);
331          }
332          resultsByGroupName[groupName].appendThreadSlices(
333              rangeOfInterest, thread);
334
335          allGroup.appendThreadSlices(rangeOfInterest, thread);
336        }, this);
337
338        // Helper function for working with the produced group.
339        var getValueFromGroup = function(group) {
340          if (this.groupingUnit_ == WALL_TIME_GROUPING_UNIT)
341            return group.wallTime;
342          return group.cpuTime;
343        }.bind(this);
344
345        // Create summary.
346        var summaryText = document.createElement('div');
347        summaryText.appendChild(tr.ui.b.createSpan({
348          textContent: 'Total ' + this.groupingUnit_ + ': ',
349          bold: true}));
350        summaryText.appendChild(tr.v.ui.createScalarSpan(
351            getValueFromGroup(allGroup), {
352              unit: tr.v.Unit.byName.timeDurationInMs,
353              ownerDocument: this.ownerDocument
354            }));
355        resultArea.appendChild(summaryText);
356
357        // If needed, add in the idle time.
358        var extraValue = 0;
359        var extraData = [];
360        if (this.showCpuIdleTime_ &&
361            this.groupingUnit_ === CPU_TIME_GROUPING_UNIT &&
362            this.model.kernel.bestGuessAtCpuCount !== undefined) {
363          var maxCpuTime = rangeOfInterest.range *
364              this.model.kernel.bestGuessAtCpuCount;
365          var idleTime = Math.max(0, maxCpuTime - allGroup.cpuTime);
366          extraData.push({
367            label: 'CPU Idle',
368            value: idleTime,
369            valueText: tr.v.Unit.byName.timeDurationInMs.format(idleTime)
370          });
371          extraValue += idleTime;
372        }
373
374        // Create the actual chart.
375        var otherGroup = this.generateResultsForGroup(this.model_, 'Other');
376        var groups = this.trimPieChartData(
377            tr.b.dictionaryValues(resultsByGroupName),
378            otherGroup,
379            getValueFromGroup,
380            extraValue);
381
382        if (groups.length == 0) {
383          resultArea.appendChild(tr.ui.b.createSpan({textContent: 'No data'}));
384          return undefined;
385        }
386
387        this.chart_ = this.createPieChartFromResultGroups(
388            groups,
389            this.groupingUnit_ + ' breakdown by ' + this.groupBy_,
390            getValueFromGroup, extraData);
391        resultArea.appendChild(this.chart_);
392
393        this.chart_.addEventListener('click', function() {
394          var event = new tr.model.RequestSelectionChangeEvent();
395          event.selection = new tr.c.EventSet([]);
396          this.dispatchEvent(event);
397        });
398        this.chart_.setSize(this.chart_.getMinSize());
399      },
400
401      get selection() {
402        return selection_;
403      },
404
405      set selection(selection) {
406        this.selection_ = selection;
407
408        if (this.chart_ === undefined)
409          return;
410
411        if (selection.timeSummaryGroupName)
412          this.chart_.highlightedLegendKey = selection.timeSummaryGroupName;
413        else
414          this.chart_.highlightedLegendKey = undefined;
415      },
416
417      get rangeOfInterest() {
418        return this.rangeOfInterest_;
419      },
420
421      set rangeOfInterest(rangeOfInterest) {
422        this.rangeOfInterest_ = rangeOfInterest;
423        this.updateContents_();
424      },
425
426      supportsModel: function(model) {
427        return {
428          supported: false
429        };
430      },
431
432      get textLabel() {
433        return 'Time Summary';
434      }
435    });
436  }());
437  </script>
438</polymer-element>
439