1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// This module handles the UI for the profiling. A memory profile is obtained
6// asking the webservice to profile some data sources (through /profile/create)
7// as, for instance, some mmaps dumps or some heap traces.
8// Regardless of the data source, a profile consists of three main concepts:
9// 1. A tree of buckets, each one having a name and a set of 1+ values (see
10//    /classification/rules.py,results.py), one value per metric (see below).
11// 2. A set of snapshots, identifying the times when the dump was taken. All the
12//    snapshots have the same shape and the same nodes (but their values can
13//    obviously differ).
14// 3. A set of metrics, identifying the cardinality (how many) and the semantic
15//    (what do they mean) of the values of the nodes in the results tree.
16//
17// From a graphical viewpoint a profile is displayed using two charts:
18// - A tree (organizational) chart which shows, for a given snapshot and metric,
19//   the taxonomy of the buckets and their corresponding value.
20// - A time series (scattered area) chart which shows, for a given metric and a
21//   given bucket, its evolution over time (and of its direct children).
22
23profiler = new (function() {
24
25this.rulesets = {'nheap': [], 'mmap': []};
26this.treeData_ = null;
27this.treeChart_ = null;
28this.timeSeriesData_ = null;
29this.timeSeriesChart_ = null;
30this.isRedrawing_ = false;
31this.profileId_ = null;  // The profile id retrieved on /ajax/profile/create.
32this.times_ = [];  // Snapshot times: [0,4,8] -> 3 snapshots x 4 sec.
33this.metrics_ = [];  // Keys in the result tree, e.g., ['RSS', 'PSS'].
34this.curTime_ = null;  // Time of the snapshot currently displayed.
35this.curMetric_ = null;  // Index (rel. to |metrics_|) currently displayed.
36this.curBucket_ = null;  // Index (rel. to the tree) currently displayed.
37
38this.onDomReady_ = function() {
39  this.treeChart_ = new google.visualization.OrgChart($('#prof-tree_chart')[0]);
40  this.timeSeriesChart_ = new google.visualization.SteppedAreaChart(
41      $('#prof-time_chart')[0]);
42
43  // Setup the UI event listeners to trigger the onUiParamsChange_ event.
44  google.visualization.events.addListener(this.treeChart_, 'select',
45                                          this.onUiParamsChange_.bind(this));
46  $('#prof-metric').on('change', this.onUiParamsChange_.bind(this));
47  $('#prof-time').slider({range: 'max', min: 0, max: 0, value: 0,
48                          change: this.onUiParamsChange_.bind(this)});
49
50  // Load the available profiler rules.
51  webservice.ajaxRequest('/profile/rules',
52                         this.OnRulesAjaxResponse_.bind(this));
53};
54
55this.profileCachedMmapDump = function(mmapDumpId, ruleset) {
56  // Creates a profile using the data grabbed during a recent mmap dump.
57  // This is used to get a quick overview (only one snapshot), of the memory
58  // without doing a full periodic trace first.
59  ruleset = ruleset || this.rulesets['mmap'][0];
60  webservice.ajaxRequest('/profile/create',  // This is a POST request.
61                         this.onProfileAjaxResponse_.bind(this, ruleset),
62                         null,  // use the default error handler.
63                         {type: 'mmap',
64                          source: 'cache',
65                          id: mmapDumpId,
66                          ruleset: ruleset});
67};
68
69this.profileArchivedMmaps = function(archiveName, snapshots, ruleset) {
70  ruleset = ruleset || this.rulesets['mmap'][0];
71  // Creates a mmap profile using the data from the storage.
72  webservice.ajaxRequest('/profile/create',  // This is a POST request.
73                         this.onProfileAjaxResponse_.bind(this, ruleset),
74                         null,  // use the default error handler.
75                         {type: 'mmap',
76                          source: 'archive',
77                          archive: archiveName,
78                          snapshots: snapshots,
79                          ruleset: ruleset});
80};
81
82this.profileArchivedNHeaps = function(archiveName, snapshots, ruleset) {
83  // Creates a native-heap profile using the data from the storage.
84  ruleset = ruleset || this.rulesets['nheap'][0];
85  webservice.ajaxRequest('/profile/create',  // This is a POST request.
86                         this.onProfileAjaxResponse_.bind(this, ruleset),
87                         null,  // use the default error handler.
88                         {type: 'nheap',
89                          source: 'archive',
90                          archive: archiveName,
91                          snapshots: snapshots,
92                          ruleset: ruleset});
93};
94
95this.OnRulesAjaxResponse_ = function(data) {
96  // This AJAX response contains essentially the directory listing of the
97  // memory_inspector/classification_rules/ folder.
98  console.assert('nheap' in data && 'mmap' in data);
99  this.rulesets = data;
100};
101
102this.onProfileAjaxResponse_ = function(ruleset, data) {
103  // This AJAX response contains a summary of the profile requested via the
104  // /profile endpoint, which consists of:
105  // - The number of snapshots (and their corresponding time) in an array.
106  //   e.g., [0, 3 ,6] indicates that the profile contains three snapshots taken
107  //   respectively at T=0, T=3 and T=6 sec.
108  // - A list of profile metrics, e.g., ['RSS', 'P. Dirty'] indicates that every
109  //   node in the result tree is a 2-tuple.
110  // After this response, the concrete data for the charts can be fetched using
111  // the /ajax/profile/{ID}/tree and /ajax/profile/{ID}/time_serie endpoints.
112  this.profileId_ = data.id;
113  this.times_ = data.times;  // An array of integers.
114  this.metrics_ = data.metrics;  // An array of strings.
115  this.curBucket_ = data.rootBucket;  // URI of the bucket, e.g., Total/Libs/.
116  this.curTime_ = data.times[0];
117  this.curMetric_ = 0;
118
119  // Populate the rules label with the ruleset used for generating this profile.
120  $('#prof-ruleset').text(ruleset);
121
122  // Populate the "metrics" select box.
123  $('#prof-metric').empty();
124  this.metrics_.forEach(function(metric) {
125    $('#prof-metric').append($('<option/>').text(metric));
126  }, this);
127
128  // Setup the bounds of the snapshots slider.
129  $('#prof-time').slider('option', 'max', this.times_.length - 1);
130
131  // Fetch the actual chart data (via /profile/{ID}/...) and redraw the charts.
132  this.updateCharts();
133};
134
135this.onUiParamsChange_ = function() {
136  // Triggered whenever any of the UI params (the metric select, the snapshot
137  // slider or the selected bucket in the tree) changes.
138  this.curMetric_ = $('#prof-metric').prop('selectedIndex');
139  this.curTime_ = this.times_[$('#prof-time').slider('value')];
140  $('#prof-time_label').text(this.curTime_);
141  var selBucket = this.treeChart_.getSelection();
142  if (selBucket.length)
143    this.curBucket_ = this.treeData_.getValue(selBucket[0].row, 0);
144  this.updateCharts();
145};
146
147this.updateCharts = function() {
148  if (!this.profileId_)
149    return;
150
151  var profileUri = '/profile/' + this.profileId_;
152  webservice.ajaxRequest(
153      profileUri +'/tree/' + this.curMetric_ + '/' + this.curTime_,
154      this.onTreeAjaxResponse_.bind(this));
155  webservice.ajaxRequest(
156      profileUri +'/time_serie/' + this.curMetric_ + '/' + this.curBucket_,
157      this.onTimeSerieAjaxResponse_.bind(this));
158};
159
160this.onTreeAjaxResponse_ = function(data) {
161  this.treeData_ = new google.visualization.DataTable(data);
162  this.redrawTree_();
163};
164
165this.onTimeSerieAjaxResponse_ = function(data) {
166  this.timeSeriesData_ = new google.visualization.DataTable(data);
167  this.redrawTimeSerie_();
168};
169
170this.redrawTree_ = function() {
171  // isRedrawing_ is used here to break the avalanche chain that would be caused
172  // by redraw changing the node selection, triggering in turn another redraw.
173  if (!this.treeData_ || this.isRedrawing_)
174    return;
175
176  this.isRedrawing_ = true;
177  var savedSelection = this.treeChart_.getSelection();
178  this.treeChart_.draw(this.treeData_, {allowHtml: true});
179
180  // "If we want things to stay as they are, things will have to change."
181  // (work around GChart bug, as if we didn't have enough problems on our own).
182  this.treeChart_.setSelection([{row: null, column: null}]);
183  this.treeChart_.setSelection(savedSelection);
184  this.isRedrawing_ = false;
185};
186
187this.redrawTimeSerie_ = function() {
188  if (!this.timeSeriesData_)
189    return;
190
191  var metric = this.metrics_[this.curMetric_];
192  this.timeSeriesChart_.draw(this.timeSeriesData_, {
193      title: metric + ' over time for ' + this.curBucket_,
194      isStacked: true,
195      hAxis: {title: 'Time [sec.]'},
196      vAxis: {title: this.metrics_[this.curMetric_] + ' [KB]'}});
197};
198
199this.redraw = function() {
200  this.redrawTree_();
201  this.redrawTimeSerie_();
202};
203
204$(document).ready(this.onDomReady_.bind(this));
205
206})();