1 // Copyright (c) 2012 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 * @fileoverview TraceEventImporter imports TraceEvent-formatted data 7 * into the provided timeline model. 8 */ 9 base.require('timeline_model'); 10 base.require('timeline_color_scheme'); 11 base.exportTo('tracing', function() { 12 13 function TraceEventImporter(model, eventData) { 14 this.importPriority = 1; 15 this.model_ = model; 16 17 if (typeof(eventData) === 'string' || eventData instanceof String) { 18 // If the event data begins with a [, then we know it should end with a ]. 19 // The reason we check for this is because some tracing implementations 20 // cannot guarantee that a ']' gets written to the trace file. So, we are 21 // forgiving and if this is obviously the case, we fix it up before 22 // throwing the string at JSON.parse. 23 if (eventData[0] == '[') { 24 n = eventData.length; 25 if (eventData[n - 1] == '\n') { 26 eventData = eventData.substring(0, n - 1); 27 n--; 28 29 if (eventData[n - 1] == '\r') { 30 eventData = eventData.substring(0, n - 1); 31 n--; 32 } 33 } 34 35 if (eventData[n - 1] == ',') 36 eventData = eventData.substring(0, n - 1); 37 if (eventData[n - 1] != ']') 38 eventData = eventData + ']'; 39 } 40 41 this.events_ = JSON.parse(eventData); 42 43 } else { 44 this.events_ = eventData; 45 } 46 47 // Some trace_event implementations put the actual trace events 48 // inside a container. E.g { ... , traceEvents: [ ] } 49 // If we see that, just pull out the trace events. 50 if (this.events_.traceEvents) { 51 this.events_ = this.events_.traceEvents; 52 for (fieldName in this.events_) { 53 if (fieldName == 'traceEvents') 54 continue; 55 this.model_.metadata.push({name: fieldName, 56 value: this.events_[fieldName]}); 57 } 58 } 59 60 // Async events need to be processed durign finalizeEvents 61 this.allAsyncEvents_ = []; 62 } 63 64 /** 65 * @return {boolean} Whether obj is a TraceEvent array. 66 */ 67 TraceEventImporter.canImport = function(eventData) { 68 // May be encoded JSON. But we dont want to parse it fully yet. 69 // Use a simple heuristic: 70 // - eventData that starts with [ are probably trace_event 71 // - eventData that starts with { are probably trace_event 72 // May be encoded JSON. Treat files that start with { as importable by us. 73 if (typeof(eventData) === 'string' || eventData instanceof String) { 74 return eventData[0] == '{' || eventData[0] == '['; 75 } 76 77 // Might just be an array of events 78 if (eventData instanceof Array && eventData.length && eventData[0].ph) 79 return true; 80 81 // Might be an object with a traceEvents field in it. 82 if (eventData.traceEvents) 83 return eventData.traceEvents instanceof Array && 84 eventData.traceEvents[0].ph; 85 86 return false; 87 }; 88 89 TraceEventImporter.prototype = { 90 91 __proto__: Object.prototype, 92 93 /** 94 * Helper to process an 'async finish' event, which will close an open slice 95 * on a TimelineAsyncSliceGroup object. 96 */ 97 processAsyncEvent: function(index, event) { 98 var thread = this.model_.getOrCreateProcess(event.pid). 99 getOrCreateThread(event.tid); 100 this.allAsyncEvents_.push({ 101 event: event, 102 thread: thread}); 103 }, 104 105 /** 106 * Helper that creates and adds samples to a TimelineCounter object based on 107 * 'C' phase events. 108 */ 109 processCounterEvent: function(event) { 110 var ctr_name; 111 if (event.id !== undefined) 112 ctr_name = event.name + '[' + event.id + ']'; 113 else 114 ctr_name = event.name; 115 116 var ctr = this.model_.getOrCreateProcess(event.pid) 117 .getOrCreateCounter(event.cat, ctr_name); 118 // Initialize the counter's series fields if needed. 119 if (ctr.numSeries == 0) { 120 for (var seriesName in event.args) { 121 ctr.seriesNames.push(seriesName); 122 ctr.seriesColors.push( 123 tracing.getStringColorId(ctr.name + '.' + seriesName)); 124 } 125 if (ctr.numSeries == 0) { 126 this.model_.importErrors.push('Expected counter ' + event.name + 127 ' to have at least one argument to use as a value.'); 128 // Drop the counter. 129 delete ctr.parent.counters[ctr.name]; 130 return; 131 } 132 } 133 134 // Add the sample values. 135 ctr.timestamps.push(event.ts / 1000); 136 for (var i = 0; i < ctr.numSeries; i++) { 137 var seriesName = ctr.seriesNames[i]; 138 if (event.args[seriesName] === undefined) { 139 ctr.samples.push(0); 140 continue; 141 } 142 ctr.samples.push(event.args[seriesName]); 143 } 144 }, 145 146 /** 147 * Walks through the events_ list and outputs the structures discovered to 148 * model_. 149 */ 150 importEvents: function() { 151 // Walk through events 152 var events = this.events_; 153 // Some events cannot be handled until we have done a first pass over the 154 // data set. So, accumulate them into a temporary data structure. 155 var second_pass_events = []; 156 for (var eI = 0; eI < events.length; eI++) { 157 var event = events[eI]; 158 if (event.ph == 'B') { 159 var thread = this.model_.getOrCreateProcess(event.pid) 160 .getOrCreateThread(event.tid); 161 if (!thread.isTimestampValidForBeginOrEnd(event.ts / 1000)) { 162 this.model_.importErrors.push( 163 'Timestamps are moving backward.'); 164 continue; 165 } 166 thread.beginSlice(event.cat, event.name, event.ts / 1000, event.args); 167 } else if (event.ph == 'E') { 168 var thread = this.model_.getOrCreateProcess(event.pid) 169 .getOrCreateThread(event.tid); 170 if (!thread.isTimestampValidForBeginOrEnd(event.ts / 1000)) { 171 this.model_.importErrors.push( 172 'Timestamps are moving backward.'); 173 continue; 174 } 175 if (!thread.openSliceCount) { 176 this.model_.importErrors.push( 177 'E phase event without a matching B phase event.'); 178 continue; 179 } 180 181 var slice = thread.endSlice(event.ts / 1000); 182 for (var arg in event.args) { 183 if (slice.args[arg] !== undefined) { 184 this.model_.importErrors.push( 185 'Both the B and E phases of ' + slice.name + 186 'provided values for argument ' + arg + '. ' + 187 'The value of the E phase event will be used.'); 188 } 189 slice.args[arg] = event.args[arg]; 190 } 191 192 } else if (event.ph == 'S') { 193 this.processAsyncEvent(eI, event); 194 } else if (event.ph == 'F') { 195 this.processAsyncEvent(eI, event); 196 } else if (event.ph == 'T') { 197 this.processAsyncEvent(eI, event); 198 } else if (event.ph == 'I') { 199 // Treat an Instant event as a duration 0 slice. 200 // TimelineSliceTrack's redraw() knows how to handle this. 201 var thread = this.model_.getOrCreateProcess(event.pid) 202 .getOrCreateThread(event.tid); 203 thread.beginSlice(event.cat, event.name, event.ts / 1000, event.args); 204 thread.endSlice(event.ts / 1000); 205 } else if (event.ph == 'C') { 206 this.processCounterEvent(event); 207 } else if (event.ph == 'M') { 208 if (event.name == 'thread_name') { 209 var thread = this.model_.getOrCreateProcess(event.pid) 210 .getOrCreateThread(event.tid); 211 thread.name = event.args.name; 212 } else { 213 this.model_.importErrors.push( 214 'Unrecognized metadata name: ' + event.name); 215 } 216 } else if (event.ph == 's') { 217 // NB: toss until there's proper support 218 } else if (event.ph == 't') { 219 // NB: toss until there's proper support 220 } else if (event.ph == 'f') { 221 // NB: toss until there's proper support 222 } else { 223 this.model_.importErrors.push( 224 'Unrecognized event phase: ' + event.ph + 225 '(' + event.name + ')'); 226 } 227 } 228 }, 229 230 /** 231 * Called by the TimelineModel after all other importers have imported their 232 * events. 233 */ 234 finalizeImport: function() { 235 this.createAsyncSlices_(); 236 }, 237 238 createAsyncSlices_: function() { 239 if (this.allAsyncEvents_.length == 0) 240 return; 241 242 this.allAsyncEvents_.sort(function(x, y) { 243 return x.event.ts - y.event.ts; 244 }); 245 246 var asyncEventStatesByNameThenID = {}; 247 248 var allAsyncEvents = this.allAsyncEvents_; 249 for (var i = 0; i < allAsyncEvents.length; i++) { 250 var asyncEventState = allAsyncEvents[i]; 251 252 var event = asyncEventState.event; 253 var name = event.name; 254 if (name === undefined) { 255 this.model_.importErrors.push( 256 'Async events (ph: S, T or F) require an name parameter.'); 257 continue; 258 } 259 260 var id = event.id; 261 if (id === undefined) { 262 this.model_.importErrors.push( 263 'Async events (ph: S, T or F) require an id parameter.'); 264 continue; 265 } 266 267 // TODO(simonjam): Add a synchronous tick on the appropriate thread. 268 269 if (event.ph == 'S') { 270 if (asyncEventStatesByNameThenID[name] === undefined) 271 asyncEventStatesByNameThenID[name] = {}; 272 if (asyncEventStatesByNameThenID[name][id]) { 273 this.model_.importErrors.push( 274 'At ' + event.ts + ', a slice of the same id ' + id + 275 ' was alrady open.'); 276 continue; 277 } 278 asyncEventStatesByNameThenID[name][id] = []; 279 asyncEventStatesByNameThenID[name][id].push(asyncEventState); 280 } else { 281 if (asyncEventStatesByNameThenID[name] === undefined) { 282 this.model_.importErrors.push( 283 'At ' + event.ts + ', no slice named ' + name + 284 ' was open.'); 285 continue; 286 } 287 if (asyncEventStatesByNameThenID[name][id] === undefined) { 288 this.model_.importErrors.push( 289 'At ' + event.ts + ', no slice named ' + name + 290 ' with id=' + id + ' was open.'); 291 continue; 292 } 293 var events = asyncEventStatesByNameThenID[name][id]; 294 events.push(asyncEventState); 295 296 if (event.ph == 'F') { 297 // Create a slice from start to end. 298 var slice = new tracing.TimelineAsyncSlice( 299 events[0].event.cat, 300 name, 301 tracing.getStringColorId(name), 302 events[0].event.ts / 1000); 303 304 slice.duration = (event.ts / 1000) - (events[0].event.ts / 1000); 305 306 slice.startThread = events[0].thread; 307 slice.endThread = asyncEventState.thread; 308 slice.id = id; 309 slice.args = events[0].event.args; 310 slice.subSlices = []; 311 312 // Create subSlices for each step. 313 for (var j = 1; j < events.length; ++j) { 314 var subName = name; 315 if (events[j - 1].event.ph == 'T') 316 subName = name + ':' + events[j - 1].event.args.step; 317 var subSlice = new tracing.TimelineAsyncSlice( 318 events[0].event.cat, 319 subName, 320 tracing.getStringColorId(name + j), 321 events[j - 1].event.ts / 1000); 322 323 subSlice.duration = 324 (events[j].event.ts / 1000) - (events[j - 1].event.ts / 1000); 325 326 subSlice.startThread = events[j - 1].thread; 327 subSlice.endThread = events[j].thread; 328 subSlice.id = id; 329 subSlice.args = events[j - 1].event.args; 330 331 slice.subSlices.push(subSlice); 332 } 333 334 // The args for the finish event go in the last subSlice. 335 var lastSlice = slice.subSlices[slice.subSlices.length - 1]; 336 for (var arg in event.args) 337 lastSlice.args[arg] = event.args[arg]; 338 339 // Add |slice| to the start-thread's asyncSlices. 340 slice.startThread.asyncSlices.push(slice); 341 delete asyncEventStatesByNameThenID[name][id]; 342 } 343 } 344 } 345 } 346 }; 347 348 tracing.TimelineModel.registerImporter(TraceEventImporter); 349 350 return { 351 TraceEventImporter: TraceEventImporter 352 }; 353 }); 354