|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 var EIDETICKER_BASE_URL = "http://eideticker.wrla.ch/"; |
|
8 |
|
9 var gDebugLog = false; |
|
10 var gDebugTrace = false; |
|
11 var gLocation = window.location + ""; |
|
12 if (gLocation.indexOf("file:") == 0) { |
|
13 gDebugLog = true; |
|
14 gDebugTrace = true; |
|
15 PROFILERLOG("Turning on logging+tracing since cleopatra is served from the file protocol"); |
|
16 } |
|
17 // Use for verbose tracing, otherwise use log |
|
18 function PROFILERTRACE(msg) { |
|
19 if (gDebugTrace) |
|
20 PROFILERLOG(msg); |
|
21 } |
|
22 function PROFILERLOG(msg) { |
|
23 if (gDebugLog) { |
|
24 msg = "Cleo: " + msg; |
|
25 console.log(msg); |
|
26 if (window.dump) |
|
27 window.dump(msg + "\n"); |
|
28 } |
|
29 } |
|
30 function PROFILERERROR(msg) { |
|
31 msg = "Cleo: " + msg; |
|
32 console.log(msg); |
|
33 if (window.dump) |
|
34 window.dump(msg + "\n"); |
|
35 } |
|
36 function enableProfilerTracing() { |
|
37 gDebugLog = true; |
|
38 gDebugTrace = true; |
|
39 Parser.updateLogSetting(); |
|
40 } |
|
41 function enableProfilerLogging() { |
|
42 gDebugLog = true; |
|
43 Parser.updateLogSetting(); |
|
44 } |
|
45 |
|
46 function removeAllChildren(element) { |
|
47 while (element.firstChild) { |
|
48 element.removeChild(element.firstChild); |
|
49 } |
|
50 } |
|
51 |
|
52 function FileList() { |
|
53 this._container = document.createElement("ul"); |
|
54 this._container.id = "fileList"; |
|
55 this._selectedFileItem = null; |
|
56 this._fileItemList = []; |
|
57 } |
|
58 |
|
59 FileList.prototype = { |
|
60 getContainer: function FileList_getContainer() { |
|
61 return this._container; |
|
62 }, |
|
63 |
|
64 clearFiles: function FileList_clearFiles() { |
|
65 this.fileItemList = []; |
|
66 this._selectedFileItem = null; |
|
67 this._container.innerHTML = ""; |
|
68 }, |
|
69 |
|
70 loadProfileListFromLocalStorage: function FileList_loadProfileListFromLocalStorage() { |
|
71 var self = this; |
|
72 gLocalStorage.getProfileList(function(profileList) { |
|
73 for (var i = profileList.length - 1; i >= 0; i--) { |
|
74 (function closure() { |
|
75 // This only carries info about the profile and the access key to retrieve it. |
|
76 var profileInfo = profileList[i]; |
|
77 //PROFILERTRACE("Profile list from local storage: " + JSON.stringify(profileInfo)); |
|
78 var dateObj = new Date(profileInfo.date); |
|
79 var fileEntry = self.addFile(profileInfo, dateObj.toLocaleString(), function fileEntryClick() { |
|
80 PROFILERLOG("open: " + profileInfo.profileKey + "\n"); |
|
81 loadLocalStorageProfile(profileInfo.profileKey); |
|
82 }); |
|
83 })(); |
|
84 } |
|
85 }); |
|
86 gLocalStorage.onProfileListChange(function(profileList) { |
|
87 self.clearFiles(); |
|
88 self.loadProfileListFromLocalStorage(); |
|
89 }); |
|
90 }, |
|
91 |
|
92 addFile: function FileList_addFile(profileInfo, description, onselect) { |
|
93 var li = document.createElement("li"); |
|
94 |
|
95 var fileName; |
|
96 if (profileInfo.profileKey && profileInfo.profileKey.indexOf("http://profile-store.commondatastorage.googleapis.com/") >= 0) { |
|
97 fileName = profileInfo.profileKey.substring(54); |
|
98 fileName = fileName.substring(0, 8) + "..." + fileName.substring(28); |
|
99 } else { |
|
100 fileName = profileInfo.name; |
|
101 } |
|
102 li.fileName = fileName || "(New Profile)"; |
|
103 li.description = description || "(empty)"; |
|
104 |
|
105 li.className = "fileListItem"; |
|
106 if (!this._selectedFileItem) { |
|
107 li.classList.add("selected"); |
|
108 this._selectedFileItem = li; |
|
109 } |
|
110 |
|
111 var self = this; |
|
112 li.onclick = function() { |
|
113 self.setSelection(li); |
|
114 if (onselect) |
|
115 onselect(); |
|
116 } |
|
117 |
|
118 var fileListItemTitleSpan = document.createElement("span"); |
|
119 fileListItemTitleSpan.className = "fileListItemTitle"; |
|
120 fileListItemTitleSpan.textContent = li.fileName; |
|
121 li.appendChild(fileListItemTitleSpan); |
|
122 |
|
123 var fileListItemDescriptionSpan = document.createElement("span"); |
|
124 fileListItemDescriptionSpan.className = "fileListItemDescription"; |
|
125 fileListItemDescriptionSpan.textContent = li.description; |
|
126 li.appendChild(fileListItemDescriptionSpan); |
|
127 |
|
128 this._container.appendChild(li); |
|
129 |
|
130 this._fileItemList.push(li); |
|
131 |
|
132 return li; |
|
133 }, |
|
134 |
|
135 setSelection: function FileList_setSelection(fileEntry) { |
|
136 if (this._selectedFileItem) { |
|
137 this._selectedFileItem.classList.remove("selected"); |
|
138 } |
|
139 this._selectedFileItem = fileEntry; |
|
140 fileEntry.classList.add("selected"); |
|
141 if (this._selectedFileItem.onselect) |
|
142 this._selectedFileItem.onselect(); |
|
143 }, |
|
144 |
|
145 profileParsingFinished: function FileList_profileParsingFinished() { |
|
146 //this._container.querySelector(".fileListItemTitle").textContent = "Current Profile"; |
|
147 //this._container.querySelector(".fileListItemDescription").textContent = gNumSamples + " Samples"; |
|
148 } |
|
149 } |
|
150 |
|
151 function treeObjSort(a, b) { |
|
152 return b.counter - a.counter; |
|
153 } |
|
154 |
|
155 function ProfileTreeManager() { |
|
156 this.treeView = new TreeView(); |
|
157 this.treeView.setColumns([ |
|
158 { name: "sampleCount", title: gStrings["Running Time"] }, |
|
159 { name: "selfSampleCount", title: gStrings["Self"] }, |
|
160 { name: "resource", title: "" }, |
|
161 { name: "symbolName", title: gStrings["Symbol Name"] } |
|
162 ]); |
|
163 var self = this; |
|
164 this.treeView.addEventListener("select", function (frameData) { |
|
165 self.highlightFrame(frameData); |
|
166 if (window.comparator_setSelection) { |
|
167 window.comparator_setSelection(gTreeManager.serializeCurrentSelectionSnapshot(), frameData); |
|
168 } |
|
169 }); |
|
170 this.treeView.addEventListener("contextMenuClick", function (e) { |
|
171 self._onContextMenuClick(e); |
|
172 }); |
|
173 this.treeView.addEventListener("focusCallstackButtonClicked", function (frameData) { |
|
174 // NOTE: Not in the original Cleopatra source code. |
|
175 notifyParent("displaysource", { |
|
176 line: frameData.scriptLocation.lineInformation, |
|
177 uri: frameData.scriptLocation.scriptURI, |
|
178 isChrome: /^otherhost_*/.test(frameData.library) |
|
179 }); |
|
180 }); |
|
181 this._container = document.createElement("div"); |
|
182 this._container.className = "tree"; |
|
183 this._container.appendChild(this.treeView.getContainer()); |
|
184 |
|
185 // If this is set when the tree changes the snapshot is immediately restored. |
|
186 this._savedSnapshot = null; |
|
187 this._allowNonContiguous = false; |
|
188 } |
|
189 ProfileTreeManager.prototype = { |
|
190 getContainer: function ProfileTreeManager_getContainer() { |
|
191 return this._container; |
|
192 }, |
|
193 highlightFrame: function Treedisplay_highlightFrame(frameData) { |
|
194 setHighlightedCallstack(this._getCallstackUpTo(frameData), this._getHeaviestCallstack(frameData)); |
|
195 }, |
|
196 dataIsOutdated: function ProfileTreeManager_dataIsOutdated() { |
|
197 this.treeView.dataIsOutdated(); |
|
198 }, |
|
199 saveSelectionSnapshot: function ProfileTreeManager_saveSelectionSnapshot(isJavascriptOnly) { |
|
200 this._savedSnapshot = this.treeView.getSelectionSnapshot(isJavascriptOnly); |
|
201 }, |
|
202 saveReverseSelectionSnapshot: function ProfileTreeManager_saveReverseSelectionSnapshot(isJavascriptOnly) { |
|
203 this._savedSnapshot = this.treeView.getReverseSelectionSnapshot(isJavascriptOnly); |
|
204 }, |
|
205 hasNonTrivialSelection: function ProfileTreeManager_hasNonTrivialSelection() { |
|
206 return this.treeView.getSelectionSnapshot().length > 1; |
|
207 }, |
|
208 serializeCurrentSelectionSnapshot: function ProfileTreeManager_serializeCurrentSelectionSnapshot() { |
|
209 return JSON.stringify(this.treeView.getSelectionSnapshot()); |
|
210 }, |
|
211 restoreSerializedSelectionSnapshot: function ProfileTreeManager_restoreSerializedSelectionSnapshot(selection) { |
|
212 this._savedSnapshot = JSON.parse(selection); |
|
213 }, |
|
214 _restoreSelectionSnapshot: function ProfileTreeManager__restoreSelectionSnapshot(snapshot, allowNonContiguous) { |
|
215 return this.treeView.restoreSelectionSnapshot(snapshot, allowNonContiguous); |
|
216 }, |
|
217 setSelection: function ProfileTreeManager_setSelection(frames) { |
|
218 return this.treeView.setSelection(frames); |
|
219 }, |
|
220 _getCallstackUpTo: function ProfileTreeManager__getCallstackUpTo(frame) { |
|
221 var callstack = []; |
|
222 var curr = frame; |
|
223 while (curr != null) { |
|
224 if (curr.name != null) { |
|
225 var subCallstack = curr.fullFrameNamesAsInSample.clone(); |
|
226 subCallstack.reverse(); |
|
227 callstack = callstack.concat(subCallstack); |
|
228 } |
|
229 curr = curr.parent; |
|
230 } |
|
231 callstack.reverse(); |
|
232 if (gInvertCallstack) |
|
233 callstack.shift(); // remove (total) |
|
234 return callstack; |
|
235 }, |
|
236 _getHeaviestCallstack: function ProfileTreeManager__getHeaviestCallstack(frame) { |
|
237 // FIXME: This gets the first leaf which is not the heaviest leaf. |
|
238 while(frame.children && frame.children.length > 0) { |
|
239 var nextFrame = frame.children[0].getData(); |
|
240 if (!nextFrame) |
|
241 break; |
|
242 frame = nextFrame; |
|
243 } |
|
244 return this._getCallstackUpTo(frame); |
|
245 }, |
|
246 _onContextMenuClick: function ProfileTreeManager__onContextMenuClick(e) { |
|
247 var node = e.node; |
|
248 var menuItem = e.menuItem; |
|
249 |
|
250 if (menuItem == "View Source") { |
|
251 // Remove anything after ( since MXR doesn't handle search with the arguments. |
|
252 var symbol = node.name.split("(")[0]; |
|
253 window.open("http://mxr.mozilla.org/mozilla-central/search?string=" + symbol, "View Source"); |
|
254 } else if (menuItem == "View JS Source") { |
|
255 viewJSSource(node); |
|
256 } else if (menuItem == "Plugin View: Pie") { |
|
257 focusOnPluginView("protovis", {type:"pie"}); |
|
258 } else if (menuItem == "Plugin View: Tree") { |
|
259 focusOnPluginView("protovis", {type:"tree"}); |
|
260 } else if (menuItem == "Google Search") { |
|
261 var symbol = node.name; |
|
262 window.open("https://www.google.ca/search?q=" + symbol, "View Source"); |
|
263 } else if (menuItem == "Focus Frame") { |
|
264 var symbol = node.fullFrameNamesAsInSample[0]; // TODO: we only function one symbol when callpath merging is on, fix that |
|
265 focusOnSymbol(symbol, node.name); |
|
266 } else if (menuItem == "Focus Callstack") { |
|
267 var focusedCallstack = this._getCallstackUpTo(node); |
|
268 focusOnCallstack(focusedCallstack, node.name); |
|
269 } |
|
270 }, |
|
271 setAllowNonContiguous: function ProfileTreeManager_setAllowNonContiguous() { |
|
272 this._allowNonContiguous = true; |
|
273 }, |
|
274 display: function ProfileTreeManager_display(tree, symbols, functions, resources, useFunctions, filterByName) { |
|
275 this.treeView.display(this.convertToJSTreeData(tree, symbols, functions, useFunctions), resources, filterByName); |
|
276 if (this._savedSnapshot) { |
|
277 var old = this._savedSnapshot.clone(); |
|
278 this._restoreSelectionSnapshot(this._savedSnapshot, this._allowNonContiguous); |
|
279 this._savedSnapshot = old; |
|
280 this._allowNonContiguous = false; |
|
281 } |
|
282 }, |
|
283 convertToJSTreeData: function ProfileTreeManager__convertToJSTreeData(rootNode, symbols, functions, useFunctions) { |
|
284 var totalSamples = rootNode.counter; |
|
285 function createTreeViewNode(node, parent) { |
|
286 var curObj = {}; |
|
287 curObj.parent = parent; |
|
288 curObj.counter = node.counter; |
|
289 var selfCounter = node.counter; |
|
290 for (var i = 0; i < node.children.length; ++i) { |
|
291 selfCounter -= node.children[i].counter; |
|
292 } |
|
293 curObj.selfCounter = selfCounter; |
|
294 curObj.ratio = node.counter / totalSamples; |
|
295 curObj.fullFrameNamesAsInSample = node.mergedNames ? node.mergedNames : [node.name]; |
|
296 if (!(node.name in (useFunctions ? functions : symbols))) { |
|
297 curObj.name = node.name; |
|
298 curObj.library = ""; |
|
299 } else { |
|
300 var functionObj = useFunctions ? functions[node.name] : functions[symbols[node.name].functionIndex]; |
|
301 var info = { |
|
302 functionName: functionObj.functionName, |
|
303 libraryName: functionObj.libraryName, |
|
304 lineInformation: useFunctions ? "" : symbols[node.name].lineInformation |
|
305 }; |
|
306 curObj.name = (info.functionName + " " + info.lineInformation).trim(); |
|
307 curObj.library = info.libraryName; |
|
308 curObj.isJSFrame = functionObj.isJSFrame; |
|
309 if (functionObj.scriptLocation) { |
|
310 curObj.scriptLocation = functionObj.scriptLocation; |
|
311 } |
|
312 } |
|
313 if (node.children.length) { |
|
314 curObj.children = getChildrenObjects(node.children, curObj); |
|
315 } |
|
316 return curObj; |
|
317 } |
|
318 function getChildrenObjects(children, parent) { |
|
319 var sortedChildren = children.slice(0).sort(treeObjSort); |
|
320 return sortedChildren.map(function (child) { |
|
321 var createdNode = null; |
|
322 return { |
|
323 getData: function () { |
|
324 if (!createdNode) { |
|
325 createdNode = createTreeViewNode(child, parent); |
|
326 } |
|
327 return createdNode; |
|
328 } |
|
329 }; |
|
330 }); |
|
331 } |
|
332 return getChildrenObjects([rootNode], null); |
|
333 }, |
|
334 }; |
|
335 |
|
336 function SampleBar() { |
|
337 this._container = document.createElement("div"); |
|
338 this._container.id = "sampleBar"; |
|
339 this._container.className = "sideBar"; |
|
340 |
|
341 this._header = document.createElement("h2"); |
|
342 this._header.innerHTML = "Selection - Most time spent in:"; |
|
343 this._header.alt = "This shows the heaviest leaf of the selected sample. Use this to get a quick glimpse of where the selection is spending most of its time."; |
|
344 this._container.appendChild(this._header); |
|
345 |
|
346 this._text = document.createElement("ul"); |
|
347 this._text.style.whiteSpace = "pre"; |
|
348 this._text.innerHTML = "Sample text"; |
|
349 this._container.appendChild(this._text); |
|
350 } |
|
351 |
|
352 SampleBar.prototype = { |
|
353 getContainer: function SampleBar_getContainer() { |
|
354 return this._container; |
|
355 }, |
|
356 setSample: function SampleBar_setSample(sample) { |
|
357 var str = ""; |
|
358 var list = []; |
|
359 |
|
360 this._text.innerHTML = ""; |
|
361 |
|
362 for (var i = 0; i < sample.length; i++) { |
|
363 var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex]; |
|
364 if (!functionObj) |
|
365 continue; |
|
366 var functionItem = document.createElement("li"); |
|
367 var functionLink = document.createElement("a"); |
|
368 functionLink.textContent = functionLink.title = functionObj.functionName; |
|
369 functionLink.href = "#"; |
|
370 functionItem.appendChild(functionLink); |
|
371 this._text.appendChild(functionItem); |
|
372 list.push(functionObj.functionName); |
|
373 functionLink.selectIndex = i; |
|
374 functionLink.onclick = function() { |
|
375 var selectedFrames = []; |
|
376 if (gInvertCallstack) { |
|
377 for (var i = 0; i <= this.selectIndex; i++) { |
|
378 var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex]; |
|
379 selectedFrames.push(functionObj.functionName); |
|
380 } |
|
381 } else { |
|
382 for (var i = sample.length - 1; i >= this.selectIndex; i--) { |
|
383 var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex]; |
|
384 selectedFrames.push(functionObj.functionName); |
|
385 } |
|
386 } |
|
387 gTreeManager.setSelection(selectedFrames); |
|
388 return false; |
|
389 } |
|
390 } |
|
391 return list; |
|
392 }, |
|
393 } |
|
394 |
|
395 |
|
396 function PluginView() { |
|
397 this._container = document.createElement("div"); |
|
398 this._container.className = "pluginview"; |
|
399 this._container.style.visibility = 'hidden'; |
|
400 this._iframe = document.createElement("iframe"); |
|
401 this._iframe.className = "pluginviewIFrame"; |
|
402 this._container.appendChild(this._iframe); |
|
403 this._container.style.top = ""; |
|
404 } |
|
405 PluginView.prototype = { |
|
406 getContainer: function PluginView_getContainer() { |
|
407 return this._container; |
|
408 }, |
|
409 hide: function() { |
|
410 // get rid of the scrollbars |
|
411 this._container.style.top = ""; |
|
412 this._container.style.visibility = 'hidden'; |
|
413 }, |
|
414 show: function() { |
|
415 // This creates extra scrollbar so only do it when needed |
|
416 this._container.style.top = "0px"; |
|
417 this._container.style.visibility = ''; |
|
418 }, |
|
419 display: function(pluginName, param, data) { |
|
420 this._iframe.src = "js/plugins/" + pluginName + "/index.html"; |
|
421 var self = this; |
|
422 this._iframe.onload = function() { |
|
423 self._iframe.contentWindow.initCleopatraPlugin(data, param, gSymbols); |
|
424 } |
|
425 this.show(); |
|
426 }, |
|
427 } |
|
428 |
|
429 function HistogramView() { |
|
430 this._container = document.createElement("div"); |
|
431 this._container.className = "histogram"; |
|
432 |
|
433 this._canvas = this._createCanvas(); |
|
434 this._container.appendChild(this._canvas); |
|
435 |
|
436 this._rangeSelector = new RangeSelector(this._canvas, this); |
|
437 this._rangeSelector.enableRangeSelectionOnHistogram(); |
|
438 this._container.appendChild(this._rangeSelector.getContainer()); |
|
439 |
|
440 this._busyCover = document.createElement("div"); |
|
441 this._busyCover.className = "busyCover"; |
|
442 this._container.appendChild(this._busyCover); |
|
443 |
|
444 this._histogramData = []; |
|
445 } |
|
446 HistogramView.prototype = { |
|
447 dataIsOutdated: function HistogramView_dataIsOutdated() { |
|
448 this._busyCover.classList.add("busy"); |
|
449 }, |
|
450 _createCanvas: function HistogramView__createCanvas() { |
|
451 var canvas = document.createElement("canvas"); |
|
452 canvas.height = 60; |
|
453 canvas.style.width = "100%"; |
|
454 canvas.style.height = "100%"; |
|
455 return canvas; |
|
456 }, |
|
457 getContainer: function HistogramView_getContainer() { |
|
458 return this._container; |
|
459 }, |
|
460 selectRange: function HistogramView_selectRange(start, end) { |
|
461 this._rangeSelector._finishSelection(start, end); |
|
462 }, |
|
463 showVideoFramePosition: function HistogramView_showVideoFramePosition(frame) { |
|
464 if (!this._frameStart || !this._frameStart[frame]) |
|
465 return; |
|
466 var frameStart = this._frameStart[frame]; |
|
467 // Now we look for the frame end. Because we can swap frame we don't present we have to look ahead |
|
468 // in the stream if frame+1 doesn't exist. |
|
469 var frameEnd = this._frameStart[frame+1]; |
|
470 for (var i = 0; i < 10 && !frameEnd; i++) { |
|
471 frameEnd = this._frameStart[frame+1+i]; |
|
472 } |
|
473 this._rangeSelector.showVideoRange(frameStart, frameEnd); |
|
474 }, |
|
475 showVideoPosition: function HistogramView_showVideoPosition(position) { |
|
476 // position in 0..1 |
|
477 this._rangeSelector.showVideoPosition(position); |
|
478 }, |
|
479 _gatherMarkersList: function HistogramView__gatherMarkersList(histogramData) { |
|
480 var markers = []; |
|
481 for (var i = 0; i < histogramData.length; ++i) { |
|
482 var step = histogramData[i]; |
|
483 if ("marker" in step) { |
|
484 markers.push({ |
|
485 index: i, |
|
486 name: step.marker |
|
487 }); |
|
488 } |
|
489 } |
|
490 return markers; |
|
491 }, |
|
492 _calculateWidthMultiplier: function () { |
|
493 var minWidth = 2000; |
|
494 return Math.ceil(minWidth / this._widthSum); |
|
495 }, |
|
496 histogramClick: function HistogramView_histogramClick(index) { |
|
497 var sample = this._histogramData[index]; |
|
498 var frames = sample.frames; |
|
499 var list = gSampleBar.setSample(frames[0]); |
|
500 gTreeManager.setSelection(list); |
|
501 setHighlightedCallstack(frames[0], frames[0]); |
|
502 }, |
|
503 display: function HistogramView_display(histogramData, frameStart, widthSum, highlightedCallstack) { |
|
504 this._histogramData = histogramData; |
|
505 this._frameStart = frameStart; |
|
506 this._widthSum = widthSum; |
|
507 this._widthMultiplier = this._calculateWidthMultiplier(); |
|
508 this._canvas.width = this._widthMultiplier * this._widthSum; |
|
509 this._render(highlightedCallstack); |
|
510 this._busyCover.classList.remove("busy"); |
|
511 }, |
|
512 _scheduleRender: function HistogramView__scheduleRender(highlightedCallstack) { |
|
513 var self = this; |
|
514 if (self._pendingAnimationFrame != null) { |
|
515 return; |
|
516 } |
|
517 self._pendingAnimationFrame = requestAnimationFrame(function anim_frame() { |
|
518 cancelAnimationFrame(self._pendingAnimationFrame); |
|
519 self._pendingAnimationFrame = null; |
|
520 self._render(highlightedCallstack); |
|
521 self._busyCover.classList.remove("busy"); |
|
522 }); |
|
523 }, |
|
524 _render: function HistogramView__render(highlightedCallstack) { |
|
525 var ctx = this._canvas.getContext("2d"); |
|
526 var height = this._canvas.height; |
|
527 ctx.setTransform(this._widthMultiplier, 0, 0, 1, 0, 0); |
|
528 ctx.font = "20px Georgia"; |
|
529 ctx.clearRect(0, 0, this._widthSum, height); |
|
530 |
|
531 var self = this; |
|
532 var markerCount = 0; |
|
533 for (var i = 0; i < this._histogramData.length; i++) { |
|
534 var step = this._histogramData[i]; |
|
535 var isSelected = self._isStepSelected(step, highlightedCallstack); |
|
536 var isInRangeSelector = self._isInRangeSelector(i); |
|
537 if (isSelected) { |
|
538 ctx.fillStyle = "green"; |
|
539 } else if (isInRangeSelector) { |
|
540 ctx.fillStyle = "blue"; |
|
541 } else { |
|
542 ctx.fillStyle = step.color; |
|
543 } |
|
544 var roundedHeight = Math.round(step.value * height); |
|
545 ctx.fillRect(step.x, height - roundedHeight, step.width, roundedHeight); |
|
546 } |
|
547 |
|
548 this._finishedRendering = true; |
|
549 }, |
|
550 highlightedCallstackChanged: function HistogramView_highlightedCallstackChanged(highlightedCallstack) { |
|
551 this._scheduleRender(highlightedCallstack); |
|
552 }, |
|
553 _isInRangeSelector: function HistogramView_isInRangeSelector(index) { |
|
554 return false; |
|
555 }, |
|
556 _isStepSelected: function HistogramView__isStepSelected(step, highlightedCallstack) { |
|
557 if ("marker" in step) |
|
558 return false; |
|
559 |
|
560 search_frames: for (var i = 0; i < step.frames.length; i++) { |
|
561 var frames = step.frames[i]; |
|
562 |
|
563 if (frames.length < highlightedCallstack.length || |
|
564 highlightedCallstack.length <= (gInvertCallstack ? 0 : 1)) |
|
565 continue; |
|
566 |
|
567 var compareFrames = frames; |
|
568 if (gInvertCallstack) { |
|
569 for (var j = 0; j < highlightedCallstack.length; j++) { |
|
570 var compareFrameIndex = compareFrames.length - 1 - j; |
|
571 if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) { |
|
572 continue search_frames; |
|
573 } |
|
574 } |
|
575 } else { |
|
576 for (var j = 0; j < highlightedCallstack.length; j++) { |
|
577 var compareFrameIndex = j; |
|
578 if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) { |
|
579 continue search_frames; |
|
580 } |
|
581 } |
|
582 } |
|
583 return true; |
|
584 }; |
|
585 return false; |
|
586 }, |
|
587 getHistogramData: function HistogramView__getHistogramData() { |
|
588 return this._histogramData; |
|
589 }, |
|
590 _getStepColor: function HistogramView__getStepColor(step) { |
|
591 if ("responsiveness" in step.extraInfo) { |
|
592 var res = step.extraInfo.responsiveness; |
|
593 var redComponent = Math.round(255 * Math.min(1, res / kDelayUntilWorstResponsiveness)); |
|
594 return "rgb(" + redComponent + ",0,0)"; |
|
595 } |
|
596 |
|
597 return "rgb(0,0,0)"; |
|
598 }, |
|
599 }; |
|
600 |
|
601 function RangeSelector(graph, histogram) { |
|
602 this._histogram = histogram; |
|
603 this.container = document.createElement("div"); |
|
604 this.container.className = "rangeSelectorContainer"; |
|
605 this._graph = graph; |
|
606 this._selectedRange = { startX: 0, endX: 0 }; |
|
607 this._selectedSampleRange = { start: 0, end: 0 }; |
|
608 |
|
609 this._highlighter = document.createElement("div"); |
|
610 this._highlighter.className = "histogramHilite collapsed"; |
|
611 this.container.appendChild(this._highlighter); |
|
612 |
|
613 this._mouseMarker = document.createElement("div"); |
|
614 this._mouseMarker.className = "histogramMouseMarker"; |
|
615 this.container.appendChild(this._mouseMarker); |
|
616 } |
|
617 RangeSelector.prototype = { |
|
618 getContainer: function RangeSelector_getContainer() { |
|
619 return this.container; |
|
620 }, |
|
621 // echo the location off the mouse on the histogram |
|
622 drawMouseMarker: function RangeSelector_drawMouseMarker(x) { |
|
623 var mouseMarker = this._mouseMarker; |
|
624 mouseMarker.style.left = x + "px"; |
|
625 }, |
|
626 showVideoPosition: function RangeSelector_showVideoPosition(position) { |
|
627 this.drawMouseMarker(position * (this._graph.parentNode.clientWidth-1)); |
|
628 PROFILERLOG("Show video position: " + position); |
|
629 }, |
|
630 drawHiliteRectangle: function RangeSelector_drawHiliteRectangle(x, y, width, height) { |
|
631 var hilite = this._highlighter; |
|
632 hilite.style.left = x + "px"; |
|
633 hilite.style.top = "0"; |
|
634 hilite.style.width = width + "px"; |
|
635 hilite.style.height = height + "px"; |
|
636 }, |
|
637 clearCurrentRangeSelection: function RangeSelector_clearCurrentRangeSelection() { |
|
638 try { |
|
639 this.changeEventSuppressed = true; |
|
640 var children = this.selector.childNodes; |
|
641 for (var i = 0; i < children.length; ++i) { |
|
642 children[i].selected = false; |
|
643 } |
|
644 } finally { |
|
645 this.changeEventSuppressed = false; |
|
646 } |
|
647 }, |
|
648 showVideoRange: function RangeSelector_showVideoRange(startIndex, endIndex) { |
|
649 if (!endIndex || endIndex < 0) |
|
650 endIndex = gCurrentlyShownSampleData.length; |
|
651 |
|
652 var len = this._graph.parentNode.getBoundingClientRect().right - this._graph.parentNode.getBoundingClientRect().left; |
|
653 this._selectedRange.startX = startIndex * len / this._histogram._histogramData.length; |
|
654 this._selectedRange.endX = endIndex * len / this._histogram._histogramData.length; |
|
655 var width = this._selectedRange.endX - this._selectedRange.startX; |
|
656 var height = this._graph.parentNode.clientHeight; |
|
657 this._highlighter.classList.remove("collapsed"); |
|
658 this.drawHiliteRectangle(this._selectedRange.startX, 0, width, height); |
|
659 //this._finishSelection(startIndex, endIndex); |
|
660 }, |
|
661 enableRangeSelectionOnHistogram: function RangeSelector_enableRangeSelectionOnHistogram() { |
|
662 var graph = this._graph; |
|
663 var isDrawingRectangle = false; |
|
664 var origX, origY; |
|
665 var self = this; |
|
666 // Compute this on the mouse down rather then forcing a sync reflow |
|
667 // every frame. |
|
668 var boundingRect = null; |
|
669 function histogramClick(clickX, clickY) { |
|
670 clickX = Math.min(clickX, graph.parentNode.getBoundingClientRect().right); |
|
671 clickX = clickX - graph.parentNode.getBoundingClientRect().left; |
|
672 var index = self._histogramIndexFromPoint(clickX); |
|
673 self._histogram.histogramClick(index); |
|
674 } |
|
675 function updateHiliteRectangle(newX, newY) { |
|
676 newX = Math.min(newX, boundingRect.right); |
|
677 var startX = Math.min(newX, origX) - boundingRect.left; |
|
678 var startY = 0; |
|
679 var width = Math.abs(newX - origX); |
|
680 var height = graph.parentNode.clientHeight; |
|
681 if (startX < 0) { |
|
682 width += startX; |
|
683 startX = 0; |
|
684 } |
|
685 self._selectedRange.startX = startX; |
|
686 self._selectedRange.endX = startX + width; |
|
687 self.drawHiliteRectangle(startX, startY, width, height); |
|
688 } |
|
689 function updateMouseMarker(newX) { |
|
690 self.drawMouseMarker(newX - graph.parentNode.getBoundingClientRect().left); |
|
691 } |
|
692 graph.addEventListener("mousedown", function(e) { |
|
693 if (e.button != 0) |
|
694 return; |
|
695 graph.style.cursor = "col-resize"; |
|
696 isDrawingRectangle = true; |
|
697 self.beginHistogramSelection(); |
|
698 origX = e.pageX; |
|
699 origY = e.pageY; |
|
700 boundingRect = graph.parentNode.getBoundingClientRect(); |
|
701 if (this.setCapture) |
|
702 this.setCapture(); |
|
703 // Reset the highlight rectangle |
|
704 updateHiliteRectangle(e.pageX, e.pageY); |
|
705 e.preventDefault(); |
|
706 this._movedDuringClick = false; |
|
707 }, false); |
|
708 graph.addEventListener("mouseup", function(e) { |
|
709 graph.style.cursor = "default"; |
|
710 if (!this._movedDuringClick) { |
|
711 isDrawingRectangle = false; |
|
712 // Handle as a click on the histogram. Select the sample: |
|
713 histogramClick(e.pageX, e.pageY); |
|
714 } else if (isDrawingRectangle) { |
|
715 isDrawingRectangle = false; |
|
716 updateHiliteRectangle(e.pageX, e.pageY); |
|
717 self.finishHistogramSelection(e.pageX != origX); |
|
718 if (e.pageX == origX) { |
|
719 // Simple click in the histogram |
|
720 var index = self._sampleIndexFromPoint(e.pageX - graph.parentNode.getBoundingClientRect().left); |
|
721 // TODO Select this sample in the tree view |
|
722 var sample = gCurrentlyShownSampleData[index]; |
|
723 } |
|
724 } |
|
725 }, false); |
|
726 graph.addEventListener("mousemove", function(e) { |
|
727 this._movedDuringClick = true; |
|
728 if (isDrawingRectangle) { |
|
729 updateMouseMarker(-1); // Clear |
|
730 updateHiliteRectangle(e.pageX, e.pageY); |
|
731 } else { |
|
732 updateMouseMarker(e.pageX); |
|
733 } |
|
734 }, false); |
|
735 graph.addEventListener("mouseout", function(e) { |
|
736 updateMouseMarker(-1); // Clear |
|
737 }, false); |
|
738 }, |
|
739 beginHistogramSelection: function RangeSelector_beginHistgramSelection() { |
|
740 var hilite = this._highlighter; |
|
741 hilite.classList.remove("finished"); |
|
742 hilite.classList.add("selecting"); |
|
743 hilite.classList.remove("collapsed"); |
|
744 if (this._transientRestrictionEnteringAffordance) { |
|
745 this._transientRestrictionEnteringAffordance.discard(); |
|
746 } |
|
747 }, |
|
748 _finishSelection: function RangeSelector__finishSelection(start, end) { |
|
749 var newFilterChain = gSampleFilters.concat({ type: "RangeSampleFilter", start: start, end: end }); |
|
750 var self = this; |
|
751 self._transientRestrictionEnteringAffordance = gBreadcrumbTrail.add({ |
|
752 title: gStrings["Sample Range"] + " [" + start + ", " + (end + 1) + "]", |
|
753 enterCallback: function () { |
|
754 gSampleFilters = newFilterChain; |
|
755 self.collapseHistogramSelection(); |
|
756 filtersChanged(); |
|
757 } |
|
758 }); |
|
759 }, |
|
760 finishHistogramSelection: function RangeSelector_finishHistgramSelection(isSomethingSelected) { |
|
761 var self = this; |
|
762 var hilite = this._highlighter; |
|
763 hilite.classList.remove("selecting"); |
|
764 if (isSomethingSelected) { |
|
765 hilite.classList.add("finished"); |
|
766 var start = this._sampleIndexFromPoint(this._selectedRange.startX); |
|
767 var end = this._sampleIndexFromPoint(this._selectedRange.endX); |
|
768 self._finishSelection(start, end); |
|
769 } else { |
|
770 hilite.classList.add("collapsed"); |
|
771 } |
|
772 }, |
|
773 collapseHistogramSelection: function RangeSelector_collapseHistogramSelection() { |
|
774 var hilite = this._highlighter; |
|
775 hilite.classList.add("collapsed"); |
|
776 }, |
|
777 _sampleIndexFromPoint: function RangeSelector__sampleIndexFromPoint(x) { |
|
778 // XXX this is completely wrong, fix please |
|
779 var totalSamples = parseFloat(gCurrentlyShownSampleData.length); |
|
780 var width = parseFloat(this._graph.parentNode.clientWidth); |
|
781 var factor = totalSamples / width; |
|
782 return parseInt(parseFloat(x) * factor); |
|
783 }, |
|
784 _histogramIndexFromPoint: function RangeSelector__histogramIndexFromPoint(x) { |
|
785 // XXX this is completely wrong, fix please |
|
786 var totalSamples = parseFloat(this._histogram._histogramData.length); |
|
787 var width = parseFloat(this._graph.parentNode.clientWidth); |
|
788 var factor = totalSamples / width; |
|
789 return parseInt(parseFloat(x) * factor); |
|
790 }, |
|
791 }; |
|
792 |
|
793 function videoPaneTimeChange(video) { |
|
794 if (!gMeta || !gMeta.frameStart) |
|
795 return; |
|
796 |
|
797 var frame = gVideoPane.getCurrentFrameNumber(); |
|
798 //var frameStart = gMeta.frameStart[frame]; |
|
799 //var frameEnd = gMeta.frameStart[frame+1]; // If we don't have a frameEnd assume the end of the profile |
|
800 |
|
801 gHistogramView.showVideoFramePosition(frame); |
|
802 } |
|
803 |
|
804 |
|
805 window.onpopstate = function(ev) { |
|
806 return; // Conflicts with document url |
|
807 if (!gBreadcrumbTrail) |
|
808 return; |
|
809 |
|
810 gBreadcrumbTrail.pop(); |
|
811 if (ev.state) { |
|
812 if (ev.state.action === "popbreadcrumb") { |
|
813 //gBreadcrumbTrail.pop(); |
|
814 } |
|
815 } |
|
816 } |
|
817 |
|
818 function BreadcrumbTrail() { |
|
819 this._breadcrumbs = []; |
|
820 this._selectedBreadcrumbIndex = -1; |
|
821 |
|
822 this._containerElement = document.createElement("div"); |
|
823 this._containerElement.className = "breadcrumbTrail"; |
|
824 var self = this; |
|
825 this._containerElement.addEventListener("click", function (e) { |
|
826 if (!e.target.classList.contains("breadcrumbTrailItem")) |
|
827 return; |
|
828 self._enter(e.target.breadcrumbIndex); |
|
829 }); |
|
830 } |
|
831 BreadcrumbTrail.prototype = { |
|
832 getContainer: function BreadcrumbTrail_getContainer() { |
|
833 return this._containerElement; |
|
834 }, |
|
835 /** |
|
836 * Add a breadcrumb. The breadcrumb parameter is an object with the following |
|
837 * properties: |
|
838 * - title: The text that will be shown in the breadcrumb's button. |
|
839 * - enterCallback: A function that will be called when entering this |
|
840 * breadcrumb. |
|
841 */ |
|
842 add: function BreadcrumbTrail_add(breadcrumb) { |
|
843 for (var i = this._breadcrumbs.length - 1; i > this._selectedBreadcrumbIndex; i--) { |
|
844 var rearLi = this._breadcrumbs[i]; |
|
845 if (!rearLi.breadcrumbIsTransient) |
|
846 throw "Can only add new breadcrumbs if after the current one there are only transient ones."; |
|
847 rearLi.breadcrumbDiscarder.discard(); |
|
848 } |
|
849 var div = document.createElement("div"); |
|
850 div.className = "breadcrumbTrailItem"; |
|
851 div.textContent = breadcrumb.title; |
|
852 var index = this._breadcrumbs.length; |
|
853 div.breadcrumbIndex = index; |
|
854 div.breadcrumbEnterCallback = breadcrumb.enterCallback; |
|
855 div.breadcrumbIsTransient = true; |
|
856 div.style.zIndex = 1000 - index; |
|
857 this._containerElement.appendChild(div); |
|
858 this._breadcrumbs.push(div); |
|
859 if (index == 0) |
|
860 this._enter(index); |
|
861 var self = this; |
|
862 div.breadcrumbDiscarder = { |
|
863 discard: function () { |
|
864 if (div.breadcrumbIsTransient) { |
|
865 self._deleteBeyond(index - 1); |
|
866 delete div.breadcrumbIsTransient; |
|
867 delete div.breadcrumbDiscarder; |
|
868 } |
|
869 } |
|
870 }; |
|
871 return div.breadcrumbDiscarder; |
|
872 }, |
|
873 addAndEnter: function BreadcrumbTrail_addAndEnter(breadcrumb) { |
|
874 var removalHandle = this.add(breadcrumb); |
|
875 this._enter(this._breadcrumbs.length - 1); |
|
876 }, |
|
877 pop : function BreadcrumbTrail_pop() { |
|
878 if (this._breadcrumbs.length-2 >= 0) |
|
879 this._enter(this._breadcrumbs.length-2); |
|
880 }, |
|
881 enterLastItem: function BreadcrumbTrail_enterLastItem(forceSelection) { |
|
882 this._enter(this._breadcrumbs.length-1, forceSelection); |
|
883 }, |
|
884 _enter: function BreadcrumbTrail__select(index, forceSelection) { |
|
885 if (index == this._selectedBreadcrumbIndex) |
|
886 return; |
|
887 if (forceSelection) { |
|
888 gTreeManager.restoreSerializedSelectionSnapshot(forceSelection); |
|
889 } else { |
|
890 gTreeManager.saveSelectionSnapshot(); |
|
891 } |
|
892 var prevSelected = this._breadcrumbs[this._selectedBreadcrumbIndex]; |
|
893 if (prevSelected) |
|
894 prevSelected.classList.remove("selected"); |
|
895 var li = this._breadcrumbs[index]; |
|
896 if (this === gBreadcrumbTrail && index != 0) { |
|
897 // Support for back button, disabled until the forward button is implemented. |
|
898 //var state = {action: "popbreadcrumb",}; |
|
899 //window.history.pushState(state, "Cleopatra"); |
|
900 } |
|
901 |
|
902 delete li.breadcrumbIsTransient; |
|
903 li.classList.add("selected"); |
|
904 this._deleteBeyond(index); |
|
905 this._selectedBreadcrumbIndex = index; |
|
906 li.breadcrumbEnterCallback(); |
|
907 // Add history state |
|
908 }, |
|
909 _deleteBeyond: function BreadcrumbTrail__deleteBeyond(index) { |
|
910 while (this._breadcrumbs.length > index + 1) { |
|
911 this._hide(this._breadcrumbs[index + 1]); |
|
912 this._breadcrumbs.splice(index + 1, 1); |
|
913 } |
|
914 }, |
|
915 _hide: function BreadcrumbTrail__hide(breadcrumb) { |
|
916 delete breadcrumb.breadcrumbIsTransient; |
|
917 breadcrumb.classList.add("deleted"); |
|
918 setTimeout(function () { |
|
919 breadcrumb.parentNode.removeChild(breadcrumb); |
|
920 }, 1000); |
|
921 }, |
|
922 }; |
|
923 |
|
924 function maxResponsiveness() { |
|
925 var data = gCurrentlyShownSampleData; |
|
926 var maxRes = 0.0; |
|
927 for (var i = 0; i < data.length; ++i) { |
|
928 if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["responsiveness"]) |
|
929 continue; |
|
930 if (maxRes < data[i].extraInfo["responsiveness"]) |
|
931 maxRes = data[i].extraInfo["responsiveness"]; |
|
932 } |
|
933 return maxRes; |
|
934 } |
|
935 |
|
936 function effectiveInterval() { |
|
937 var data = gCurrentlyShownSampleData; |
|
938 var interval = 0.0; |
|
939 var sampleCount = 0; |
|
940 var timeCount = 0; |
|
941 var lastTime = null; |
|
942 for (var i = 0; i < data.length; ++i) { |
|
943 if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["time"]) { |
|
944 lastTime = null; |
|
945 continue; |
|
946 } |
|
947 if (lastTime) { |
|
948 sampleCount++; |
|
949 timeCount += data[i].extraInfo["time"] - lastTime; |
|
950 } |
|
951 lastTime = data[i].extraInfo["time"]; |
|
952 } |
|
953 var effectiveInterval = timeCount/sampleCount; |
|
954 // Biggest diff |
|
955 var biggestDiff = 0; |
|
956 lastTime = null; |
|
957 for (var i = 0; i < data.length; ++i) { |
|
958 if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["time"]) { |
|
959 lastTime = null; |
|
960 continue; |
|
961 } |
|
962 if (lastTime) { |
|
963 if (biggestDiff < Math.abs(effectiveInterval - (data[i].extraInfo["time"] - lastTime))) |
|
964 biggestDiff = Math.abs(effectiveInterval - (data[i].extraInfo["time"] - lastTime)); |
|
965 } |
|
966 lastTime = data[i].extraInfo["time"]; |
|
967 } |
|
968 |
|
969 if (effectiveInterval != effectiveInterval) |
|
970 return "Time info not collected"; |
|
971 |
|
972 return (effectiveInterval).toFixed(2) + " ms ±" + biggestDiff.toFixed(2); |
|
973 } |
|
974 |
|
975 function numberOfCurrentlyShownSamples() { |
|
976 var data = gCurrentlyShownSampleData; |
|
977 var num = 0; |
|
978 for (var i = 0; i < data.length; ++i) { |
|
979 if (data[i]) |
|
980 num++; |
|
981 } |
|
982 return num; |
|
983 } |
|
984 |
|
985 function avgResponsiveness() { |
|
986 var data = gCurrentlyShownSampleData; |
|
987 var totalRes = 0.0; |
|
988 for (var i = 0; i < data.length; ++i) { |
|
989 if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["responsiveness"]) |
|
990 continue; |
|
991 totalRes += data[i].extraInfo["responsiveness"]; |
|
992 } |
|
993 return totalRes / numberOfCurrentlyShownSamples(); |
|
994 } |
|
995 |
|
996 function copyProfile() { |
|
997 window.prompt ("Copy to clipboard: Ctrl+C, Enter", document.getElementById("data").value); |
|
998 } |
|
999 |
|
1000 function saveProfileToLocalStorage() { |
|
1001 Parser.getSerializedProfile(true, function (serializedProfile) { |
|
1002 gLocalStorage.storeLocalProfile(serializedProfile, gMeta.profileId, function profileSaved() { |
|
1003 |
|
1004 }); |
|
1005 }); |
|
1006 } |
|
1007 function downloadProfile() { |
|
1008 Parser.getSerializedProfile(true, function (serializedProfile) { |
|
1009 var blob = new Blob([serializedProfile], { "type": "application/octet-stream" }); |
|
1010 location.href = window.URL.createObjectURL(blob); |
|
1011 }); |
|
1012 } |
|
1013 |
|
1014 function promptUploadProfile(selected) { |
|
1015 var overlay = document.createElement("div"); |
|
1016 overlay.style.position = "absolute"; |
|
1017 overlay.style.top = 0; |
|
1018 overlay.style.left = 0; |
|
1019 overlay.style.width = "100%"; |
|
1020 overlay.style.height = "100%"; |
|
1021 overlay.style.backgroundColor = "transparent"; |
|
1022 |
|
1023 var bg = document.createElement("div"); |
|
1024 bg.style.position = "absolute"; |
|
1025 bg.style.top = 0; |
|
1026 bg.style.left = 0; |
|
1027 bg.style.width = "100%"; |
|
1028 bg.style.height = "100%"; |
|
1029 bg.style.opacity = "0.6"; |
|
1030 bg.style.backgroundColor = "#aaaaaa"; |
|
1031 overlay.appendChild(bg); |
|
1032 |
|
1033 var contentDiv = document.createElement("div"); |
|
1034 contentDiv.className = "sideBar"; |
|
1035 contentDiv.style.position = "absolute"; |
|
1036 contentDiv.style.top = "50%"; |
|
1037 contentDiv.style.left = "50%"; |
|
1038 contentDiv.style.width = "40em"; |
|
1039 contentDiv.style.height = "20em"; |
|
1040 contentDiv.style.marginLeft = "-20em"; |
|
1041 contentDiv.style.marginTop = "-10em"; |
|
1042 contentDiv.style.padding = "10px"; |
|
1043 contentDiv.style.border = "2px solid black"; |
|
1044 contentDiv.style.backgroundColor = "rgb(219, 223, 231)"; |
|
1045 overlay.appendChild(contentDiv); |
|
1046 |
|
1047 var noticeHTML = ""; |
|
1048 noticeHTML += "<center><h2 style='font-size: 2em'>Upload Profile - Privacy Notice</h2></center>"; |
|
1049 noticeHTML += "You're about to upload your profile publicly where anyone will be able to access it. "; |
|
1050 noticeHTML += "To better diagnose performance problems profiles include the following information:"; |
|
1051 noticeHTML += "<ul>"; |
|
1052 noticeHTML += " <li>The <b>URLs</b> and scripts of the tabs that were executing.</li>"; |
|
1053 noticeHTML += " <li>The <b>metadata of all your Add-ons</b> to identify slow Add-ons.</li>"; |
|
1054 noticeHTML += " <li>Firefox build and runtime configuration.</li>"; |
|
1055 noticeHTML += "</ul><br>"; |
|
1056 noticeHTML += "To view all the information you can download the full profile to a file and open the json structure with a text editor.<br><br>"; |
|
1057 contentDiv.innerHTML = noticeHTML; |
|
1058 |
|
1059 var cancelButton = document.createElement("input"); |
|
1060 cancelButton.style.position = "absolute"; |
|
1061 cancelButton.style.bottom = "10px"; |
|
1062 cancelButton.type = "button"; |
|
1063 cancelButton.value = "Cancel"; |
|
1064 cancelButton.onclick = function() { |
|
1065 document.body.removeChild(overlay); |
|
1066 } |
|
1067 contentDiv.appendChild(cancelButton); |
|
1068 |
|
1069 var uploadButton = document.createElement("input"); |
|
1070 uploadButton.style.position = "absolute"; |
|
1071 uploadButton.style.right = "10px"; |
|
1072 uploadButton.style.bottom = "10px"; |
|
1073 uploadButton.type = "button"; |
|
1074 uploadButton.value = "Upload"; |
|
1075 uploadButton.onclick = function() { |
|
1076 document.body.removeChild(overlay); |
|
1077 uploadProfile(selected); |
|
1078 } |
|
1079 contentDiv.appendChild(uploadButton); |
|
1080 |
|
1081 document.body.appendChild(overlay); |
|
1082 } |
|
1083 |
|
1084 function uploadProfile(selected) { |
|
1085 Parser.getSerializedProfile(!selected, function (dataToUpload) { |
|
1086 var oXHR = new XMLHttpRequest(); |
|
1087 oXHR.onload = function (oEvent) { |
|
1088 if (oXHR.status == 200) { |
|
1089 gReportID = oXHR.responseText; |
|
1090 updateDocumentURL(); |
|
1091 document.getElementById("upload_status").innerHTML = "Success! Use this <a id='linkElem'>link</a>"; |
|
1092 document.getElementById("linkElem").href = document.URL; |
|
1093 } else { |
|
1094 document.getElementById("upload_status").innerHTML = "Error " + oXHR.status + " occurred uploading your file."; |
|
1095 } |
|
1096 }; |
|
1097 oXHR.onerror = function (oEvent) { |
|
1098 document.getElementById("upload_status").innerHTML = "Error " + oXHR.status + " occurred uploading your file."; |
|
1099 } |
|
1100 oXHR.upload.onprogress = function(oEvent) { |
|
1101 if (oEvent.lengthComputable) { |
|
1102 var progress = Math.round((oEvent.loaded / oEvent.total)*100); |
|
1103 if (progress == 100) { |
|
1104 document.getElementById("upload_status").innerHTML = "Uploading: Waiting for server side compression"; |
|
1105 } else { |
|
1106 document.getElementById("upload_status").innerHTML = "Uploading: " + Math.round((oEvent.loaded / oEvent.total)*100) + "%"; |
|
1107 } |
|
1108 } |
|
1109 }; |
|
1110 |
|
1111 var dataSize; |
|
1112 if (dataToUpload.length > 1024*1024) { |
|
1113 dataSize = (dataToUpload.length/1024/1024).toFixed(1) + " MB(s)"; |
|
1114 } else { |
|
1115 dataSize = (dataToUpload.length/1024).toFixed(1) + " KB(s)"; |
|
1116 } |
|
1117 |
|
1118 var formData = new FormData(); |
|
1119 formData.append("file", dataToUpload); |
|
1120 document.getElementById("upload_status").innerHTML = "Uploading Profile (" + dataSize + ")"; |
|
1121 oXHR.open("POST", "http://profile-store.appspot.com/store", true); |
|
1122 oXHR.send(formData); |
|
1123 }); |
|
1124 } |
|
1125 |
|
1126 function populate_skip_symbol() { |
|
1127 var skipSymbolCtrl = document.getElementById('skipsymbol') |
|
1128 //skipSymbolCtrl.options = gSkipSymbols; |
|
1129 for (var i = 0; i < gSkipSymbols.length; i++) { |
|
1130 var elOptNew = document.createElement('option'); |
|
1131 elOptNew.text = gSkipSymbols[i]; |
|
1132 elOptNew.value = gSkipSymbols[i]; |
|
1133 elSel.add(elOptNew); |
|
1134 } |
|
1135 |
|
1136 } |
|
1137 |
|
1138 function delete_skip_symbol() { |
|
1139 var skipSymbol = document.getElementById('skipsymbol').value |
|
1140 } |
|
1141 |
|
1142 function add_skip_symbol() { |
|
1143 |
|
1144 } |
|
1145 |
|
1146 var gFilterChangeCallback = null; |
|
1147 var gFilterChangeDelay = 1200; |
|
1148 function filterOnChange() { |
|
1149 if (gFilterChangeCallback != null) { |
|
1150 clearTimeout(gFilterChangeCallback); |
|
1151 gFilterChangeCallback = null; |
|
1152 } |
|
1153 |
|
1154 gFilterChangeCallback = setTimeout(filterUpdate, gFilterChangeDelay); |
|
1155 } |
|
1156 function filterUpdate() { |
|
1157 gFilterChangeCallback = null; |
|
1158 |
|
1159 filtersChanged(); |
|
1160 |
|
1161 var filterNameInput = document.getElementById("filterName"); |
|
1162 if (filterNameInput != null) { |
|
1163 changeFocus(filterNameInput); |
|
1164 } |
|
1165 } |
|
1166 |
|
1167 // Maps document id to a tooltip description |
|
1168 var tooltip = { |
|
1169 "mergeFunctions" : "Ignore line information and merge samples based on function names.", |
|
1170 "showJank" : "Show only samples with >50ms responsiveness.", |
|
1171 "showJS" : "Show only samples which involve running chrome or content Javascript code.", |
|
1172 "mergeUnbranched" : "Collapse unbranched call paths in the call tree into a single node.", |
|
1173 "filterName" : "Show only samples with a frame containing the filter as a substring.", |
|
1174 "invertCallstack" : "Invert the callstack (Heavy view) to find the most expensive leaf functions.", |
|
1175 "upload" : "Upload the full profile to public cloud storage to share with others.", |
|
1176 "upload_select" : "Upload only the selected view.", |
|
1177 "download" : "Initiate a download of the full profile.", |
|
1178 } |
|
1179 |
|
1180 function addTooltips() { |
|
1181 for (var elemId in tooltip) { |
|
1182 var elem = document.getElementById(elemId); |
|
1183 if (!elem) |
|
1184 continue; |
|
1185 if (elem.parentNode.nodeName.toLowerCase() == "label") |
|
1186 elem = elem.parentNode; |
|
1187 elem.title = tooltip[elemId]; |
|
1188 } |
|
1189 } |
|
1190 |
|
1191 function InfoBar() { |
|
1192 this._container = document.createElement("div"); |
|
1193 this._container.id = "infoBar"; |
|
1194 this._container.className = "sideBar"; |
|
1195 } |
|
1196 |
|
1197 InfoBar.prototype = { |
|
1198 getContainer: function InfoBar_getContainer() { |
|
1199 return this._container; |
|
1200 }, |
|
1201 display: function InfoBar_display() { |
|
1202 function getMetaFeatureString() { |
|
1203 features = "<dt>Stackwalk:</dt><dd>" + (gMeta.stackwalk ? "True" : "False") + "</dd>"; |
|
1204 features += "<dt>Jank:</dt><dd>" + (gMeta.stackwalk ? "True" : "False") + "</dd>"; |
|
1205 return features; |
|
1206 } |
|
1207 function getPlatformInfo() { |
|
1208 return gMeta.oscpu + " (" + gMeta.toolkit + ")"; |
|
1209 } |
|
1210 var infobar = this._container; |
|
1211 var infoText = ""; |
|
1212 |
|
1213 if (gMeta) { |
|
1214 infoText += "<h2>Profile Info</h2>\n<dl>\n"; |
|
1215 infoText += "<dt>Product:</dt><dd>" + gMeta.product + "</dd>"; |
|
1216 infoText += "<dt>Platform:</dt><dd>" + getPlatformInfo() + "</dd>"; |
|
1217 infoText += getMetaFeatureString(); |
|
1218 infoText += "<dt>Interval:</dt><dd>" + gMeta.interval + "Â ms</dd></dl>"; |
|
1219 } |
|
1220 infoText += "<h2>Selection Info</h2>\n<dl>\n"; |
|
1221 infoText += " <dt>Avg. Responsiveness:</dt><dd>" + avgResponsiveness().toFixed(2) + "Â ms</dd>\n"; |
|
1222 infoText += " <dt>Max Responsiveness:</dt><dd>" + maxResponsiveness().toFixed(2) + "Â ms</dd>\n"; |
|
1223 infoText += " <dt>Real Interval:</dt><dd>" + effectiveInterval() + "</dd>"; |
|
1224 infoText += "</dl>\n"; |
|
1225 infoText += "<h2>Pre Filtering</h2>\n"; |
|
1226 // Disable for now since it's buggy and not useful |
|
1227 //infoText += "<label><input type='checkbox' id='mergeFunctions' " + (gMergeFunctions ?" checked='true' ":" ") + " onchange='toggleMergeFunctions()'/>Functions, not lines</label><br>\n"; |
|
1228 |
|
1229 var filterNameInputOld = document.getElementById("filterName"); |
|
1230 infoText += "<a>Filter:\n"; |
|
1231 infoText += "<input type='search' id='filterName' oninput='filterOnChange()'/></a>\n"; |
|
1232 |
|
1233 infoText += "<h2>Post Filtering</h2>\n"; |
|
1234 infoText += "<label><input type='checkbox' id='showJank' " + (gJankOnly ?" checked='true' ":" ") + " onchange='toggleJank()'/>Show Jank only</label>\n"; |
|
1235 infoText += "<h2>View Options</h2>\n"; |
|
1236 infoText += "<label><input type='checkbox' id='showJS' " + (gJavascriptOnly ?" checked='true' ":" ") + " onchange='toggleJavascriptOnly()'/>Javascript only</label><br>\n"; |
|
1237 infoText += "<label><input type='checkbox' id='mergeUnbranched' " + (gMergeUnbranched ?" checked='true' ":" ") + " onchange='toggleMergeUnbranched()'/>Merge unbranched call paths</label><br>\n"; |
|
1238 infoText += "<label><input type='checkbox' id='invertCallstack' " + (gInvertCallstack ?" checked='true' ":" ") + " onchange='toggleInvertCallStack()'/>Invert callstack</label><br>\n"; |
|
1239 |
|
1240 infoText += "<h2>Share</h2>\n"; |
|
1241 infoText += "<div id='upload_status' aria-live='polite'>No upload in progress</div><br>\n"; |
|
1242 infoText += "<input type='button' id='upload' value='Upload full profile'>\n"; |
|
1243 infoText += "<input type='button' id='upload_select' value='Upload view'><br>\n"; |
|
1244 infoText += "<input type='button' id='download' value='Download full profile'>\n"; |
|
1245 |
|
1246 infoText += "<h2>Compare</h2>\n"; |
|
1247 infoText += "<input type='button' id='compare' value='Compare'>\n"; |
|
1248 |
|
1249 //infoText += "<br>\n"; |
|
1250 //infoText += "Skip functions:<br>\n"; |
|
1251 //infoText += "<select size=8 id='skipsymbol'></select><br />" |
|
1252 //infoText += "<input type='button' id='delete_skipsymbol' value='Delete'/><br />\n"; |
|
1253 //infoText += "<input type='button' id='add_skipsymbol' value='Add'/><br />\n"; |
|
1254 |
|
1255 infobar.innerHTML = infoText; |
|
1256 addTooltips(); |
|
1257 |
|
1258 var filterNameInputNew = document.getElementById("filterName"); |
|
1259 if (filterNameInputOld != null && filterNameInputNew != null) { |
|
1260 filterNameInputNew.parentNode.replaceChild(filterNameInputOld, filterNameInputNew); |
|
1261 //filterNameInputNew.value = filterNameInputOld.value; |
|
1262 } else if (gQueryParamFilterName != null) { |
|
1263 filterNameInputNew.value = gQueryParamFilterName; |
|
1264 gQueryParamFilterName = null; |
|
1265 } |
|
1266 document.getElementById('compare').onclick = function() { |
|
1267 openProfileCompare(); |
|
1268 } |
|
1269 document.getElementById('upload').onclick = function() { |
|
1270 promptUploadProfile(false); |
|
1271 }; |
|
1272 document.getElementById('download').onclick = downloadProfile; |
|
1273 document.getElementById('upload_select').onclick = function() { |
|
1274 promptUploadProfile(true); |
|
1275 }; |
|
1276 //document.getElementById('delete_skipsymbol').onclick = delete_skip_symbol; |
|
1277 //document.getElementById('add_skipsymbol').onclick = add_skip_symbol; |
|
1278 |
|
1279 //populate_skip_symbol(); |
|
1280 } |
|
1281 } |
|
1282 |
|
1283 var gNumSamples = 0; |
|
1284 var gMeta = null; |
|
1285 var gSymbols = {}; |
|
1286 var gFunctions = {}; |
|
1287 var gResources = {}; |
|
1288 var gHighlightedCallstack = []; |
|
1289 var gFrameView = null; |
|
1290 var gTreeManager = null; |
|
1291 var gSampleBar = null; |
|
1292 var gBreadcrumbTrail = null; |
|
1293 var gHistogramView = null; |
|
1294 var gDiagnosticBar = null; |
|
1295 var gVideoPane = null; |
|
1296 var gPluginView = null; |
|
1297 var gFileList = null; |
|
1298 var gInfoBar = null; |
|
1299 var gMainArea = null; |
|
1300 var gCurrentlyShownSampleData = null; |
|
1301 var gSkipSymbols = ["test2", "test1"]; |
|
1302 var gAppendVideoCapture = null; |
|
1303 var gQueryParamFilterName = null; |
|
1304 var gRestoreSelection = null; |
|
1305 var gReportID = null; |
|
1306 |
|
1307 function getTextData() { |
|
1308 var data = []; |
|
1309 var samples = gCurrentlyShownSampleData; |
|
1310 for (var i = 0; i < samples.length; i++) { |
|
1311 data.push(samples[i].lines.join("\n")); |
|
1312 } |
|
1313 return data.join("\n"); |
|
1314 } |
|
1315 |
|
1316 function loadProfileFile(fileList) { |
|
1317 if (fileList.length == 0) |
|
1318 return; |
|
1319 var file = fileList[0]; |
|
1320 var reporter = enterProgressUI(); |
|
1321 var subreporters = reporter.addSubreporters({ |
|
1322 fileLoading: 1000, |
|
1323 parsing: 1000 |
|
1324 }); |
|
1325 |
|
1326 var reader = new FileReader(); |
|
1327 reader.onloadend = function () { |
|
1328 subreporters.fileLoading.finish(); |
|
1329 loadRawProfile(subreporters.parsing, reader.result); |
|
1330 }; |
|
1331 reader.onprogress = function (e) { |
|
1332 subreporters.fileLoading.setProgress(e.loaded / e.total); |
|
1333 }; |
|
1334 reader.readAsText(file, "utf-8"); |
|
1335 subreporters.fileLoading.begin("Reading local file..."); |
|
1336 } |
|
1337 |
|
1338 function loadLocalStorageProfile(profileKey) { |
|
1339 var reporter = enterProgressUI(); |
|
1340 var subreporters = reporter.addSubreporters({ |
|
1341 fileLoading: 1000, |
|
1342 parsing: 1000 |
|
1343 }); |
|
1344 |
|
1345 gLocalStorage.getProfile(profileKey, function(profile) { |
|
1346 subreporters.fileLoading.finish(); |
|
1347 loadRawProfile(subreporters.parsing, profile, profileKey); |
|
1348 }); |
|
1349 subreporters.fileLoading.begin("Reading local storage..."); |
|
1350 } |
|
1351 |
|
1352 function appendVideoCapture(videoCapture) { |
|
1353 if (videoCapture.indexOf("://") == -1) { |
|
1354 videoCapture = EIDETICKER_BASE_URL + videoCapture; |
|
1355 } |
|
1356 gAppendVideoCapture = videoCapture; |
|
1357 } |
|
1358 |
|
1359 function loadZippedProfileURL(url) { |
|
1360 var reporter = enterProgressUI(); |
|
1361 var subreporters = reporter.addSubreporters({ |
|
1362 fileLoading: 1000, |
|
1363 parsing: 1000 |
|
1364 }); |
|
1365 |
|
1366 // Crude way to detect if we're using a relative URL or not :( |
|
1367 if (url.indexOf("://") == -1) { |
|
1368 url = EIDETICKER_BASE_URL + url; |
|
1369 } |
|
1370 reporter.begin("Fetching " + url); |
|
1371 PROFILERTRACE("Fetch url: " + url); |
|
1372 |
|
1373 function onerror(e) { |
|
1374 PROFILERERROR("zip.js error"); |
|
1375 PROFILERERROR(JSON.stringify(e)); |
|
1376 } |
|
1377 |
|
1378 zip.workerScriptsPath = "js/zip.js/"; |
|
1379 zip.createReader(new zip.HttpReader(url), function(zipReader) { |
|
1380 subreporters.fileLoading.setProgress(0.4); |
|
1381 zipReader.getEntries(function(entries) { |
|
1382 for (var i = 0; i < entries.length; i++) { |
|
1383 var entry = entries[i]; |
|
1384 PROFILERTRACE("Zip file: " + entry.filename); |
|
1385 if (entry.filename === "symbolicated_profile.txt") { |
|
1386 reporter.begin("Decompressing " + url); |
|
1387 subreporters.fileLoading.setProgress(0.8); |
|
1388 entry.getData(new zip.TextWriter(), function(profileText) { |
|
1389 subreporters.fileLoading.finish(); |
|
1390 loadRawProfile(subreporters.parsing, profileText); |
|
1391 }); |
|
1392 return; |
|
1393 } |
|
1394 onerror("symbolicated_profile.txt not found in zip file."); |
|
1395 } |
|
1396 }); |
|
1397 }, onerror); |
|
1398 } |
|
1399 |
|
1400 function loadProfileURL(url) { |
|
1401 var reporter = enterProgressUI(); |
|
1402 var subreporters = reporter.addSubreporters({ |
|
1403 fileLoading: 1000, |
|
1404 parsing: 1000 |
|
1405 }); |
|
1406 |
|
1407 var xhr = new XMLHttpRequest(); |
|
1408 xhr.open("GET", url, true); |
|
1409 xhr.responseType = "text"; |
|
1410 xhr.onreadystatechange = function (e) { |
|
1411 if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 0)) { |
|
1412 subreporters.fileLoading.finish(); |
|
1413 PROFILERLOG("Got profile from '" + url + "'."); |
|
1414 if (xhr.responseText == null || xhr.responseText === "") { |
|
1415 subreporters.fileLoading.begin("Profile '" + url + "' is empty. Did you set the CORS headers?"); |
|
1416 return; |
|
1417 } |
|
1418 loadRawProfile(subreporters.parsing, xhr.responseText, url); |
|
1419 } |
|
1420 }; |
|
1421 xhr.onerror = function (e) { |
|
1422 subreporters.fileLoading.begin("Error fetching profile :(. URL: '" + url + "'. Did you set the CORS headers?"); |
|
1423 } |
|
1424 xhr.onprogress = function (e) { |
|
1425 if (e.lengthComputable && (e.loaded <= e.total)) { |
|
1426 subreporters.fileLoading.setProgress(e.loaded / e.total); |
|
1427 } else { |
|
1428 subreporters.fileLoading.setProgress(NaN); |
|
1429 } |
|
1430 }; |
|
1431 xhr.send(null); |
|
1432 subreporters.fileLoading.begin("Loading remote file..."); |
|
1433 } |
|
1434 |
|
1435 function loadProfile(rawProfile) { |
|
1436 if (!rawProfile) |
|
1437 return; |
|
1438 var reporter = enterProgressUI(); |
|
1439 loadRawProfile(reporter, rawProfile); |
|
1440 } |
|
1441 |
|
1442 function loadRawProfile(reporter, rawProfile, profileId) { |
|
1443 PROFILERLOG("Parse raw profile: ~" + rawProfile.length + " bytes"); |
|
1444 reporter.begin("Parsing..."); |
|
1445 if (rawProfile == null || rawProfile.length === 0) { |
|
1446 reporter.begin("Profile is null or empty"); |
|
1447 return; |
|
1448 } |
|
1449 var startTime = Date.now(); |
|
1450 var parseRequest = Parser.parse(rawProfile, { |
|
1451 appendVideoCapture : gAppendVideoCapture, |
|
1452 profileId: profileId, |
|
1453 }); |
|
1454 parseRequest.addEventListener("progress", function (progress, action) { |
|
1455 if (action) |
|
1456 reporter.setAction(action); |
|
1457 reporter.setProgress(progress); |
|
1458 }); |
|
1459 parseRequest.addEventListener("finished", function (result) { |
|
1460 reporter.finish(); |
|
1461 gMeta = result.meta; |
|
1462 gNumSamples = result.numSamples; |
|
1463 gSymbols = result.symbols; |
|
1464 gFunctions = result.functions; |
|
1465 gResources = result.resources; |
|
1466 enterFinishedProfileUI(); |
|
1467 gFileList.profileParsingFinished(); |
|
1468 }); |
|
1469 } |
|
1470 |
|
1471 var gImportFromAddonSubreporters = null; |
|
1472 |
|
1473 window.addEventListener("message", function messageFromAddon(msg) { |
|
1474 // This is triggered by the profiler add-on. |
|
1475 var o = JSON.parse(msg.data); |
|
1476 switch (o.task) { |
|
1477 case "importFromAddonStart": |
|
1478 var totalReporter = enterProgressUI(); |
|
1479 gImportFromAddonSubreporters = totalReporter.addSubreporters({ |
|
1480 import: 10000, |
|
1481 parsing: 1000 |
|
1482 }); |
|
1483 gImportFromAddonSubreporters.import.begin("Symbolicating..."); |
|
1484 break; |
|
1485 case "importFromAddonProgress": |
|
1486 gImportFromAddonSubreporters.import.setProgress(o.progress); |
|
1487 if (o.action != null) { |
|
1488 gImportFromAddonSubreporters.import.setAction(o.action); |
|
1489 } |
|
1490 break; |
|
1491 case "importFromAddonFinish": |
|
1492 importFromAddonFinish(o.rawProfile); |
|
1493 break; |
|
1494 } |
|
1495 }); |
|
1496 |
|
1497 function importFromAddonFinish(rawProfile) { |
|
1498 gImportFromAddonSubreporters.import.finish(); |
|
1499 loadRawProfile(gImportFromAddonSubreporters.parsing, rawProfile); |
|
1500 } |
|
1501 |
|
1502 var gInvertCallstack = false; |
|
1503 function toggleInvertCallStack() { |
|
1504 gTreeManager.saveReverseSelectionSnapshot(gJavascriptOnly); |
|
1505 gInvertCallstack = !gInvertCallstack; |
|
1506 var startTime = Date.now(); |
|
1507 viewOptionsChanged(); |
|
1508 } |
|
1509 |
|
1510 var gMergeUnbranched = false; |
|
1511 function toggleMergeUnbranched() { |
|
1512 gMergeUnbranched = !gMergeUnbranched; |
|
1513 viewOptionsChanged(); |
|
1514 } |
|
1515 |
|
1516 var gMergeFunctions = true; |
|
1517 function toggleMergeFunctions() { |
|
1518 gMergeFunctions = !gMergeFunctions; |
|
1519 filtersChanged(); |
|
1520 } |
|
1521 |
|
1522 var gJankOnly = false; |
|
1523 var gJankThreshold = 50 /* ms */; |
|
1524 function toggleJank(/* optional */ threshold) { |
|
1525 // Currently we have no way to change the threshold in the UI |
|
1526 // once we add this we will need to change the tooltip. |
|
1527 gJankOnly = !gJankOnly; |
|
1528 if (threshold != null ) { |
|
1529 gJankThreshold = threshold; |
|
1530 } |
|
1531 filtersChanged(); |
|
1532 } |
|
1533 |
|
1534 var gJavascriptOnly = false; |
|
1535 function toggleJavascriptOnly() { |
|
1536 if (gJavascriptOnly) { |
|
1537 // When going from JS only to non js there's going to be new C++ |
|
1538 // frames in the selection so we need to restore the selection |
|
1539 // while allowing non contigous symbols to be in the stack (the c++ ones) |
|
1540 gTreeManager.setAllowNonContiguous(); |
|
1541 } |
|
1542 gJavascriptOnly = !gJavascriptOnly; |
|
1543 gTreeManager.saveSelectionSnapshot(gJavascriptOnly); |
|
1544 filtersChanged(); |
|
1545 } |
|
1546 |
|
1547 var gSampleFilters = []; |
|
1548 function focusOnSymbol(focusSymbol, name) { |
|
1549 var newFilterChain = gSampleFilters.concat([{type: "FocusedFrameSampleFilter", name: name, focusedSymbol: focusSymbol}]); |
|
1550 gBreadcrumbTrail.addAndEnter({ |
|
1551 title: name, |
|
1552 enterCallback: function () { |
|
1553 gSampleFilters = newFilterChain; |
|
1554 filtersChanged(); |
|
1555 } |
|
1556 }); |
|
1557 } |
|
1558 |
|
1559 function focusOnCallstack(focusedCallstack, name, overwriteCallstack) { |
|
1560 var invertCallstack = gInvertCallstack; |
|
1561 |
|
1562 if (overwriteCallstack != null) { |
|
1563 invertCallstack = overwriteCallstack; |
|
1564 } |
|
1565 var filter = { |
|
1566 type: !invertCallstack ? "FocusedCallstackPostfixSampleFilter" : "FocusedCallstackPrefixSampleFilter", |
|
1567 name: name, |
|
1568 focusedCallstack: focusedCallstack, |
|
1569 appliesToJS: gJavascriptOnly |
|
1570 }; |
|
1571 var newFilterChain = gSampleFilters.concat([filter]); |
|
1572 gBreadcrumbTrail.addAndEnter({ |
|
1573 title: name, |
|
1574 enterCallback: function () { |
|
1575 gSampleFilters = newFilterChain; |
|
1576 filtersChanged(); |
|
1577 } |
|
1578 }) |
|
1579 } |
|
1580 |
|
1581 function focusOnPluginView(pluginName, param) { |
|
1582 var filter = { |
|
1583 type: "PluginView", |
|
1584 pluginName: pluginName, |
|
1585 param: param, |
|
1586 }; |
|
1587 var newFilterChain = gSampleFilters.concat([filter]); |
|
1588 gBreadcrumbTrail.addAndEnter({ |
|
1589 title: "Plugin View: " + pluginName, |
|
1590 enterCallback: function () { |
|
1591 gSampleFilters = newFilterChain; |
|
1592 filtersChanged(); |
|
1593 } |
|
1594 }) |
|
1595 } |
|
1596 |
|
1597 function viewJSSource(sample) { |
|
1598 var sourceView = new SourceView(); |
|
1599 sourceView.setScriptLocation(sample.scriptLocation); |
|
1600 sourceView.setSource(gMeta.js.source[sample.scriptLocation.scriptURI]); |
|
1601 gMainArea.appendChild(sourceView.getContainer()); |
|
1602 |
|
1603 } |
|
1604 |
|
1605 function setHighlightedCallstack(samples, heaviestSample) { |
|
1606 PROFILERTRACE("highlight: " + samples); |
|
1607 gHighlightedCallstack = samples; |
|
1608 gHistogramView.highlightedCallstackChanged(gHighlightedCallstack); |
|
1609 if (!gInvertCallstack) { |
|
1610 // Always show heavy |
|
1611 heaviestSample = heaviestSample.clone().reverse(); |
|
1612 } |
|
1613 |
|
1614 if (gSampleBar) { |
|
1615 gSampleBar.setSample(heaviestSample); |
|
1616 } |
|
1617 } |
|
1618 |
|
1619 function enterMainUI() { |
|
1620 var uiContainer = document.createElement("div"); |
|
1621 uiContainer.id = "ui"; |
|
1622 |
|
1623 gFileList = new FileList(); |
|
1624 uiContainer.appendChild(gFileList.getContainer()); |
|
1625 |
|
1626 //gFileList.addFile(); |
|
1627 gFileList.loadProfileListFromLocalStorage(); |
|
1628 |
|
1629 gInfoBar = new InfoBar(); |
|
1630 uiContainer.appendChild(gInfoBar.getContainer()); |
|
1631 |
|
1632 gMainArea = document.createElement("div"); |
|
1633 gMainArea.id = "mainarea"; |
|
1634 uiContainer.appendChild(gMainArea); |
|
1635 document.body.appendChild(uiContainer); |
|
1636 |
|
1637 var profileEntryPane = document.createElement("div"); |
|
1638 profileEntryPane.className = "profileEntryPane"; |
|
1639 profileEntryPane.innerHTML = '' + |
|
1640 '<h1>Upload your profile here:</h1>' + |
|
1641 '<input type="file" id="datafile" onchange="loadProfileFile(this.files);">' + |
|
1642 '<h1>Or, alternatively, enter your profile data here:</h1>' + |
|
1643 '<textarea rows=20 cols=80 id=data autofocus spellcheck=false></textarea>' + |
|
1644 '<p><button onclick="loadProfile(document.getElementById(\'data\').value);">Parse</button></p>' + |
|
1645 ''; |
|
1646 |
|
1647 gMainArea.appendChild(profileEntryPane); |
|
1648 } |
|
1649 |
|
1650 function enterProgressUI() { |
|
1651 var profileProgressPane = document.createElement("div"); |
|
1652 profileProgressPane.className = "profileProgressPane"; |
|
1653 |
|
1654 var progressLabel = document.createElement("a"); |
|
1655 profileProgressPane.appendChild(progressLabel); |
|
1656 |
|
1657 var progressBar = document.createElement("progress"); |
|
1658 profileProgressPane.appendChild(progressBar); |
|
1659 |
|
1660 var totalProgressReporter = new ProgressReporter(); |
|
1661 totalProgressReporter.addListener(function (r) { |
|
1662 var progress = r.getProgress(); |
|
1663 progressLabel.innerHTML = r.getAction(); |
|
1664 if (isNaN(progress)) |
|
1665 progressBar.removeAttribute("value"); |
|
1666 else |
|
1667 progressBar.value = progress; |
|
1668 }); |
|
1669 |
|
1670 gMainArea.appendChild(profileProgressPane); |
|
1671 |
|
1672 Parser.updateLogSetting(); |
|
1673 |
|
1674 return totalProgressReporter; |
|
1675 } |
|
1676 |
|
1677 function enterFinishedProfileUI() { |
|
1678 saveProfileToLocalStorage(); |
|
1679 |
|
1680 var finishedProfilePaneBackgroundCover = document.createElement("div"); |
|
1681 finishedProfilePaneBackgroundCover.className = "finishedProfilePaneBackgroundCover"; |
|
1682 |
|
1683 var finishedProfilePane = document.createElement("table"); |
|
1684 var rowIndex = 0; |
|
1685 var currRow; |
|
1686 finishedProfilePane.style.width = "100%"; |
|
1687 finishedProfilePane.style.height = "100%"; |
|
1688 finishedProfilePane.border = "0"; |
|
1689 finishedProfilePane.cellPadding = "0"; |
|
1690 finishedProfilePane.cellSpacing = "0"; |
|
1691 finishedProfilePane.borderCollapse = "collapse"; |
|
1692 finishedProfilePane.className = "finishedProfilePane"; |
|
1693 setTimeout(function() { |
|
1694 // Work around a webkit bug. For some reason the table doesn't show up |
|
1695 // until some actions happen such as focusing this box |
|
1696 var filterNameInput = document.getElementById("filterName"); |
|
1697 if (filterNameInput != null) { |
|
1698 changeFocus(filterNameInput); |
|
1699 } |
|
1700 }, 100); |
|
1701 |
|
1702 gBreadcrumbTrail = new BreadcrumbTrail(); |
|
1703 currRow = finishedProfilePane.insertRow(rowIndex++); |
|
1704 currRow.insertCell(0).appendChild(gBreadcrumbTrail.getContainer()); |
|
1705 |
|
1706 gHistogramView = new HistogramView(); |
|
1707 currRow = finishedProfilePane.insertRow(rowIndex++); |
|
1708 currRow.insertCell(0).appendChild(gHistogramView.getContainer()); |
|
1709 |
|
1710 if (false && gLocation.indexOf("file:") == 0) { |
|
1711 // Local testing for frameView |
|
1712 gFrameView = new FrameView(); |
|
1713 currRow = finishedProfilePane.insertRow(rowIndex++); |
|
1714 currRow.insertCell(0).appendChild(gFrameView.getContainer()); |
|
1715 } |
|
1716 |
|
1717 gDiagnosticBar = new DiagnosticBar(); |
|
1718 gDiagnosticBar.setDetailsListener(function(details) { |
|
1719 if (details.indexOf("bug ") == 0) { |
|
1720 window.open('https://bugzilla.mozilla.org/show_bug.cgi?id=' + details.substring(4)); |
|
1721 } else { |
|
1722 var sourceView = new SourceView(); |
|
1723 sourceView.setText("Diagnostic", js_beautify(details)); |
|
1724 gMainArea.appendChild(sourceView.getContainer()); |
|
1725 } |
|
1726 }); |
|
1727 currRow = finishedProfilePane.insertRow(rowIndex++); |
|
1728 currRow.insertCell(0).appendChild(gDiagnosticBar.getContainer()); |
|
1729 |
|
1730 // For testing: |
|
1731 //gMeta.videoCapture = { |
|
1732 // src: "http://videos-cdn.mozilla.net/brand/Mozilla_Firefox_Manifesto_v0.2_640.webm", |
|
1733 //}; |
|
1734 |
|
1735 if (gMeta && gMeta.videoCapture) { |
|
1736 gVideoPane = new VideoPane(gMeta.videoCapture); |
|
1737 gVideoPane.onTimeChange(videoPaneTimeChange); |
|
1738 currRow = finishedProfilePane.insertRow(rowIndex++); |
|
1739 currRow.insertCell(0).appendChild(gVideoPane.getContainer()); |
|
1740 } |
|
1741 |
|
1742 var treeContainerDiv = document.createElement("div"); |
|
1743 treeContainerDiv.className = "treeContainer"; |
|
1744 treeContainerDiv.style.width = "100%"; |
|
1745 treeContainerDiv.style.height = "100%"; |
|
1746 |
|
1747 gTreeManager = new ProfileTreeManager(); |
|
1748 currRow = finishedProfilePane.insertRow(rowIndex++); |
|
1749 currRow.style.height = "100%"; |
|
1750 var cell = currRow.insertCell(0); |
|
1751 cell.appendChild(treeContainerDiv); |
|
1752 treeContainerDiv.appendChild(gTreeManager.getContainer()); |
|
1753 |
|
1754 gSampleBar = new SampleBar(); |
|
1755 treeContainerDiv.appendChild(gSampleBar.getContainer()); |
|
1756 |
|
1757 // sampleBar |
|
1758 |
|
1759 gPluginView = new PluginView(); |
|
1760 //currRow = finishedProfilePane.insertRow(4); |
|
1761 treeContainerDiv.appendChild(gPluginView.getContainer()); |
|
1762 |
|
1763 gMainArea.appendChild(finishedProfilePaneBackgroundCover); |
|
1764 gMainArea.appendChild(finishedProfilePane); |
|
1765 |
|
1766 var currentBreadcrumb = gSampleFilters; |
|
1767 gBreadcrumbTrail.add({ |
|
1768 title: gStrings["Complete Profile"], |
|
1769 enterCallback: function () { |
|
1770 gSampleFilters = []; |
|
1771 filtersChanged(); |
|
1772 } |
|
1773 }); |
|
1774 if (currentBreadcrumb == null || currentBreadcrumb.length == 0) { |
|
1775 gTreeManager.restoreSerializedSelectionSnapshot(gRestoreSelection); |
|
1776 viewOptionsChanged(); |
|
1777 } |
|
1778 for (var i = 0; i < currentBreadcrumb.length; i++) { |
|
1779 var filter = currentBreadcrumb[i]; |
|
1780 var forceSelection = null; |
|
1781 if (gRestoreSelection != null && i == currentBreadcrumb.length - 1) { |
|
1782 forceSelection = gRestoreSelection; |
|
1783 } |
|
1784 switch (filter.type) { |
|
1785 case "FocusedFrameSampleFilter": |
|
1786 focusOnSymbol(filter.name, filter.symbolName); |
|
1787 gBreadcrumbTrail.enterLastItem(forceSelection); |
|
1788 case "FocusedCallstackPrefixSampleFilter": |
|
1789 focusOnCallstack(filter.focusedCallstack, filter.name, false); |
|
1790 gBreadcrumbTrail.enterLastItem(forceSelection); |
|
1791 case "FocusedCallstackPostfixSampleFilter": |
|
1792 focusOnCallstack(filter.focusedCallstack, filter.name, true); |
|
1793 gBreadcrumbTrail.enterLastItem(forceSelection); |
|
1794 case "RangeSampleFilter": |
|
1795 gHistogramView.selectRange(filter.start, filter.end); |
|
1796 gBreadcrumbTrail.enterLastItem(forceSelection); |
|
1797 } |
|
1798 } |
|
1799 } |
|
1800 |
|
1801 // Make all focus change events go through this function. |
|
1802 // This function will mediate the focus changes in case |
|
1803 // that we're in a compare view. In a compare view an inactive |
|
1804 // instance of cleopatra should not steal focus from the active |
|
1805 // cleopatra instance. |
|
1806 function changeFocus(elem) { |
|
1807 if (window.comparator_changeFocus) { |
|
1808 window.comparator_changeFocus(elem); |
|
1809 } else { |
|
1810 PROFILERLOG("FOCUS\n\n\n\n\n\n\n\n\n"); |
|
1811 elem.focus(); |
|
1812 } |
|
1813 } |
|
1814 |
|
1815 function comparator_receiveSelection(snapshot, frameData) { |
|
1816 gTreeManager.restoreSerializedSelectionSnapshot(snapshot); |
|
1817 if (frameData) |
|
1818 gTreeManager.highlightFrame(frameData); |
|
1819 viewOptionsChanged(); |
|
1820 } |
|
1821 |
|
1822 function filtersChanged() { |
|
1823 if (window.comparator_setSelection) { |
|
1824 // window.comparator_setSelection(gTreeManager.serializeCurrentSelectionSnapshot(), null); |
|
1825 } |
|
1826 updateDocumentURL(); |
|
1827 var data = { symbols: {}, functions: {}, samples: [] }; |
|
1828 |
|
1829 gHistogramView.dataIsOutdated(); |
|
1830 var filterNameInput = document.getElementById("filterName"); |
|
1831 var updateRequest = Parser.updateFilters({ |
|
1832 mergeFunctions: gMergeFunctions, |
|
1833 nameFilter: (filterNameInput && filterNameInput.value) || gQueryParamFilterName || "", |
|
1834 sampleFilters: gSampleFilters, |
|
1835 jankOnly: gJankOnly, |
|
1836 javascriptOnly: gJavascriptOnly |
|
1837 }); |
|
1838 var start = Date.now(); |
|
1839 updateRequest.addEventListener("finished", function (filteredSamples) { |
|
1840 gCurrentlyShownSampleData = filteredSamples; |
|
1841 gInfoBar.display(); |
|
1842 |
|
1843 if (gSampleFilters.length > 0 && gSampleFilters[gSampleFilters.length-1].type === "PluginView") { |
|
1844 start = Date.now(); |
|
1845 gPluginView.display(gSampleFilters[gSampleFilters.length-1].pluginName, gSampleFilters[gSampleFilters.length-1].param, |
|
1846 gCurrentlyShownSampleData, gHighlightedCallstack); |
|
1847 } else { |
|
1848 gPluginView.hide(); |
|
1849 } |
|
1850 }); |
|
1851 |
|
1852 var histogramRequest = Parser.calculateHistogramData(); |
|
1853 histogramRequest.addEventListener("finished", function (data) { |
|
1854 start = Date.now(); |
|
1855 gHistogramView.display(data.histogramData, data.frameStart, data.widthSum, gHighlightedCallstack); |
|
1856 if (gFrameView) |
|
1857 gFrameView.display(data.histogramData, data.frameStart, data.widthSum, gHighlightedCallstack); |
|
1858 }); |
|
1859 |
|
1860 if (gDiagnosticBar) { |
|
1861 var diagnosticsRequest = Parser.calculateDiagnosticItems(gMeta); |
|
1862 diagnosticsRequest.addEventListener("finished", function (diagnosticItems) { |
|
1863 start = Date.now(); |
|
1864 gDiagnosticBar.display(diagnosticItems); |
|
1865 }); |
|
1866 } |
|
1867 |
|
1868 viewOptionsChanged(); |
|
1869 } |
|
1870 |
|
1871 function viewOptionsChanged() { |
|
1872 gTreeManager.dataIsOutdated(); |
|
1873 var filterNameInput = document.getElementById("filterName"); |
|
1874 var updateViewOptionsRequest = Parser.updateViewOptions({ |
|
1875 invertCallstack: gInvertCallstack, |
|
1876 mergeUnbranched: gMergeUnbranched |
|
1877 }); |
|
1878 updateViewOptionsRequest.addEventListener("finished", function (calltree) { |
|
1879 var start = Date.now(); |
|
1880 gTreeManager.display(calltree, gSymbols, gFunctions, gResources, gMergeFunctions, filterNameInput && filterNameInput.value); |
|
1881 }); |
|
1882 } |
|
1883 |
|
1884 function loadQueryData(queryDataOriginal) { |
|
1885 var isFiltersChanged = false; |
|
1886 var queryData = {}; |
|
1887 for (var i in queryDataOriginal) { |
|
1888 queryData[i] = unQueryEscape(queryDataOriginal[i]); |
|
1889 } |
|
1890 if (queryData.search) { |
|
1891 gQueryParamFilterName = queryData.search; |
|
1892 isFiltersChanged = true; |
|
1893 } |
|
1894 if (queryData.jankOnly) { |
|
1895 gJankOnly = queryData.jankOnly; |
|
1896 isFiltersChanged = true; |
|
1897 } |
|
1898 if (queryData.javascriptOnly) { |
|
1899 gJavascriptOnly = queryData.javascriptOnly; |
|
1900 isFiltersChanged = true; |
|
1901 } |
|
1902 if (queryData.mergeUnbranched) { |
|
1903 gMergeUnbranched = queryData.mergeUnbranched; |
|
1904 isFiltersChanged = true; |
|
1905 } |
|
1906 if (queryData.invertCallback) { |
|
1907 gInvertCallstack = queryData.invertCallback; |
|
1908 isFiltersChanged = true; |
|
1909 } |
|
1910 if (queryData.report) { |
|
1911 gReportID = queryData.report; |
|
1912 } |
|
1913 if (queryData.filter) { |
|
1914 var filterChain = JSON.parse(queryData.filter); |
|
1915 gSampleFilters = filterChain; |
|
1916 } |
|
1917 if (queryData.selection) { |
|
1918 var selection = queryData.selection; |
|
1919 gRestoreSelection = selection; |
|
1920 } |
|
1921 |
|
1922 if (isFiltersChanged) { |
|
1923 //filtersChanged(); |
|
1924 } |
|
1925 } |
|
1926 |
|
1927 function unQueryEscape(str) { |
|
1928 return decodeURIComponent(str); |
|
1929 } |
|
1930 |
|
1931 function queryEscape(str) { |
|
1932 return encodeURIComponent(encodeURIComponent(str)); |
|
1933 } |
|
1934 |
|
1935 function updateDocumentURL() { |
|
1936 location.hash = getDocumentHashString(); |
|
1937 return document.location; |
|
1938 } |
|
1939 |
|
1940 function getDocumentHashString() { |
|
1941 var query = ""; |
|
1942 if (gReportID) { |
|
1943 if (query != "") |
|
1944 query += "&"; |
|
1945 query += "report=" + queryEscape(gReportID); |
|
1946 } |
|
1947 if (document.getElementById("filterName") != null && |
|
1948 document.getElementById("filterName").value != null && |
|
1949 document.getElementById("filterName").value != "") { |
|
1950 if (query != "") |
|
1951 query += "&"; |
|
1952 query += "search=" + queryEscape(document.getElementById("filterName").value); |
|
1953 } |
|
1954 // For now don't restore the view rest |
|
1955 return query; |
|
1956 if (gJankOnly) { |
|
1957 if (query != "") |
|
1958 query += "&"; |
|
1959 query += "jankOnly=" + queryEscape(gJankOnly); |
|
1960 } |
|
1961 if (gJavascriptOnly) { |
|
1962 if (query != "") |
|
1963 query += "&"; |
|
1964 query += "javascriptOnly=" + queryEscape(gJavascriptOnly); |
|
1965 } |
|
1966 if (gMergeUnbranched) { |
|
1967 if (query != "") |
|
1968 query += "&"; |
|
1969 query += "mergeUnbranched=" + queryEscape(gMergeUnbranched); |
|
1970 } |
|
1971 if (gInvertCallstack) { |
|
1972 if (query != "") |
|
1973 query += "&"; |
|
1974 query += "invertCallback=" + queryEscape(gInvertCallstack); |
|
1975 } |
|
1976 if (gSampleFilters && gSampleFilters.length != 0) { |
|
1977 if (query != "") |
|
1978 query += "&"; |
|
1979 query += "filter=" + queryEscape(JSON.stringify(gSampleFilters)); |
|
1980 } |
|
1981 if (gTreeManager.hasNonTrivialSelection()) { |
|
1982 if (query != "") |
|
1983 query += "&"; |
|
1984 query += "selection=" + queryEscape(gTreeManager.serializeCurrentSelectionSnapshot()); |
|
1985 } |
|
1986 if (!gReportID) { |
|
1987 query = "uploadProfileFirst!"; |
|
1988 } |
|
1989 |
|
1990 return query; |
|
1991 } |
|
1992 |