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 */ 9cr.define('tracing', function() { 10 function ThreadState(tid) { 11 this.openSlices = []; 12 } 13 14 function TraceEventImporter(model, eventData) { 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] != ']' && eventData[n - 1] != '\n') { 26 eventData = eventData + ']'; 27 } else if (eventData[n - 2] != ']' && eventData[n - 1] == '\n') { 28 eventData = eventData + ']'; 29 } else if (eventData[n - 3] != ']' && eventData[n - 2] == '\r' && 30 eventData[n - 1] == '\n') { 31 eventData = eventData + ']'; 32 } 33 } 34 this.events_ = JSON.parse(eventData); 35 36 } else { 37 this.events_ = eventData; 38 } 39 40 // Some trace_event implementations put the actual trace events 41 // inside a container. E.g { ... , traceEvents: [ ] } 42 // 43 // If we see that, just pull out the trace events. 44 if (this.events_.traceEvents) 45 this.events_ = this.events_.traceEvents; 46 47 // To allow simple indexing of threads, we store all the threads by a 48 // PTID. A ptid is a pid and tid joined together x:y fashion, eg 49 // 1024:130. The ptid is a unique key for a thread in the trace. 50 this.threadStateByPTID_ = {}; 51 52 // Async events need to be processed durign finalizeEvents 53 this.allAsyncEvents_ = []; 54 } 55 56 /** 57 * @return {boolean} Whether obj is a TraceEvent array. 58 */ 59 TraceEventImporter.canImport = function(eventData) { 60 // May be encoded JSON. But we dont want to parse it fully yet. 61 // Use a simple heuristic: 62 // - eventData that starts with [ are probably trace_event 63 // - eventData that starts with { are probably trace_event 64 // May be encoded JSON. Treat files that start with { as importable by us. 65 if (typeof(eventData) === 'string' || eventData instanceof String) { 66 return eventData[0] == '{' || eventData[0] == '['; 67 } 68 69 // Might just be an array of events 70 if (eventData instanceof Array && eventData.length && eventData[0].ph) 71 return true; 72 73 // Might be an object with a traceEvents field in it. 74 if (eventData.traceEvents) 75 return eventData.traceEvents instanceof Array && 76 eventData.traceEvents[0].ph; 77 78 return false; 79 }; 80 81 TraceEventImporter.prototype = { 82 83 __proto__: Object.prototype, 84 85 /** 86 * Helper to process a 'begin' event (e.g. initiate a slice). 87 * @param {ThreadState} state Thread state (holds slices). 88 * @param {Object} event The current trace event. 89 */ 90 processBeginEvent: function(index, state, event) { 91 var colorId = tracing.getStringColorId(event.name); 92 var slice = 93 { index: index, 94 slice: new tracing.TimelineThreadSlice(event.name, colorId, 95 event.ts / 1000, 96 event.args) }; 97 98 if (event.uts) 99 slice.slice.startInUserTime = event.uts / 1000; 100 101 if (event.args['ui-nest'] === '0') { 102 this.model_.importErrors.push('ui-nest no longer supported.'); 103 return; 104 } 105 106 state.openSlices.push(slice); 107 }, 108 109 /** 110 * Helper to process an 'end' event (e.g. close a slice). 111 * @param {ThreadState} state Thread state (holds slices). 112 * @param {Object} event The current trace event. 113 */ 114 processEndEvent: function(state, event) { 115 if (event.args['ui-nest'] === '0') { 116 this.model_.importErrors.push('ui-nest no longer supported.'); 117 return; 118 } 119 if (state.openSlices.length == 0) { 120 // Ignore E events that are unmatched. 121 return; 122 } 123 var slice = state.openSlices.pop().slice; 124 slice.duration = (event.ts / 1000) - slice.start; 125 if (event.uts) 126 slice.durationInUserTime = (event.uts / 1000) - slice.startInUserTime; 127 for (var arg in event.args) 128 slice.args[arg] = event.args[arg]; 129 130 // Store the slice on the correct subrow. 131 var thread = this.model_.getOrCreateProcess(event.pid). 132 getOrCreateThread(event.tid); 133 var subRowIndex = state.openSlices.length; 134 thread.getSubrow(subRowIndex).push(slice); 135 136 // Add the slice to the subSlices array of its parent. 137 if (state.openSlices.length) { 138 var parentSlice = state.openSlices[state.openSlices.length - 1]; 139 parentSlice.slice.subSlices.push(slice); 140 } 141 }, 142 143 /** 144 * Helper to process an 'async finish' event, which will close an open slice 145 * on a TimelineAsyncSliceGroup object. 146 **/ 147 processAsyncEvent: function(index, state, event) { 148 var thread = this.model_.getOrCreateProcess(event.pid). 149 getOrCreateThread(event.tid); 150 this.allAsyncEvents_.push({ 151 event: event, 152 thread: thread}); 153 }, 154 155 /** 156 * Helper function that closes any open slices. This happens when a trace 157 * ends before an 'E' phase event can get posted. When that happens, this 158 * closes the slice at the highest timestamp we recorded and sets the 159 * didNotFinish flag to true. 160 */ 161 autoCloseOpenSlices: function() { 162 // We need to know the model bounds in order to assign an end-time to 163 // the open slices. 164 this.model_.updateBounds(); 165 166 // The model's max value in the trace is wrong at this point if there are 167 // un-closed events. To close those events, we need the true global max 168 // value. To compute this, build a list of timestamps that weren't 169 // included in the max calculation, then compute the real maximum based on 170 // that. 171 var openTimestamps = []; 172 for (var ptid in this.threadStateByPTID_) { 173 var state = this.threadStateByPTID_[ptid]; 174 for (var i = 0; i < state.openSlices.length; i++) { 175 var slice = state.openSlices[i]; 176 openTimestamps.push(slice.slice.start); 177 for (var s = 0; s < slice.slice.subSlices.length; s++) { 178 var subSlice = slice.slice.subSlices[s]; 179 openTimestamps.push(subSlice.start); 180 if (subSlice.duration) 181 openTimestamps.push(subSlice.end); 182 } 183 } 184 } 185 186 // Figure out the maximum value of model.maxTimestamp and 187 // Math.max(openTimestamps). Made complicated by the fact that the model 188 // timestamps might be undefined. 189 var realMaxTimestamp; 190 if (this.model_.maxTimestamp) { 191 realMaxTimestamp = Math.max(this.model_.maxTimestamp, 192 Math.max.apply(Math, openTimestamps)); 193 } else { 194 realMaxTimestamp = Math.max.apply(Math, openTimestamps); 195 } 196 197 // Automatically close any slices are still open. These occur in a number 198 // of reasonable situations, e.g. deadlock. This pass ensures the open 199 // slices make it into the final model. 200 for (var ptid in this.threadStateByPTID_) { 201 var state = this.threadStateByPTID_[ptid]; 202 while (state.openSlices.length > 0) { 203 var slice = state.openSlices.pop(); 204 slice.slice.duration = realMaxTimestamp - slice.slice.start; 205 slice.slice.didNotFinish = true; 206 var event = this.events_[slice.index]; 207 208 // Store the slice on the correct subrow. 209 var thread = this.model_.getOrCreateProcess(event.pid) 210 .getOrCreateThread(event.tid); 211 var subRowIndex = state.openSlices.length; 212 thread.getSubrow(subRowIndex).push(slice.slice); 213 214 // Add the slice to the subSlices array of its parent. 215 if (state.openSlices.length) { 216 var parentSlice = state.openSlices[state.openSlices.length - 1]; 217 parentSlice.slice.subSlices.push(slice.slice); 218 } 219 } 220 } 221 }, 222 223 /** 224 * Helper that creates and adds samples to a TimelineCounter object based on 225 * 'C' phase events. 226 */ 227 processCounterEvent: function(event) { 228 var ctr_name; 229 if (event.id !== undefined) 230 ctr_name = event.name + '[' + event.id + ']'; 231 else 232 ctr_name = event.name; 233 234 var ctr = this.model_.getOrCreateProcess(event.pid) 235 .getOrCreateCounter(event.cat, ctr_name); 236 // Initialize the counter's series fields if needed. 237 if (ctr.numSeries == 0) { 238 for (var seriesName in event.args) { 239 ctr.seriesNames.push(seriesName); 240 ctr.seriesColors.push( 241 tracing.getStringColorId(ctr.name + '.' + seriesName)); 242 } 243 if (ctr.numSeries == 0) { 244 this.model_.importErrors.push('Expected counter ' + event.name + 245 ' to have at least one argument to use as a value.'); 246 // Drop the counter. 247 delete ctr.parent.counters[ctr.name]; 248 return; 249 } 250 } 251 252 // Add the sample values. 253 ctr.timestamps.push(event.ts / 1000); 254 for (var i = 0; i < ctr.numSeries; i++) { 255 var seriesName = ctr.seriesNames[i]; 256 if (event.args[seriesName] === undefined) { 257 ctr.samples.push(0); 258 continue; 259 } 260 ctr.samples.push(event.args[seriesName]); 261 } 262 }, 263 264 /** 265 * Walks through the events_ list and outputs the structures discovered to 266 * model_. 267 */ 268 importEvents: function() { 269 // Walk through events 270 var events = this.events_; 271 // Some events cannot be handled until we have done a first pass over the 272 // data set. So, accumulate them into a temporary data structure. 273 var second_pass_events = []; 274 for (var eI = 0; eI < events.length; eI++) { 275 var event = events[eI]; 276 var ptid = tracing.TimelineThread.getPTIDFromPidAndTid( 277 event.pid, event.tid); 278 279 if (!(ptid in this.threadStateByPTID_)) 280 this.threadStateByPTID_[ptid] = new ThreadState(); 281 var state = this.threadStateByPTID_[ptid]; 282 283 if (event.ph == 'B') { 284 this.processBeginEvent(eI, state, event); 285 } else if (event.ph == 'E') { 286 this.processEndEvent(state, event); 287 } else if (event.ph == 'S') { 288 this.processAsyncEvent(eI, state, event); 289 } else if (event.ph == 'F') { 290 this.processAsyncEvent(eI, state, event); 291 } else if (event.ph == 'T') { 292 this.processAsyncEvent(eI, state, event); 293 } else if (event.ph == 'I') { 294 // Treat an Instant event as a duration 0 slice. 295 // TimelineSliceTrack's redraw() knows how to handle this. 296 this.processBeginEvent(eI, state, event); 297 this.processEndEvent(state, event); 298 } else if (event.ph == 'C') { 299 this.processCounterEvent(event); 300 } else if (event.ph == 'M') { 301 if (event.name == 'thread_name') { 302 var thread = this.model_.getOrCreateProcess(event.pid) 303 .getOrCreateThread(event.tid); 304 thread.name = event.args.name; 305 } else { 306 this.model_.importErrors.push( 307 'Unrecognized metadata name: ' + event.name); 308 } 309 } else { 310 this.model_.importErrors.push( 311 'Unrecognized event phase: ' + event.ph + 312 '(' + event.name + ')'); 313 } 314 } 315 316 // Autoclose any open slices. 317 var hasOpenSlices = false; 318 for (var ptid in this.threadStateByPTID_) { 319 var state = this.threadStateByPTID_[ptid]; 320 hasOpenSlices |= state.openSlices.length > 0; 321 } 322 if (hasOpenSlices) 323 this.autoCloseOpenSlices(); 324 }, 325 326 /** 327 * Called by the TimelineModel after all other importers have imported their 328 * events. This function creates async slices for any async events we saw. 329 */ 330 finalizeImport: function() { 331 if (this.allAsyncEvents_.length == 0) 332 return; 333 334 this.allAsyncEvents_.sort(function(x, y) { 335 return x.event.ts - y.event.ts; 336 }); 337 338 var asyncEventStatesByNameThenID = {}; 339 340 var allAsyncEvents = this.allAsyncEvents_; 341 for (var i = 0; i < allAsyncEvents.length; i++) { 342 var asyncEventState = allAsyncEvents[i]; 343 344 var event = asyncEventState.event; 345 var name = event.name; 346 if (name === undefined) { 347 this.model_.importErrors.push( 348 'Async events (ph: S, T or F) require an name parameter.'); 349 continue; 350 } 351 352 var id = event.id; 353 if (id === undefined) { 354 this.model_.importErrors.push( 355 'Async events (ph: S, T or F) require an id parameter.'); 356 continue; 357 } 358 359 // TODO(simonjam): Add a synchronous tick on the appropriate thread. 360 361 if (event.ph == 'S') { 362 if (asyncEventStatesByNameThenID[name] === undefined) 363 asyncEventStatesByNameThenID[name] = {}; 364 if (asyncEventStatesByNameThenID[name][id]) { 365 this.model_.importErrors.push( 366 'At ' + event.ts + ', an slice of the same id ' + id + 367 ' was alrady open.'); 368 continue; 369 } 370 asyncEventStatesByNameThenID[name][id] = []; 371 asyncEventStatesByNameThenID[name][id].push(asyncEventState); 372 } else { 373 if (asyncEventStatesByNameThenID[name] === undefined) { 374 this.model_.importErrors.push( 375 'At ' + event.ts + ', no slice named ' + name + 376 ' was open.'); 377 continue; 378 } 379 if (asyncEventStatesByNameThenID[name][id] === undefined) { 380 this.model_.importErrors.push( 381 'At ' + event.ts + ', no slice named ' + name + 382 ' with id=' + id + ' was open.'); 383 continue; 384 } 385 var events = asyncEventStatesByNameThenID[name][id]; 386 events.push(asyncEventState); 387 388 if (event.ph == 'F') { 389 // Create a slice from start to end. 390 var slice = new tracing.TimelineAsyncSlice( 391 name, 392 tracing.getStringColorId(name), 393 events[0].event.ts / 1000); 394 395 slice.duration = (event.ts / 1000) - (events[0].event.ts / 1000); 396 397 slice.startThread = events[0].thread; 398 slice.endThread = asyncEventState.thread; 399 slice.id = id; 400 slice.args = events[0].event.args; 401 slice.subSlices = []; 402 403 // Create subSlices for each step. 404 for (var j = 1; j < events.length; ++j) { 405 var subName = name; 406 if (events[j - 1].event.ph == 'T') 407 subName = name + ':' + events[j - 1].event.args.step; 408 var subSlice = new tracing.TimelineAsyncSlice( 409 subName, 410 tracing.getStringColorId(name + j), 411 events[j - 1].event.ts / 1000); 412 413 subSlice.duration = 414 (events[j].event.ts / 1000) - (events[j - 1].event.ts / 1000); 415 416 subSlice.startThread = events[j - 1].thread; 417 subSlice.endThread = events[j].thread; 418 subSlice.id = id; 419 subSlice.args = events[j - 1].event.args; 420 421 slice.subSlices.push(subSlice); 422 } 423 424 // The args for the finish event go in the last subSlice. 425 var lastSlice = slice.subSlices[slice.subSlices.length - 1]; 426 for (var arg in event.args) 427 lastSlice.args[arg] = event.args[arg]; 428 429 // Add |slice| to the start-thread's asyncSlices. 430 slice.startThread.asyncSlices.push(slice); 431 delete asyncEventStatesByNameThenID[name][id]; 432 } 433 } 434 } 435 } 436 }; 437 438 tracing.TimelineModel.registerImporter(TraceEventImporter); 439 440 return { 441 TraceEventImporter: TraceEventImporter 442 }; 443}); 444