OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011 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 /** | |
7 * @fileoverview TimelineModel is a parsed representation of the | |
8 * TraceEvents obtained from base/trace_event in which the begin-end | |
9 * tokens are converted into a hierarchy of processes, threads, | |
10 * subrows, and slices. | |
11 * | |
12 * The building block of the model is a slice. A slice is roughly | |
13 * equivalent to function call executing on a specific thread. As a | |
14 * result, slices may have one or more subslices. | |
15 * | |
16 * A thread contains one or more subrows of slices. Row 0 corresponds to | |
17 * the "root" slices, e.g. the topmost slices. Row 1 contains slices that | |
18 * are nested 1 deep in the stack, and so on. We use these subrows to draw | |
19 * nesting tasks. | |
20 * | |
21 */ | |
22 cr.define('gpu', function() { | |
23 /** | |
24 * A TimelineSlice represents an interval of time on a given thread | |
25 * associated with a specific trace event. For example, | |
26 * TRACE_EVENT_BEGIN1("x","myArg", 7) at time=0.1ms | |
27 * TRACE_EVENT_END() at time=0.3ms | |
28 * Results in a single timeline slice from 0.1 with duration 0.2. | |
29 * | |
30 * All time units are stored in milliseconds. | |
31 * @constructor | |
32 */ | |
33 function TimelineSlice(title, colorId, start, args) { | |
34 this.title = title; | |
35 this.start = start; | |
36 this.colorId = colorId; | |
37 this.args = args; | |
38 this.didNotFinish = false; | |
39 this.subSlices = []; | |
40 } | |
41 | |
42 TimelineSlice.prototype = { | |
43 selected: false, | |
44 | |
45 duration: undefined, | |
46 | |
47 get end() { | |
48 return this.start + this.duration; | |
49 } | |
50 }; | |
51 | |
52 /** | |
53 * A TimelineThread stores all the trace events collected for a particular | |
54 * thread. We organize the slices on a thread by "subrows," where subrow 0 | |
55 * has all the root slices, subrow 1 those nested 1 deep, and so on. There | |
56 * is also a set of non-nested subrows. | |
57 * | |
58 * @constructor | |
59 */ | |
60 function TimelineThread(parent, tid) { | |
61 this.parent = parent; | |
62 this.tid = tid; | |
63 this.subRows = [[]]; | |
64 this.nonNestedSubRows = []; | |
65 } | |
66 | |
67 TimelineThread.prototype = { | |
68 /** | |
69 * Name of the thread, if present. | |
70 */ | |
71 name: undefined, | |
72 | |
73 getSubrow: function(i) { | |
74 while (i >= this.subRows.length) | |
75 this.subRows.push([]); | |
76 return this.subRows[i]; | |
77 }, | |
78 | |
79 addNonNestedSlice: function(slice) { | |
80 for (var i = 0; i < this.nonNestedSubRows.length; i++) { | |
81 var currSubRow = this.nonNestedSubRows[i]; | |
82 var lastSlice = currSubRow[currSubRow.length - 1]; | |
83 if (slice.start >= lastSlice.start + lastSlice.duration) { | |
84 currSubRow.push(slice); | |
85 return; | |
86 } | |
87 } | |
88 this.nonNestedSubRows.push([slice]); | |
89 }, | |
90 | |
91 /** | |
92 * Updates the minTimestamp and maxTimestamp fields based on the | |
93 * current slices and nonNestedSubRows attached to the thread. | |
94 */ | |
95 updateBounds: function() { | |
96 var values = []; | |
97 var slices; | |
98 if (this.subRows[0].length != 0) { | |
99 slices = this.subRows[0]; | |
100 values.push(slices[0].start); | |
101 values.push(slices[slices.length - 1].end); | |
102 } | |
103 for (var i = 0; i < this.nonNestedSubRows.length; ++i) { | |
104 slices = this.nonNestedSubRows[i]; | |
105 values.push(slices[0].start); | |
106 values.push(slices[slices.length - 1].end); | |
107 } | |
108 if (values.length) { | |
109 this.minTimestamp = Math.min.apply(Math, values); | |
110 this.maxTimestamp = Math.max.apply(Math, values); | |
111 } else { | |
112 this.minTimestamp = undefined; | |
113 this.maxTimestamp = undefined; | |
114 } | |
115 } | |
116 | |
117 }; | |
118 | |
119 /** | |
120 * Comparison between threads that orders first by pid, | |
121 * then by names, then by tid. | |
122 */ | |
123 TimelineThread.compare = function(x,y) { | |
124 if(x.parent.pid != y.parent.pid) { | |
125 return x.parent.pid - y.parent.pid; | |
126 } | |
127 | |
128 if (x.name && y.name) { | |
129 var tmp = x.name.localeCompare(y.name); | |
130 if (tmp == 0) | |
131 return x.tid - y.tid; | |
132 return tmp; | |
133 } else if (x.name) { | |
134 return -1; | |
135 } else if (y.name){ | |
136 return 1; | |
137 } else { | |
138 return x.tid - y.tid; | |
139 } | |
140 }; | |
141 | |
142 | |
143 /** | |
144 * The TimelineProcess represents a single process in the | |
145 * trace. Right now, we keep this around purely for bookkeeping | |
146 * reasons. | |
147 * @constructor | |
148 */ | |
149 function TimelineProcess(pid) { | |
150 this.pid = pid; | |
151 this.threads = {}; | |
152 }; | |
153 | |
154 TimelineProcess.prototype = { | |
155 getThread: function(tid) { | |
156 if (!this.threads[tid]) | |
157 this.threads[tid] = new TimelineThread(this, tid); | |
158 return this.threads[tid]; | |
159 } | |
160 }; | |
161 | |
162 /** | |
163 * Builds a model from an array of TraceEvent objects. | |
164 * @param {Array} events An array of TraceEvents created by | |
165 * TraceEvent.ToJSON(). | |
166 * @constructor | |
167 */ | |
168 function TimelineModel(events) { | |
169 this.processes = {}; | |
170 this.importErrors = []; | |
171 | |
172 if (events) | |
173 this.importEvents(events); | |
174 } | |
175 | |
176 TimelineModel.prototype = { | |
177 __proto__: cr.EventTarget.prototype, | |
178 | |
179 getProcess: function(pid) { | |
180 if (!this.processes[pid]) | |
181 this.processes[pid] = new TimelineProcess(pid); | |
182 return this.processes[pid]; | |
183 }, | |
184 | |
185 /** | |
186 * The import takes an array of json-ified TraceEvents and adds them into | |
187 * the TimelineModel as processes, threads, and slices. | |
188 */ | |
189 importEvents: function(events) { | |
190 // A ptid is a pid and tid joined together x:y fashion, eg 1024:130 | |
191 // The ptid is a unique key for a thread in the trace. | |
192 this.importErrors = []; | |
193 | |
194 // Threadstate | |
195 const numColorIds = 30; | |
196 function ThreadState(tid) { | |
197 this.openSlices = []; | |
198 this.openNonNestedSlices = {}; | |
199 } | |
200 var threadStateByPTID = {}; | |
201 | |
202 var nameToColorMap = {}; | |
203 function getColor(name) { | |
204 if (!(name in nameToColorMap)) { | |
205 // Compute a simplistic hashcode of the string so we get consistent | |
206 // coloring across traces. | |
207 var hash = 0; | |
208 for (var i = 0; i < name.length; ++i) | |
209 hash = (hash + 37 * hash + 11 * name.charCodeAt(i)) % 0xFFFFFFFF; | |
210 nameToColorMap[name] = hash % numColorIds; | |
211 } | |
212 return nameToColorMap[name]; | |
213 } | |
214 | |
215 var self = this; | |
216 | |
217 /** | |
218 * Helper to process a 'begin' event (e.g. initiate a slice). | |
219 * @param {ThreadState} state Thread state (holds slices). | |
220 * @param {Object} event The current trace event. | |
221 */ | |
222 function processBegin(state, event) { | |
223 var colorId = getColor(event.name); | |
224 var slice = | |
225 { index: eI, | |
226 slice: new TimelineSlice(event.name, colorId, event.ts, | |
227 event.args) }; | |
228 if (event.args['ui-nest'] === '0') { | |
229 var sliceID = event.name; | |
230 for (var x in event.args) | |
231 sliceID += ';' + event.args[x]; | |
232 if (state.openNonNestedSlices[sliceID]) | |
233 this.importErrors.push('Event ' + sliceID + ' already open.'); | |
234 state.openNonNestedSlices[sliceID] = slice; | |
235 } else { | |
236 state.openSlices.push(slice); | |
237 } | |
238 } | |
239 | |
240 /** | |
241 * Helper to process an 'end' event (e.g. close a slice). | |
242 * @param {ThreadState} state Thread state (holds slices). | |
243 * @param {Object} event The current trace event. | |
244 */ | |
245 function processEnd(state, event) { | |
246 if (event.args['ui-nest'] === '0') { | |
247 var sliceID = event.name; | |
248 for (var x in event.args) | |
249 sliceID += ';' + event.args[x]; | |
250 var slice = state.openNonNestedSlices[sliceID]; | |
251 if (!slice) | |
252 return; | |
253 slice.slice.duration = event.ts - slice.slice.start; | |
254 | |
255 // Store the slice in a non-nested subrow. | |
256 var thread = self.getProcess(event.pid).getThread(event.tid); | |
257 thread.addNonNestedSlice(slice.slice); | |
258 delete state.openNonNestedSlices[name]; | |
259 } else { | |
260 if (state.openSlices.length == 0) { | |
261 // Ignore E events that are unmatched. | |
262 return; | |
263 } | |
264 var slice = state.openSlices.pop().slice; | |
265 slice.duration = event.ts - slice.start; | |
266 | |
267 // Store the slice on the correct subrow. | |
268 var thread = self.getProcess(event.pid).getThread(event.tid); | |
269 var subRowIndex = state.openSlices.length; | |
270 thread.getSubrow(subRowIndex).push(slice); | |
271 | |
272 // Add the slice to the subSlices array of its parent. | |
273 if (state.openSlices.length) { | |
274 var parentSlice = state.openSlices[state.openSlices.length - 1]; | |
275 parentSlice.slice.subSlices.push(slice); | |
276 } | |
277 } | |
278 } | |
279 | |
280 // Walk through events | |
281 for (var eI = 0; eI < events.length; eI++) { | |
282 var event = events[eI]; | |
283 var ptid = event.pid + ':' + event.tid; | |
284 | |
285 if (!(ptid in threadStateByPTID)) | |
286 threadStateByPTID[ptid] = new ThreadState(); | |
287 var state = threadStateByPTID[ptid]; | |
288 | |
289 if (event.ph == 'B') { | |
290 processBegin(state, event); | |
291 } else if (event.ph == 'E') { | |
292 processEnd(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 processBegin(state, event); | |
297 processEnd(state, event); | |
298 } else if (event.ph == 'M') { | |
299 if (event.name == 'thread_name') { | |
300 var thread = this.getProcess(event.pid).getThread(event.tid); | |
301 thread.name = event.args.name; | |
302 } else { | |
303 this.importErrors.push('Unrecognized metadata name: ' + event.name); | |
304 } | |
305 } else { | |
306 this.importErrors.push('Unrecognized event phase: ' + event.ph + | |
307 '(' + event.name + ')'); | |
308 } | |
309 } | |
310 this.pruneEmptyThreads(); | |
311 this.updateBounds(); | |
312 | |
313 // Add end events for any events that are still on the stack. These | |
314 // are events that were still open when trace was ended, and can often | |
315 // indicate deadlock behavior. | |
316 for (var ptid in threadStateByPTID) { | |
317 var state = threadStateByPTID[ptid]; | |
318 while (state.openSlices.length > 0) { | |
319 var slice = state.openSlices.pop(); | |
320 slice.slice.duration = this.maxTimestamp - slice.slice.start; | |
321 slice.slice.didNotFinish = true; | |
322 var event = events[slice.index]; | |
323 | |
324 // Store the slice on the correct subrow. | |
325 var thread = this.getProcess(event.pid).getThread(event.tid); | |
326 var subRowIndex = state.openSlices.length; | |
327 thread.getSubrow(subRowIndex).push(slice.slice); | |
328 | |
329 // Add the slice to the subSlices array of its parent. | |
330 if (state.openSlices.length) { | |
331 var parentSlice = state.openSlices[state.openSlices.length - 1]; | |
332 parentSlice.slice.subSlices.push(slice.slice); | |
333 } | |
334 } | |
335 } | |
336 | |
337 this.shiftWorldToMicroseconds(); | |
338 | |
339 var boost = (this.maxTimestamp - this.minTimestamp) * 0.15; | |
340 this.minTimestamp = this.minTimestamp - boost; | |
341 this.maxTimestamp = this.maxTimestamp + boost; | |
342 }, | |
343 | |
344 /** | |
345 * Removes threads from the model that have no subrows. | |
346 */ | |
347 pruneEmptyThreads: function() { | |
348 for (var pid in this.processes) { | |
349 var process = this.processes[pid]; | |
350 var prunedThreads = []; | |
351 for (var tid in process.threads) { | |
352 var thread = process.threads[tid]; | |
353 if (thread.subRows[0].length || thread.nonNestedSubRows.legnth) | |
354 prunedThreads.push(thread); | |
355 } | |
356 process.threads = prunedThreads; | |
357 } | |
358 }, | |
359 | |
360 updateBounds: function() { | |
361 var wmin = Infinity; | |
362 var wmax = -wmin; | |
363 var threads = this.getAllThreads(); | |
364 for (var tI = 0; tI < threads.length; tI++) { | |
365 var thread = threads[tI]; | |
366 thread.updateBounds(); | |
367 if (thread.minTimestamp != undefined && | |
368 thread.maxTimestamp != undefined) { | |
369 wmin = Math.min(wmin, thread.minTimestamp); | |
370 wmax = Math.max(wmax, thread.maxTimestamp); | |
371 } | |
372 } | |
373 this.minTimestamp = wmin; | |
374 this.maxTimestamp = wmax; | |
375 }, | |
376 | |
377 shiftWorldToMicroseconds: function() { | |
378 var timeBase = this.minTimestamp; | |
379 var threads = this.getAllThreads(); | |
380 for (var tI = 0; tI < threads.length; tI++) { | |
381 var thread = threads[tI]; | |
382 var shiftSubRow = function(subRow) { | |
383 for (var tS = 0; tS < subRow.length; tS++) { | |
384 var slice = subRow[tS]; | |
385 slice.start = (slice.start - timeBase) / 1000; | |
386 slice.duration /= 1000; | |
387 } | |
388 }; | |
389 for (var tSR = 0; tSR < thread.subRows.length; tSR++) { | |
390 shiftSubRow(thread.subRows[tSR]); | |
391 } | |
392 for (var tSR = 0; tSR < thread.nonNestedSubRows.length; tSR++) { | |
393 shiftSubRow(thread.nonNestedSubRows[tSR]); | |
394 } | |
395 } | |
396 | |
397 this.updateBounds(); | |
398 }, | |
399 | |
400 getAllThreads: function() { | |
401 var threads = []; | |
402 for (var pid in this.processes) { | |
403 var process = this.processes[pid]; | |
404 for (var tid in process.threads) { | |
405 threads.push(process.threads[tid]); | |
406 } | |
407 } | |
408 return threads; | |
409 } | |
410 | |
411 }; | |
412 | |
413 return { | |
414 TimelineSlice: TimelineSlice, | |
415 TimelineThread: TimelineThread, | |
416 TimelineProcess: TimelineProcess, | |
417 TimelineModel: TimelineModel | |
418 }; | |
419 }); | |
OLD | NEW |