snapshotslist.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  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. /* import-globals-from canvasdebugger.js */
  5. /* globals window, document */
  6. "use strict";
  7. /**
  8. * Functions handling the recorded animation frame snapshots UI.
  9. */
  10. var SnapshotsListView = Heritage.extend(WidgetMethods, {
  11. /**
  12. * Initialization function, called when the tool is started.
  13. */
  14. initialize: function () {
  15. this.widget = new SideMenuWidget($("#snapshots-list"), {
  16. showArrows: true
  17. });
  18. this._onSelect = this._onSelect.bind(this);
  19. this._onClearButtonClick = this._onClearButtonClick.bind(this);
  20. this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
  21. this._onImportButtonClick = this._onImportButtonClick.bind(this);
  22. this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
  23. this._onRecordSuccess = this._onRecordSuccess.bind(this);
  24. this._onRecordFailure = this._onRecordFailure.bind(this);
  25. this._stopRecordingAnimation = this._stopRecordingAnimation.bind(this);
  26. window.on(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton);
  27. this.emptyText = L10N.getStr("noSnapshotsText");
  28. this.widget.addEventListener("select", this._onSelect, false);
  29. },
  30. /**
  31. * Destruction function, called when the tool is closed.
  32. */
  33. destroy: function () {
  34. clearNamedTimeout("canvas-actor-recording");
  35. window.off(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton);
  36. this.widget.removeEventListener("select", this._onSelect, false);
  37. },
  38. /**
  39. * Adds a snapshot entry to this container.
  40. *
  41. * @return object
  42. * The newly inserted item.
  43. */
  44. addSnapshot: function () {
  45. let contents = document.createElement("hbox");
  46. contents.className = "snapshot-item";
  47. let thumbnail = document.createElementNS(HTML_NS, "canvas");
  48. thumbnail.className = "snapshot-item-thumbnail";
  49. thumbnail.width = CanvasFront.THUMBNAIL_SIZE;
  50. thumbnail.height = CanvasFront.THUMBNAIL_SIZE;
  51. let title = document.createElement("label");
  52. title.className = "plain snapshot-item-title";
  53. title.setAttribute("value",
  54. L10N.getFormatStr("snapshotsList.itemLabel", this.itemCount + 1));
  55. let calls = document.createElement("label");
  56. calls.className = "plain snapshot-item-calls";
  57. calls.setAttribute("value",
  58. L10N.getStr("snapshotsList.loadingLabel"));
  59. let save = document.createElement("label");
  60. save.className = "plain snapshot-item-save";
  61. save.addEventListener("click", this._onSaveButtonClick, false);
  62. let spacer = document.createElement("spacer");
  63. spacer.setAttribute("flex", "1");
  64. let footer = document.createElement("hbox");
  65. footer.className = "snapshot-item-footer";
  66. footer.appendChild(save);
  67. let details = document.createElement("vbox");
  68. details.className = "snapshot-item-details";
  69. details.appendChild(title);
  70. details.appendChild(calls);
  71. details.appendChild(spacer);
  72. details.appendChild(footer);
  73. contents.appendChild(thumbnail);
  74. contents.appendChild(details);
  75. // Append a recorded snapshot item to this container.
  76. return this.push([contents], {
  77. attachment: {
  78. // The snapshot and function call actors, along with the thumbnails
  79. // will be available as soon as recording finishes.
  80. actor: null,
  81. calls: null,
  82. thumbnails: null,
  83. screenshot: null
  84. }
  85. });
  86. },
  87. /**
  88. * Removes the last snapshot added, in the event no requestAnimationFrame loop was found.
  89. */
  90. removeLastSnapshot: function () {
  91. this.removeAt(this.itemCount - 1);
  92. // If this is the only item, revert back to the empty notice
  93. if (this.itemCount === 0) {
  94. $("#empty-notice").hidden = false;
  95. $("#waiting-notice").hidden = true;
  96. }
  97. },
  98. /**
  99. * Customizes a shapshot in this container.
  100. *
  101. * @param Item snapshotItem
  102. * An item inserted via `SnapshotsListView.addSnapshot`.
  103. * @param object snapshotActor
  104. * The frame snapshot actor received from the backend.
  105. * @param object snapshotOverview
  106. * Additional data about the snapshot received from the backend.
  107. */
  108. customizeSnapshot: function (snapshotItem, snapshotActor, snapshotOverview) {
  109. // Make sure the function call actors are stored on the item,
  110. // to be used when populating the CallsListView.
  111. snapshotItem.attachment.actor = snapshotActor;
  112. let functionCalls = snapshotItem.attachment.calls = snapshotOverview.calls;
  113. let thumbnails = snapshotItem.attachment.thumbnails = snapshotOverview.thumbnails;
  114. let screenshot = snapshotItem.attachment.screenshot = snapshotOverview.screenshot;
  115. let lastThumbnail = thumbnails[thumbnails.length - 1];
  116. let { width, height, flipped, pixels } = lastThumbnail;
  117. let thumbnailNode = $(".snapshot-item-thumbnail", snapshotItem.target);
  118. thumbnailNode.setAttribute("flipped", flipped);
  119. drawImage(thumbnailNode, width, height, pixels, { centered: true });
  120. let callsNode = $(".snapshot-item-calls", snapshotItem.target);
  121. let drawCalls = functionCalls.filter(e => CanvasFront.DRAW_CALLS.has(e.name));
  122. let drawCallsStr = PluralForm.get(drawCalls.length,
  123. L10N.getStr("snapshotsList.drawCallsLabel"));
  124. let funcCallsStr = PluralForm.get(functionCalls.length,
  125. L10N.getStr("snapshotsList.functionCallsLabel"));
  126. callsNode.setAttribute("value",
  127. drawCallsStr.replace("#1", drawCalls.length) + ", " +
  128. funcCallsStr.replace("#1", functionCalls.length));
  129. let saveNode = $(".snapshot-item-save", snapshotItem.target);
  130. saveNode.setAttribute("disabled", !!snapshotItem.isLoadedFromDisk);
  131. saveNode.setAttribute("value", snapshotItem.isLoadedFromDisk
  132. ? L10N.getStr("snapshotsList.loadedLabel")
  133. : L10N.getStr("snapshotsList.saveLabel"));
  134. // Make sure there's always a selected item available.
  135. if (!this.selectedItem) {
  136. this.selectedIndex = 0;
  137. }
  138. },
  139. /**
  140. * The select listener for this container.
  141. */
  142. _onSelect: function ({ detail: snapshotItem }) {
  143. // Check to ensure the attachment has an actor, like
  144. // an in-progress recording.
  145. if (!snapshotItem || !snapshotItem.attachment.actor) {
  146. return;
  147. }
  148. let { calls, thumbnails, screenshot } = snapshotItem.attachment;
  149. $("#reload-notice").hidden = true;
  150. $("#empty-notice").hidden = true;
  151. $("#waiting-notice").hidden = false;
  152. $("#debugging-pane-contents").hidden = true;
  153. $("#screenshot-container").hidden = true;
  154. $("#snapshot-filmstrip").hidden = true;
  155. Task.spawn(function* () {
  156. // Wait for a few milliseconds between presenting the function calls,
  157. // screenshot and thumbnails, to allow each component being
  158. // sequentially drawn. This gives the illusion of snappiness.
  159. yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
  160. CallsListView.showCalls(calls);
  161. $("#debugging-pane-contents").hidden = false;
  162. $("#waiting-notice").hidden = true;
  163. yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
  164. CallsListView.showThumbnails(thumbnails);
  165. $("#snapshot-filmstrip").hidden = false;
  166. yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
  167. CallsListView.showScreenshot(screenshot);
  168. $("#screenshot-container").hidden = false;
  169. window.emit(EVENTS.SNAPSHOT_RECORDING_SELECTED);
  170. });
  171. },
  172. /**
  173. * The click listener for the "clear" button in this container.
  174. */
  175. _onClearButtonClick: function () {
  176. Task.spawn(function* () {
  177. SnapshotsListView.empty();
  178. CallsListView.empty();
  179. $("#reload-notice").hidden = true;
  180. $("#empty-notice").hidden = true;
  181. $("#waiting-notice").hidden = true;
  182. if (yield gFront.isInitialized()) {
  183. $("#empty-notice").hidden = false;
  184. } else {
  185. $("#reload-notice").hidden = false;
  186. }
  187. $("#debugging-pane-contents").hidden = true;
  188. $("#screenshot-container").hidden = true;
  189. $("#snapshot-filmstrip").hidden = true;
  190. window.emit(EVENTS.SNAPSHOTS_LIST_CLEARED);
  191. });
  192. },
  193. /**
  194. * The click listener for the "record" button in this container.
  195. */
  196. _onRecordButtonClick: function () {
  197. this._disableRecordButton();
  198. if (this._recording) {
  199. this._stopRecordingAnimation();
  200. return;
  201. }
  202. // Insert a "dummy" snapshot item in the view, to hint that recording
  203. // has now started. However, wait for a few milliseconds before actually
  204. // starting the recording, since that might block rendering and prevent
  205. // the dummy snapshot item from being drawn.
  206. this.addSnapshot();
  207. // If this is the first item, immediately show the "Loading…" notice.
  208. if (this.itemCount == 1) {
  209. $("#empty-notice").hidden = true;
  210. $("#waiting-notice").hidden = false;
  211. }
  212. this._recordAnimation();
  213. },
  214. /**
  215. * Makes the record button able to be clicked again.
  216. */
  217. _enableRecordButton: function () {
  218. $("#record-snapshot").removeAttribute("disabled");
  219. },
  220. /**
  221. * Makes the record button unable to be clicked.
  222. */
  223. _disableRecordButton: function () {
  224. $("#record-snapshot").setAttribute("disabled", true);
  225. },
  226. /**
  227. * Begins recording an animation.
  228. */
  229. _recordAnimation: Task.async(function* () {
  230. if (this._recording) {
  231. return;
  232. }
  233. this._recording = true;
  234. $("#record-snapshot").setAttribute("checked", "true");
  235. setNamedTimeout("canvas-actor-recording", CANVAS_ACTOR_RECORDING_ATTEMPT, this._stopRecordingAnimation);
  236. yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY);
  237. window.emit(EVENTS.SNAPSHOT_RECORDING_STARTED);
  238. gFront.recordAnimationFrame().then(snapshot => {
  239. if (snapshot) {
  240. this._onRecordSuccess(snapshot);
  241. } else {
  242. this._onRecordFailure();
  243. }
  244. });
  245. // Wait another delay before reenabling the button to stop the recording
  246. // if a recording is not found.
  247. yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY);
  248. this._enableRecordButton();
  249. }),
  250. /**
  251. * Stops recording animation. Called when a click on the stopwatch occurs during a recording,
  252. * or if a recording times out.
  253. */
  254. _stopRecordingAnimation: Task.async(function* () {
  255. clearNamedTimeout("canvas-actor-recording");
  256. let actorCanStop = yield gTarget.actorHasMethod("canvas", "stopRecordingAnimationFrame");
  257. if (actorCanStop) {
  258. yield gFront.stopRecordingAnimationFrame();
  259. }
  260. // If actor does not have the method to stop recording (Fx39+),
  261. // manually call the record failure method. This will call a connection failure
  262. // on disconnect as a result of `gFront.recordAnimationFrame()` never resolving,
  263. // but this is better than it hanging when there is no requestAnimationFrame anyway.
  264. else {
  265. this._onRecordFailure();
  266. }
  267. this._recording = false;
  268. $("#record-snapshot").removeAttribute("checked");
  269. this._enableRecordButton();
  270. }),
  271. /**
  272. * Resolves from the front's recordAnimationFrame to setup the interface with the screenshots.
  273. */
  274. _onRecordSuccess: Task.async(function* (snapshotActor) {
  275. // Clear bail-out case if frame found in CANVAS_ACTOR_RECORDING_ATTEMPT milliseconds
  276. clearNamedTimeout("canvas-actor-recording");
  277. let snapshotItem = this.getItemAtIndex(this.itemCount - 1);
  278. let snapshotOverview = yield snapshotActor.getOverview();
  279. this.customizeSnapshot(snapshotItem, snapshotActor, snapshotOverview);
  280. this._recording = false;
  281. $("#record-snapshot").removeAttribute("checked");
  282. window.emit(EVENTS.SNAPSHOT_RECORDING_COMPLETED);
  283. window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED);
  284. }),
  285. /**
  286. * Called as a reject from the front's recordAnimationFrame.
  287. */
  288. _onRecordFailure: function () {
  289. clearNamedTimeout("canvas-actor-recording");
  290. showNotification(gToolbox, "canvas-debugger-timeout", L10N.getStr("recordingTimeoutFailure"));
  291. window.emit(EVENTS.SNAPSHOT_RECORDING_CANCELLED);
  292. window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED);
  293. this.removeLastSnapshot();
  294. },
  295. /**
  296. * The click listener for the "import" button in this container.
  297. */
  298. _onImportButtonClick: function () {
  299. let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  300. fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen);
  301. fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json");
  302. fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*");
  303. if (fp.show() != Ci.nsIFilePicker.returnOK) {
  304. return;
  305. }
  306. let channel = NetUtil.newChannel({
  307. uri: NetUtil.newURI(fp.file), loadUsingSystemPrincipal: true});
  308. channel.contentType = "text/plain";
  309. NetUtil.asyncFetch(channel, (inputStream, status) => {
  310. if (!Components.isSuccessCode(status)) {
  311. console.error("Could not import recorded animation frame snapshot file.");
  312. return;
  313. }
  314. try {
  315. let string = NetUtil.readInputStreamToString(inputStream, inputStream.available());
  316. var data = JSON.parse(string);
  317. } catch (e) {
  318. console.error("Could not read animation frame snapshot file.");
  319. return;
  320. }
  321. if (data.fileType != CALLS_LIST_SERIALIZER_IDENTIFIER) {
  322. console.error("Unrecognized animation frame snapshot file.");
  323. return;
  324. }
  325. // Add a `isLoadedFromDisk` flag on everything to avoid sending invalid
  326. // requests to the backend, since we're not dealing with actors anymore.
  327. let snapshotItem = this.addSnapshot();
  328. snapshotItem.isLoadedFromDisk = true;
  329. data.calls.forEach(e => e.isLoadedFromDisk = true);
  330. this.customizeSnapshot(snapshotItem, data.calls, data);
  331. });
  332. },
  333. /**
  334. * The click listener for the "save" button of each item in this container.
  335. */
  336. _onSaveButtonClick: function (e) {
  337. let snapshotItem = this.getItemForElement(e.target);
  338. let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  339. fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave);
  340. fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json");
  341. fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*");
  342. fp.defaultString = "snapshot.json";
  343. // Start serializing all the function call actors for the specified snapshot,
  344. // while the nsIFilePicker dialog is being opened. Snappy.
  345. let serialized = Task.spawn(function* () {
  346. let data = {
  347. fileType: CALLS_LIST_SERIALIZER_IDENTIFIER,
  348. version: CALLS_LIST_SERIALIZER_VERSION,
  349. calls: [],
  350. thumbnails: [],
  351. screenshot: null
  352. };
  353. let functionCalls = snapshotItem.attachment.calls;
  354. let thumbnails = snapshotItem.attachment.thumbnails;
  355. let screenshot = snapshotItem.attachment.screenshot;
  356. // Prepare all the function calls for serialization.
  357. yield DevToolsUtils.yieldingEach(functionCalls, (call, i) => {
  358. let { type, name, file, line, timestamp, argsPreview, callerPreview } = call;
  359. return call.getDetails().then(({ stack }) => {
  360. data.calls[i] = {
  361. type: type,
  362. name: name,
  363. file: file,
  364. line: line,
  365. stack: stack,
  366. timestamp: timestamp,
  367. argsPreview: argsPreview,
  368. callerPreview: callerPreview
  369. };
  370. });
  371. });
  372. // Prepare all the thumbnails for serialization.
  373. yield DevToolsUtils.yieldingEach(thumbnails, (thumbnail, i) => {
  374. let { index, width, height, flipped, pixels } = thumbnail;
  375. data.thumbnails.push({ index, width, height, flipped, pixels });
  376. });
  377. // Prepare the screenshot for serialization.
  378. let { index, width, height, flipped, pixels } = screenshot;
  379. data.screenshot = { index, width, height, flipped, pixels };
  380. let string = JSON.stringify(data);
  381. let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
  382. createInstance(Ci.nsIScriptableUnicodeConverter);
  383. converter.charset = "UTF-8";
  384. return converter.convertToInputStream(string);
  385. });
  386. // Open the nsIFilePicker and wait for the function call actors to finish
  387. // being serialized, in order to save the generated JSON data to disk.
  388. fp.open({ done: result => {
  389. if (result == Ci.nsIFilePicker.returnCancel) {
  390. return;
  391. }
  392. let footer = $(".snapshot-item-footer", snapshotItem.target);
  393. let save = $(".snapshot-item-save", snapshotItem.target);
  394. // Show a throbber and a "Saving…" label if serializing isn't immediate.
  395. setNamedTimeout("call-list-save", CALLS_LIST_SLOW_SAVE_DELAY, () => {
  396. footer.classList.add("devtools-throbber");
  397. save.setAttribute("disabled", "true");
  398. save.setAttribute("value", L10N.getStr("snapshotsList.savingLabel"));
  399. });
  400. serialized.then(inputStream => {
  401. let outputStream = FileUtils.openSafeFileOutputStream(fp.file);
  402. NetUtil.asyncCopy(inputStream, outputStream, status => {
  403. if (!Components.isSuccessCode(status)) {
  404. console.error("Could not save recorded animation frame snapshot file.");
  405. }
  406. clearNamedTimeout("call-list-save");
  407. footer.classList.remove("devtools-throbber");
  408. save.removeAttribute("disabled");
  409. save.setAttribute("value", L10N.getStr("snapshotsList.saveLabel"));
  410. });
  411. });
  412. }});
  413. }
  414. });
  415. function showNotification(toolbox, name, message) {
  416. let notificationBox = toolbox.getNotificationBox();
  417. let notification = notificationBox.getNotificationWithValue(name);
  418. if (!notification) {
  419. notificationBox.appendNotification(message, name, "", notificationBox.PRIORITY_WARNING_HIGH);
  420. }
  421. }