callslist.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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 details about a single recorded animation frame snapshot
  9. * (the calls list, rendering preview, thumbnails filmstrip etc.).
  10. */
  11. var CallsListView = Heritage.extend(WidgetMethods, {
  12. /**
  13. * Initialization function, called when the tool is started.
  14. */
  15. initialize: function () {
  16. this.widget = new SideMenuWidget($("#calls-list"));
  17. this._slider = $("#calls-slider");
  18. this._searchbox = $("#calls-searchbox");
  19. this._filmstrip = $("#snapshot-filmstrip");
  20. this._onSelect = this._onSelect.bind(this);
  21. this._onSlideMouseDown = this._onSlideMouseDown.bind(this);
  22. this._onSlideMouseUp = this._onSlideMouseUp.bind(this);
  23. this._onSlide = this._onSlide.bind(this);
  24. this._onSearch = this._onSearch.bind(this);
  25. this._onScroll = this._onScroll.bind(this);
  26. this._onExpand = this._onExpand.bind(this);
  27. this._onStackFileClick = this._onStackFileClick.bind(this);
  28. this._onThumbnailClick = this._onThumbnailClick.bind(this);
  29. this.widget.addEventListener("select", this._onSelect, false);
  30. this._slider.addEventListener("mousedown", this._onSlideMouseDown, false);
  31. this._slider.addEventListener("mouseup", this._onSlideMouseUp, false);
  32. this._slider.addEventListener("change", this._onSlide, false);
  33. this._searchbox.addEventListener("input", this._onSearch, false);
  34. this._filmstrip.addEventListener("wheel", this._onScroll, false);
  35. },
  36. /**
  37. * Destruction function, called when the tool is closed.
  38. */
  39. destroy: function () {
  40. this.widget.removeEventListener("select", this._onSelect, false);
  41. this._slider.removeEventListener("mousedown", this._onSlideMouseDown, false);
  42. this._slider.removeEventListener("mouseup", this._onSlideMouseUp, false);
  43. this._slider.removeEventListener("change", this._onSlide, false);
  44. this._searchbox.removeEventListener("input", this._onSearch, false);
  45. this._filmstrip.removeEventListener("wheel", this._onScroll, false);
  46. },
  47. /**
  48. * Populates this container with a list of function calls.
  49. *
  50. * @param array functionCalls
  51. * A list of function call actors received from the backend.
  52. */
  53. showCalls: function (functionCalls) {
  54. this.empty();
  55. for (let i = 0, len = functionCalls.length; i < len; i++) {
  56. let call = functionCalls[i];
  57. let view = document.createElement("vbox");
  58. view.className = "call-item-view devtools-monospace";
  59. view.setAttribute("flex", "1");
  60. let contents = document.createElement("hbox");
  61. contents.className = "call-item-contents";
  62. contents.setAttribute("align", "center");
  63. contents.addEventListener("dblclick", this._onExpand);
  64. view.appendChild(contents);
  65. let index = document.createElement("label");
  66. index.className = "plain call-item-index";
  67. index.setAttribute("flex", "1");
  68. index.setAttribute("value", i + 1);
  69. let gutter = document.createElement("hbox");
  70. gutter.className = "call-item-gutter";
  71. gutter.appendChild(index);
  72. contents.appendChild(gutter);
  73. if (call.callerPreview) {
  74. let context = document.createElement("label");
  75. context.className = "plain call-item-context";
  76. context.setAttribute("value", call.callerPreview);
  77. contents.appendChild(context);
  78. let separator = document.createElement("label");
  79. separator.className = "plain call-item-separator";
  80. separator.setAttribute("value", ".");
  81. contents.appendChild(separator);
  82. }
  83. let name = document.createElement("label");
  84. name.className = "plain call-item-name";
  85. name.setAttribute("value", call.name);
  86. contents.appendChild(name);
  87. let argsPreview = document.createElement("label");
  88. argsPreview.className = "plain call-item-args";
  89. argsPreview.setAttribute("crop", "end");
  90. argsPreview.setAttribute("flex", "100");
  91. // Getters and setters are displayed differently from regular methods.
  92. if (call.type == CallWatcherFront.METHOD_FUNCTION) {
  93. argsPreview.setAttribute("value", "(" + call.argsPreview + ")");
  94. } else {
  95. argsPreview.setAttribute("value", " = " + call.argsPreview);
  96. }
  97. contents.appendChild(argsPreview);
  98. let location = document.createElement("label");
  99. location.className = "plain call-item-location";
  100. location.setAttribute("value", getFileName(call.file) + ":" + call.line);
  101. location.setAttribute("crop", "start");
  102. location.setAttribute("flex", "1");
  103. location.addEventListener("mousedown", this._onExpand);
  104. contents.appendChild(location);
  105. // Append a function call item to this container.
  106. this.push([view], {
  107. staged: true,
  108. attachment: {
  109. actor: call
  110. }
  111. });
  112. // Highlight certain calls that are probably more interesting than
  113. // everything else, making it easier to quickly glance over them.
  114. if (CanvasFront.DRAW_CALLS.has(call.name)) {
  115. view.setAttribute("draw-call", "");
  116. }
  117. if (CanvasFront.INTERESTING_CALLS.has(call.name)) {
  118. view.setAttribute("interesting-call", "");
  119. }
  120. }
  121. // Flushes all the prepared function call items into this container.
  122. this.commit();
  123. window.emit(EVENTS.CALL_LIST_POPULATED);
  124. // Resetting the function selection slider's value (shown in this
  125. // container's toolbar) would trigger a selection event, which should be
  126. // ignored in this case.
  127. this._ignoreSliderChanges = true;
  128. this._slider.value = 0;
  129. this._slider.max = functionCalls.length - 1;
  130. this._ignoreSliderChanges = false;
  131. },
  132. /**
  133. * Displays an image in the rendering preview of this container, generated
  134. * for the specified draw call in the recorded animation frame snapshot.
  135. *
  136. * @param array screenshot
  137. * A single "snapshot-image" instance received from the backend.
  138. */
  139. showScreenshot: function (screenshot) {
  140. let { index, width, height, scaling, flipped, pixels } = screenshot;
  141. let screenshotNode = $("#screenshot-image");
  142. screenshotNode.setAttribute("flipped", flipped);
  143. drawBackground("screenshot-rendering", width, height, pixels);
  144. let dimensionsNode = $("#screenshot-dimensions");
  145. let actualWidth = (width / scaling) | 0;
  146. let actualHeight = (height / scaling) | 0;
  147. dimensionsNode.setAttribute("value",
  148. SHARED_L10N.getFormatStr("dimensions", actualWidth, actualHeight));
  149. window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED);
  150. },
  151. /**
  152. * Populates this container's footer with a list of thumbnails, one generated
  153. * for each draw call in the recorded animation frame snapshot.
  154. *
  155. * @param array thumbnails
  156. * An array of "snapshot-image" instances received from the backend.
  157. */
  158. showThumbnails: function (thumbnails) {
  159. while (this._filmstrip.hasChildNodes()) {
  160. this._filmstrip.firstChild.remove();
  161. }
  162. for (let thumbnail of thumbnails) {
  163. this.appendThumbnail(thumbnail);
  164. }
  165. window.emit(EVENTS.THUMBNAILS_DISPLAYED);
  166. },
  167. /**
  168. * Displays an image in the thumbnails list of this container, generated
  169. * for the specified draw call in the recorded animation frame snapshot.
  170. *
  171. * @param array thumbnail
  172. * A single "snapshot-image" instance received from the backend.
  173. */
  174. appendThumbnail: function (thumbnail) {
  175. let { index, width, height, flipped, pixels } = thumbnail;
  176. let thumbnailNode = document.createElementNS(HTML_NS, "canvas");
  177. thumbnailNode.setAttribute("flipped", flipped);
  178. thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_SIZE, width);
  179. thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_SIZE, height);
  180. drawImage(thumbnailNode, width, height, pixels, { centered: true });
  181. thumbnailNode.className = "filmstrip-thumbnail";
  182. thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index);
  183. thumbnailNode.setAttribute("index", index);
  184. this._filmstrip.appendChild(thumbnailNode);
  185. },
  186. /**
  187. * Sets the currently highlighted thumbnail in this container.
  188. * A screenshot will always correlate to a thumbnail in the filmstrip,
  189. * both being identified by the same 'index' of the context function call.
  190. *
  191. * @param number index
  192. * The context function call's index.
  193. */
  194. set highlightedThumbnail(index) {
  195. let currHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + index + "']");
  196. if (currHighlightedThumbnail == null) {
  197. return;
  198. }
  199. let prevIndex = this._highlightedThumbnailIndex;
  200. let prevHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + prevIndex + "']");
  201. if (prevHighlightedThumbnail) {
  202. prevHighlightedThumbnail.removeAttribute("highlighted");
  203. }
  204. currHighlightedThumbnail.setAttribute("highlighted", "");
  205. currHighlightedThumbnail.scrollIntoView();
  206. this._highlightedThumbnailIndex = index;
  207. },
  208. /**
  209. * Gets the currently highlighted thumbnail in this container.
  210. * @return number
  211. */
  212. get highlightedThumbnail() {
  213. return this._highlightedThumbnailIndex;
  214. },
  215. /**
  216. * The select listener for this container.
  217. */
  218. _onSelect: function ({ detail: callItem }) {
  219. if (!callItem) {
  220. return;
  221. }
  222. // Some of the stepping buttons don't make sense specifically while the
  223. // last function call is selected.
  224. if (this.selectedIndex == this.itemCount - 1) {
  225. $("#resume").setAttribute("disabled", "true");
  226. $("#step-over").setAttribute("disabled", "true");
  227. $("#step-out").setAttribute("disabled", "true");
  228. } else {
  229. $("#resume").removeAttribute("disabled");
  230. $("#step-over").removeAttribute("disabled");
  231. $("#step-out").removeAttribute("disabled");
  232. }
  233. // Correlate the currently selected item with the function selection
  234. // slider's value. Avoid triggering a redundant selection event.
  235. this._ignoreSliderChanges = true;
  236. this._slider.value = this.selectedIndex;
  237. this._ignoreSliderChanges = false;
  238. // Can't generate screenshots for function call actors loaded from disk.
  239. // XXX: Bug 984844.
  240. if (callItem.attachment.actor.isLoadedFromDisk) {
  241. return;
  242. }
  243. // To keep continuous selection buttery smooth (for example, while pressing
  244. // the DOWN key or moving the slider), only display the screenshot after
  245. // any kind of user input stops.
  246. setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => {
  247. return !this._isSliding;
  248. }, () => {
  249. let frameSnapshot = SnapshotsListView.selectedItem.attachment.actor;
  250. let functionCall = callItem.attachment.actor;
  251. frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => {
  252. this.showScreenshot(screenshot);
  253. this.highlightedThumbnail = screenshot.index;
  254. }).catch(e => console.error(e));
  255. });
  256. },
  257. /**
  258. * The mousedown listener for the call selection slider.
  259. */
  260. _onSlideMouseDown: function () {
  261. this._isSliding = true;
  262. },
  263. /**
  264. * The mouseup listener for the call selection slider.
  265. */
  266. _onSlideMouseUp: function () {
  267. this._isSliding = false;
  268. },
  269. /**
  270. * The change listener for the call selection slider.
  271. */
  272. _onSlide: function () {
  273. // Avoid performing any operations when programatically changing the value.
  274. if (this._ignoreSliderChanges) {
  275. return;
  276. }
  277. let selectedFunctionCallIndex = this.selectedIndex = this._slider.value;
  278. // While sliding, immediately show the most relevant thumbnail for a
  279. // function call, for a nice diff-like animation effect between draws.
  280. let thumbnails = SnapshotsListView.selectedItem.attachment.thumbnails;
  281. let thumbnail = getThumbnailForCall(thumbnails, selectedFunctionCallIndex);
  282. // Avoid drawing and highlighting if the selected function call has the
  283. // same thumbnail as the last one.
  284. if (thumbnail.index == this.highlightedThumbnail) {
  285. return;
  286. }
  287. // If a thumbnail wasn't found (e.g. the backend avoids creating thumbnails
  288. // when rendering offscreen), simply defer to the first available one.
  289. if (thumbnail.index == -1) {
  290. thumbnail = thumbnails[0];
  291. }
  292. let { index, width, height, flipped, pixels } = thumbnail;
  293. this.highlightedThumbnail = index;
  294. let screenshotNode = $("#screenshot-image");
  295. screenshotNode.setAttribute("flipped", flipped);
  296. drawBackground("screenshot-rendering", width, height, pixels);
  297. },
  298. /**
  299. * The input listener for the calls searchbox.
  300. */
  301. _onSearch: function (e) {
  302. let lowerCaseSearchToken = this._searchbox.value.toLowerCase();
  303. this.filterContents(e => {
  304. let call = e.attachment.actor;
  305. let name = call.name.toLowerCase();
  306. let file = call.file.toLowerCase();
  307. let line = call.line.toString().toLowerCase();
  308. let args = call.argsPreview.toLowerCase();
  309. return name.includes(lowerCaseSearchToken) ||
  310. file.includes(lowerCaseSearchToken) ||
  311. line.includes(lowerCaseSearchToken) ||
  312. args.includes(lowerCaseSearchToken);
  313. });
  314. },
  315. /**
  316. * The wheel listener for the filmstrip that contains all the thumbnails.
  317. */
  318. _onScroll: function (e) {
  319. this._filmstrip.scrollLeft += e.deltaX;
  320. },
  321. /**
  322. * The click/dblclick listener for an item or location url in this container.
  323. * When expanding an item, it's corresponding call stack will be displayed.
  324. */
  325. _onExpand: function (e) {
  326. let callItem = this.getItemForElement(e.target);
  327. let view = $(".call-item-view", callItem.target);
  328. // If the call stack nodes were already created, simply re-show them
  329. // or jump to the corresponding file and line in the Debugger if a
  330. // location link was clicked.
  331. if (view.hasAttribute("call-stack-populated")) {
  332. let isExpanded = view.getAttribute("call-stack-expanded") == "true";
  333. // If clicking on the location, jump to the Debugger.
  334. if (e.target.classList.contains("call-item-location")) {
  335. let { file, line } = callItem.attachment.actor;
  336. this._viewSourceInDebugger(file, line);
  337. return;
  338. }
  339. // Otherwise hide the call stack.
  340. else {
  341. view.setAttribute("call-stack-expanded", !isExpanded);
  342. $(".call-item-stack", view).hidden = isExpanded;
  343. return;
  344. }
  345. }
  346. let list = document.createElement("vbox");
  347. list.className = "call-item-stack";
  348. view.setAttribute("call-stack-populated", "");
  349. view.setAttribute("call-stack-expanded", "true");
  350. view.appendChild(list);
  351. /**
  352. * Creates a function call nodes in this container for a stack.
  353. */
  354. let display = stack => {
  355. for (let i = 1; i < stack.length; i++) {
  356. let call = stack[i];
  357. let contents = document.createElement("hbox");
  358. contents.className = "call-item-stack-fn";
  359. contents.style.paddingInlineStart = (i * STACK_FUNC_INDENTATION) + "px";
  360. let name = document.createElement("label");
  361. name.className = "plain call-item-stack-fn-name";
  362. name.setAttribute("value", "↳ " + call.name + "()");
  363. contents.appendChild(name);
  364. let spacer = document.createElement("spacer");
  365. spacer.setAttribute("flex", "100");
  366. contents.appendChild(spacer);
  367. let location = document.createElement("label");
  368. location.className = "plain call-item-stack-fn-location";
  369. location.setAttribute("value", getFileName(call.file) + ":" + call.line);
  370. location.setAttribute("crop", "start");
  371. location.setAttribute("flex", "1");
  372. location.addEventListener("mousedown", e => this._onStackFileClick(e, call));
  373. contents.appendChild(location);
  374. list.appendChild(contents);
  375. }
  376. window.emit(EVENTS.CALL_STACK_DISPLAYED);
  377. };
  378. // If this animation snapshot is loaded from disk, there are no corresponding
  379. // backend actors available and the data is immediately available.
  380. let functionCall = callItem.attachment.actor;
  381. if (functionCall.isLoadedFromDisk) {
  382. display(functionCall.stack);
  383. }
  384. // ..otherwise we need to request the function call stack from the backend.
  385. else {
  386. callItem.attachment.actor.getDetails().then(fn => display(fn.stack));
  387. }
  388. },
  389. /**
  390. * The click listener for a location link in the call stack.
  391. *
  392. * @param string file
  393. * The url of the source owning the function.
  394. * @param number line
  395. * The line of the respective function.
  396. */
  397. _onStackFileClick: function (e, { file, line }) {
  398. this._viewSourceInDebugger(file, line);
  399. },
  400. /**
  401. * The click listener for a thumbnail in the filmstrip.
  402. *
  403. * @param number index
  404. * The function index in the recorded animation frame snapshot.
  405. */
  406. _onThumbnailClick: function (e, index) {
  407. this.selectedIndex = index;
  408. },
  409. /**
  410. * The click listener for the "resume" button in this container's toolbar.
  411. */
  412. _onResume: function () {
  413. // Jump to the next draw call in the recorded animation frame snapshot.
  414. let drawCall = getNextDrawCall(this.items, this.selectedItem);
  415. if (drawCall) {
  416. this.selectedItem = drawCall;
  417. return;
  418. }
  419. // If there are no more draw calls, just jump to the last context call.
  420. this._onStepOut();
  421. },
  422. /**
  423. * The click listener for the "step over" button in this container's toolbar.
  424. */
  425. _onStepOver: function () {
  426. this.selectedIndex++;
  427. },
  428. /**
  429. * The click listener for the "step in" button in this container's toolbar.
  430. */
  431. _onStepIn: function () {
  432. if (this.selectedIndex == -1) {
  433. this._onResume();
  434. return;
  435. }
  436. let callItem = this.selectedItem;
  437. let { file, line } = callItem.attachment.actor;
  438. this._viewSourceInDebugger(file, line);
  439. },
  440. /**
  441. * The click listener for the "step out" button in this container's toolbar.
  442. */
  443. _onStepOut: function () {
  444. this.selectedIndex = this.itemCount - 1;
  445. },
  446. /**
  447. * Opens the specified file and line in the debugger. Falls back to Firefox's View Source.
  448. */
  449. _viewSourceInDebugger: function (file, line) {
  450. gToolbox.viewSourceInDebugger(file, line).then(success => {
  451. if (success) {
  452. window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
  453. } else {
  454. window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
  455. }
  456. });
  457. }
  458. });