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