|
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 file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 "use strict"; |
|
5 |
|
6 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; |
|
7 |
|
8 Cu.import("resource://gre/modules/Services.jsm"); |
|
9 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
10 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm"); |
|
11 Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); |
|
12 |
|
13 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; |
|
14 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; |
|
15 const EventEmitter = require("devtools/toolkit/event-emitter"); |
|
16 const { CallWatcherFront } = require("devtools/server/actors/call-watcher"); |
|
17 const { CanvasFront } = require("devtools/server/actors/canvas"); |
|
18 |
|
19 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
20 "resource://gre/modules/Task.jsm"); |
|
21 |
|
22 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
|
23 "resource://gre/modules/PluralForm.jsm"); |
|
24 |
|
25 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
|
26 "resource://gre/modules/FileUtils.jsm"); |
|
27 |
|
28 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
29 "resource://gre/modules/NetUtil.jsm"); |
|
30 |
|
31 XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils", |
|
32 "resource://gre/modules/devtools/DevToolsUtils.jsm"); |
|
33 |
|
34 // The panel's window global is an EventEmitter firing the following events: |
|
35 const EVENTS = { |
|
36 // When the UI is reset from tab navigation. |
|
37 UI_RESET: "CanvasDebugger:UIReset", |
|
38 |
|
39 // When all the animation frame snapshots are removed by the user. |
|
40 SNAPSHOTS_LIST_CLEARED: "CanvasDebugger:SnapshotsListCleared", |
|
41 |
|
42 // When an animation frame snapshot starts/finishes being recorded. |
|
43 SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted", |
|
44 SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished", |
|
45 |
|
46 // When an animation frame snapshot was selected and all its data displayed. |
|
47 SNAPSHOT_RECORDING_SELECTED: "CanvasDebugger:SnapshotRecordingSelected", |
|
48 |
|
49 // After all the function calls associated with an animation frame snapshot |
|
50 // are displayed in the UI. |
|
51 CALL_LIST_POPULATED: "CanvasDebugger:CallListPopulated", |
|
52 |
|
53 // After the stack associated with a call in an animation frame snapshot |
|
54 // is displayed in the UI. |
|
55 CALL_STACK_DISPLAYED: "CanvasDebugger:CallStackDisplayed", |
|
56 |
|
57 // After a screenshot associated with a call in an animation frame snapshot |
|
58 // is displayed in the UI. |
|
59 CALL_SCREENSHOT_DISPLAYED: "CanvasDebugger:ScreenshotDisplayed", |
|
60 |
|
61 // After all the thumbnails associated with an animation frame snapshot |
|
62 // are displayed in the UI. |
|
63 THUMBNAILS_DISPLAYED: "CanvasDebugger:ThumbnailsDisplayed", |
|
64 |
|
65 // When a source is shown in the JavaScript Debugger at a specific location. |
|
66 SOURCE_SHOWN_IN_JS_DEBUGGER: "CanvasDebugger:SourceShownInJsDebugger", |
|
67 SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "CanvasDebugger:SourceNotFoundInJsDebugger" |
|
68 }; |
|
69 |
|
70 const HTML_NS = "http://www.w3.org/1999/xhtml"; |
|
71 const STRINGS_URI = "chrome://browser/locale/devtools/canvasdebugger.properties" |
|
72 |
|
73 const SNAPSHOT_START_RECORDING_DELAY = 10; // ms |
|
74 const SNAPSHOT_DATA_EXPORT_MAX_BLOCK = 1000; // ms |
|
75 const SNAPSHOT_DATA_DISPLAY_DELAY = 10; // ms |
|
76 const SCREENSHOT_DISPLAY_DELAY = 100; // ms |
|
77 const STACK_FUNC_INDENTATION = 14; // px |
|
78 |
|
79 // This identifier string is simply used to tentatively ascertain whether or not |
|
80 // a JSON loaded from disk is actually something generated by this tool or not. |
|
81 // It isn't, of course, a definitive verification, but a Good Enoughâ„¢ |
|
82 // approximation before continuing the import. Don't localize this. |
|
83 const CALLS_LIST_SERIALIZER_IDENTIFIER = "Recorded Animation Frame Snapshot"; |
|
84 const CALLS_LIST_SERIALIZER_VERSION = 1; |
|
85 const CALLS_LIST_SLOW_SAVE_DELAY = 100; // ms |
|
86 |
|
87 /** |
|
88 * The current target and the Canvas front, set by this tool's host. |
|
89 */ |
|
90 let gToolbox, gTarget, gFront; |
|
91 |
|
92 /** |
|
93 * Initializes the canvas debugger controller and views. |
|
94 */ |
|
95 function startupCanvasDebugger() { |
|
96 return promise.all([ |
|
97 EventsHandler.initialize(), |
|
98 SnapshotsListView.initialize(), |
|
99 CallsListView.initialize() |
|
100 ]); |
|
101 } |
|
102 |
|
103 /** |
|
104 * Destroys the canvas debugger controller and views. |
|
105 */ |
|
106 function shutdownCanvasDebugger() { |
|
107 return promise.all([ |
|
108 EventsHandler.destroy(), |
|
109 SnapshotsListView.destroy(), |
|
110 CallsListView.destroy() |
|
111 ]); |
|
112 } |
|
113 |
|
114 /** |
|
115 * Functions handling target-related lifetime events. |
|
116 */ |
|
117 let EventsHandler = { |
|
118 /** |
|
119 * Listen for events emitted by the current tab target. |
|
120 */ |
|
121 initialize: function() { |
|
122 this._onTabNavigated = this._onTabNavigated.bind(this); |
|
123 gTarget.on("will-navigate", this._onTabNavigated); |
|
124 gTarget.on("navigate", this._onTabNavigated); |
|
125 }, |
|
126 |
|
127 /** |
|
128 * Remove events emitted by the current tab target. |
|
129 */ |
|
130 destroy: function() { |
|
131 gTarget.off("will-navigate", this._onTabNavigated); |
|
132 gTarget.off("navigate", this._onTabNavigated); |
|
133 }, |
|
134 |
|
135 /** |
|
136 * Called for each location change in the debugged tab. |
|
137 */ |
|
138 _onTabNavigated: function(event) { |
|
139 if (event != "will-navigate") { |
|
140 return; |
|
141 } |
|
142 // Make sure the backend is prepared to handle <canvas> contexts. |
|
143 gFront.setup({ reload: false }); |
|
144 |
|
145 // Reset UI. |
|
146 SnapshotsListView.empty(); |
|
147 CallsListView.empty(); |
|
148 |
|
149 $("#record-snapshot").removeAttribute("checked"); |
|
150 $("#record-snapshot").removeAttribute("disabled"); |
|
151 $("#record-snapshot").hidden = false; |
|
152 |
|
153 $("#reload-notice").hidden = true; |
|
154 $("#empty-notice").hidden = false; |
|
155 $("#import-notice").hidden = true; |
|
156 |
|
157 $("#debugging-pane-contents").hidden = true; |
|
158 $("#screenshot-container").hidden = true; |
|
159 $("#snapshot-filmstrip").hidden = true; |
|
160 |
|
161 window.emit(EVENTS.UI_RESET); |
|
162 } |
|
163 }; |
|
164 |
|
165 /** |
|
166 * Functions handling the recorded animation frame snapshots UI. |
|
167 */ |
|
168 let SnapshotsListView = Heritage.extend(WidgetMethods, { |
|
169 /** |
|
170 * Initialization function, called when the tool is started. |
|
171 */ |
|
172 initialize: function() { |
|
173 this.widget = new SideMenuWidget($("#snapshots-list"), { |
|
174 showArrows: true |
|
175 }); |
|
176 |
|
177 this._onSelect = this._onSelect.bind(this); |
|
178 this._onClearButtonClick = this._onClearButtonClick.bind(this); |
|
179 this._onRecordButtonClick = this._onRecordButtonClick.bind(this); |
|
180 this._onImportButtonClick = this._onImportButtonClick.bind(this); |
|
181 this._onSaveButtonClick = this._onSaveButtonClick.bind(this); |
|
182 |
|
183 this.emptyText = L10N.getStr("noSnapshotsText"); |
|
184 this.widget.addEventListener("select", this._onSelect, false); |
|
185 }, |
|
186 |
|
187 /** |
|
188 * Destruction function, called when the tool is closed. |
|
189 */ |
|
190 destroy: function() { |
|
191 this.widget.removeEventListener("select", this._onSelect, false); |
|
192 }, |
|
193 |
|
194 /** |
|
195 * Adds a snapshot entry to this container. |
|
196 * |
|
197 * @return object |
|
198 * The newly inserted item. |
|
199 */ |
|
200 addSnapshot: function() { |
|
201 let contents = document.createElement("hbox"); |
|
202 contents.className = "snapshot-item"; |
|
203 |
|
204 let thumbnail = document.createElementNS(HTML_NS, "canvas"); |
|
205 thumbnail.className = "snapshot-item-thumbnail"; |
|
206 thumbnail.width = CanvasFront.THUMBNAIL_HEIGHT; |
|
207 thumbnail.height = CanvasFront.THUMBNAIL_HEIGHT; |
|
208 |
|
209 let title = document.createElement("label"); |
|
210 title.className = "plain snapshot-item-title"; |
|
211 title.setAttribute("value", |
|
212 L10N.getFormatStr("snapshotsList.itemLabel", this.itemCount + 1)); |
|
213 |
|
214 let calls = document.createElement("label"); |
|
215 calls.className = "plain snapshot-item-calls"; |
|
216 calls.setAttribute("value", |
|
217 L10N.getStr("snapshotsList.loadingLabel")); |
|
218 |
|
219 let save = document.createElement("label"); |
|
220 save.className = "plain snapshot-item-save"; |
|
221 save.addEventListener("click", this._onSaveButtonClick, false); |
|
222 |
|
223 let spacer = document.createElement("spacer"); |
|
224 spacer.setAttribute("flex", "1"); |
|
225 |
|
226 let footer = document.createElement("hbox"); |
|
227 footer.className = "snapshot-item-footer"; |
|
228 footer.appendChild(save); |
|
229 |
|
230 let details = document.createElement("vbox"); |
|
231 details.className = "snapshot-item-details"; |
|
232 details.appendChild(title); |
|
233 details.appendChild(calls); |
|
234 details.appendChild(spacer); |
|
235 details.appendChild(footer); |
|
236 |
|
237 contents.appendChild(thumbnail); |
|
238 contents.appendChild(details); |
|
239 |
|
240 // Append a recorded snapshot item to this container. |
|
241 return this.push([contents], { |
|
242 attachment: { |
|
243 // The snapshot and function call actors, along with the thumbnails |
|
244 // will be available as soon as recording finishes. |
|
245 actor: null, |
|
246 calls: null, |
|
247 thumbnails: null, |
|
248 screenshot: null |
|
249 } |
|
250 }); |
|
251 }, |
|
252 |
|
253 /** |
|
254 * Customizes a shapshot in this container. |
|
255 * |
|
256 * @param Item snapshotItem |
|
257 * An item inserted via `SnapshotsListView.addSnapshot`. |
|
258 * @param object snapshotActor |
|
259 * The frame snapshot actor received from the backend. |
|
260 * @param object snapshotOverview |
|
261 * Additional data about the snapshot received from the backend. |
|
262 */ |
|
263 customizeSnapshot: function(snapshotItem, snapshotActor, snapshotOverview) { |
|
264 // Make sure the function call actors are stored on the item, |
|
265 // to be used when populating the CallsListView. |
|
266 snapshotItem.attachment.actor = snapshotActor; |
|
267 let functionCalls = snapshotItem.attachment.calls = snapshotOverview.calls; |
|
268 let thumbnails = snapshotItem.attachment.thumbnails = snapshotOverview.thumbnails; |
|
269 let screenshot = snapshotItem.attachment.screenshot = snapshotOverview.screenshot; |
|
270 |
|
271 let lastThumbnail = thumbnails[thumbnails.length - 1]; |
|
272 let { width, height, flipped, pixels } = lastThumbnail; |
|
273 |
|
274 let thumbnailNode = $(".snapshot-item-thumbnail", snapshotItem.target); |
|
275 thumbnailNode.setAttribute("flipped", flipped); |
|
276 drawImage(thumbnailNode, width, height, pixels, { centered: true }); |
|
277 |
|
278 let callsNode = $(".snapshot-item-calls", snapshotItem.target); |
|
279 let drawCalls = functionCalls.filter(e => CanvasFront.DRAW_CALLS.has(e.name)); |
|
280 |
|
281 let drawCallsStr = PluralForm.get(drawCalls.length, |
|
282 L10N.getStr("snapshotsList.drawCallsLabel")); |
|
283 let funcCallsStr = PluralForm.get(functionCalls.length, |
|
284 L10N.getStr("snapshotsList.functionCallsLabel")); |
|
285 |
|
286 callsNode.setAttribute("value", |
|
287 drawCallsStr.replace("#1", drawCalls.length) + ", " + |
|
288 funcCallsStr.replace("#1", functionCalls.length)); |
|
289 |
|
290 let saveNode = $(".snapshot-item-save", snapshotItem.target); |
|
291 saveNode.setAttribute("disabled", !!snapshotItem.isLoadedFromDisk); |
|
292 saveNode.setAttribute("value", snapshotItem.isLoadedFromDisk |
|
293 ? L10N.getStr("snapshotsList.loadedLabel") |
|
294 : L10N.getStr("snapshotsList.saveLabel")); |
|
295 |
|
296 // Make sure there's always a selected item available. |
|
297 if (!this.selectedItem) { |
|
298 this.selectedIndex = 0; |
|
299 } |
|
300 }, |
|
301 |
|
302 /** |
|
303 * The select listener for this container. |
|
304 */ |
|
305 _onSelect: function({ detail: snapshotItem }) { |
|
306 if (!snapshotItem) { |
|
307 return; |
|
308 } |
|
309 let { calls, thumbnails, screenshot } = snapshotItem.attachment; |
|
310 |
|
311 $("#reload-notice").hidden = true; |
|
312 $("#empty-notice").hidden = true; |
|
313 $("#import-notice").hidden = false; |
|
314 |
|
315 $("#debugging-pane-contents").hidden = true; |
|
316 $("#screenshot-container").hidden = true; |
|
317 $("#snapshot-filmstrip").hidden = true; |
|
318 |
|
319 Task.spawn(function*() { |
|
320 // Wait for a few milliseconds between presenting the function calls, |
|
321 // screenshot and thumbnails, to allow each component being |
|
322 // sequentially drawn. This gives the illusion of snappiness. |
|
323 |
|
324 yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); |
|
325 CallsListView.showCalls(calls); |
|
326 $("#debugging-pane-contents").hidden = false; |
|
327 $("#import-notice").hidden = true; |
|
328 |
|
329 yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); |
|
330 CallsListView.showThumbnails(thumbnails); |
|
331 $("#snapshot-filmstrip").hidden = false; |
|
332 |
|
333 yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); |
|
334 CallsListView.showScreenshot(screenshot); |
|
335 $("#screenshot-container").hidden = false; |
|
336 |
|
337 window.emit(EVENTS.SNAPSHOT_RECORDING_SELECTED); |
|
338 }); |
|
339 }, |
|
340 |
|
341 /** |
|
342 * The click listener for the "clear" button in this container. |
|
343 */ |
|
344 _onClearButtonClick: function() { |
|
345 Task.spawn(function*() { |
|
346 SnapshotsListView.empty(); |
|
347 CallsListView.empty(); |
|
348 |
|
349 $("#reload-notice").hidden = true; |
|
350 $("#empty-notice").hidden = true; |
|
351 $("#import-notice").hidden = true; |
|
352 |
|
353 if (yield gFront.isInitialized()) { |
|
354 $("#empty-notice").hidden = false; |
|
355 } else { |
|
356 $("#reload-notice").hidden = false; |
|
357 } |
|
358 |
|
359 $("#debugging-pane-contents").hidden = true; |
|
360 $("#screenshot-container").hidden = true; |
|
361 $("#snapshot-filmstrip").hidden = true; |
|
362 |
|
363 window.emit(EVENTS.SNAPSHOTS_LIST_CLEARED); |
|
364 }); |
|
365 }, |
|
366 |
|
367 /** |
|
368 * The click listener for the "record" button in this container. |
|
369 */ |
|
370 _onRecordButtonClick: function() { |
|
371 Task.spawn(function*() { |
|
372 $("#record-snapshot").setAttribute("checked", "true"); |
|
373 $("#record-snapshot").setAttribute("disabled", "true"); |
|
374 |
|
375 // Insert a "dummy" snapshot item in the view, to hint that recording |
|
376 // has now started. However, wait for a few milliseconds before actually |
|
377 // starting the recording, since that might block rendering and prevent |
|
378 // the dummy snapshot item from being drawn. |
|
379 let snapshotItem = this.addSnapshot(); |
|
380 |
|
381 // If this is the first item, immediately show the "Loading…" notice. |
|
382 if (this.itemCount == 1) { |
|
383 $("#empty-notice").hidden = true; |
|
384 $("#import-notice").hidden = false; |
|
385 } |
|
386 |
|
387 yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY); |
|
388 window.emit(EVENTS.SNAPSHOT_RECORDING_STARTED); |
|
389 |
|
390 let snapshotActor = yield gFront.recordAnimationFrame(); |
|
391 let snapshotOverview = yield snapshotActor.getOverview(); |
|
392 this.customizeSnapshot(snapshotItem, snapshotActor, snapshotOverview); |
|
393 |
|
394 $("#record-snapshot").removeAttribute("checked"); |
|
395 $("#record-snapshot").removeAttribute("disabled"); |
|
396 |
|
397 window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED); |
|
398 }.bind(this)); |
|
399 }, |
|
400 |
|
401 /** |
|
402 * The click listener for the "import" button in this container. |
|
403 */ |
|
404 _onImportButtonClick: function() { |
|
405 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); |
|
406 fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen); |
|
407 fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json"); |
|
408 fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*"); |
|
409 |
|
410 if (fp.show() != Ci.nsIFilePicker.returnOK) { |
|
411 return; |
|
412 } |
|
413 |
|
414 let channel = NetUtil.newChannel(fp.file); |
|
415 channel.contentType = "text/plain"; |
|
416 |
|
417 NetUtil.asyncFetch(channel, (inputStream, status) => { |
|
418 if (!Components.isSuccessCode(status)) { |
|
419 console.error("Could not import recorded animation frame snapshot file."); |
|
420 return; |
|
421 } |
|
422 try { |
|
423 let string = NetUtil.readInputStreamToString(inputStream, inputStream.available()); |
|
424 var data = JSON.parse(string); |
|
425 } catch (e) { |
|
426 console.error("Could not read animation frame snapshot file."); |
|
427 return; |
|
428 } |
|
429 if (data.fileType != CALLS_LIST_SERIALIZER_IDENTIFIER) { |
|
430 console.error("Unrecognized animation frame snapshot file."); |
|
431 return; |
|
432 } |
|
433 |
|
434 // Add a `isLoadedFromDisk` flag on everything to avoid sending invalid |
|
435 // requests to the backend, since we're not dealing with actors anymore. |
|
436 let snapshotItem = this.addSnapshot(); |
|
437 snapshotItem.isLoadedFromDisk = true; |
|
438 data.calls.forEach(e => e.isLoadedFromDisk = true); |
|
439 |
|
440 // Create array buffers from the parsed pixel arrays. |
|
441 for (let thumbnail of data.thumbnails) { |
|
442 let thumbnailPixelsArray = thumbnail.pixels.split(","); |
|
443 thumbnail.pixels = new Uint32Array(thumbnailPixelsArray); |
|
444 } |
|
445 let screenshotPixelsArray = data.screenshot.pixels.split(","); |
|
446 data.screenshot.pixels = new Uint32Array(screenshotPixelsArray); |
|
447 |
|
448 this.customizeSnapshot(snapshotItem, data.calls, data); |
|
449 }); |
|
450 }, |
|
451 |
|
452 /** |
|
453 * The click listener for the "save" button of each item in this container. |
|
454 */ |
|
455 _onSaveButtonClick: function(e) { |
|
456 let snapshotItem = this.getItemForElement(e.target); |
|
457 |
|
458 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); |
|
459 fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave); |
|
460 fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json"); |
|
461 fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*"); |
|
462 fp.defaultString = "snapshot.json"; |
|
463 |
|
464 // Start serializing all the function call actors for the specified snapshot, |
|
465 // while the nsIFilePicker dialog is being opened. Snappy. |
|
466 let serialized = Task.spawn(function*() { |
|
467 let data = { |
|
468 fileType: CALLS_LIST_SERIALIZER_IDENTIFIER, |
|
469 version: CALLS_LIST_SERIALIZER_VERSION, |
|
470 calls: [], |
|
471 thumbnails: [], |
|
472 screenshot: null |
|
473 }; |
|
474 let functionCalls = snapshotItem.attachment.calls; |
|
475 let thumbnails = snapshotItem.attachment.thumbnails; |
|
476 let screenshot = snapshotItem.attachment.screenshot; |
|
477 |
|
478 // Prepare all the function calls for serialization. |
|
479 yield DevToolsUtils.yieldingEach(functionCalls, (call, i) => { |
|
480 let { type, name, file, line, argsPreview, callerPreview } = call; |
|
481 return call.getDetails().then(({ stack }) => { |
|
482 data.calls[i] = { |
|
483 type: type, |
|
484 name: name, |
|
485 file: file, |
|
486 line: line, |
|
487 stack: stack, |
|
488 argsPreview: argsPreview, |
|
489 callerPreview: callerPreview |
|
490 }; |
|
491 }); |
|
492 }); |
|
493 |
|
494 // Prepare all the thumbnails for serialization. |
|
495 yield DevToolsUtils.yieldingEach(thumbnails, (thumbnail, i) => { |
|
496 let { index, width, height, flipped, pixels } = thumbnail; |
|
497 data.thumbnails.push({ |
|
498 index: index, |
|
499 width: width, |
|
500 height: height, |
|
501 flipped: flipped, |
|
502 pixels: Array.join(pixels, ",") |
|
503 }); |
|
504 }); |
|
505 |
|
506 // Prepare the screenshot for serialization. |
|
507 let { index, width, height, flipped, pixels } = screenshot; |
|
508 data.screenshot = { |
|
509 index: index, |
|
510 width: width, |
|
511 height: height, |
|
512 flipped: flipped, |
|
513 pixels: Array.join(pixels, ",") |
|
514 }; |
|
515 |
|
516 let string = JSON.stringify(data); |
|
517 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. |
|
518 createInstance(Ci.nsIScriptableUnicodeConverter); |
|
519 |
|
520 converter.charset = "UTF-8"; |
|
521 return converter.convertToInputStream(string); |
|
522 }); |
|
523 |
|
524 // Open the nsIFilePicker and wait for the function call actors to finish |
|
525 // being serialized, in order to save the generated JSON data to disk. |
|
526 fp.open({ done: result => { |
|
527 if (result == Ci.nsIFilePicker.returnCancel) { |
|
528 return; |
|
529 } |
|
530 let footer = $(".snapshot-item-footer", snapshotItem.target); |
|
531 let save = $(".snapshot-item-save", snapshotItem.target); |
|
532 |
|
533 // Show a throbber and a "Saving…" label if serializing isn't immediate. |
|
534 setNamedTimeout("call-list-save", CALLS_LIST_SLOW_SAVE_DELAY, () => { |
|
535 footer.setAttribute("saving", ""); |
|
536 save.setAttribute("disabled", "true"); |
|
537 save.setAttribute("value", L10N.getStr("snapshotsList.savingLabel")); |
|
538 }); |
|
539 |
|
540 serialized.then(inputStream => { |
|
541 let outputStream = FileUtils.openSafeFileOutputStream(fp.file); |
|
542 |
|
543 NetUtil.asyncCopy(inputStream, outputStream, status => { |
|
544 if (!Components.isSuccessCode(status)) { |
|
545 console.error("Could not save recorded animation frame snapshot file."); |
|
546 } |
|
547 clearNamedTimeout("call-list-save"); |
|
548 footer.removeAttribute("saving"); |
|
549 save.removeAttribute("disabled"); |
|
550 save.setAttribute("value", L10N.getStr("snapshotsList.saveLabel")); |
|
551 }); |
|
552 }); |
|
553 }}); |
|
554 } |
|
555 }); |
|
556 |
|
557 /** |
|
558 * Functions handling details about a single recorded animation frame snapshot |
|
559 * (the calls list, rendering preview, thumbnails filmstrip etc.). |
|
560 */ |
|
561 let CallsListView = Heritage.extend(WidgetMethods, { |
|
562 /** |
|
563 * Initialization function, called when the tool is started. |
|
564 */ |
|
565 initialize: function() { |
|
566 this.widget = new SideMenuWidget($("#calls-list")); |
|
567 this._slider = $("#calls-slider"); |
|
568 this._searchbox = $("#calls-searchbox"); |
|
569 this._filmstrip = $("#snapshot-filmstrip"); |
|
570 |
|
571 this._onSelect = this._onSelect.bind(this); |
|
572 this._onSlideMouseDown = this._onSlideMouseDown.bind(this); |
|
573 this._onSlideMouseUp = this._onSlideMouseUp.bind(this); |
|
574 this._onSlide = this._onSlide.bind(this); |
|
575 this._onSearch = this._onSearch.bind(this); |
|
576 this._onScroll = this._onScroll.bind(this); |
|
577 this._onExpand = this._onExpand.bind(this); |
|
578 this._onStackFileClick = this._onStackFileClick.bind(this); |
|
579 this._onThumbnailClick = this._onThumbnailClick.bind(this); |
|
580 |
|
581 this.widget.addEventListener("select", this._onSelect, false); |
|
582 this._slider.addEventListener("mousedown", this._onSlideMouseDown, false); |
|
583 this._slider.addEventListener("mouseup", this._onSlideMouseUp, false); |
|
584 this._slider.addEventListener("change", this._onSlide, false); |
|
585 this._searchbox.addEventListener("input", this._onSearch, false); |
|
586 this._filmstrip.addEventListener("wheel", this._onScroll, false); |
|
587 }, |
|
588 |
|
589 /** |
|
590 * Destruction function, called when the tool is closed. |
|
591 */ |
|
592 destroy: function() { |
|
593 this.widget.removeEventListener("select", this._onSelect, false); |
|
594 this._slider.removeEventListener("mousedown", this._onSlideMouseDown, false); |
|
595 this._slider.removeEventListener("mouseup", this._onSlideMouseUp, false); |
|
596 this._slider.removeEventListener("change", this._onSlide, false); |
|
597 this._searchbox.removeEventListener("input", this._onSearch, false); |
|
598 this._filmstrip.removeEventListener("wheel", this._onScroll, false); |
|
599 }, |
|
600 |
|
601 /** |
|
602 * Populates this container with a list of function calls. |
|
603 * |
|
604 * @param array functionCalls |
|
605 * A list of function call actors received from the backend. |
|
606 */ |
|
607 showCalls: function(functionCalls) { |
|
608 this.empty(); |
|
609 |
|
610 for (let i = 0, len = functionCalls.length; i < len; i++) { |
|
611 let call = functionCalls[i]; |
|
612 |
|
613 let view = document.createElement("vbox"); |
|
614 view.className = "call-item-view devtools-monospace"; |
|
615 view.setAttribute("flex", "1"); |
|
616 |
|
617 let contents = document.createElement("hbox"); |
|
618 contents.className = "call-item-contents"; |
|
619 contents.setAttribute("align", "center"); |
|
620 contents.addEventListener("dblclick", this._onExpand); |
|
621 view.appendChild(contents); |
|
622 |
|
623 let index = document.createElement("label"); |
|
624 index.className = "plain call-item-index"; |
|
625 index.setAttribute("flex", "1"); |
|
626 index.setAttribute("value", i + 1); |
|
627 |
|
628 let gutter = document.createElement("hbox"); |
|
629 gutter.className = "call-item-gutter"; |
|
630 gutter.appendChild(index); |
|
631 contents.appendChild(gutter); |
|
632 |
|
633 // Not all function calls have a caller that was stringified (e.g. |
|
634 // context calls have a "gl" or "ctx" caller preview). |
|
635 if (call.callerPreview) { |
|
636 let context = document.createElement("label"); |
|
637 context.className = "plain call-item-context"; |
|
638 context.setAttribute("value", call.callerPreview); |
|
639 contents.appendChild(context); |
|
640 |
|
641 let separator = document.createElement("label"); |
|
642 separator.className = "plain call-item-separator"; |
|
643 separator.setAttribute("value", "."); |
|
644 contents.appendChild(separator); |
|
645 } |
|
646 |
|
647 let name = document.createElement("label"); |
|
648 name.className = "plain call-item-name"; |
|
649 name.setAttribute("value", call.name); |
|
650 contents.appendChild(name); |
|
651 |
|
652 let argsPreview = document.createElement("label"); |
|
653 argsPreview.className = "plain call-item-args"; |
|
654 argsPreview.setAttribute("crop", "end"); |
|
655 argsPreview.setAttribute("flex", "100"); |
|
656 // Getters and setters are displayed differently from regular methods. |
|
657 if (call.type == CallWatcherFront.METHOD_FUNCTION) { |
|
658 argsPreview.setAttribute("value", "(" + call.argsPreview + ")"); |
|
659 } else { |
|
660 argsPreview.setAttribute("value", " = " + call.argsPreview); |
|
661 } |
|
662 contents.appendChild(argsPreview); |
|
663 |
|
664 let location = document.createElement("label"); |
|
665 location.className = "plain call-item-location"; |
|
666 location.setAttribute("value", getFileName(call.file) + ":" + call.line); |
|
667 location.setAttribute("crop", "start"); |
|
668 location.setAttribute("flex", "1"); |
|
669 location.addEventListener("mousedown", this._onExpand); |
|
670 contents.appendChild(location); |
|
671 |
|
672 // Append a function call item to this container. |
|
673 this.push([view], { |
|
674 staged: true, |
|
675 attachment: { |
|
676 actor: call |
|
677 } |
|
678 }); |
|
679 |
|
680 // Highlight certain calls that are probably more interesting than |
|
681 // everything else, making it easier to quickly glance over them. |
|
682 if (CanvasFront.DRAW_CALLS.has(call.name)) { |
|
683 view.setAttribute("draw-call", ""); |
|
684 } |
|
685 if (CanvasFront.INTERESTING_CALLS.has(call.name)) { |
|
686 view.setAttribute("interesting-call", ""); |
|
687 } |
|
688 } |
|
689 |
|
690 // Flushes all the prepared function call items into this container. |
|
691 this.commit(); |
|
692 window.emit(EVENTS.CALL_LIST_POPULATED); |
|
693 |
|
694 // Resetting the function selection slider's value (shown in this |
|
695 // container's toolbar) would trigger a selection event, which should be |
|
696 // ignored in this case. |
|
697 this._ignoreSliderChanges = true; |
|
698 this._slider.value = 0; |
|
699 this._slider.max = functionCalls.length - 1; |
|
700 this._ignoreSliderChanges = false; |
|
701 }, |
|
702 |
|
703 /** |
|
704 * Displays an image in the rendering preview of this container, generated |
|
705 * for the specified draw call in the recorded animation frame snapshot. |
|
706 * |
|
707 * @param array screenshot |
|
708 * A single "snapshot-image" instance received from the backend. |
|
709 */ |
|
710 showScreenshot: function(screenshot) { |
|
711 let { index, width, height, flipped, pixels } = screenshot; |
|
712 |
|
713 let screenshotNode = $("#screenshot-image"); |
|
714 screenshotNode.setAttribute("flipped", flipped); |
|
715 drawBackground("screenshot-rendering", width, height, pixels); |
|
716 |
|
717 let dimensionsNode = $("#screenshot-dimensions"); |
|
718 dimensionsNode.setAttribute("value", ~~width + " x " + ~~height); |
|
719 |
|
720 window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED); |
|
721 }, |
|
722 |
|
723 /** |
|
724 * Populates this container's footer with a list of thumbnails, one generated |
|
725 * for each draw call in the recorded animation frame snapshot. |
|
726 * |
|
727 * @param array thumbnails |
|
728 * An array of "snapshot-image" instances received from the backend. |
|
729 */ |
|
730 showThumbnails: function(thumbnails) { |
|
731 while (this._filmstrip.hasChildNodes()) { |
|
732 this._filmstrip.firstChild.remove(); |
|
733 } |
|
734 for (let thumbnail of thumbnails) { |
|
735 this.appendThumbnail(thumbnail); |
|
736 } |
|
737 |
|
738 window.emit(EVENTS.THUMBNAILS_DISPLAYED); |
|
739 }, |
|
740 |
|
741 /** |
|
742 * Displays an image in the thumbnails list of this container, generated |
|
743 * for the specified draw call in the recorded animation frame snapshot. |
|
744 * |
|
745 * @param array thumbnail |
|
746 * A single "snapshot-image" instance received from the backend. |
|
747 */ |
|
748 appendThumbnail: function(thumbnail) { |
|
749 let { index, width, height, flipped, pixels } = thumbnail; |
|
750 |
|
751 let thumbnailNode = document.createElementNS(HTML_NS, "canvas"); |
|
752 thumbnailNode.setAttribute("flipped", flipped); |
|
753 thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_HEIGHT, width); |
|
754 thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_HEIGHT, height); |
|
755 drawImage(thumbnailNode, width, height, pixels, { centered: true }); |
|
756 |
|
757 thumbnailNode.className = "filmstrip-thumbnail"; |
|
758 thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index); |
|
759 thumbnailNode.setAttribute("index", index); |
|
760 this._filmstrip.appendChild(thumbnailNode); |
|
761 }, |
|
762 |
|
763 /** |
|
764 * Sets the currently highlighted thumbnail in this container. |
|
765 * A screenshot will always correlate to a thumbnail in the filmstrip, |
|
766 * both being identified by the same 'index' of the context function call. |
|
767 * |
|
768 * @param number index |
|
769 * The context function call's index. |
|
770 */ |
|
771 set highlightedThumbnail(index) { |
|
772 let currHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + index + "']"); |
|
773 if (currHighlightedThumbnail == null) { |
|
774 return; |
|
775 } |
|
776 |
|
777 let prevIndex = this._highlightedThumbnailIndex |
|
778 let prevHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + prevIndex + "']"); |
|
779 if (prevHighlightedThumbnail) { |
|
780 prevHighlightedThumbnail.removeAttribute("highlighted"); |
|
781 } |
|
782 |
|
783 currHighlightedThumbnail.setAttribute("highlighted", ""); |
|
784 currHighlightedThumbnail.scrollIntoView(); |
|
785 this._highlightedThumbnailIndex = index; |
|
786 }, |
|
787 |
|
788 /** |
|
789 * Gets the currently highlighted thumbnail in this container. |
|
790 * @return number |
|
791 */ |
|
792 get highlightedThumbnail() { |
|
793 return this._highlightedThumbnailIndex; |
|
794 }, |
|
795 |
|
796 /** |
|
797 * The select listener for this container. |
|
798 */ |
|
799 _onSelect: function({ detail: callItem }) { |
|
800 if (!callItem) { |
|
801 return; |
|
802 } |
|
803 |
|
804 // Some of the stepping buttons don't make sense specifically while the |
|
805 // last function call is selected. |
|
806 if (this.selectedIndex == this.itemCount - 1) { |
|
807 $("#resume").setAttribute("disabled", "true"); |
|
808 $("#step-over").setAttribute("disabled", "true"); |
|
809 $("#step-out").setAttribute("disabled", "true"); |
|
810 } else { |
|
811 $("#resume").removeAttribute("disabled"); |
|
812 $("#step-over").removeAttribute("disabled"); |
|
813 $("#step-out").removeAttribute("disabled"); |
|
814 } |
|
815 |
|
816 // Correlate the currently selected item with the function selection |
|
817 // slider's value. Avoid triggering a redundant selection event. |
|
818 this._ignoreSliderChanges = true; |
|
819 this._slider.value = this.selectedIndex; |
|
820 this._ignoreSliderChanges = false; |
|
821 |
|
822 // Can't generate screenshots for function call actors loaded from disk. |
|
823 // XXX: Bug 984844. |
|
824 if (callItem.attachment.actor.isLoadedFromDisk) { |
|
825 return; |
|
826 } |
|
827 |
|
828 // To keep continuous selection buttery smooth (for example, while pressing |
|
829 // the DOWN key or moving the slider), only display the screenshot after |
|
830 // any kind of user input stops. |
|
831 setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => { |
|
832 return !this._isSliding; |
|
833 }, () => { |
|
834 let frameSnapshot = SnapshotsListView.selectedItem.attachment.actor |
|
835 let functionCall = callItem.attachment.actor; |
|
836 frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => { |
|
837 this.showScreenshot(screenshot); |
|
838 this.highlightedThumbnail = screenshot.index; |
|
839 }); |
|
840 }); |
|
841 }, |
|
842 |
|
843 /** |
|
844 * The mousedown listener for the call selection slider. |
|
845 */ |
|
846 _onSlideMouseDown: function() { |
|
847 this._isSliding = true; |
|
848 }, |
|
849 |
|
850 /** |
|
851 * The mouseup listener for the call selection slider. |
|
852 */ |
|
853 _onSlideMouseUp: function() { |
|
854 this._isSliding = false; |
|
855 }, |
|
856 |
|
857 /** |
|
858 * The change listener for the call selection slider. |
|
859 */ |
|
860 _onSlide: function() { |
|
861 // Avoid performing any operations when programatically changing the value. |
|
862 if (this._ignoreSliderChanges) { |
|
863 return; |
|
864 } |
|
865 let selectedFunctionCallIndex = this.selectedIndex = this._slider.value; |
|
866 |
|
867 // While sliding, immediately show the most relevant thumbnail for a |
|
868 // function call, for a nice diff-like animation effect between draws. |
|
869 let thumbnails = SnapshotsListView.selectedItem.attachment.thumbnails; |
|
870 let thumbnail = getThumbnailForCall(thumbnails, selectedFunctionCallIndex); |
|
871 |
|
872 // Avoid drawing and highlighting if the selected function call has the |
|
873 // same thumbnail as the last one. |
|
874 if (thumbnail.index == this.highlightedThumbnail) { |
|
875 return; |
|
876 } |
|
877 // If a thumbnail wasn't found (e.g. the backend avoids creating thumbnails |
|
878 // when rendering offscreen), simply defer to the first available one. |
|
879 if (thumbnail.index == -1) { |
|
880 thumbnail = thumbnails[0]; |
|
881 } |
|
882 |
|
883 let { index, width, height, flipped, pixels } = thumbnail; |
|
884 this.highlightedThumbnail = index; |
|
885 |
|
886 let screenshotNode = $("#screenshot-image"); |
|
887 screenshotNode.setAttribute("flipped", flipped); |
|
888 drawBackground("screenshot-rendering", width, height, pixels); |
|
889 }, |
|
890 |
|
891 /** |
|
892 * The input listener for the calls searchbox. |
|
893 */ |
|
894 _onSearch: function(e) { |
|
895 let lowerCaseSearchToken = this._searchbox.value.toLowerCase(); |
|
896 |
|
897 this.filterContents(e => { |
|
898 let call = e.attachment.actor; |
|
899 let name = call.name.toLowerCase(); |
|
900 let file = call.file.toLowerCase(); |
|
901 let line = call.line.toString().toLowerCase(); |
|
902 let args = call.argsPreview.toLowerCase(); |
|
903 |
|
904 return name.contains(lowerCaseSearchToken) || |
|
905 file.contains(lowerCaseSearchToken) || |
|
906 line.contains(lowerCaseSearchToken) || |
|
907 args.contains(lowerCaseSearchToken); |
|
908 }); |
|
909 }, |
|
910 |
|
911 /** |
|
912 * The wheel listener for the filmstrip that contains all the thumbnails. |
|
913 */ |
|
914 _onScroll: function(e) { |
|
915 this._filmstrip.scrollLeft += e.deltaX; |
|
916 }, |
|
917 |
|
918 /** |
|
919 * The click/dblclick listener for an item or location url in this container. |
|
920 * When expanding an item, it's corresponding call stack will be displayed. |
|
921 */ |
|
922 _onExpand: function(e) { |
|
923 let callItem = this.getItemForElement(e.target); |
|
924 let view = $(".call-item-view", callItem.target); |
|
925 |
|
926 // If the call stack nodes were already created, simply re-show them |
|
927 // or jump to the corresponding file and line in the Debugger if a |
|
928 // location link was clicked. |
|
929 if (view.hasAttribute("call-stack-populated")) { |
|
930 let isExpanded = view.getAttribute("call-stack-expanded") == "true"; |
|
931 |
|
932 // If clicking on the location, jump to the Debugger. |
|
933 if (e.target.classList.contains("call-item-location")) { |
|
934 let { file, line } = callItem.attachment.actor; |
|
935 viewSourceInDebugger(file, line); |
|
936 return; |
|
937 } |
|
938 // Otherwise hide the call stack. |
|
939 else { |
|
940 view.setAttribute("call-stack-expanded", !isExpanded); |
|
941 $(".call-item-stack", view).hidden = isExpanded; |
|
942 return; |
|
943 } |
|
944 } |
|
945 |
|
946 let list = document.createElement("vbox"); |
|
947 list.className = "call-item-stack"; |
|
948 view.setAttribute("call-stack-populated", ""); |
|
949 view.setAttribute("call-stack-expanded", "true"); |
|
950 view.appendChild(list); |
|
951 |
|
952 /** |
|
953 * Creates a function call nodes in this container for a stack. |
|
954 */ |
|
955 let display = stack => { |
|
956 for (let i = 1; i < stack.length; i++) { |
|
957 let call = stack[i]; |
|
958 |
|
959 let contents = document.createElement("hbox"); |
|
960 contents.className = "call-item-stack-fn"; |
|
961 contents.style.MozPaddingStart = (i * STACK_FUNC_INDENTATION) + "px"; |
|
962 |
|
963 let name = document.createElement("label"); |
|
964 name.className = "plain call-item-stack-fn-name"; |
|
965 name.setAttribute("value", "↳ " + call.name + "()"); |
|
966 contents.appendChild(name); |
|
967 |
|
968 let spacer = document.createElement("spacer"); |
|
969 spacer.setAttribute("flex", "100"); |
|
970 contents.appendChild(spacer); |
|
971 |
|
972 let location = document.createElement("label"); |
|
973 location.className = "plain call-item-stack-fn-location"; |
|
974 location.setAttribute("value", getFileName(call.file) + ":" + call.line); |
|
975 location.setAttribute("crop", "start"); |
|
976 location.setAttribute("flex", "1"); |
|
977 location.addEventListener("mousedown", e => this._onStackFileClick(e, call)); |
|
978 contents.appendChild(location); |
|
979 |
|
980 list.appendChild(contents); |
|
981 } |
|
982 |
|
983 window.emit(EVENTS.CALL_STACK_DISPLAYED); |
|
984 }; |
|
985 |
|
986 // If this animation snapshot is loaded from disk, there are no corresponding |
|
987 // backend actors available and the data is immediately available. |
|
988 let functionCall = callItem.attachment.actor; |
|
989 if (functionCall.isLoadedFromDisk) { |
|
990 display(functionCall.stack); |
|
991 } |
|
992 // ..otherwise we need to request the function call stack from the backend. |
|
993 else { |
|
994 callItem.attachment.actor.getDetails().then(fn => display(fn.stack)); |
|
995 } |
|
996 }, |
|
997 |
|
998 /** |
|
999 * The click listener for a location link in the call stack. |
|
1000 * |
|
1001 * @param string file |
|
1002 * The url of the source owning the function. |
|
1003 * @param number line |
|
1004 * The line of the respective function. |
|
1005 */ |
|
1006 _onStackFileClick: function(e, { file, line }) { |
|
1007 viewSourceInDebugger(file, line); |
|
1008 }, |
|
1009 |
|
1010 /** |
|
1011 * The click listener for a thumbnail in the filmstrip. |
|
1012 * |
|
1013 * @param number index |
|
1014 * The function index in the recorded animation frame snapshot. |
|
1015 */ |
|
1016 _onThumbnailClick: function(e, index) { |
|
1017 this.selectedIndex = index; |
|
1018 }, |
|
1019 |
|
1020 /** |
|
1021 * The click listener for the "resume" button in this container's toolbar. |
|
1022 */ |
|
1023 _onResume: function() { |
|
1024 // Jump to the next draw call in the recorded animation frame snapshot. |
|
1025 let drawCall = getNextDrawCall(this.items, this.selectedItem); |
|
1026 if (drawCall) { |
|
1027 this.selectedItem = drawCall; |
|
1028 return; |
|
1029 } |
|
1030 |
|
1031 // If there are no more draw calls, just jump to the last context call. |
|
1032 this._onStepOut(); |
|
1033 }, |
|
1034 |
|
1035 /** |
|
1036 * The click listener for the "step over" button in this container's toolbar. |
|
1037 */ |
|
1038 _onStepOver: function() { |
|
1039 this.selectedIndex++; |
|
1040 }, |
|
1041 |
|
1042 /** |
|
1043 * The click listener for the "step in" button in this container's toolbar. |
|
1044 */ |
|
1045 _onStepIn: function() { |
|
1046 if (this.selectedIndex == -1) { |
|
1047 this._onResume(); |
|
1048 return; |
|
1049 } |
|
1050 let callItem = this.selectedItem; |
|
1051 let { file, line } = callItem.attachment.actor; |
|
1052 viewSourceInDebugger(file, line); |
|
1053 }, |
|
1054 |
|
1055 /** |
|
1056 * The click listener for the "step out" button in this container's toolbar. |
|
1057 */ |
|
1058 _onStepOut: function() { |
|
1059 this.selectedIndex = this.itemCount - 1; |
|
1060 } |
|
1061 }); |
|
1062 |
|
1063 /** |
|
1064 * Localization convenience methods. |
|
1065 */ |
|
1066 let L10N = new ViewHelpers.L10N(STRINGS_URI); |
|
1067 |
|
1068 /** |
|
1069 * Convenient way of emitting events from the panel window. |
|
1070 */ |
|
1071 EventEmitter.decorate(this); |
|
1072 |
|
1073 /** |
|
1074 * DOM query helpers. |
|
1075 */ |
|
1076 function $(selector, target = document) target.querySelector(selector); |
|
1077 function $all(selector, target = document) target.querySelectorAll(selector); |
|
1078 |
|
1079 /** |
|
1080 * Helper for getting an nsIURL instance out of a string. |
|
1081 */ |
|
1082 function nsIURL(url, store = nsIURL.store) { |
|
1083 if (store.has(url)) { |
|
1084 return store.get(url); |
|
1085 } |
|
1086 let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL); |
|
1087 store.set(url, uri); |
|
1088 return uri; |
|
1089 } |
|
1090 |
|
1091 // The cache used in the `nsIURL` function. |
|
1092 nsIURL.store = new Map(); |
|
1093 |
|
1094 /** |
|
1095 * Gets the fileName part of a string which happens to be an URL. |
|
1096 */ |
|
1097 function getFileName(url) { |
|
1098 try { |
|
1099 let { fileName } = nsIURL(url); |
|
1100 return fileName || "/"; |
|
1101 } catch (e) { |
|
1102 // This doesn't look like a url, or nsIURL can't handle it. |
|
1103 return ""; |
|
1104 } |
|
1105 } |
|
1106 |
|
1107 /** |
|
1108 * Gets an image data object containing a buffer large enough to hold |
|
1109 * width * height pixels. |
|
1110 * |
|
1111 * This method avoids allocating memory and tries to reuse a common buffer |
|
1112 * as much as possible. |
|
1113 * |
|
1114 * @param number w |
|
1115 * The desired image data storage width. |
|
1116 * @param number h |
|
1117 * The desired image data storage height. |
|
1118 * @return ImageData |
|
1119 * The requested image data buffer. |
|
1120 */ |
|
1121 function getImageDataStorage(ctx, w, h) { |
|
1122 let storage = getImageDataStorage.cache; |
|
1123 if (storage && storage.width == w && storage.height == h) { |
|
1124 return storage; |
|
1125 } |
|
1126 return getImageDataStorage.cache = ctx.createImageData(w, h); |
|
1127 } |
|
1128 |
|
1129 // The cache used in the `getImageDataStorage` function. |
|
1130 getImageDataStorage.cache = null; |
|
1131 |
|
1132 /** |
|
1133 * Draws image data into a canvas. |
|
1134 * |
|
1135 * This method makes absolutely no assumptions about the canvas element |
|
1136 * dimensions, or pre-existing rendering. It's a dumb proxy that copies pixels. |
|
1137 * |
|
1138 * @param HTMLCanvasElement canvas |
|
1139 * The canvas element to put the image data into. |
|
1140 * @param number width |
|
1141 * The image data width. |
|
1142 * @param number height |
|
1143 * The image data height. |
|
1144 * @param pixels |
|
1145 * An array buffer view of the image data. |
|
1146 * @param object options |
|
1147 * Additional options supported by this operation: |
|
1148 * - centered: specifies whether the image data should be centered |
|
1149 * when copied in the canvas; this is useful when the |
|
1150 * supplied pixels don't completely cover the canvas. |
|
1151 */ |
|
1152 function drawImage(canvas, width, height, pixels, options = {}) { |
|
1153 let ctx = canvas.getContext("2d"); |
|
1154 |
|
1155 // FrameSnapshot actors return "snapshot-image" type instances with just an |
|
1156 // empty pixel array if the source image is completely transparent. |
|
1157 if (pixels.length <= 1) { |
|
1158 ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
1159 return; |
|
1160 } |
|
1161 |
|
1162 let arrayBuffer = new Uint8Array(pixels.buffer); |
|
1163 let imageData = getImageDataStorage(ctx, width, height); |
|
1164 imageData.data.set(arrayBuffer); |
|
1165 |
|
1166 if (options.centered) { |
|
1167 let left = (canvas.width - width) / 2; |
|
1168 let top = (canvas.height - height) / 2; |
|
1169 ctx.putImageData(imageData, left, top); |
|
1170 } else { |
|
1171 ctx.putImageData(imageData, 0, 0); |
|
1172 } |
|
1173 } |
|
1174 |
|
1175 /** |
|
1176 * Draws image data into a canvas, and sets that as the rendering source for |
|
1177 * an element with the specified id as the -moz-element background image. |
|
1178 * |
|
1179 * @param string id |
|
1180 * The id of the -moz-element background image. |
|
1181 * @param number width |
|
1182 * The image data width. |
|
1183 * @param number height |
|
1184 * The image data height. |
|
1185 * @param pixels |
|
1186 * An array buffer view of the image data. |
|
1187 */ |
|
1188 function drawBackground(id, width, height, pixels) { |
|
1189 let canvas = document.createElementNS(HTML_NS, "canvas"); |
|
1190 canvas.width = width; |
|
1191 canvas.height = height; |
|
1192 |
|
1193 drawImage(canvas, width, height, pixels); |
|
1194 document.mozSetImageElement(id, canvas); |
|
1195 |
|
1196 // Used in tests. Not emitting an event because this shouldn't be "interesting". |
|
1197 if (window._onMozSetImageElement) { |
|
1198 window._onMozSetImageElement(pixels); |
|
1199 } |
|
1200 } |
|
1201 |
|
1202 /** |
|
1203 * Iterates forward to find the next draw call in a snapshot. |
|
1204 */ |
|
1205 function getNextDrawCall(calls, call) { |
|
1206 for (let i = calls.indexOf(call) + 1, len = calls.length; i < len; i++) { |
|
1207 let nextCall = calls[i]; |
|
1208 let name = nextCall.attachment.actor.name; |
|
1209 if (CanvasFront.DRAW_CALLS.has(name)) { |
|
1210 return nextCall; |
|
1211 } |
|
1212 } |
|
1213 return null; |
|
1214 } |
|
1215 |
|
1216 /** |
|
1217 * Iterates backwards to find the most recent screenshot for a function call |
|
1218 * in a snapshot loaded from disk. |
|
1219 */ |
|
1220 function getScreenshotFromCallLoadedFromDisk(calls, call) { |
|
1221 for (let i = calls.indexOf(call); i >= 0; i--) { |
|
1222 let prevCall = calls[i]; |
|
1223 let screenshot = prevCall.screenshot; |
|
1224 if (screenshot) { |
|
1225 return screenshot; |
|
1226 } |
|
1227 } |
|
1228 return CanvasFront.INVALID_SNAPSHOT_IMAGE; |
|
1229 } |
|
1230 |
|
1231 /** |
|
1232 * Iterates backwards to find the most recent thumbnail for a function call. |
|
1233 */ |
|
1234 function getThumbnailForCall(thumbnails, index) { |
|
1235 for (let i = thumbnails.length - 1; i >= 0; i--) { |
|
1236 let thumbnail = thumbnails[i]; |
|
1237 if (thumbnail.index <= index) { |
|
1238 return thumbnail; |
|
1239 } |
|
1240 } |
|
1241 return CanvasFront.INVALID_SNAPSHOT_IMAGE; |
|
1242 } |
|
1243 |
|
1244 /** |
|
1245 * Opens/selects the debugger in this toolbox and jumps to the specified |
|
1246 * file name and line number. |
|
1247 */ |
|
1248 function viewSourceInDebugger(url, line) { |
|
1249 let showSource = ({ DebuggerView }) => { |
|
1250 if (DebuggerView.Sources.containsValue(url)) { |
|
1251 DebuggerView.setEditorLocation(url, line, { noDebug: true }).then(() => { |
|
1252 window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); |
|
1253 }, () => { |
|
1254 window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER); |
|
1255 }); |
|
1256 } |
|
1257 } |
|
1258 |
|
1259 // If the Debugger was already open, switch to it and try to show the |
|
1260 // source immediately. Otherwise, initialize it and wait for the sources |
|
1261 // to be added first. |
|
1262 let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger"); |
|
1263 gToolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => { |
|
1264 if (debuggerAlreadyOpen) { |
|
1265 showSource(dbg); |
|
1266 } else { |
|
1267 dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg)); |
|
1268 } |
|
1269 }); |
|
1270 } |