1// Copyright 2013 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/**
6 * This class provides data access interface for dump file profiler.
7 * @constructor
8 */
9var Profiler = function(jsonData, template) {
10  this.jsonData_ = jsonData;
11  // Initialize template with templates information.
12  this.template_ = template ||
13      (jsonData.default_template &&
14       jsonData.templates[jsonData.default_template]) ||
15      jsonData.templates['l2'];
16  // Initialize selected category, and nothing selected at first.
17  this.selected_ = null;
18
19  // Trigger event.
20  this.callbacks_ = {};
21};
22
23/**
24 * Mimic Eventemitter in node. Add new listener for event.
25 * @param {string} event
26 * @param {Function} callback
27 */
28Profiler.prototype.addListener = function(event, callback) {
29  if (!this.callbacks_[event])
30    this.callbacks_[event] = $.Callbacks();
31  this.callbacks_[event].add(callback);
32};
33
34/**
35 * This function will emit the event.
36 * @param {string} event
37 */
38Profiler.prototype.emit = function(event) {
39  // Listeners should be able to receive arbitrary number of parameters.
40  var eventArguments = Array.prototype.slice.call(arguments, 1);
41
42  if (this.callbacks_[event])
43    this.callbacks_[event].fire.apply(this, eventArguments);
44};
45
46/**
47 * Remove listener from event.
48 * @param {string} event
49 * @param {Function} callback
50 */
51Profiler.prototype.removeListener = function(event, callback) {
52  if (this.callbacks_[event])
53    this.callbacks_[event].remove(callback);
54};
55
56/**
57 * Calcualte initial models according default template.
58 */
59Profiler.prototype.reparse = function() {
60  this.models_ = this.parseTemplate_();
61  this.emit('changed', this.models_);
62};
63
64/**
65 * Get current breakdown template.
66 * @return {Object} current breakdown template.
67 */
68Profiler.prototype.getTemplate = function() {
69  return this.template_;
70};
71
72/**
73 * Get run_id of current profiler.
74 * @return {string} run_id of current profiler.
75 */
76Profiler.prototype.getRunId = function() {
77  return this.jsonData_['run_id'];
78};
79
80/**
81 * To be called by view when new model being selected.
82 * And then triggers all relative views to update.
83 * @param {string} id Model id.
84 * @param {Object} pos Clicked position.
85 */
86Profiler.prototype.setSelected = function(id, pos) {
87  this.selected_ = id;
88  this.emit('changed:selected', id, pos);
89};
90
91/**
92 * Get all models throughout the whole timeline of given id.
93 * @param {string} id Model id.
94 * @return {Array.<Object>} model array of given id.
95 */
96Profiler.prototype.getModelsbyId = function(id) {
97  function find(model) {
98    if (model.id === id)
99      return model;
100    if ('children' in model)
101      return model.children.reduce(function(previous, current) {
102        var matched = find(current);
103        if (matched)
104          previous = matched;
105        return previous;
106      }, null);
107  }
108
109  return this.models_.reduce(function(previous, current) {
110    var matched = find(current);
111    if (matched)
112      previous.push(matched);
113    return previous;
114  }, []);
115};
116
117/**
118 * Get current sub of given model, return undefined if sub dont exist.
119 * @param {string} id Model id.
120 * @return {undefined|string} world-breakdown like 'vm-map'.
121 */
122Profiler.prototype.getCurSubById = function(id) {
123  // Root won't has breakdown.
124  var path = id.split(',').splice(1);
125  if (!path.length) return null;
126
127  var tmpl = this.template_;
128  var curSub = path.reduce(function(previous, current, index) {
129    return previous[2][current];
130  }, tmpl);
131
132  // return
133  return curSub && curSub[0] + ',' + curSub[1];
134};
135
136/**
137 * Generate and then reparse new template when new sub was selected.
138 * @param {string|null} sub World-breakdown like 'vm-map'.
139 */
140Profiler.prototype.setSub = function(sub) {
141  var selected = this.selected_;
142  var path = selected.split(',');
143  var key = path[path.length - 1];
144
145  // Add sub breakdown to template.
146  var models = this.getModelsbyId(selected);
147  var subTmpl = sub.split(',');
148  subTmpl.push({});
149  models[0].template[2][key] = subTmpl;
150
151  // Recalculate new template.
152  this.reparse();
153};
154
155/**
156 * Remove children of figured node and reparse whole tree.
157 * @param {string} id World-breakdown like 'vm-map'.
158 */
159Profiler.prototype.unsetSub = function(id) {
160  var models = this.getModelsbyId(id);
161  if (!('template' in models[0]))
162    return;
163
164  var path = id.split(',');
165  var key = path[path.length - 1];
166  if (!(key in models[0].template[2]))
167    return;
168  delete (models[0].template[2][key]);
169
170  // Recalculate new template.
171  this.reparse();
172};
173
174/**
175 * Calculate the model of certain snapshot.
176 * @param {string} template Local template.
177 * @param {Object} snapshot Current snapshot.
178 * @param {Object} worldUnits Mapping of world units.
179 * @param {Array.<number>} localUnits Array of local units.
180 * @param {string} name Local node path.
181 * @return {Object} Return model, total size and remaining units.
182 * @private
183 */
184Profiler.prototype.accumulate_ = function(
185  template, snapshot, worldUnits, localUnits, name) {
186  var self = this;
187  var totalSize = 0;
188  var worldName = template[0];
189  var breakdownName = template[1];
190  var categories = snapshot.worlds[worldName].breakdown[breakdownName];
191  var matchedUnitsSet = {};
192  var model = {
193    name: name || worldName + '-' + breakdownName,
194    time: snapshot.time,
195    children: []
196  };
197
198  localUnits.sort(function(a, b) { return b - a; });
199  Object.keys(categories).forEach(function(categoryName) {
200    var category = categories[categoryName];
201    if (category['hidden'] === true)
202      return;
203    category.units.sort(function(a, b) { return b - a; });
204    // Filter units.
205    var matchedUnits = intersectionOfSorted(category.units, localUnits);
206    matchedUnits.forEach(function(unit) {
207      matchedUnitsSet[unit] = unit;
208    });
209
210    // Accumulate categories.
211    var size = matchedUnits.reduce(function(previous, current) {
212      return previous + worldUnits[worldName][current];
213    }, 0);
214    totalSize += size;
215
216    // Handle subs options if exists.
217    var child = null;
218    if (!(categoryName in template[2])) {
219      // Calculate child for current category.
220      child = {
221        name: categoryName,
222        size: size
223      };
224      if ('subs' in category && category.subs.length) {
225        child.subs = category.subs;
226        child.template = template;
227      }
228
229      model.children.push(child);
230    } else {
231      // Calculate child recursively.
232      var subTemplate = template[2][categoryName];
233      var subWorldName = subTemplate[0];
234      var retVal = null;
235
236      if (subWorldName === worldName) {
237        // If subs is in the same world, units should be filtered.
238        retVal = self.accumulate_(subTemplate, snapshot, worldUnits,
239          matchedUnits, categoryName);
240        if ('subs' in category && category.subs.length) {
241          retVal.model.subs = category.subs;
242          retVal.model.template = template;
243        }
244        model.children.push(retVal.model);
245        // Don't output remaining item without any unit.
246        if (!retVal.remainderUnits.length)
247          return;
248
249        // Sum up remaining units size.
250        var remainSize =
251          retVal.remainderUnits.reduce(function(previous, current) {
252            return previous + worldUnits[subWorldName][current];
253          }, 0);
254
255        retVal.model.children.push({
256          name: categoryName + '-remaining',
257          size: remainSize
258        });
259      } else {
260        // If subs is in different world, use all units in that world.
261        var subLocalUnits = Object.keys(worldUnits[subWorldName]);
262        subLocalUnits = subLocalUnits.map(function(unitID) {
263          return parseInt(unitID, 10);
264        });
265
266        retVal = self.accumulate_(subTemplate, snapshot, worldUnits,
267          subLocalUnits, categoryName);
268        if ('subs' in category && category.subs.length) {
269          retVal.model.subs = category.subs;
270          retVal.model.template = template;
271        }
272        model.children.push(retVal.model);
273
274        if (size > retVal.totalSize) {
275          retVal.model.children.push({
276            name: categoryName + '-remaining',
277            size: size - retVal.totalSize
278          });
279        } else if (size < retVal.totalSize) {
280          // Output WARNING when sub-breakdown size is larger.
281          console.log('WARNING: size of sub-breakdown is larger');
282        }
283      }
284    }
285  });
286
287  var remainderUnits = localUnits.reduce(function(previous, current) {
288    if (!(current in matchedUnitsSet))
289      previous.push(current);
290    return previous;
291  }, []);
292
293  return {
294    model: model,
295    totalSize: totalSize,
296    remainderUnits: remainderUnits
297  };
298};
299
300/**
301 * Parse template and calculate models of the whole timeline.
302 * @return {Array.<Object>} Models of the whole timeline.
303 * @private
304 */
305Profiler.prototype.parseTemplate_ = function() {
306  function calModelId(model, localPath) {
307    // Create unique id for every model.
308    model.id = localPath.length ?
309      localPath.join() + ',' + model.name : model.name;
310
311    if ('children' in model) {
312      model.children.forEach(function(child, index) {
313        var childPath = localPath.slice(0);
314        childPath.push(model.name);
315        calModelId(child, childPath);
316      });
317    }
318  }
319
320  var self = this;
321
322  return self.jsonData_.snapshots.map(function(snapshot) {
323    var worldUnits = {};
324    for (var worldName in snapshot.worlds) {
325      worldUnits[worldName] = {};
326      var units = snapshot.worlds[worldName].units;
327      for (var unitID in units)
328        worldUnits[worldName][unitID] = units[unitID][0];
329    }
330    var localUnits = Object.keys(worldUnits[self.template_[0]]);
331    localUnits = localUnits.map(function(unitID) {
332      return parseInt(unitID, 10);
333    });
334
335    var retVal =
336      self.accumulate_(self.template_, snapshot, worldUnits, localUnits);
337    calModelId(retVal.model, []);
338    return retVal.model;
339  });
340};
341