Backed out 13 changesets (bug 1342237, bug 1403944, bug 1403938, bug 1528296) for subsuite gpu failures CLOSED TREE
authorCiure Andrei <aciure@mozilla.com>
Tue, 12 Mar 2019 18:57:01 +0200
changeset 521563 75b49fcc27432b8fbc25ea3c5226445fd25190df
parent 521562 e2316f37b9882820db8e31203058cbecb004de64
child 521564 2b5f40fc403b63dab1d17de84126d809cd333c37
push id10867
push userdvarga@mozilla.com
push dateThu, 14 Mar 2019 15:20:45 +0000
treeherdermozilla-beta@abad13547875 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1342237, 1403944, 1403938, 1528296
milestone67.0a1
backs outeee6b5f77d67da4aeeb6ea34a518e6e1c3de18f0
285c56c2914625c8418613cdbe35436c355ea4dc
47fff0f9b424034516384836dc2cab0cb33556b6
cebff9f6f81144df8470cca5b4d411d30e7828a5
78be67cfabe883eef52f2a2d5b91d93cfb9ff21b
3e055733ee5773587c6714edaaf96ddd22cc2bca
2d321819c273f488fc70ece6ed6f44f2f073d3c1
c8fcdc7c2e0bc402a7a2f67e6656f644ce2c7758
f1003c2742c2ba16973aac9ed5af2114bb4aafea
06a583daf142961e0d82201c275969c56c70d530
f75b89bf0aa4cef1de79752590de7abc78eda800
90579bc6554c7c70bdd114d9d1c4375e2c53cdb9
488d49d434f8d0e7c40faf8971a93e6205a4a098
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 13 changesets (bug 1342237, bug 1403944, bug 1403938, bug 1528296) for subsuite gpu failures CLOSED TREE Backed out changeset eee6b5f77d67 (bug 1528296) Backed out changeset 285c56c29146 (bug 1528296) Backed out changeset 47fff0f9b424 (bug 1528296) Backed out changeset cebff9f6f811 (bug 1528296) Backed out changeset 78be67cfabe8 (bug 1528296) Backed out changeset 3e055733ee57 (bug 1528296) Backed out changeset 2d321819c273 (bug 1528296) Backed out changeset c8fcdc7c2e0b (bug 1403944) Backed out changeset f1003c2742c2 (bug 1403944) Backed out changeset 06a583daf142 (bug 1342237) Backed out changeset f75b89bf0aa4 (bug 1342237) Backed out changeset 90579bc6554c (bug 1403938) Backed out changeset 488d49d434f8 (bug 1403938)
devtools/.eslintrc.js
devtools/client/canvasdebugger/callslist.js
devtools/client/canvasdebugger/canvasdebugger.js
devtools/client/canvasdebugger/index.xul
devtools/client/canvasdebugger/moz.build
devtools/client/canvasdebugger/panel.js
devtools/client/canvasdebugger/snapshotslist.js
devtools/client/canvasdebugger/test/.eslintrc.js
devtools/client/canvasdebugger/test/browser.ini
devtools/client/canvasdebugger/test/browser_canvas-actor-test-01.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-02.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-03.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-04.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-05.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-06.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-07.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-08.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-09.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-10.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-11.js
devtools/client/canvasdebugger/test/browser_canvas-actor-test-12.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-call-highlight.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-call-list.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-call-search.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-clear.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-img-screenshots.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-01.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-02.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-open.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-record-01.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-record-02.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-record-03.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-record-04.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-01.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-02.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-01.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-02.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-stepping.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-01.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-02.js
devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-03.js
devtools/client/canvasdebugger/test/browser_profiling-canvas.js
devtools/client/canvasdebugger/test/browser_profiling-webgl.js
devtools/client/canvasdebugger/test/call-watcher-actor.js
devtools/client/canvasdebugger/test/call-watcher-front.js
devtools/client/canvasdebugger/test/call-watcher-spec.js
devtools/client/canvasdebugger/test/doc_no-canvas.html
devtools/client/canvasdebugger/test/doc_raf-begin.html
devtools/client/canvasdebugger/test/doc_raf-no-canvas.html
devtools/client/canvasdebugger/test/doc_settimeout.html
devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html
devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html
devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html
devtools/client/canvasdebugger/test/doc_simple-canvas.html
devtools/client/canvasdebugger/test/doc_webgl-bindings.html
devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html
devtools/client/canvasdebugger/test/doc_webgl-drawElements.html
devtools/client/canvasdebugger/test/doc_webgl-enum.html
devtools/client/canvasdebugger/test/head.js
devtools/client/debugger/new/src/client/firefox/types.js
devtools/client/definitions.js
devtools/client/framework/test/browser_target_support.js
devtools/client/framework/toolbox-options.js
devtools/client/framework/toolbox.js
devtools/client/jar.mn
devtools/client/locales/en-US/canvasdebugger.dtd
devtools/client/locales/en-US/canvasdebugger.properties
devtools/client/locales/en-US/shadereditor.dtd
devtools/client/locales/en-US/shadereditor.properties
devtools/client/locales/en-US/shared.properties
devtools/client/locales/en-US/startup.properties
devtools/client/locales/en-US/toolbox.properties
devtools/client/locales/en-US/webaudioeditor.dtd
devtools/client/locales/en-US/webaudioeditor.properties
devtools/client/moz.build
devtools/client/netmonitor/test/browser_net_accessibility-01.js
devtools/client/preferences/devtools-client.js
devtools/client/shadereditor/index.xul
devtools/client/shadereditor/moz.build
devtools/client/shadereditor/panel.js
devtools/client/shadereditor/shadereditor.js
devtools/client/shadereditor/test/.eslintrc.js
devtools/client/shadereditor/test/browser.ini
devtools/client/shadereditor/test/browser_se_aaa_run_first_leaktest.js
devtools/client/shadereditor/test/browser_se_bfcache.js
devtools/client/shadereditor/test/browser_se_editors-contents.js
devtools/client/shadereditor/test/browser_se_editors-error-gutter.js
devtools/client/shadereditor/test/browser_se_editors-error-tooltip.js
devtools/client/shadereditor/test/browser_se_editors-lazy-init.js
devtools/client/shadereditor/test/browser_se_first-run.js
devtools/client/shadereditor/test/browser_se_navigation.js
devtools/client/shadereditor/test/browser_se_programs-blackbox-01.js
devtools/client/shadereditor/test/browser_se_programs-blackbox-02.js
devtools/client/shadereditor/test/browser_se_programs-cache.js
devtools/client/shadereditor/test/browser_se_programs-highlight-01.js
devtools/client/shadereditor/test/browser_se_programs-highlight-02.js
devtools/client/shadereditor/test/browser_se_programs-list.js
devtools/client/shadereditor/test/browser_se_shaders-edit-01.js
devtools/client/shadereditor/test/browser_se_shaders-edit-02.js
devtools/client/shadereditor/test/browser_se_shaders-edit-03.js
devtools/client/shadereditor/test/browser_webgl-actor-test-01.js
devtools/client/shadereditor/test/browser_webgl-actor-test-02.js
devtools/client/shadereditor/test/browser_webgl-actor-test-03.js
devtools/client/shadereditor/test/browser_webgl-actor-test-04.js
devtools/client/shadereditor/test/browser_webgl-actor-test-05.js
devtools/client/shadereditor/test/browser_webgl-actor-test-06.js
devtools/client/shadereditor/test/browser_webgl-actor-test-07.js
devtools/client/shadereditor/test/browser_webgl-actor-test-08.js
devtools/client/shadereditor/test/browser_webgl-actor-test-09.js
devtools/client/shadereditor/test/browser_webgl-actor-test-10.js
devtools/client/shadereditor/test/browser_webgl-actor-test-11.js
devtools/client/shadereditor/test/browser_webgl-actor-test-12.js
devtools/client/shadereditor/test/browser_webgl-actor-test-13.js
devtools/client/shadereditor/test/browser_webgl-actor-test-14.js
devtools/client/shadereditor/test/browser_webgl-actor-test-15.js
devtools/client/shadereditor/test/browser_webgl-actor-test-16.js
devtools/client/shadereditor/test/browser_webgl-actor-test-17.js
devtools/client/shadereditor/test/browser_webgl-actor-test-18.js
devtools/client/shadereditor/test/doc_blended-geometry.html
devtools/client/shadereditor/test/doc_multiple-contexts.html
devtools/client/shadereditor/test/doc_overlapping-geometry.html
devtools/client/shadereditor/test/doc_shader-order.html
devtools/client/shadereditor/test/doc_simple-canvas.html
devtools/client/shadereditor/test/head.js
devtools/client/shared/telemetry.js
devtools/client/shared/test/browser.ini
devtools/client/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
devtools/client/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
devtools/client/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
devtools/client/shared/widgets/SideMenuWidget.jsm
devtools/client/shared/widgets/moz.build
devtools/client/shared/widgets/view-helpers.js
devtools/client/shared/widgets/widgets.css
devtools/client/themes/canvasdebugger.css
devtools/client/themes/images/canvasdebugger-step-in.svg
devtools/client/themes/images/canvasdebugger-step-out.svg
devtools/client/themes/images/canvasdebugger-step-over.svg
devtools/client/themes/images/filters.svg
devtools/client/themes/images/power.svg
devtools/client/themes/images/tool-shadereditor.svg
devtools/client/themes/shadereditor.css
devtools/client/themes/toolbars.css
devtools/client/themes/toolbox.css
devtools/client/themes/webaudioeditor.css
devtools/client/themes/widgets.css
devtools/client/webaudioeditor/controller.js
devtools/client/webaudioeditor/includes.js
devtools/client/webaudioeditor/index.xul
devtools/client/webaudioeditor/models.js
devtools/client/webaudioeditor/moz.build
devtools/client/webaudioeditor/panel.js
devtools/client/webaudioeditor/test/.eslintrc.js
devtools/client/webaudioeditor/test/440hz_sine.ogg
devtools/client/webaudioeditor/test/browser.ini
devtools/client/webaudioeditor/test/browser_audionode-actor-add-automation-event.js
devtools/client/webaudioeditor/test/browser_audionode-actor-bypass.js
devtools/client/webaudioeditor/test/browser_audionode-actor-bypassable.js
devtools/client/webaudioeditor/test/browser_audionode-actor-connectnode-disconnect.js
devtools/client/webaudioeditor/test/browser_audionode-actor-connectparam.js
devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-01.js
devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-02.js
devtools/client/webaudioeditor/test/browser_audionode-actor-get-automation-data-03.js
devtools/client/webaudioeditor/test/browser_audionode-actor-get-param-flags.js
devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-01.js
devtools/client/webaudioeditor/test/browser_audionode-actor-get-params-02.js
devtools/client/webaudioeditor/test/browser_audionode-actor-get-set-param.js
devtools/client/webaudioeditor/test/browser_audionode-actor-source.js
devtools/client/webaudioeditor/test/browser_audionode-actor-type.js
devtools/client/webaudioeditor/test/browser_callwatcher-01.js
devtools/client/webaudioeditor/test/browser_callwatcher-02.js
devtools/client/webaudioeditor/test/browser_wa_automation-view-01.js
devtools/client/webaudioeditor/test/browser_wa_automation-view-02.js
devtools/client/webaudioeditor/test/browser_wa_controller-01.js
devtools/client/webaudioeditor/test/browser_wa_destroy-node-01.js
devtools/client/webaudioeditor/test/browser_wa_first-run.js
devtools/client/webaudioeditor/test/browser_wa_graph-click.js
devtools/client/webaudioeditor/test/browser_wa_graph-markers.js
devtools/client/webaudioeditor/test/browser_wa_graph-render-01.js
devtools/client/webaudioeditor/test/browser_wa_graph-render-02.js
devtools/client/webaudioeditor/test/browser_wa_graph-render-03.js
devtools/client/webaudioeditor/test/browser_wa_graph-render-04.js
devtools/client/webaudioeditor/test/browser_wa_graph-render-05.js
devtools/client/webaudioeditor/test/browser_wa_graph-render-06.js
devtools/client/webaudioeditor/test/browser_wa_graph-selected.js
devtools/client/webaudioeditor/test/browser_wa_graph-zoom.js
devtools/client/webaudioeditor/test/browser_wa_inspector-bypass-01.js
devtools/client/webaudioeditor/test/browser_wa_inspector-toggle.js
devtools/client/webaudioeditor/test/browser_wa_inspector-width.js
devtools/client/webaudioeditor/test/browser_wa_inspector.js
devtools/client/webaudioeditor/test/browser_wa_navigate.js
devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-01.js
devtools/client/webaudioeditor/test/browser_wa_properties-view-edit-02.js
devtools/client/webaudioeditor/test/browser_wa_properties-view-media-nodes.js
devtools/client/webaudioeditor/test/browser_wa_properties-view-params-objects.js
devtools/client/webaudioeditor/test/browser_wa_properties-view-params.js
devtools/client/webaudioeditor/test/browser_wa_properties-view.js
devtools/client/webaudioeditor/test/browser_wa_reset-01.js
devtools/client/webaudioeditor/test/browser_wa_reset-02.js
devtools/client/webaudioeditor/test/browser_wa_reset-03.js
devtools/client/webaudioeditor/test/browser_wa_reset-04.js
devtools/client/webaudioeditor/test/browser_webaudio-actor-automation-event.js
devtools/client/webaudioeditor/test/browser_webaudio-actor-connect-param.js
devtools/client/webaudioeditor/test/browser_webaudio-actor-destroy-node.js
devtools/client/webaudioeditor/test/browser_webaudio-actor-simple.js
devtools/client/webaudioeditor/test/doc_automation.html
devtools/client/webaudioeditor/test/doc_buffer-and-array.html
devtools/client/webaudioeditor/test/doc_bug_1112378.html
devtools/client/webaudioeditor/test/doc_bug_1125817.html
devtools/client/webaudioeditor/test/doc_bug_1130901.html
devtools/client/webaudioeditor/test/doc_bug_1141261.html
devtools/client/webaudioeditor/test/doc_complex-context.html
devtools/client/webaudioeditor/test/doc_connect-multi-param.html
devtools/client/webaudioeditor/test/doc_connect-param.html
devtools/client/webaudioeditor/test/doc_destroy-nodes.html
devtools/client/webaudioeditor/test/doc_iframe-context.html
devtools/client/webaudioeditor/test/doc_media-node-creation.html
devtools/client/webaudioeditor/test/doc_simple-context.html
devtools/client/webaudioeditor/test/doc_simple-node-creation.html
devtools/client/webaudioeditor/test/head.js
devtools/client/webaudioeditor/views/automation.js
devtools/client/webaudioeditor/views/context.js
devtools/client/webaudioeditor/views/inspector.js
devtools/client/webaudioeditor/views/properties.js
devtools/client/webaudioeditor/views/utils.js
devtools/server/actors/canvas.js
devtools/server/actors/canvas/moz.build
devtools/server/actors/canvas/primitive.js
devtools/server/actors/moz.build
devtools/server/actors/object/property-iterator.js
devtools/server/actors/utils/actor-registry.js
devtools/server/actors/utils/audionodes.json
devtools/server/actors/utils/automation-timeline.js
devtools/server/actors/utils/call-watcher.js
devtools/server/actors/utils/function-call.js
devtools/server/actors/utils/moz.build
devtools/server/actors/webaudio.js
devtools/server/actors/webgl.js
devtools/shared/fronts/canvas.js
devtools/shared/fronts/function-call.js
devtools/shared/fronts/moz.build
devtools/shared/fronts/webaudio.js
devtools/shared/fronts/webgl.js
devtools/shared/specs/canvas.js
devtools/shared/specs/function-call.js
devtools/shared/specs/index.js
devtools/shared/specs/moz.build
devtools/shared/specs/webaudio.js
devtools/shared/specs/webgl.js
testing/runtimes/mochitest-devtools-chrome-e10s.runtimes.json
testing/runtimes/mochitest-devtools-chrome.runtimes.json
--- a/devtools/.eslintrc.js
+++ b/devtools/.eslintrc.js
@@ -101,16 +101,41 @@ module.exports = {
       "client/framework/**",
       "client/scratchpad/**",
       "client/webide/**",
     ],
     "rules": {
       "strict": "off",
     }
   }, {
+    "files": [
+      // Note: Bug 1403938 may be removing canvasdebugger, check before
+      // doing more work on enabling these rules.
+      "client/canvasdebugger/**",
+      // Note: Bug 1342237 may be removing shadereditor, check before
+      // doing more work on enabling these rules.
+      "client/shadereditor/**",
+      // Note: Bug 1403944 may be removing webaudioeditor, check before
+      // doing more work on enabling these rules.
+      "client/webaudioeditor/**",
+    ],
+    "rules": {
+      "consistent-return": "off",
+      "max-len": "off",
+      "mozilla/no-aArgs": "off",
+      "mozilla/var-only-at-top-level": "off",
+      "no-redeclare": "off",
+      "no-return-assign": "off",
+      "no-shadow": "off",
+      "no-undef": "off",
+      "no-unused-vars": "off",
+      "no-useless-call": "off",
+      "strict": "off",
+    }
+  }, {
     // For all head*.js files, turn off no-unused-vars at a global level
     "files": [
       "**/head*.js",
     ],
     "rules": {
       "no-unused-vars": ["error", {"args": "none", "vars": "local"}],
     }
   }, {
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/callslist.js
@@ -0,0 +1,453 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* import-globals-from canvasdebugger.js */
+/* globals window, document */
+"use strict";
+
+const { METHOD_FUNCTION } = require("devtools/shared/fronts/function-call");
+/**
+ * Functions handling details about a single recorded animation frame snapshot
+ * (the calls list, rendering preview, thumbnails filmstrip etc.).
+ */
+var CallsListView = extend(WidgetMethods, {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize: function() {
+    this.widget = new SideMenuWidget($("#calls-list"));
+    this._searchbox = $("#calls-searchbox");
+    this._filmstrip = $("#snapshot-filmstrip");
+
+    this._onSelect = this._onSelect.bind(this);
+    this._onSearch = this._onSearch.bind(this);
+    this._onScroll = this._onScroll.bind(this);
+    this._onExpand = this._onExpand.bind(this);
+    this._onStackFileClick = this._onStackFileClick.bind(this);
+    this._onThumbnailClick = this._onThumbnailClick.bind(this);
+
+    this.widget.addEventListener("select", this._onSelect);
+    this._searchbox.addEventListener("input", this._onSearch);
+    this._filmstrip.addEventListener("wheel", this._onScroll);
+  },
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy: function() {
+    this.widget.removeEventListener("select", this._onSelect);
+    this._searchbox.removeEventListener("input", this._onSearch);
+    this._filmstrip.removeEventListener("wheel", this._onScroll);
+  },
+
+  /**
+   * Populates this container with a list of function calls.
+   *
+   * @param array functionCalls
+   *        A list of function call actors received from the backend.
+   */
+  showCalls: function(functionCalls) {
+    this.empty();
+
+    for (let i = 0, len = functionCalls.length; i < len; i++) {
+      const call = functionCalls[i];
+
+      const view = document.createElement("vbox");
+      view.className = "call-item-view devtools-monospace";
+      view.setAttribute("flex", "1");
+
+      const contents = document.createElement("hbox");
+      contents.className = "call-item-contents";
+      contents.setAttribute("align", "center");
+      contents.addEventListener("dblclick", this._onExpand);
+      view.appendChild(contents);
+
+      const index = document.createElement("label");
+      index.className = "plain call-item-index";
+      index.setAttribute("flex", "1");
+      index.setAttribute("value", i + 1);
+
+      const gutter = document.createElement("hbox");
+      gutter.className = "call-item-gutter";
+      gutter.appendChild(index);
+      contents.appendChild(gutter);
+
+      if (call.callerPreview) {
+        const context = document.createElement("label");
+        context.className = "plain call-item-context";
+        context.setAttribute("value", call.callerPreview);
+        contents.appendChild(context);
+
+        const separator = document.createElement("label");
+        separator.className = "plain call-item-separator";
+        separator.setAttribute("value", ".");
+        contents.appendChild(separator);
+      }
+
+      const name = document.createElement("label");
+      name.className = "plain call-item-name";
+      name.setAttribute("value", call.name);
+      contents.appendChild(name);
+
+      const argsPreview = document.createElement("label");
+      argsPreview.className = "plain call-item-args";
+      argsPreview.setAttribute("crop", "end");
+      argsPreview.setAttribute("flex", "100");
+      // Getters and setters are displayed differently from regular methods.
+      if (call.type == METHOD_FUNCTION) {
+        argsPreview.setAttribute("value", "(" + call.argsPreview + ")");
+      } else {
+        argsPreview.setAttribute("value", " = " + call.argsPreview);
+      }
+      contents.appendChild(argsPreview);
+
+      const location = document.createElement("label");
+      location.className = "plain call-item-location";
+      location.setAttribute("value", getFileName(call.file) + ":" + call.line);
+      location.setAttribute("crop", "start");
+      location.setAttribute("flex", "1");
+      location.addEventListener("mousedown", this._onExpand);
+      contents.appendChild(location);
+
+      // Append a function call item to this container.
+      this.push([view], {
+        staged: true,
+        attachment: {
+          actor: call,
+        },
+      });
+
+      // Highlight certain calls that are probably more interesting than
+      // everything else, making it easier to quickly glance over them.
+      if (CanvasFront.DRAW_CALLS.has(call.name)) {
+        view.setAttribute("draw-call", "");
+      }
+      if (CanvasFront.INTERESTING_CALLS.has(call.name)) {
+        view.setAttribute("interesting-call", "");
+      }
+    }
+
+    // Flushes all the prepared function call items into this container.
+    this.commit();
+    window.emit(EVENTS.CALL_LIST_POPULATED);
+  },
+
+  /**
+   * Displays an image in the rendering preview of this container, generated
+   * for the specified draw call in the recorded animation frame snapshot.
+   *
+   * @param array screenshot
+   *        A single "snapshot-image" instance received from the backend.
+   */
+  showScreenshot: function(screenshot) {
+    const { index, width, height, scaling, flipped, pixels } = screenshot;
+
+    const screenshotNode = $("#screenshot-image");
+    screenshotNode.setAttribute("flipped", flipped);
+    drawBackground("screenshot-rendering", width, height, pixels);
+
+    const dimensionsNode = $("#screenshot-dimensions");
+    const actualWidth = (width / scaling) | 0;
+    const actualHeight = (height / scaling) | 0;
+    dimensionsNode.setAttribute("value",
+      SHARED_L10N.getFormatStr("dimensions", actualWidth, actualHeight));
+
+    window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED);
+  },
+
+  /**
+   * Populates this container's footer with a list of thumbnails, one generated
+   * for each draw call in the recorded animation frame snapshot.
+   *
+   * @param array thumbnails
+   *        An array of "snapshot-image" instances received from the backend.
+   */
+  showThumbnails: function(thumbnails) {
+    while (this._filmstrip.hasChildNodes()) {
+      this._filmstrip.firstChild.remove();
+    }
+    for (const thumbnail of thumbnails) {
+      this.appendThumbnail(thumbnail);
+    }
+
+    window.emit(EVENTS.THUMBNAILS_DISPLAYED);
+  },
+
+  /**
+   * Displays an image in the thumbnails list of this container, generated
+   * for the specified draw call in the recorded animation frame snapshot.
+   *
+   * @param array thumbnail
+   *        A single "snapshot-image" instance received from the backend.
+   */
+  appendThumbnail: function(thumbnail) {
+    const { index, width, height, flipped, pixels } = thumbnail;
+
+    const thumbnailNode = document.createElementNS(HTML_NS, "canvas");
+    thumbnailNode.setAttribute("flipped", flipped);
+    thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_SIZE, width);
+    thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_SIZE, height);
+    drawImage(thumbnailNode, width, height, pixels, { centered: true });
+
+    thumbnailNode.className = "filmstrip-thumbnail";
+    thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index);
+    thumbnailNode.setAttribute("index", index);
+    this._filmstrip.appendChild(thumbnailNode);
+  },
+
+  /**
+   * Sets the currently highlighted thumbnail in this container.
+   * A screenshot will always correlate to a thumbnail in the filmstrip,
+   * both being identified by the same 'index' of the context function call.
+   *
+   * @param number index
+   *        The context function call's index.
+   */
+  set highlightedThumbnail(index) {
+    const currHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + index + "']");
+    if (currHighlightedThumbnail == null) {
+      return;
+    }
+
+    const prevIndex = this._highlightedThumbnailIndex;
+    const prevHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + prevIndex + "']");
+    if (prevHighlightedThumbnail) {
+      prevHighlightedThumbnail.removeAttribute("highlighted");
+    }
+
+    currHighlightedThumbnail.setAttribute("highlighted", "");
+    currHighlightedThumbnail.scrollIntoView();
+    this._highlightedThumbnailIndex = index;
+  },
+
+  /**
+   * Gets the currently highlighted thumbnail in this container.
+   * @return number
+   */
+  get highlightedThumbnail() {
+    return this._highlightedThumbnailIndex;
+  },
+
+  /**
+   * The select listener for this container.
+   */
+  _onSelect: function({ detail: callItem }) {
+    if (!callItem) {
+      return;
+    }
+
+    // Some of the stepping buttons don't make sense specifically while the
+    // last function call is selected.
+    if (this.selectedIndex == this.itemCount - 1) {
+      $("#resume").setAttribute("disabled", "true");
+      $("#step-over").setAttribute("disabled", "true");
+      $("#step-out").setAttribute("disabled", "true");
+    } else {
+      $("#resume").removeAttribute("disabled");
+      $("#step-over").removeAttribute("disabled");
+      $("#step-out").removeAttribute("disabled");
+    }
+
+    // Can't generate screenshots for function call actors loaded from disk.
+    // XXX: Bug 984844.
+    if (callItem.attachment.actor.isLoadedFromDisk) {
+      return;
+    }
+
+    // To keep continuous selection buttery smooth (for example, while pressing
+    // the DOWN key or moving the slider), only display the screenshot after
+    // any kind of user input stops.
+    setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => {
+      return !this._isSliding;
+    }, () => {
+      const frameSnapshot = SnapshotsListView.selectedItem.attachment.actor;
+      const functionCall = callItem.attachment.actor;
+      frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => {
+        this.showScreenshot(screenshot);
+        this.highlightedThumbnail = screenshot.index;
+      }).catch(console.error);
+    });
+  },
+
+  /**
+   * The input listener for the calls searchbox.
+   */
+  _onSearch: function(e) {
+    const lowerCaseSearchToken = this._searchbox.value.toLowerCase();
+
+    this.filterContents(e => {
+      const call = e.attachment.actor;
+      const name = call.name.toLowerCase();
+      const file = call.file.toLowerCase();
+      const line = call.line.toString().toLowerCase();
+      const args = call.argsPreview.toLowerCase();
+
+      return name.includes(lowerCaseSearchToken) ||
+             file.includes(lowerCaseSearchToken) ||
+             line.includes(lowerCaseSearchToken) ||
+             args.includes(lowerCaseSearchToken);
+    });
+  },
+
+  /**
+   * The wheel listener for the filmstrip that contains all the thumbnails.
+   */
+  _onScroll: function(e) {
+    this._filmstrip.scrollLeft += e.deltaX;
+  },
+
+  /**
+   * The click/dblclick listener for an item or location url in this container.
+   * When expanding an item, it's corresponding call stack will be displayed.
+   */
+  _onExpand: function(e) {
+    const callItem = this.getItemForElement(e.target);
+    const view = $(".call-item-view", callItem.target);
+
+    // If the call stack nodes were already created, simply re-show them
+    // or jump to the corresponding file and line in the Debugger if a
+    // location link was clicked.
+    if (view.hasAttribute("call-stack-populated")) {
+      const isExpanded = view.getAttribute("call-stack-expanded") == "true";
+
+      // If clicking on the location, jump to the Debugger.
+      if (e.target.classList.contains("call-item-location")) {
+        const { file, line } = callItem.attachment.actor;
+        this._viewSourceInDebugger(file, line);
+        return;
+      }
+      // Otherwise hide the call stack.
+
+      view.setAttribute("call-stack-expanded", !isExpanded);
+      $(".call-item-stack", view).hidden = isExpanded;
+      return;
+    }
+
+    const list = document.createElement("vbox");
+    list.className = "call-item-stack";
+    view.setAttribute("call-stack-populated", "");
+    view.setAttribute("call-stack-expanded", "true");
+    view.appendChild(list);
+
+    /**
+     * Creates a function call nodes in this container for a stack.
+     */
+    const display = stack => {
+      for (let i = 1; i < stack.length; i++) {
+        const call = stack[i];
+
+        const contents = document.createElement("hbox");
+        contents.className = "call-item-stack-fn";
+        contents.style.paddingInlineStart = (i * STACK_FUNC_INDENTATION) + "px";
+
+        const name = document.createElement("label");
+        name.className = "plain call-item-stack-fn-name";
+        name.setAttribute("value", "↳ " + call.name + "()");
+        contents.appendChild(name);
+
+        const spacer = document.createElement("spacer");
+        spacer.setAttribute("flex", "100");
+        contents.appendChild(spacer);
+
+        const location = document.createElement("label");
+        location.className = "plain call-item-stack-fn-location";
+        location.setAttribute("value", getFileName(call.file) + ":" + call.line);
+        location.setAttribute("crop", "start");
+        location.setAttribute("flex", "1");
+        location.addEventListener("mousedown", e => this._onStackFileClick(e, call));
+        contents.appendChild(location);
+
+        list.appendChild(contents);
+      }
+
+      window.emit(EVENTS.CALL_STACK_DISPLAYED);
+    };
+
+    // If this animation snapshot is loaded from disk, there are no corresponding
+    // backend actors available and the data is immediately available.
+    const functionCall = callItem.attachment.actor;
+    if (functionCall.isLoadedFromDisk) {
+      display(functionCall.stack);
+    } else {
+      // ..otherwise we need to request the function call stack from the backend.
+      callItem.attachment.actor.getDetails().then(fn => display(fn.stack));
+    }
+  },
+
+  /**
+   * The click listener for a location link in the call stack.
+   *
+   * @param string file
+   *        The url of the source owning the function.
+   * @param number line
+   *        The line of the respective function.
+   */
+  _onStackFileClick: function(e, { file, line }) {
+    this._viewSourceInDebugger(file, line);
+  },
+
+  /**
+   * The click listener for a thumbnail in the filmstrip.
+   *
+   * @param number index
+   *        The function index in the recorded animation frame snapshot.
+   */
+  _onThumbnailClick: function(e, index) {
+    this.selectedIndex = index;
+  },
+
+  /**
+   * The click listener for the "resume" button in this container's toolbar.
+   */
+  _onResume: function() {
+    // Jump to the next draw call in the recorded animation frame snapshot.
+    const drawCall = getNextDrawCall(this.items, this.selectedItem);
+    if (drawCall) {
+      this.selectedItem = drawCall;
+      return;
+    }
+
+    // If there are no more draw calls, just jump to the last context call.
+    this._onStepOut();
+  },
+
+  /**
+   * The click listener for the "step over" button in this container's toolbar.
+   */
+  _onStepOver: function() {
+    this.selectedIndex++;
+  },
+
+  /**
+   * The click listener for the "step in" button in this container's toolbar.
+   */
+  _onStepIn: function() {
+    if (this.selectedIndex == -1) {
+      this._onResume();
+      return;
+    }
+    const callItem = this.selectedItem;
+    const { file, line } = callItem.attachment.actor;
+    this._viewSourceInDebugger(file, line);
+  },
+
+  /**
+   * The click listener for the "step out" button in this container's toolbar.
+   */
+  _onStepOut: function() {
+    this.selectedIndex = this.itemCount - 1;
+  },
+
+  /**
+   * Opens the specified file and line in the debugger. Falls back to Firefox's View Source.
+   */
+  _viewSourceInDebugger: function(file, line) {
+    gToolbox.viewSourceInDebugger(file, line).then(success => {
+      if (success) {
+        window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+      } else {
+        window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+      }
+    });
+  },
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/canvasdebugger.js
@@ -0,0 +1,334 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { CanvasFront } = require("devtools/shared/fronts/canvas");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { extend } = require("devtools/shared/extend");
+const flags = require("devtools/shared/flags");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const { PluralForm } = require("devtools/shared/plural-form");
+const { WidgetMethods, setNamedTimeout, clearNamedTimeout,
+        setConditionalTimeout } = require("devtools/client/shared/widgets/view-helpers");
+
+// Use privileged promise in panel documents to prevent having them to freeze
+// during toolbox destruction. See bug 1402779.
+const Promise = require("Promise");
+
+const CANVAS_ACTOR_RECORDING_ATTEMPT = flags.testing ? 500 : 5000;
+
+ChromeUtils.defineModuleGetter(this, "FileUtils",
+  "resource://gre/modules/FileUtils.jsm");
+
+ChromeUtils.defineModuleGetter(this, "NetUtil",
+  "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function() {
+  return require("devtools/shared/webconsole/network-helper");
+});
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+  // When the UI is reset from tab navigation.
+  UI_RESET: "CanvasDebugger:UIReset",
+
+  // When all the animation frame snapshots are removed by the user.
+  SNAPSHOTS_LIST_CLEARED: "CanvasDebugger:SnapshotsListCleared",
+
+  // When an animation frame snapshot starts/finishes being recorded, and
+  // whether it was completed succesfully or cancelled.
+  SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted",
+  SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished",
+  SNAPSHOT_RECORDING_COMPLETED: "CanvasDebugger:SnapshotRecordingCompleted",
+  SNAPSHOT_RECORDING_CANCELLED: "CanvasDebugger:SnapshotRecordingCancelled",
+
+  // When an animation frame snapshot was selected and all its data displayed.
+  SNAPSHOT_RECORDING_SELECTED: "CanvasDebugger:SnapshotRecordingSelected",
+
+  // After all the function calls associated with an animation frame snapshot
+  // are displayed in the UI.
+  CALL_LIST_POPULATED: "CanvasDebugger:CallListPopulated",
+
+  // After the stack associated with a call in an animation frame snapshot
+  // is displayed in the UI.
+  CALL_STACK_DISPLAYED: "CanvasDebugger:CallStackDisplayed",
+
+  // After a screenshot associated with a call in an animation frame snapshot
+  // is displayed in the UI.
+  CALL_SCREENSHOT_DISPLAYED: "CanvasDebugger:ScreenshotDisplayed",
+
+  // After all the thumbnails associated with an animation frame snapshot
+  // are displayed in the UI.
+  THUMBNAILS_DISPLAYED: "CanvasDebugger:ThumbnailsDisplayed",
+
+  // When a source is shown in the JavaScript Debugger at a specific location.
+  SOURCE_SHOWN_IN_JS_DEBUGGER: "CanvasDebugger:SourceShownInJsDebugger",
+  SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "CanvasDebugger:SourceNotFoundInJsDebugger",
+};
+XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const STRINGS_URI = "devtools/client/locales/canvasdebugger.properties";
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+
+const SNAPSHOT_START_RECORDING_DELAY = 10; // ms
+const SNAPSHOT_DATA_EXPORT_MAX_BLOCK = 1000; // ms
+const SNAPSHOT_DATA_DISPLAY_DELAY = 10; // ms
+const SCREENSHOT_DISPLAY_DELAY = 100; // ms
+const STACK_FUNC_INDENTATION = 14; // px
+
+// This identifier string is simply used to tentatively ascertain whether or not
+// a JSON loaded from disk is actually something generated by this tool or not.
+// It isn't, of course, a definitive verification, but a Good Enough™
+// approximation before continuing the import. Don't localize this.
+const CALLS_LIST_SERIALIZER_IDENTIFIER = "Recorded Animation Frame Snapshot";
+const CALLS_LIST_SERIALIZER_VERSION = 1;
+const CALLS_LIST_SLOW_SAVE_DELAY = 100; // ms
+
+/**
+ * The current target and the Canvas front, set by this tool's host.
+ */
+var gToolbox, gTarget, gFront;
+
+/**
+ * Initializes the canvas debugger controller and views.
+ */
+function startupCanvasDebugger() {
+  return Promise.all([
+    EventsHandler.initialize(),
+    SnapshotsListView.initialize(),
+    CallsListView.initialize(),
+  ]);
+}
+
+/**
+ * Destroys the canvas debugger controller and views.
+ */
+function shutdownCanvasDebugger() {
+  return Promise.all([
+    EventsHandler.destroy(),
+    SnapshotsListView.destroy(),
+    CallsListView.destroy(),
+  ]);
+}
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+var EventsHandler = {
+  /**
+   * Listen for events emitted by the current tab target.
+   */
+  initialize: function() {
+    // Make sure the backend is prepared to handle <canvas> contexts.
+    // Since actors are created lazily on the first request to them, we need to send an
+    // early request to ensure the CallWatcherActor is running and watching for new window
+    // globals.
+    gFront.setup({ reload: false });
+
+    this._onTabWillNavigate = this._onTabWillNavigate.bind(this);
+    gTarget.on("will-navigate", this._onTabWillNavigate);
+  },
+
+  /**
+   * Remove events emitted by the current tab target.
+   */
+  destroy: function() {
+    gTarget.off("will-navigate", this._onTabWillNavigate);
+  },
+
+  /**
+   * Called for each location change in the debugged tab.
+   */
+  _onTabWillNavigate: function() {
+    // Reset UI.
+    SnapshotsListView.empty();
+    CallsListView.empty();
+
+    $("#record-snapshot").removeAttribute("checked");
+    $("#record-snapshot").removeAttribute("disabled");
+    $("#record-snapshot").hidden = false;
+
+    $("#reload-notice").hidden = true;
+    $("#empty-notice").hidden = false;
+    $("#waiting-notice").hidden = true;
+
+    $("#debugging-pane-contents").hidden = true;
+    $("#screenshot-container").hidden = true;
+    $("#snapshot-filmstrip").hidden = true;
+
+    window.emit(EVENTS.UI_RESET);
+  },
+};
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(STRINGS_URI);
+var SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * DOM query helpers.
+ */
+var $ = (selector, target = document) => target.querySelector(selector);
+var $all = (selector, target = document) => target.querySelectorAll(selector);
+
+/**
+ * Gets the fileName part of a string which happens to be an URL.
+ */
+function getFileName(url) {
+  try {
+    const { fileName } = NetworkHelper.nsIURL(url);
+    return fileName || "/";
+  } catch (e) {
+    // This doesn't look like a url, or nsIURL can't handle it.
+    return "";
+  }
+}
+
+/**
+ * Gets an image data object containing a buffer large enough to hold
+ * width * height pixels.
+ *
+ * This method avoids allocating memory and tries to reuse a common buffer
+ * as much as possible.
+ *
+ * @param number w
+ *        The desired image data storage width.
+ * @param number h
+ *        The desired image data storage height.
+ * @return ImageData
+ *         The requested image data buffer.
+ */
+function getImageDataStorage(ctx, w, h) {
+  const storage = getImageDataStorage.cache;
+  if (storage && storage.width == w && storage.height == h) {
+    return storage;
+  }
+  return getImageDataStorage.cache = ctx.createImageData(w, h);
+}
+
+// The cache used in the `getImageDataStorage` function.
+getImageDataStorage.cache = null;
+
+/**
+ * Draws image data into a canvas.
+ *
+ * This method makes absolutely no assumptions about the canvas element
+ * dimensions, or pre-existing rendering. It's a dumb proxy that copies pixels.
+ *
+ * @param HTMLCanvasElement canvas
+ *        The canvas element to put the image data into.
+ * @param number width
+ *        The image data width.
+ * @param number height
+ *        The image data height.
+ * @param array pixels
+ *        An array buffer view of the image data.
+ * @param object options
+ *        Additional options supported by this operation:
+ *          - centered: specifies whether the image data should be centered
+ *                      when copied in the canvas; this is useful when the
+ *                      supplied pixels don't completely cover the canvas.
+ */
+function drawImage(canvas, width, height, pixels, options = {}) {
+  const ctx = canvas.getContext("2d");
+
+  // FrameSnapshot actors return "snapshot-image" type instances with just an
+  // empty pixel array if the source image is completely transparent.
+  if (pixels.length <= 1) {
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    return;
+  }
+
+  const imageData = getImageDataStorage(ctx, width, height);
+  imageData.data.set(pixels);
+
+  if (options.centered) {
+    const left = (canvas.width - width) / 2;
+    const top = (canvas.height - height) / 2;
+    ctx.putImageData(imageData, left, top);
+  } else {
+    ctx.putImageData(imageData, 0, 0);
+  }
+}
+
+/**
+ * Draws image data into a canvas, and sets that as the rendering source for
+ * an element with the specified id as the -moz-element background image.
+ *
+ * @param string id
+ *        The id of the -moz-element background image.
+ * @param number width
+ *        The image data width.
+ * @param number height
+ *        The image data height.
+ * @param array pixels
+ *        An array buffer view of the image data.
+ */
+function drawBackground(id, width, height, pixels) {
+  const canvas = document.createElementNS(HTML_NS, "canvas");
+  canvas.width = width;
+  canvas.height = height;
+
+  drawImage(canvas, width, height, pixels);
+  document.mozSetImageElement(id, canvas);
+
+  // Used in tests. Not emitting an event because this shouldn't be "interesting".
+  if (window._onMozSetImageElement) {
+    window._onMozSetImageElement(pixels);
+  }
+}
+
+/**
+ * Iterates forward to find the next draw call in a snapshot.
+ */
+function getNextDrawCall(calls, call) {
+  for (let i = calls.indexOf(call) + 1, len = calls.length; i < len; i++) {
+    const nextCall = calls[i];
+    const name = nextCall.attachment.actor.name;
+    if (CanvasFront.DRAW_CALLS.has(name)) {
+      return nextCall;
+    }
+  }
+  return null;
+}
+
+/**
+ * Iterates backwards to find the most recent screenshot for a function call
+ * in a snapshot loaded from disk.
+ */
+function getScreenshotFromCallLoadedFromDisk(calls, call) {
+  for (let i = calls.indexOf(call); i >= 0; i--) {
+    const prevCall = calls[i];
+    const screenshot = prevCall.screenshot;
+    if (screenshot) {
+      return screenshot;
+    }
+  }
+  return CanvasFront.INVALID_SNAPSHOT_IMAGE;
+}
+
+/**
+ * Iterates backwards to find the most recent thumbnail for a function call.
+ */
+function getThumbnailForCall(thumbnails, index) {
+  for (let i = thumbnails.length - 1; i >= 0; i--) {
+    const thumbnail = thumbnails[i];
+    if (thumbnail.index <= index) {
+      return thumbnail;
+    }
+  }
+  return CanvasFront.INVALID_SNAPSHOT_IMAGE;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/index.xul
@@ -0,0 +1,131 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/canvasdebugger.css" type="text/css"?>
+<!DOCTYPE window [
+  <!ENTITY % canvasDebuggerDTD SYSTEM "chrome://devtools/locale/canvasdebugger.dtd">
+  %canvasDebuggerDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script src="chrome://devtools/content/shared/theme-switching.js"/>
+  <script type="application/javascript" src="canvasdebugger.js"/>
+  <script type="application/javascript" src="callslist.js"/>
+  <script type="application/javascript" src="snapshotslist.js"/>
+
+  <hbox class="theme-body" flex="1">
+    <vbox id="snapshots-pane">
+      <toolbar id="snapshots-toolbar"
+               class="devtools-toolbar">
+        <hbox id="snapshots-controls">
+          <toolbarbutton id="clear-snapshots"
+                         class="devtools-toolbarbutton devtools-clear-icon"
+                         oncommand="SnapshotsListView._onClearButtonClick()"
+                         tooltiptext="&canvasDebuggerUI.clearSnapshots;"/>
+          <toolbarbutton id="record-snapshot"
+                         class="devtools-toolbarbutton"
+                         oncommand="SnapshotsListView._onRecordButtonClick()"
+                         tooltiptext="&canvasDebuggerUI.recordSnapshot.tooltip;"
+                         hidden="true"/>
+          <toolbarbutton id="import-snapshot"
+                         class="devtools-toolbarbutton"
+                         oncommand="SnapshotsListView._onImportButtonClick()"
+                         tooltiptext="&canvasDebuggerUI.importSnapshot;"/>
+        </hbox>
+      </toolbar>
+      <vbox id="snapshots-list" flex="1"/>
+    </vbox>
+
+    <vbox id="debugging-pane" class="devtools-main-content" flex="1">
+      <hbox id="reload-notice"
+            class="notice-container"
+            align="center"
+            pack="center"
+            flex="1">
+        <button id="reload-notice-button"
+                class="devtools-toolbarbutton"
+                standalone="true"
+                label="&canvasDebuggerUI.reloadNotice1;"
+                oncommand="gFront.setup({ reload: true })"/>
+        <label id="reload-notice-label"
+               class="plain"
+               value="&canvasDebuggerUI.reloadNotice2;"/>
+      </hbox>
+
+      <hbox id="empty-notice"
+            class="notice-container"
+            align="center"
+            pack="center"
+            flex="1"
+            hidden="true">
+        <label value="&canvasDebuggerUI.emptyNotice1;"/>
+        <button id="canvas-debugging-empty-notice-button"
+                class="devtools-toolbarbutton"
+                standalone="true"
+                oncommand="SnapshotsListView._onRecordButtonClick()"/>
+        <label value="&canvasDebuggerUI.emptyNotice2;"/>
+      </hbox>
+
+      <hbox id="waiting-notice"
+            class="notice-container devtools-throbber"
+            align="center"
+            pack="center"
+            flex="1"
+            hidden="true">
+        <label id="requests-menu-waiting-notice-label"
+               class="plain"
+               value="&canvasDebuggerUI.waitingNotice;"/>
+      </hbox>
+
+      <box id="debugging-pane-contents"
+           class="devtools-responsive-container"
+           flex="1"
+           hidden="true">
+        <vbox id="calls-list-container" flex="1">
+          <toolbar id="debugging-toolbar"
+                   class="devtools-toolbar">
+            <hbox id="debugging-controls"
+                  class="devtools-toolbarbutton-group">
+              <toolbarbutton id="resume"
+                             class="devtools-toolbarbutton"
+                             oncommand="CallsListView._onResume()"/>
+              <toolbarbutton id="step-over"
+                             class="devtools-toolbarbutton"
+                             oncommand="CallsListView._onStepOver()"/>
+              <toolbarbutton id="step-in"
+                             class="devtools-toolbarbutton"
+                             oncommand="CallsListView._onStepIn()"/>
+              <toolbarbutton id="step-out"
+                             class="devtools-toolbarbutton"
+                             oncommand="CallsListView._onStepOut()"/>
+            </hbox>
+            <toolbarbutton id="debugging-toolbar-sizer-button"
+                           class="devtools-toolbarbutton"
+                           label=""/>
+            <textbox id="calls-searchbox"
+                     class="devtools-filterinput"
+                     placeholder="&canvasDebuggerUI.searchboxPlaceholder;"
+                     flex="1"/>
+          </toolbar>
+          <vbox id="calls-list" flex="1"/>
+        </vbox>
+
+        <splitter class="devtools-side-splitter"/>
+
+        <vbox id="screenshot-container"
+              hidden="true">
+          <vbox id="screenshot-image" flex="1"/>
+          <label id="screenshot-dimensions" class="plain"/>
+        </vbox>
+      </box>
+
+      <hbox id="snapshot-filmstrip"
+            hidden="true"/>
+    </vbox>
+
+  </hbox>
+</window>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/moz.build
@@ -0,0 +1,13 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'panel.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'Canvas Debugger')
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/panel.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Cc, Ci, Cu, Cr } = require("chrome");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { CanvasFront } = require("devtools/shared/fronts/canvas");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+function CanvasDebuggerPanel(iframeWindow, toolbox) {
+  this.panelWin = iframeWindow;
+  this._toolbox = toolbox;
+  this._destroyer = null;
+
+  EventEmitter.decorate(this);
+}
+
+exports.CanvasDebuggerPanel = CanvasDebuggerPanel;
+
+CanvasDebuggerPanel.prototype = {
+  /**
+   * Open is effectively an asynchronous constructor.
+   *
+   * @return object
+   *         A promise that is resolved when the Canvas Debugger completes opening.
+   */
+  open: async function() {
+    this.panelWin.gToolbox = this._toolbox;
+    this.panelWin.gTarget = this.target;
+    this.panelWin.gFront = await this.target.getFront("canvas");
+
+    await this.panelWin.startupCanvasDebugger();
+
+    this.isReady = true;
+    this.emit("ready");
+    return this;
+  },
+
+  // DevToolPanel API
+
+  get target() {
+    return this._toolbox.target;
+  },
+
+  destroy: function() {
+    // Make sure this panel is not already destroyed.
+    if (this._destroyer) {
+      return this._destroyer;
+    }
+
+    return this._destroyer = this.panelWin.shutdownCanvasDebugger().then(() => {
+      // Destroy front to ensure packet handler is removed from client
+      this.panelWin.gFront.destroy();
+      this.emit("destroyed");
+    });
+  },
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/snapshotslist.js
@@ -0,0 +1,549 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* import-globals-from canvasdebugger.js */
+/* globals window, document */
+"use strict";
+
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+
+/**
+ * Functions handling the recorded animation frame snapshots UI.
+ */
+var SnapshotsListView = extend(WidgetMethods, {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize: function() {
+    this.widget = new SideMenuWidget($("#snapshots-list"), {
+      showArrows: true,
+    });
+
+    this._onSelect = this._onSelect.bind(this);
+    this._onClearButtonClick = this._onClearButtonClick.bind(this);
+    this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
+    this._onImportButtonClick = this._onImportButtonClick.bind(this);
+    this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
+    this._onRecordSuccess = this._onRecordSuccess.bind(this);
+    this._onRecordFailure = this._onRecordFailure.bind(this);
+    this._stopRecordingAnimation = this._stopRecordingAnimation.bind(this);
+
+    window.on(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton);
+    this.emptyText = L10N.getStr("noSnapshotsText");
+    this.widget.addEventListener("select", this._onSelect);
+  },
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy: function() {
+    clearNamedTimeout("canvas-actor-recording");
+    window.off(EVENTS.SNAPSHOT_RECORDING_FINISHED, this._enableRecordButton);
+    this.widget.removeEventListener("select", this._onSelect);
+  },
+
+  /**
+   * Adds a snapshot entry to this container.
+   *
+   * @return object
+   *         The newly inserted item.
+   */
+  addSnapshot: function() {
+    const contents = document.createElement("hbox");
+    contents.className = "snapshot-item";
+
+    const thumbnail = document.createElementNS(HTML_NS, "canvas");
+    thumbnail.className = "snapshot-item-thumbnail";
+    thumbnail.width = CanvasFront.THUMBNAIL_SIZE;
+    thumbnail.height = CanvasFront.THUMBNAIL_SIZE;
+
+    const title = document.createElement("label");
+    title.className = "plain snapshot-item-title";
+    title.setAttribute("value",
+      L10N.getFormatStr("snapshotsList.itemLabel", this.itemCount + 1));
+
+    const calls = document.createElement("label");
+    calls.className = "plain snapshot-item-calls";
+    calls.setAttribute("value",
+      L10N.getStr("snapshotsList.loadingLabel"));
+
+    const save = document.createElement("label");
+    save.className = "plain snapshot-item-save";
+    save.addEventListener("click", this._onSaveButtonClick);
+
+    const spacer = document.createElement("spacer");
+    spacer.setAttribute("flex", "1");
+
+    const footer = document.createElement("hbox");
+    footer.className = "snapshot-item-footer";
+    footer.appendChild(save);
+
+    const details = document.createElement("vbox");
+    details.className = "snapshot-item-details";
+    details.appendChild(title);
+    details.appendChild(calls);
+    details.appendChild(spacer);
+    details.appendChild(footer);
+
+    contents.appendChild(thumbnail);
+    contents.appendChild(details);
+
+    // Append a recorded snapshot item to this container.
+    return this.push([contents], {
+      attachment: {
+        // The snapshot and function call actors, along with the thumbnails
+        // will be available as soon as recording finishes.
+        actor: null,
+        calls: null,
+        thumbnails: null,
+        screenshot: null,
+      },
+    });
+  },
+
+  /**
+   * Removes the last snapshot added, in the event no requestAnimationFrame loop was found.
+   */
+  removeLastSnapshot: function() {
+    this.removeAt(this.itemCount - 1);
+    // If this is the only item, revert back to the empty notice
+    if (this.itemCount === 0) {
+      $("#empty-notice").hidden = false;
+      $("#waiting-notice").hidden = true;
+    }
+  },
+
+  /**
+   * Customizes a shapshot in this container.
+   *
+   * @param Item snapshotItem
+   *        An item inserted via `SnapshotsListView.addSnapshot`.
+   * @param object snapshotActor
+   *        The frame snapshot actor received from the backend.
+   * @param object snapshotOverview
+   *        Additional data about the snapshot received from the backend.
+   */
+  customizeSnapshot: function(snapshotItem, snapshotActor, snapshotOverview) {
+    // Make sure the function call actors are stored on the item,
+    // to be used when populating the CallsListView.
+    snapshotItem.attachment.actor = snapshotActor;
+    const functionCalls = snapshotItem.attachment.calls = snapshotOverview.calls;
+    const thumbnails = snapshotItem.attachment.thumbnails = snapshotOverview.thumbnails;
+    const screenshot = snapshotItem.attachment.screenshot = snapshotOverview.screenshot;
+
+    const lastThumbnail = thumbnails[thumbnails.length - 1];
+    const { width, height, flipped, pixels } = lastThumbnail;
+
+    const thumbnailNode = $(".snapshot-item-thumbnail", snapshotItem.target);
+    thumbnailNode.setAttribute("flipped", flipped);
+    drawImage(thumbnailNode, width, height, pixels, { centered: true });
+
+    const callsNode = $(".snapshot-item-calls", snapshotItem.target);
+    const drawCalls = functionCalls.filter(e => CanvasFront.DRAW_CALLS.has(e.name));
+
+    const drawCallsStr = PluralForm.get(drawCalls.length,
+      L10N.getStr("snapshotsList.drawCallsLabel"));
+    const funcCallsStr = PluralForm.get(functionCalls.length,
+      L10N.getStr("snapshotsList.functionCallsLabel"));
+
+    callsNode.setAttribute("value",
+      drawCallsStr.replace("#1", drawCalls.length) + ", " +
+      funcCallsStr.replace("#1", functionCalls.length));
+
+    const saveNode = $(".snapshot-item-save", snapshotItem.target);
+    saveNode.setAttribute("disabled", !!snapshotItem.isLoadedFromDisk);
+    saveNode.setAttribute("value", snapshotItem.isLoadedFromDisk
+      ? L10N.getStr("snapshotsList.loadedLabel")
+      : L10N.getStr("snapshotsList.saveLabel"));
+
+    // Make sure there's always a selected item available.
+    if (!this.selectedItem) {
+      this.selectedIndex = 0;
+    }
+  },
+
+  /**
+   * The select listener for this container.
+   */
+  _onSelect: function({ detail: snapshotItem }) {
+    // Check to ensure the attachment has an actor, like
+    // an in-progress recording.
+    if (!snapshotItem || !snapshotItem.attachment.actor) {
+      return;
+    }
+    const { calls, thumbnails, screenshot } = snapshotItem.attachment;
+
+    $("#reload-notice").hidden = true;
+    $("#empty-notice").hidden = true;
+    $("#waiting-notice").hidden = false;
+
+    $("#debugging-pane-contents").hidden = true;
+    $("#screenshot-container").hidden = true;
+    $("#snapshot-filmstrip").hidden = true;
+
+    (async function() {
+      // Wait for a few milliseconds between presenting the function calls,
+      // screenshot and thumbnails, to allow each component being
+      // sequentially drawn. This gives the illusion of snappiness.
+
+      await DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
+      CallsListView.showCalls(calls);
+      $("#debugging-pane-contents").hidden = false;
+      $("#waiting-notice").hidden = true;
+
+      await DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
+      CallsListView.showThumbnails(thumbnails);
+      $("#snapshot-filmstrip").hidden = false;
+
+      await DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY);
+      CallsListView.showScreenshot(screenshot);
+      $("#screenshot-container").hidden = false;
+
+      window.emit(EVENTS.SNAPSHOT_RECORDING_SELECTED);
+    })();
+  },
+
+  /**
+   * The click listener for the "clear" button in this container.
+   */
+  _onClearButtonClick: function() {
+    (async function() {
+      SnapshotsListView.empty();
+      CallsListView.empty();
+
+      $("#reload-notice").hidden = true;
+      $("#empty-notice").hidden = true;
+      $("#waiting-notice").hidden = true;
+
+      if (await gFront.isInitialized()) {
+        $("#empty-notice").hidden = false;
+      } else {
+        $("#reload-notice").hidden = false;
+      }
+
+      $("#debugging-pane-contents").hidden = true;
+      $("#screenshot-container").hidden = true;
+      $("#snapshot-filmstrip").hidden = true;
+
+      window.emit(EVENTS.SNAPSHOTS_LIST_CLEARED);
+    })();
+  },
+
+  /**
+   * The click listener for the "record" button in this container.
+   */
+  _onRecordButtonClick: function() {
+    this._disableRecordButton();
+
+    if (this._recording) {
+      this._stopRecordingAnimation();
+      return;
+    }
+
+    // Insert a "dummy" snapshot item in the view, to hint that recording
+    // has now started. However, wait for a few milliseconds before actually
+    // starting the recording, since that might block rendering and prevent
+    // the dummy snapshot item from being drawn.
+    this.addSnapshot();
+
+    // If this is the first item, immediately show the "Loading…" notice.
+    if (this.itemCount == 1) {
+      $("#empty-notice").hidden = true;
+      $("#waiting-notice").hidden = false;
+    }
+
+    this._recordAnimation();
+  },
+
+  /**
+   * Makes the record button able to be clicked again.
+   */
+  _enableRecordButton: function() {
+    $("#record-snapshot").removeAttribute("disabled");
+  },
+
+  /**
+   * Makes the record button unable to be clicked.
+   */
+  _disableRecordButton: function() {
+    $("#record-snapshot").setAttribute("disabled", true);
+  },
+
+  /**
+   * Begins recording an animation.
+   */
+  async _recordAnimation() {
+    if (this._recording) {
+      return;
+    }
+    this._recording = true;
+    $("#record-snapshot").setAttribute("checked", "true");
+
+    setNamedTimeout("canvas-actor-recording", CANVAS_ACTOR_RECORDING_ATTEMPT, this._stopRecordingAnimation);
+
+    await DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY);
+    window.emit(EVENTS.SNAPSHOT_RECORDING_STARTED);
+
+    gFront.recordAnimationFrame().then(snapshot => {
+      if (snapshot) {
+        this._onRecordSuccess(snapshot);
+      } else {
+        this._onRecordFailure();
+      }
+    });
+
+    // Wait another delay before reenabling the button to stop the recording
+    // if a recording is not found.
+    await DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY);
+    this._enableRecordButton();
+  },
+
+  /**
+   * Stops recording animation. Called when a click on the stopwatch occurs during a recording,
+   * or if a recording times out.
+   */
+  async _stopRecordingAnimation() {
+    clearNamedTimeout("canvas-actor-recording");
+    const actorCanStop = await gTarget.actorHasMethod("canvas", "stopRecordingAnimationFrame");
+
+    if (actorCanStop) {
+      await gFront.stopRecordingAnimationFrame();
+    } else {
+      // If actor does not have the method to stop recording (Fx39+),
+      // manually call the record failure method. This will call a connection failure
+      // on disconnect as a result of `gFront.recordAnimationFrame()` never resolving,
+      // but this is better than it hanging when there is no requestAnimationFrame anyway.
+      this._onRecordFailure();
+    }
+
+    this._recording = false;
+    $("#record-snapshot").removeAttribute("checked");
+    this._enableRecordButton();
+  },
+
+  /**
+   * Resolves from the front's recordAnimationFrame to setup the interface with the screenshots.
+   */
+  async _onRecordSuccess(snapshotActor) {
+    // Clear bail-out case if frame found in CANVAS_ACTOR_RECORDING_ATTEMPT milliseconds
+    clearNamedTimeout("canvas-actor-recording");
+    const snapshotItem = this.getItemAtIndex(this.itemCount - 1);
+    const snapshotOverview = await snapshotActor.getOverview();
+    this.customizeSnapshot(snapshotItem, snapshotActor, snapshotOverview);
+
+    this._recording = false;
+    $("#record-snapshot").removeAttribute("checked");
+
+    window.emit(EVENTS.SNAPSHOT_RECORDING_COMPLETED);
+    window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  },
+
+  /**
+   * Called as a reject from the front's recordAnimationFrame.
+   */
+  _onRecordFailure: function() {
+    clearNamedTimeout("canvas-actor-recording");
+    showNotification(gToolbox, "canvas-debugger-timeout", L10N.getStr("recordingTimeoutFailure"));
+    window.emit(EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+    window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED);
+    this.removeLastSnapshot();
+  },
+
+  /**
+   * The click listener for the "import" button in this container.
+   */
+  _onImportButtonClick: function() {
+    const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+    fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen);
+    fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json");
+    fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*");
+
+    fp.open(rv => {
+      if (rv != Ci.nsIFilePicker.returnOK) {
+        return;
+      }
+
+      const channel = NetUtil.newChannel({
+        uri: NetUtil.newURI(fp.file), loadUsingSystemPrincipal: true});
+      channel.contentType = "text/plain";
+
+      NetUtil.asyncFetch(channel, (inputStream, status) => {
+        if (!Components.isSuccessCode(status)) {
+          console.error("Could not import recorded animation frame snapshot file.");
+          return;
+        }
+        var data;
+        try {
+          const string = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+          data = JSON.parse(string);
+        } catch (e) {
+          console.error("Could not read animation frame snapshot file.");
+          return;
+        }
+        if (data.fileType != CALLS_LIST_SERIALIZER_IDENTIFIER) {
+          console.error("Unrecognized animation frame snapshot file.");
+          return;
+        }
+
+        // Add a `isLoadedFromDisk` flag on everything to avoid sending invalid
+        // requests to the backend, since we're not dealing with actors anymore.
+        const snapshotItem = this.addSnapshot();
+        snapshotItem.isLoadedFromDisk = true;
+        data.calls.forEach(e => e.isLoadedFromDisk = true);
+
+        this.customizeSnapshot(snapshotItem, data.calls, data);
+      });
+    });
+  },
+
+  /**
+   * The click listener for the "save" button of each item in this container.
+   */
+  _onSaveButtonClick: function(e) {
+    const snapshotItem = this.getItemForElement(e.target);
+
+    const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+    fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave);
+    fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json");
+    fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*");
+    fp.defaultString = "snapshot.json";
+
+    // Start serializing all the function call actors for the specified snapshot,
+    // while the nsIFilePicker dialog is being opened. Snappy.
+    const serialized = (async function() {
+      const data = {
+        fileType: CALLS_LIST_SERIALIZER_IDENTIFIER,
+        version: CALLS_LIST_SERIALIZER_VERSION,
+        calls: [],
+        thumbnails: [],
+        screenshot: null,
+      };
+      const functionCalls = snapshotItem.attachment.calls;
+      const thumbnails = snapshotItem.attachment.thumbnails;
+      const screenshot = snapshotItem.attachment.screenshot;
+
+      // Prepare all the function calls for serialization.
+      await yieldingEach(functionCalls, (call, i) => {
+        const { type, name, file, line, timestamp, argsPreview, callerPreview } = call;
+        return call.getDetails().then(({ stack }) => {
+          data.calls[i] = {
+            type: type,
+            name: name,
+            file: file,
+            line: line,
+            stack: stack,
+            timestamp: timestamp,
+            argsPreview: argsPreview,
+            callerPreview: callerPreview,
+          };
+        });
+      });
+
+      // Prepare all the thumbnails for serialization.
+      await yieldingEach(thumbnails, (thumbnail, i) => {
+        const { index, width, height, flipped, pixels } = thumbnail;
+        data.thumbnails.push({ index, width, height, flipped, pixels });
+      });
+
+      // Prepare the screenshot for serialization.
+      const { index, width, height, flipped, pixels } = screenshot;
+      data.screenshot = { index, width, height, flipped, pixels };
+
+      const string = JSON.stringify(data);
+      const converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+        .createInstance(Ci.nsIScriptableUnicodeConverter);
+
+      converter.charset = "UTF-8";
+      return converter.convertToInputStream(string);
+    })();
+
+    // Open the nsIFilePicker and wait for the function call actors to finish
+    // being serialized, in order to save the generated JSON data to disk.
+    fp.open({ done: result => {
+      if (result == Ci.nsIFilePicker.returnCancel) {
+        return;
+      }
+      const footer = $(".snapshot-item-footer", snapshotItem.target);
+      const save = $(".snapshot-item-save", snapshotItem.target);
+
+      // Show a throbber and a "Saving…" label if serializing isn't immediate.
+      setNamedTimeout("call-list-save", CALLS_LIST_SLOW_SAVE_DELAY, () => {
+        footer.classList.add("devtools-throbber");
+        save.setAttribute("disabled", "true");
+        save.setAttribute("value", L10N.getStr("snapshotsList.savingLabel"));
+      });
+
+      serialized.then(inputStream => {
+        const outputStream = FileUtils.openSafeFileOutputStream(fp.file);
+
+        NetUtil.asyncCopy(inputStream, outputStream, status => {
+          if (!Components.isSuccessCode(status)) {
+            console.error("Could not save recorded animation frame snapshot file.");
+          }
+          clearNamedTimeout("call-list-save");
+          footer.classList.remove("devtools-throbber");
+          save.removeAttribute("disabled");
+          save.setAttribute("value", L10N.getStr("snapshotsList.saveLabel"));
+        });
+      });
+    }});
+  },
+});
+
+function showNotification(toolbox, name, message) {
+  const notificationBox = toolbox.getNotificationBox();
+  const notification = notificationBox.getNotificationWithValue(name);
+  if (!notification) {
+    notificationBox.appendNotification(message, name, "", notificationBox.PRIORITY_WARNING_HIGH);
+  }
+}
+
+/**
+ * Like Array.prototype.forEach, but doesn't cause jankiness when iterating over
+ * very large arrays by yielding to the browser and continuing execution on the
+ * next tick.
+ *
+ * @param Array array
+ *        The array being iterated over.
+ * @param Function fn
+ *        The function called on each item in the array. If a promise is
+ *        returned by this function, iterating over the array will be paused
+ *        until the respective promise is resolved.
+ * @returns Promise
+ *          A promise that is resolved once the whole array has been iterated
+ *          over, and all promises returned by the fn callback are resolved.
+ */
+function yieldingEach(array, fn) {
+  const deferred = defer();
+
+  let i = 0;
+  const len = array.length;
+  const outstanding = [deferred.promise];
+
+  (function loop() {
+    const start = Date.now();
+
+    while (i < len) {
+      // Don't block the main thread for longer than 16 ms at a time. To
+      // maintain 60fps, you have to render every frame in at least 16ms; we
+      // aren't including time spent in non-JS here, but this is Good
+      // Enough(tm).
+      if (Date.now() - start > 16) {
+        DevToolsUtils.executeSoon(loop);
+        return;
+      }
+
+      try {
+        outstanding.push(fn(array[i], i++));
+      } catch (e) {
+        deferred.reject(e);
+        return;
+      }
+    }
+
+    deferred.resolve();
+  }());
+
+  return promise.all(outstanding);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+  // Extend from the shared list of defined globals for mochitests.
+  "extends": "../../../.eslintrc.mochitests.js"
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser.ini
@@ -0,0 +1,69 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+  call-watcher-actor.js
+  call-watcher-front.js
+  call-watcher-spec.js
+  doc_raf-begin.html
+  doc_settimeout.html
+  doc_no-canvas.html
+  doc_raf-no-canvas.html
+  doc_simple-canvas.html
+  doc_simple-canvas-bitmasks.html
+  doc_simple-canvas-deep-stack.html
+  doc_simple-canvas-transparent.html
+  doc_webgl-bindings.html
+  doc_webgl-enum.html
+  doc_webgl-drawArrays.html
+  doc_webgl-drawElements.html
+  head.js
+  !/devtools/client/shared/test/frame-script-utils.js
+  !/devtools/client/shared/test/shared-head.js
+  !/devtools/client/debugger/new/test/mochitest/helpers/context.js
+  !/devtools/client/shared/test/telemetry-test-helpers.js
+
+[browser_canvas-actor-test-01.js]
+[browser_canvas-actor-test-02.js]
+[browser_canvas-actor-test-03.js]
+[browser_canvas-actor-test-04.js]
+[browser_canvas-actor-test-05.js]
+[browser_canvas-actor-test-06.js]
+[browser_canvas-actor-test-07.js]
+[browser_canvas-actor-test-08.js]
+[browser_canvas-actor-test-09.js]
+subsuite = gpu
+skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
+[browser_canvas-actor-test-10.js]
+subsuite = gpu
+skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
+[browser_canvas-actor-test-11.js]
+subsuite = gpu
+skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
+[browser_canvas-actor-test-12.js]
+[browser_canvas-frontend-call-highlight.js]
+[browser_canvas-frontend-call-list.js]
+[browser_canvas-frontend-call-search.js]
+[browser_canvas-frontend-call-stack-01.js]
+[browser_canvas-frontend-call-stack-02.js]
+[browser_canvas-frontend-call-stack-03.js]
+[browser_canvas-frontend-clear.js]
+[browser_canvas-frontend-img-screenshots.js]
+[browser_canvas-frontend-img-thumbnails-01.js]
+[browser_canvas-frontend-img-thumbnails-02.js]
+[browser_canvas-frontend-open.js]
+[browser_canvas-frontend-record-01.js]
+[browser_canvas-frontend-record-02.js]
+[browser_canvas-frontend-record-03.js]
+[browser_canvas-frontend-record-04.js]
+[browser_canvas-frontend-reload-01.js]
+[browser_canvas-frontend-reload-02.js]
+[browser_canvas-frontend-snapshot-select-01.js]
+[browser_canvas-frontend-snapshot-select-02.js]
+[browser_canvas-frontend-stepping.js]
+[browser_canvas-frontend-stop-01.js]
+[browser_canvas-frontend-stop-02.js]
+[browser_canvas-frontend-stop-03.js]
+[browser_profiling-canvas.js]
+[browser_profiling-webgl.js]
+subsuite = gpu
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-01.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the canvas debugger leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCallWatcherBackend(SIMPLE_CANVAS_URL);
+
+  ok(target, "Should have a target available.");
+  ok(front, "Should have a protocol front available.");
+
+  await removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-02.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if functions calls are recorded and stored for a canvas context,
+ * and that their stack is successfully retrieved.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCallWatcherBackend(SIMPLE_CANVAS_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({
+    tracedGlobals: ["CanvasRenderingContext2D", "WebGLRenderingContext"],
+    startRecording: true,
+    performReload: true,
+    storeCalls: true,
+  });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  // Allow the content to execute some functions.
+  await waitForTick();
+
+  const functionCalls = await front.pauseRecording();
+  ok(functionCalls,
+    "An array of function call actors was sent after reloading.");
+  ok(functionCalls.length > 0,
+    "There's at least one function call actor available.");
+
+  is(functionCalls[0].type, METHOD_FUNCTION,
+    "The called function is correctly identified as a method.");
+  is(functionCalls[0].name, "clearRect",
+    "The called function's name is correct.");
+  is(functionCalls[0].file, SIMPLE_CANVAS_URL,
+    "The called function's file is correct.");
+  is(functionCalls[0].line, 25,
+    "The called function's line is correct.");
+
+  is(functionCalls[0].callerPreview, "Object",
+    "The called function's caller preview is correct.");
+  is(functionCalls[0].argsPreview, "0, 0, 128, 128",
+    "The called function's args preview is correct.");
+
+  const details = await functionCalls[1].getDetails();
+  ok(details,
+    "The first called function has some details available.");
+
+  is(details.stack.length, 3,
+    "The called function's stack depth is correct.");
+
+  is(details.stack[0].name, "fillStyle",
+    "The called function's stack is correct (1.1).");
+  is(details.stack[0].file, SIMPLE_CANVAS_URL,
+    "The called function's stack is correct (1.2).");
+  is(details.stack[0].line, 20,
+    "The called function's stack is correct (1.3).");
+
+  is(details.stack[1].name, "drawRect",
+    "The called function's stack is correct (2.1).");
+  is(details.stack[1].file, SIMPLE_CANVAS_URL,
+    "The called function's stack is correct (2.2).");
+  is(details.stack[1].line, 26,
+    "The called function's stack is correct (2.3).");
+
+  is(details.stack[2].name, "drawScene",
+    "The called function's stack is correct (3.1).");
+  is(details.stack[2].file, SIMPLE_CANVAS_URL,
+    "The called function's stack is correct (3.2).");
+  is(details.stack[2].line, 33,
+    "The called function's stack is correct (3.3).");
+
+  await removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-03.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if functions inside a single animation frame are recorded and stored
+ * for a canvas context.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  ok(snapshotActor,
+    "A snapshot actor was sent after recording.");
+
+  const animationOverview = await snapshotActor.getOverview();
+  ok(snapshotActor,
+    "An animation overview could be retrieved after recording.");
+
+  const functionCalls = animationOverview.calls;
+  ok(functionCalls,
+    "An array of function call actors was sent after recording.");
+  is(functionCalls.length, 8,
+    "The number of function call actors is correct.");
+
+  is(functionCalls[0].type, METHOD_FUNCTION,
+    "The first called function is correctly identified as a method.");
+  is(functionCalls[0].name, "clearRect",
+    "The first called function's name is correct.");
+  is(functionCalls[0].file, SIMPLE_CANVAS_URL,
+    "The first called function's file is correct.");
+  is(functionCalls[0].line, 25,
+    "The first called function's line is correct.");
+  is(functionCalls[0].argsPreview, "0, 0, 128, 128",
+    "The first called function's args preview is correct.");
+  is(functionCalls[0].callerPreview, "Object",
+    "The first called function's caller preview is correct.");
+
+  is(functionCalls[6].type, METHOD_FUNCTION,
+    "The penultimate called function is correctly identified as a method.");
+  is(functionCalls[6].name, "fillRect",
+    "The penultimate called function's name is correct.");
+  is(functionCalls[6].file, SIMPLE_CANVAS_URL,
+    "The penultimate called function's file is correct.");
+  is(functionCalls[6].line, 21,
+    "The penultimate called function's line is correct.");
+  is(functionCalls[6].argsPreview, "10, 10, 55, 50",
+    "The penultimate called function's args preview is correct.");
+  is(functionCalls[6].callerPreview, "Object",
+    "The penultimate called function's caller preview is correct.");
+
+  is(functionCalls[7].type, METHOD_FUNCTION,
+    "The last called function is correctly identified as a method.");
+  is(functionCalls[7].name, "requestAnimationFrame",
+    "The last called function's name is correct.");
+  is(functionCalls[7].file, SIMPLE_CANVAS_URL,
+    "The last called function's file is correct.");
+  is(functionCalls[7].line, 30,
+    "The last called function's line is correct.");
+  ok(functionCalls[7].argsPreview.includes("Function"),
+    "The last called function's args preview is correct.");
+  is(functionCalls[7].callerPreview, "Object",
+    "The last called function's caller preview is correct.");
+
+  await removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-04.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if draw calls inside a single animation frame generate and retrieve
+ * the correct thumbnails.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  ok(snapshotActor,
+    "A snapshot actor was sent after recording.");
+
+  const animationOverview = await snapshotActor.getOverview();
+  ok(animationOverview,
+    "An animation overview could be retrieved after recording.");
+
+  const thumbnails = animationOverview.thumbnails;
+  ok(thumbnails,
+    "An array of thumbnails was sent after recording.");
+  is(thumbnails.length, 4,
+    "The number of thumbnails is correct.");
+
+  is(thumbnails[0].index, 0,
+    "The first thumbnail's index is correct.");
+  is(thumbnails[0].width, 50,
+    "The first thumbnail's width is correct.");
+  is(thumbnails[0].height, 50,
+    "The first thumbnail's height is correct.");
+  is(thumbnails[0].flipped, false,
+    "The first thumbnail's flipped flag is correct.");
+  is([].find.call(Uint32(thumbnails[0].pixels), e => e > 0), undefined,
+    "The first thumbnail's pixels seem to be completely transparent.");
+
+  is(thumbnails[1].index, 2,
+    "The second thumbnail's index is correct.");
+  is(thumbnails[1].width, 50,
+    "The second thumbnail's width is correct.");
+  is(thumbnails[1].height, 50,
+    "The second thumbnail's height is correct.");
+  is(thumbnails[1].flipped, false,
+    "The second thumbnail's flipped flag is correct.");
+  is([].find.call(Uint32(thumbnails[1].pixels), e => e > 0), 4290822336,
+    "The second thumbnail's pixels seem to not be completely transparent.");
+
+  is(thumbnails[2].index, 4,
+    "The third thumbnail's index is correct.");
+  is(thumbnails[2].width, 50,
+    "The third thumbnail's width is correct.");
+  is(thumbnails[2].height, 50,
+    "The third thumbnail's height is correct.");
+  is(thumbnails[2].flipped, false,
+    "The third thumbnail's flipped flag is correct.");
+  is([].find.call(Uint32(thumbnails[2].pixels), e => e > 0), 4290822336,
+    "The third thumbnail's pixels seem to not be completely transparent.");
+
+  is(thumbnails[3].index, 6,
+    "The fourth thumbnail's index is correct.");
+  is(thumbnails[3].width, 50,
+    "The fourth thumbnail's width is correct.");
+  is(thumbnails[3].height, 50,
+    "The fourth thumbnail's height is correct.");
+  is(thumbnails[3].flipped, false,
+    "The fourth thumbnail's flipped flag is correct.");
+  is([].find.call(Uint32(thumbnails[3].pixels), e => e > 0), 4290822336,
+    "The fourth thumbnail's pixels seem to not be completely transparent.");
+
+  await removeTab(target.tab);
+  finish();
+}
+
+function Uint32(src) {
+  const charView = new Uint8Array(src);
+  return new Uint32Array(charView.buffer);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-05.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if draw calls inside a single animation frame generate and retrieve
+ * the correct "end result" screenshot.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  ok(snapshotActor,
+    "A snapshot actor was sent after recording.");
+
+  const animationOverview = await snapshotActor.getOverview();
+  ok(snapshotActor,
+    "An animation overview could be retrieved after recording.");
+
+  const screenshot = animationOverview.screenshot;
+  ok(screenshot,
+    "A screenshot was sent after recording.");
+
+  is(screenshot.index, 6,
+    "The screenshot's index is correct.");
+  is(screenshot.width, 128,
+    "The screenshot's width is correct.");
+  is(screenshot.height, 128,
+    "The screenshot's height is correct.");
+  is(screenshot.flipped, false,
+    "The screenshot's flipped flag is correct.");
+  is([].find.call(Uint32(screenshot.pixels), e => e > 0), 4290822336,
+    "The screenshot's pixels seem to not be completely transparent.");
+
+  await removeTab(target.tab);
+  finish();
+}
+
+function Uint32(src) {
+  const charView = new Uint8Array(src);
+  return new Uint32Array(charView.buffer);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-06.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if screenshots for arbitrary draw calls are generated properly.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_TRANSPARENT_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  const animationOverview = await snapshotActor.getOverview();
+
+  const functionCalls = animationOverview.calls;
+  ok(functionCalls,
+    "An array of function call actors was sent after recording.");
+  is(functionCalls.length, 8,
+    "The number of function call actors is correct.");
+
+  is(functionCalls[0].name, "clearRect",
+    "The first called function's name is correct.");
+  is(functionCalls[2].name, "fillRect",
+    "The second called function's name is correct.");
+  is(functionCalls[4].name, "fillRect",
+    "The third called function's name is correct.");
+  is(functionCalls[6].name, "fillRect",
+    "The fourth called function's name is correct.");
+
+  const firstDrawCallScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[0]);
+  const secondDrawCallScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[2]);
+  const thirdDrawCallScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[4]);
+  const fourthDrawCallScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[6]);
+
+  ok(firstDrawCallScreenshot,
+    "The first draw call has a screenshot attached.");
+  is(firstDrawCallScreenshot.index, 0,
+    "The first draw call has the correct screenshot index.");
+  is(firstDrawCallScreenshot.width, 128,
+    "The first draw call has the correct screenshot width.");
+  is(firstDrawCallScreenshot.height, 128,
+    "The first draw call has the correct screenshot height.");
+  is([].find.call(Uint32(firstDrawCallScreenshot.pixels), e => e > 0), undefined,
+    "The first draw call's screenshot's pixels seems to be completely transparent.");
+
+  ok(secondDrawCallScreenshot,
+    "The second draw call has a screenshot attached.");
+  is(secondDrawCallScreenshot.index, 2,
+    "The second draw call has the correct screenshot index.");
+  is(secondDrawCallScreenshot.width, 128,
+    "The second draw call has the correct screenshot width.");
+  is(secondDrawCallScreenshot.height, 128,
+    "The second draw call has the correct screenshot height.");
+  is([].find.call(Uint32(firstDrawCallScreenshot.pixels), e => e > 0), undefined,
+    "The second draw call's screenshot's pixels seems to be completely transparent.");
+
+  ok(thirdDrawCallScreenshot,
+    "The third draw call has a screenshot attached.");
+  is(thirdDrawCallScreenshot.index, 4,
+    "The third draw call has the correct screenshot index.");
+  is(thirdDrawCallScreenshot.width, 128,
+    "The third draw call has the correct screenshot width.");
+  is(thirdDrawCallScreenshot.height, 128,
+    "The third draw call has the correct screenshot height.");
+  is([].find.call(Uint32(thirdDrawCallScreenshot.pixels), e => e > 0), 2160001024,
+    "The third draw call's screenshot's pixels seems to not be completely transparent.");
+
+  ok(fourthDrawCallScreenshot,
+    "The fourth draw call has a screenshot attached.");
+  is(fourthDrawCallScreenshot.index, 6,
+    "The fourth draw call has the correct screenshot index.");
+  is(fourthDrawCallScreenshot.width, 128,
+    "The fourth draw call has the correct screenshot width.");
+  is(fourthDrawCallScreenshot.height, 128,
+    "The fourth draw call has the correct screenshot height.");
+  is([].find.call(Uint32(fourthDrawCallScreenshot.pixels), e => e > 0), 2147483839,
+    "The fourth draw call's screenshot's pixels seems to not be completely transparent.");
+
+  isnot(firstDrawCallScreenshot.pixels, secondDrawCallScreenshot.pixels,
+    "The screenshots taken on consecutive draw calls are different (1).");
+  isnot(secondDrawCallScreenshot.pixels, thirdDrawCallScreenshot.pixels,
+    "The screenshots taken on consecutive draw calls are different (2).");
+  isnot(thirdDrawCallScreenshot.pixels, fourthDrawCallScreenshot.pixels,
+    "The screenshots taken on consecutive draw calls are different (3).");
+
+  await removeTab(target.tab);
+  finish();
+}
+
+function Uint32(src) {
+  const charView = new Uint8Array(src);
+  return new Uint32Array(charView.buffer);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-07.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if screenshots for non-draw calls can still be retrieved properly,
+ * by deferring the the most recent previous draw-call.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  const animationOverview = await snapshotActor.getOverview();
+
+  const functionCalls = animationOverview.calls;
+  ok(functionCalls,
+    "An array of function call actors was sent after recording.");
+  is(functionCalls.length, 8,
+    "The number of function call actors is correct.");
+
+  const firstNonDrawCall = await functionCalls[1].getDetails();
+  const secondNonDrawCall = await functionCalls[3].getDetails();
+  const lastNonDrawCall = await functionCalls[7].getDetails();
+
+  is(firstNonDrawCall.name, "fillStyle",
+    "The first non-draw function's name is correct.");
+  is(secondNonDrawCall.name, "fillStyle",
+    "The second non-draw function's name is correct.");
+  is(lastNonDrawCall.name, "requestAnimationFrame",
+    "The last non-draw function's name is correct.");
+
+  const firstScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[1]);
+  const secondScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[3]);
+  const lastScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[7]);
+
+  ok(firstScreenshot,
+    "A screenshot was successfully retrieved for the first non-draw function.");
+  ok(secondScreenshot,
+    "A screenshot was successfully retrieved for the second non-draw function.");
+  ok(lastScreenshot,
+    "A screenshot was successfully retrieved for the last non-draw function.");
+
+  const firstActualScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[0]);
+  ok(sameArray(firstScreenshot.pixels, firstActualScreenshot.pixels),
+    "The screenshot for the first non-draw function is correct.");
+  is(firstScreenshot.width, 128,
+    "The screenshot for the first non-draw function has the correct width.");
+  is(firstScreenshot.height, 128,
+    "The screenshot for the first non-draw function has the correct height.");
+
+  const secondActualScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[2]);
+  ok(sameArray(secondScreenshot.pixels, secondActualScreenshot.pixels),
+    "The screenshot for the second non-draw function is correct.");
+  is(secondScreenshot.width, 128,
+    "The screenshot for the second non-draw function has the correct width.");
+  is(secondScreenshot.height, 128,
+    "The screenshot for the second non-draw function has the correct height.");
+
+  const lastActualScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[6]);
+  ok(sameArray(lastScreenshot.pixels, lastActualScreenshot.pixels),
+    "The screenshot for the last non-draw function is correct.");
+  is(lastScreenshot.width, 128,
+    "The screenshot for the last non-draw function has the correct width.");
+  is(lastScreenshot.height, 128,
+    "The screenshot for the last non-draw function has the correct height.");
+
+  ok(!sameArray(firstScreenshot.pixels, secondScreenshot.pixels),
+    "The screenshots taken on consecutive draw calls are different (1).");
+  ok(!sameArray(secondScreenshot.pixels, lastScreenshot.pixels),
+    "The screenshots taken on consecutive draw calls are different (2).");
+
+  await removeTab(target.tab);
+  finish();
+}
+
+function sameArray(a, b) {
+  if (a.length != b.length) {
+    return false;
+  }
+  for (let i = 0; i < a.length; i++) {
+    if (a[i] !== b[i]) {
+      return false;
+    }
+  }
+  return true;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-08.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that integers used in arguments are not cast to their constant, enum value
+ * forms if the method's signature does not expect an enum. Bug 999687.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(SIMPLE_BITMASKS_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  const animationOverview = await snapshotActor.getOverview();
+  const functionCalls = animationOverview.calls;
+
+  is(functionCalls[0].name, "clearRect",
+    "The first called function's name is correct.");
+  is(functionCalls[0].argsPreview, "0, 0, 4, 4",
+    "The first called function's args preview is not cast to enums.");
+
+  is(functionCalls[2].name, "fillRect",
+    "The fillRect called function's name is correct.");
+  is(functionCalls[2].argsPreview, "0, 0, 1, 1",
+    "The fillRect called function's args preview is not casted to enums.");
+
+  await removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-09.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that integers used in arguments are not cast to their constant, enum value
+ * forms if the method's signature does not expect an enum. Bug 999687.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(WEBGL_ENUM_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  const animationOverview = await snapshotActor.getOverview();
+  const functionCalls = animationOverview.calls;
+
+  is(functionCalls[0].name, "clear",
+    "The function's name is correct.");
+  is(functionCalls[0].argsPreview, "DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT",
+    "The bits passed into `gl.clear` have been cast to their enum values.");
+
+  is(functionCalls[1].name, "bindTexture",
+    "The function's name is correct.");
+  is(functionCalls[1].argsPreview, "TEXTURE_2D, null",
+    "The bits passed into `gl.bindTexture` have been cast to their enum values.");
+
+  await removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-10.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the correct framebuffer, renderbuffer and textures are re-bound
+ * after generating screenshots using the actor.
+ */
+
+var { CanvasFront } = require("devtools/shared/fronts/canvas");
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(WEBGL_BINDINGS_URL);
+  loadFrameScriptUtils();
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  const animationOverview = await snapshotActor.getOverview();
+  const functionCalls = animationOverview.calls;
+
+  const firstScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[0]);
+  is(firstScreenshot.index, -1,
+    "The first screenshot didn't encounter any draw call.");
+  is(firstScreenshot.scaling, 0.25,
+    "The first screenshot has the correct scaling.");
+  is(firstScreenshot.width, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
+    "The first screenshot has the correct width.");
+  is(firstScreenshot.height, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
+    "The first screenshot has the correct height.");
+  is(firstScreenshot.flipped, true,
+    "The first screenshot has the correct 'flipped' flag.");
+  is(firstScreenshot.pixels.length, 0,
+    "The first screenshot should be empty.");
+
+  is((await evalInDebuggee("gl.getParameter(gl.FRAMEBUFFER_BINDING) === customFramebuffer")),
+    true,
+    "The debuggee's gl context framebuffer wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.RENDERBUFFER_BINDING) === customRenderbuffer")),
+    true,
+    "The debuggee's gl context renderbuffer wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.TEXTURE_BINDING_2D) === customTexture")),
+    true,
+    "The debuggee's gl context texture binding wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[0]")),
+    128,
+    "The debuggee's gl context viewport's left coord. wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[1]")),
+    256,
+    "The debuggee's gl context viewport's left coord. wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[2]")),
+    384,
+    "The debuggee's gl context viewport's left coord. wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[3]")),
+    512,
+    "The debuggee's gl context viewport's left coord. wasn't changed.");
+
+  const secondScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[1]);
+  is(secondScreenshot.index, 1,
+    "The second screenshot has the correct index.");
+  is(secondScreenshot.width, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
+    "The second screenshot has the correct width.");
+  is(secondScreenshot.height, CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT,
+    "The second screenshot has the correct height.");
+  is(secondScreenshot.scaling, 0.25,
+    "The second screenshot has the correct scaling.");
+  is(secondScreenshot.flipped, true,
+    "The second screenshot has the correct 'flipped' flag.");
+  is(secondScreenshot.pixels.length, Math.pow(CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, 2) * 4,
+    "The second screenshot should not be empty.");
+  is(secondScreenshot.pixels[0], 0,
+    "The second screenshot has the correct red component.");
+  is(secondScreenshot.pixels[1], 0,
+    "The second screenshot has the correct green component.");
+  is(secondScreenshot.pixels[2], 255,
+    "The second screenshot has the correct blue component.");
+  is(secondScreenshot.pixels[3], 255,
+    "The second screenshot has the correct alpha component.");
+
+  is((await evalInDebuggee("gl.getParameter(gl.FRAMEBUFFER_BINDING) === customFramebuffer")),
+    true,
+    "The debuggee's gl context framebuffer still wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.RENDERBUFFER_BINDING) === customRenderbuffer")),
+    true,
+    "The debuggee's gl context renderbuffer still wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.TEXTURE_BINDING_2D) === customTexture")),
+    true,
+    "The debuggee's gl context texture binding still wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[0]")),
+    128,
+    "The debuggee's gl context viewport's left coord. still wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[1]")),
+    256,
+    "The debuggee's gl context viewport's left coord. still wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[2]")),
+    384,
+    "The debuggee's gl context viewport's left coord. still wasn't changed.");
+  is((await evalInDebuggee("gl.getParameter(gl.VIEWPORT)[3]")),
+    512,
+    "The debuggee's gl context viewport's left coord. still wasn't changed.");
+
+  await removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-11.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that loops using setTimeout are recorded and stored
+ * for a canvas context, and that the generated screenshots are correct.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(SET_TIMEOUT_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  ok(snapshotActor,
+    "A snapshot actor was sent after recording.");
+
+  const animationOverview = await snapshotActor.getOverview();
+  ok(snapshotActor,
+    "An animation overview could be retrieved after recording.");
+
+  const functionCalls = animationOverview.calls;
+  ok(functionCalls,
+    "An array of function call actors was sent after recording.");
+  is(functionCalls.length, 8,
+    "The number of function call actors is correct.");
+
+  is(functionCalls[0].type, METHOD_FUNCTION,
+    "The first called function is correctly identified as a method.");
+  is(functionCalls[0].name, "clearRect",
+    "The first called function's name is correct.");
+  is(functionCalls[0].file, SET_TIMEOUT_URL,
+    "The first called function's file is correct.");
+  is(functionCalls[0].line, 25,
+    "The first called function's line is correct.");
+  is(functionCalls[0].argsPreview, "0, 0, 128, 128",
+    "The first called function's args preview is correct.");
+  is(functionCalls[0].callerPreview, "Object",
+    "The first called function's caller preview is correct.");
+
+  is(functionCalls[6].type, METHOD_FUNCTION,
+    "The penultimate called function is correctly identified as a method.");
+  is(functionCalls[6].name, "fillRect",
+    "The penultimate called function's name is correct.");
+  is(functionCalls[6].file, SET_TIMEOUT_URL,
+    "The penultimate called function's file is correct.");
+  is(functionCalls[6].line, 21,
+    "The penultimate called function's line is correct.");
+  is(functionCalls[6].argsPreview, "10, 10, 55, 50",
+    "The penultimate called function's args preview is correct.");
+  is(functionCalls[6].callerPreview, "Object",
+    "The penultimate called function's caller preview is correct.");
+
+  is(functionCalls[7].type, METHOD_FUNCTION,
+    "The last called function is correctly identified as a method.");
+  is(functionCalls[7].name, "setTimeout",
+    "The last called function's name is correct.");
+  is(functionCalls[7].file, SET_TIMEOUT_URL,
+    "The last called function's file is correct.");
+  is(functionCalls[7].line, 30,
+    "The last called function's line is correct.");
+  ok(functionCalls[7].argsPreview.includes("Function"),
+    "The last called function's args preview is correct.");
+  is(functionCalls[7].callerPreview, "Object",
+    "The last called function's caller preview is correct.");
+
+  const firstNonDrawCall = await functionCalls[1].getDetails();
+  const secondNonDrawCall = await functionCalls[3].getDetails();
+  const lastNonDrawCall = await functionCalls[7].getDetails();
+
+  is(firstNonDrawCall.name, "fillStyle",
+    "The first non-draw function's name is correct.");
+  is(secondNonDrawCall.name, "fillStyle",
+    "The second non-draw function's name is correct.");
+  is(lastNonDrawCall.name, "setTimeout",
+    "The last non-draw function's name is correct.");
+
+  const firstScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[1]);
+  const secondScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[3]);
+  const lastScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[7]);
+
+  ok(firstScreenshot,
+    "A screenshot was successfully retrieved for the first non-draw function.");
+  ok(secondScreenshot,
+    "A screenshot was successfully retrieved for the second non-draw function.");
+  ok(lastScreenshot,
+    "A screenshot was successfully retrieved for the last non-draw function.");
+
+  const firstActualScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[0]);
+  ok(sameArray(firstScreenshot.pixels, firstActualScreenshot.pixels),
+    "The screenshot for the first non-draw function is correct.");
+  is(firstScreenshot.width, 128,
+    "The screenshot for the first non-draw function has the correct width.");
+  is(firstScreenshot.height, 128,
+    "The screenshot for the first non-draw function has the correct height.");
+
+  const secondActualScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[2]);
+  ok(sameArray(secondScreenshot.pixels, secondActualScreenshot.pixels),
+    "The screenshot for the second non-draw function is correct.");
+  is(secondScreenshot.width, 128,
+    "The screenshot for the second non-draw function has the correct width.");
+  is(secondScreenshot.height, 128,
+    "The screenshot for the second non-draw function has the correct height.");
+
+  const lastActualScreenshot = await snapshotActor.generateScreenshotFor(functionCalls[6]);
+  ok(sameArray(lastScreenshot.pixels, lastActualScreenshot.pixels),
+    "The screenshot for the last non-draw function is correct.");
+  is(lastScreenshot.width, 128,
+    "The screenshot for the last non-draw function has the correct width.");
+  is(lastScreenshot.height, 128,
+    "The screenshot for the last non-draw function has the correct height.");
+
+  ok(!sameArray(firstScreenshot.pixels, secondScreenshot.pixels),
+    "The screenshots taken on consecutive draw calls are different (1).");
+  ok(!sameArray(secondScreenshot.pixels, lastScreenshot.pixels),
+    "The screenshots taken on consecutive draw calls are different (2).");
+
+  await removeTab(target.tab);
+  finish();
+}
+
+function sameArray(a, b) {
+  if (a.length != b.length) {
+    return false;
+  }
+  for (let i = 0; i < a.length; i++) {
+    if (a[i] !== b[i]) {
+      return false;
+    }
+  }
+  return true;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-actor-test-12.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the recording can be disabled via stopRecordingAnimationFrame
+ * in the event no rAF loop is found.
+ */
+
+async function ifTestingSupported() {
+  const { target, front } = await initCanvasDebuggerBackend(NO_CANVAS_URL);
+  loadFrameScriptUtils();
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const startRecording = front.recordAnimationFrame();
+  await front.stopRecordingAnimationFrame();
+
+  ok(!(await startRecording),
+    "recordAnimationFrame() does not return a SnapshotActor when cancelled.");
+
+  await removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-highlight.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if certain function calls are properly highlighted in the UI.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([recordingFinished, callListPopulated]);
+
+  is(CallsListView.itemCount, 8,
+    "All the function calls should now be displayed in the UI.");
+
+  is($(".call-item-view", CallsListView.getItemAtIndex(0).target).hasAttribute("draw-call"), true,
+    "The first item's node should have a draw-call attribute.");
+  is($(".call-item-view", CallsListView.getItemAtIndex(1).target).hasAttribute("draw-call"), false,
+    "The second item's node should not have a draw-call attribute.");
+  is($(".call-item-view", CallsListView.getItemAtIndex(2).target).hasAttribute("draw-call"), true,
+    "The third item's node should have a draw-call attribute.");
+  is($(".call-item-view", CallsListView.getItemAtIndex(3).target).hasAttribute("draw-call"), false,
+    "The fourth item's node should not have a draw-call attribute.");
+  is($(".call-item-view", CallsListView.getItemAtIndex(4).target).hasAttribute("draw-call"), true,
+    "The fifth item's node should have a draw-call attribute.");
+  is($(".call-item-view", CallsListView.getItemAtIndex(5).target).hasAttribute("draw-call"), false,
+    "The sixth item's node should not have a draw-call attribute.");
+  is($(".call-item-view", CallsListView.getItemAtIndex(6).target).hasAttribute("draw-call"), true,
+    "The seventh item's node should have a draw-call attribute.");
+  is($(".call-item-view", CallsListView.getItemAtIndex(7).target).hasAttribute("draw-call"), false,
+    "The eigth item's node should not have a draw-call attribute.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-list.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if all the function calls associated with an animation frame snapshot
+ * are properly displayed in the UI.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([recordingFinished, callListPopulated]);
+
+  is(CallsListView.itemCount, 8,
+    "All the function calls should now be displayed in the UI.");
+
+  testItem(CallsListView.getItemAtIndex(0),
+    "1", "Object", "clearRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:25");
+
+  testItem(CallsListView.getItemAtIndex(1),
+    "2", "Object", "fillStyle", " = rgb(192, 192, 192)", "doc_simple-canvas.html:20");
+  testItem(CallsListView.getItemAtIndex(2),
+    "3", "Object", "fillRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:21");
+
+  testItem(CallsListView.getItemAtIndex(3),
+    "4", "Object", "fillStyle", " = rgba(0, 0, 192, 0.5)", "doc_simple-canvas.html:20");
+  testItem(CallsListView.getItemAtIndex(4),
+    "5", "Object", "fillRect", "(30, 30, 55, 50)", "doc_simple-canvas.html:21");
+
+  testItem(CallsListView.getItemAtIndex(5),
+    "6", "Object", "fillStyle", " = rgba(192, 0, 0, 0.5)", "doc_simple-canvas.html:20");
+  testItem(CallsListView.getItemAtIndex(6),
+    "7", "Object", "fillRect", "(10, 10, 55, 50)", "doc_simple-canvas.html:21");
+
+  testItem(CallsListView.getItemAtIndex(7),
+    "8", "", "requestAnimationFrame", "(Function)", "doc_simple-canvas.html:30");
+
+  function testItem(item, index, context, name, args, location) {
+    const i = CallsListView.indexOfItem(item);
+    is(i, index - 1,
+      "The item at index " + index + " is correctly displayed in the UI.");
+
+    is($(".call-item-index", item.target).getAttribute("value"), index,
+      "The item's gutter label has the correct text.");
+
+    if (context) {
+      is($(".call-item-context", item.target).getAttribute("value"), context,
+        "The item's context label has the correct text.");
+    } else {
+      is($(".call-item-context", item.target) + "", "[object XULTextElement]",
+        "The item's context label should not be available.");
+    }
+
+    is($(".call-item-name", item.target).getAttribute("value"), name,
+      "The item's name label has the correct text.");
+    is($(".call-item-args", item.target).getAttribute("value"), args,
+      "The item's args label has the correct text.");
+    is($(".call-item-location", item.target).getAttribute("value"), location,
+      "The item's location label has the correct text.");
+  }
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-search.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if filtering the items in the call list works properly.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+  const searchbox = $("#calls-searchbox");
+
+  await reload(target);
+
+  const firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([firstRecordingFinished, callListPopulated]);
+
+  is(searchbox.value, "",
+    "The searchbox should be initially empty.");
+  is(CallsListView.visibleItems.length, 8,
+    "All the items should be initially visible in the calls list.");
+
+  searchbox.focus();
+  EventUtils.sendString("clear", window);
+
+  is(searchbox.value, "clear",
+    "The searchbox should now contain the 'clear' string.");
+  is(CallsListView.visibleItems.length, 1,
+    "Only one item should now be visible in the calls list.");
+
+  is(CallsListView.visibleItems[0].attachment.actor.type, METHOD_FUNCTION,
+    "The visible item's type has the expected value.");
+  is(CallsListView.visibleItems[0].attachment.actor.name, "clearRect",
+    "The visible item's name has the expected value.");
+  is(CallsListView.visibleItems[0].attachment.actor.file, SIMPLE_CANVAS_URL,
+    "The visible item's file has the expected value.");
+  is(CallsListView.visibleItems[0].attachment.actor.line, 25,
+    "The visible item's line has the expected value.");
+  is(CallsListView.visibleItems[0].attachment.actor.argsPreview, "0, 0, 128, 128",
+    "The visible item's args have the expected value.");
+  is(CallsListView.visibleItems[0].attachment.actor.callerPreview, "Object",
+    "The visible item's caller has the expected value.");
+
+  const secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+
+  SnapshotsListView._onRecordButtonClick();
+  await secondRecordingFinished;
+
+  SnapshotsListView.selectedIndex = 1;
+  await callListPopulated;
+
+  is(searchbox.value, "clear",
+    "The searchbox should still contain the 'clear' string.");
+  is(CallsListView.visibleItems.length, 1,
+    "Only one item should still be visible in the calls list.");
+
+  for (let i = 0; i < 5; i++) {
+    searchbox.focus();
+    EventUtils.sendKey("BACK_SPACE", window);
+  }
+
+  is(searchbox.value, "",
+    "The searchbox should now be emptied.");
+  is(CallsListView.visibleItems.length, 8,
+    "All the items should be initially visible again in the calls list.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the a function call's stack is properly displayed in the UI.
+ */
+
+requestLongerTimeout(2);
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
+  const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([recordingFinished, callListPopulated]);
+
+  const callItem = CallsListView.getItemAtIndex(2);
+  const locationLink = $(".call-item-location", callItem.target);
+
+  is($(".call-item-stack", callItem.target), null,
+    "There should be no stack container available yet for the draw call.");
+
+  const callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window);
+  await callStackDisplayed;
+
+  isnot($(".call-item-stack", callItem.target), null,
+    "There should be a stack container available now for the draw call.");
+  // We may have more than 4 functions, depending on whether async
+  // stacks are available.
+  ok($all(".call-item-stack-fn", callItem.target).length >= 4,
+     "There should be at least 4 functions on the stack for the draw call.");
+
+  ok($all(".call-item-stack-fn-name", callItem.target)[0].getAttribute("value")
+    .includes("C()"),
+    "The first function on the stack has the correct name.");
+  ok($all(".call-item-stack-fn-name", callItem.target)[1].getAttribute("value")
+    .includes("B()"),
+    "The second function on the stack has the correct name.");
+  ok($all(".call-item-stack-fn-name", callItem.target)[2].getAttribute("value")
+    .includes("A()"),
+    "The third function on the stack has the correct name.");
+  ok($all(".call-item-stack-fn-name", callItem.target)[3].getAttribute("value")
+    .includes("drawRect()"),
+    "The fourth function on the stack has the correct name.");
+
+  is($all(".call-item-stack-fn-location", callItem.target)[0].getAttribute("value"),
+    "doc_simple-canvas-deep-stack.html:26",
+    "The first function on the stack has the correct location.");
+  is($all(".call-item-stack-fn-location", callItem.target)[1].getAttribute("value"),
+    "doc_simple-canvas-deep-stack.html:28",
+    "The second function on the stack has the correct location.");
+  is($all(".call-item-stack-fn-location", callItem.target)[2].getAttribute("value"),
+    "doc_simple-canvas-deep-stack.html:30",
+    "The third function on the stack has the correct location.");
+  is($all(".call-item-stack-fn-location", callItem.target)[3].getAttribute("value"),
+    "doc_simple-canvas-deep-stack.html:35",
+    "The fourth function on the stack has the correct location.");
+
+  const jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-stack-fn-location", callItem.target));
+  await jumpedToSource;
+
+  const toolbox = await gDevTools.getToolbox(target);
+  const dbg = createDebuggerContext(toolbox);
+  await validateDebuggerLocation(dbg, SIMPLE_CANVAS_DEEP_STACK_URL, 26);
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the a function call's stack is properly displayed in the UI
+ * and jumping to source in the debugger for the topmost call item works.
+ */
+
+requestLongerTimeout(2);
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
+  const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([recordingFinished, callListPopulated]);
+
+  const callItem = CallsListView.getItemAtIndex(2);
+  const locationLink = $(".call-item-location", callItem.target);
+
+  is($(".call-item-stack", callItem.target), null,
+    "There should be no stack container available yet for the draw call.");
+
+  const callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window);
+  await callStackDisplayed;
+
+  isnot($(".call-item-stack", callItem.target), null,
+    "There should be a stack container available now for the draw call.");
+  // We may have more than 4 functions, depending on whether async
+  // stacks are available.
+  ok($all(".call-item-stack-fn", callItem.target).length >= 4,
+     "There should be at least 4 functions on the stack for the draw call.");
+
+  const jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-location", callItem.target));
+  await jumpedToSource;
+
+  const toolbox = await gDevTools.getToolbox(target);
+  const dbg = createDebuggerContext(toolbox);
+  await validateDebuggerLocation(dbg, SIMPLE_CANVAS_DEEP_STACK_URL, 24);
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the a function call's stack can be shown/hidden by double-clicking
+ * on a function call item.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
+  const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([recordingFinished, callListPopulated]);
+
+  const callItem = CallsListView.getItemAtIndex(2);
+  const view = $(".call-item-view", callItem.target);
+  const contents = $(".call-item-contents", callItem.target);
+
+  is(view.hasAttribute("call-stack-populated"), false,
+    "The call item's view should not have the stack populated yet.");
+  is(view.hasAttribute("call-stack-expanded"), false,
+    "The call item's view should not have the stack populated yet.");
+  is($(".call-item-stack", callItem.target), null,
+    "There should be no stack container available yet for the draw call.");
+
+  const callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
+  EventUtils.sendMouseEvent({ type: "dblclick" }, contents, window);
+  await callStackDisplayed;
+
+  is(view.hasAttribute("call-stack-populated"), true,
+    "The call item's view should have the stack populated now.");
+  is(view.getAttribute("call-stack-expanded"), "true",
+    "The call item's view should have the stack expanded now.");
+  isnot($(".call-item-stack", callItem.target), null,
+    "There should be a stack container available now for the draw call.");
+  is($(".call-item-stack", callItem.target).hidden, false,
+    "The stack container should now be visible.");
+  // We may have more than 4 functions, depending on whether async
+  // stacks are available.
+  ok($all(".call-item-stack-fn", callItem.target).length >= 4,
+     "There should be at least 4 functions on the stack for the draw call.");
+
+  EventUtils.sendMouseEvent({ type: "dblclick" }, contents, window);
+
+  is(view.hasAttribute("call-stack-populated"), true,
+    "The call item's view should still have the stack populated.");
+  is(view.getAttribute("call-stack-expanded"), "false",
+    "The call item's view should not have the stack expanded anymore.");
+  isnot($(".call-item-stack", callItem.target), null,
+    "There should still be a stack container available for the draw call.");
+  is($(".call-item-stack", callItem.target).hidden, true,
+    "The stack container should now be hidden.");
+  // We may have more than 4 functions, depending on whether async
+  // stacks are available.
+  ok($all(".call-item-stack-fn", callItem.target).length >= 4,
+     "There should still be at least 4 functions on the stack for the draw call.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-clear.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if clearing the snapshots list works as expected.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, EVENTS, SnapshotsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  SnapshotsListView._onRecordButtonClick();
+
+  await firstRecordingFinished;
+  ok(true, "Finished recording a snapshot of the animation loop.");
+
+  is(SnapshotsListView.itemCount, 1,
+    "There should be one item available in the snapshots list.");
+
+  const secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  SnapshotsListView._onRecordButtonClick();
+
+  await secondRecordingFinished;
+  ok(true, "Finished recording another snapshot of the animation loop.");
+
+  is(SnapshotsListView.itemCount, 2,
+    "There should be two items available in the snapshots list.");
+
+  const clearingFinished = once(window, EVENTS.SNAPSHOTS_LIST_CLEARED);
+  SnapshotsListView._onClearButtonClick();
+
+  await clearingFinished;
+  ok(true, "Finished recording all snapshots.");
+
+  is(SnapshotsListView.itemCount, 0,
+    "There should be no items available in the snapshots list.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-screenshots.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if screenshots are properly displayed in the UI.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, EVENTS, SnapshotsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([recordingFinished, callListPopulated, screenshotDisplayed]);
+
+  is($("#screenshot-container").hidden, false,
+    "The screenshot container should now be visible.");
+
+  is($("#screenshot-dimensions").getAttribute("value"), "128" + "\u00D7" + "128",
+    "The screenshot dimensions label has the expected value.");
+
+  is($("#screenshot-image").getAttribute("flipped"), "false",
+    "The screenshot element should not be flipped vertically.");
+
+  ok(window.getComputedStyle($("#screenshot-image")).backgroundImage.includes("#screenshot-rendering"),
+    "The screenshot element should have an offscreen canvas element as a background.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-01.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if thumbnails are properly displayed in the UI.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, $all, EVENTS, SnapshotsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  const thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([recordingFinished, callListPopulated, thumbnailsDisplayed]);
+
+  is($all(".filmstrip-thumbnail").length, 4,
+    "There should be 4 thumbnails displayed in the UI.");
+
+  const firstThumbnail = $(".filmstrip-thumbnail[index='0']");
+  ok(firstThumbnail,
+    "The first thumbnail element should be for the function call at index 0.");
+  is(firstThumbnail.width, 50,
+    "The first thumbnail's width is correct.");
+  is(firstThumbnail.height, 50,
+    "The first thumbnail's height is correct.");
+  is(firstThumbnail.getAttribute("flipped"), "false",
+    "The first thumbnail should not be flipped vertically.");
+
+  const secondThumbnail = $(".filmstrip-thumbnail[index='2']");
+  ok(secondThumbnail,
+    "The second thumbnail element should be for the function call at index 2.");
+  is(secondThumbnail.width, 50,
+    "The second thumbnail's width is correct.");
+  is(secondThumbnail.height, 50,
+    "The second thumbnail's height is correct.");
+  is(secondThumbnail.getAttribute("flipped"), "false",
+    "The second thumbnail should not be flipped vertically.");
+
+  const thirdThumbnail = $(".filmstrip-thumbnail[index='4']");
+  ok(thirdThumbnail,
+    "The third thumbnail element should be for the function call at index 4.");
+  is(thirdThumbnail.width, 50,
+    "The third thumbnail's width is correct.");
+  is(thirdThumbnail.height, 50,
+    "The third thumbnail's height is correct.");
+  is(thirdThumbnail.getAttribute("flipped"), "false",
+    "The third thumbnail should not be flipped vertically.");
+
+  const fourthThumbnail = $(".filmstrip-thumbnail[index='6']");
+  ok(fourthThumbnail,
+    "The fourth thumbnail element should be for the function call at index 6.");
+  is(fourthThumbnail.width, 50,
+    "The fourth thumbnail's width is correct.");
+  is(fourthThumbnail.height, 50,
+    "The fourth thumbnail's height is correct.");
+  is(fourthThumbnail.getAttribute("flipped"), "false",
+    "The fourth thumbnail should not be flipped vertically.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-02.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if thumbnails are correctly linked with other UI elements like
+ * function call items and their respective screenshots.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  const thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+  const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([
+    recordingFinished,
+    callListPopulated,
+    thumbnailsDisplayed,
+    screenshotDisplayed,
+  ]);
+
+  is($all(".filmstrip-thumbnail[highlighted]").length, 0,
+    "There should be no highlighted thumbnail available yet.");
+  is(CallsListView.selectedIndex, -1,
+    "There should be no selected item in the calls list view.");
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, $all(".filmstrip-thumbnail")[0], window);
+  await once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+  info("The first draw call was selected, by clicking the first thumbnail.");
+
+  isnot($(".filmstrip-thumbnail[highlighted][index='0']"), null,
+    "There should be a highlighted thumbnail available now, for the first draw call.");
+  is($all(".filmstrip-thumbnail[highlighted]").length, 1,
+    "There should be only one highlighted thumbnail available now.");
+  is(CallsListView.selectedIndex, 0,
+    "The first draw call should be selected in the calls list view.");
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, $all(".call-item-view")[1], window);
+  await once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+  info("The second context call was selected, by clicking the second call item.");
+
+  isnot($(".filmstrip-thumbnail[highlighted][index='0']"), null,
+    "There should be a highlighted thumbnail available, for the first draw call.");
+  is($all(".filmstrip-thumbnail[highlighted]").length, 1,
+    "There should be only one highlighted thumbnail available.");
+  is(CallsListView.selectedIndex, 1,
+    "The second draw call should be selected in the calls list view.");
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, $all(".call-item-view")[2], window);
+  await once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+  info("The second draw call was selected, by clicking the third call item.");
+
+  isnot($(".filmstrip-thumbnail[highlighted][index='2']"), null,
+    "There should be a highlighted thumbnail available, for the second draw call.");
+  is($all(".filmstrip-thumbnail[highlighted]").length, 1,
+    "There should be only one highlighted thumbnail available.");
+  is(CallsListView.selectedIndex, 2,
+    "The second draw call should be selected in the calls list view.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-open.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the frontend UI is properly configured when opening the tool.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { $ } = panel.panelWin;
+
+  is($("#snapshots-pane").hasAttribute("hidden"), false,
+    "The snapshots pane should initially be visible.");
+  is($("#debugging-pane").hasAttribute("hidden"), false,
+    "The debugging pane should initially be visible.");
+
+  is($("#record-snapshot").getAttribute("hidden"), "true",
+    "The 'record snapshot' button should initially be hidden.");
+  is($("#import-snapshot").hasAttribute("hidden"), false,
+    "The 'import snapshot' button should initially be visible.");
+  is($("#clear-snapshots").hasAttribute("hidden"), false,
+    "The 'clear snapshots' button should initially be visible.");
+
+  is($("#reload-notice").hasAttribute("hidden"), false,
+    "The reload notice should initially be visible.");
+  is($("#empty-notice").getAttribute("hidden"), "true",
+    "The empty notice should initially be hidden.");
+  is($("#waiting-notice").getAttribute("hidden"), "true",
+    "The waiting notice should initially be hidden.");
+
+  is($("#screenshot-container").getAttribute("hidden"), "true",
+    "The screenshot container should initially be hidden.");
+  is($("#snapshot-filmstrip").getAttribute("hidden"), "true",
+    "The snapshot filmstrip should initially be hidden.");
+
+  is($("#debugging-pane-contents").getAttribute("hidden"), "true",
+    "The rest of the UI should initially be hidden.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-01.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests whether the frontend behaves correctly while reording a snapshot.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+  await reload(target);
+
+  is($("#record-snapshot").hasAttribute("checked"), false,
+    "The 'record snapshot' button should initially be unchecked.");
+  is($("#record-snapshot").hasAttribute("disabled"), false,
+    "The 'record snapshot' button should initially be enabled.");
+  is($("#record-snapshot").hasAttribute("hidden"), false,
+    "The 'record snapshot' button should now be visible.");
+
+  is(SnapshotsListView.itemCount, 0,
+    "There should be no items available in the snapshots list view.");
+  is(SnapshotsListView.selectedIndex, -1,
+    "There should be no selected item in the snapshots list view.");
+
+  const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  SnapshotsListView._onRecordButtonClick();
+
+  await recordingStarted;
+  ok(true, "Started recording a snapshot of the animation loop.");
+
+  is($("#record-snapshot").getAttribute("checked"), "true",
+    "The 'record snapshot' button should now be checked.");
+  is($("#record-snapshot").hasAttribute("hidden"), false,
+    "The 'record snapshot' button should still be visible.");
+
+  is(SnapshotsListView.itemCount, 1,
+    "There should be one item available in the snapshots list view now.");
+  is(SnapshotsListView.selectedIndex, -1,
+    "There should be no selected item in the snapshots list view yet.");
+
+  await recordingFinished;
+  ok(true, "Finished recording a snapshot of the animation loop.");
+
+  is($("#record-snapshot").hasAttribute("checked"), false,
+    "The 'record snapshot' button should now be unchecked.");
+  is($("#record-snapshot").hasAttribute("disabled"), false,
+    "The 'record snapshot' button should now be re-enabled.");
+  is($("#record-snapshot").hasAttribute("hidden"), false,
+    "The 'record snapshot' button should still be visible.");
+
+  is(SnapshotsListView.itemCount, 1,
+    "There should still be only one item available in the snapshots list view.");
+  is(SnapshotsListView.selectedIndex, 0,
+    "There should be one selected item in the snapshots list view now.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-02.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests whether the frontend displays a placeholder snapshot while recording.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, EVENTS, L10N, $, SnapshotsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const recordingSelected = once(window, EVENTS.SNAPSHOT_RECORDING_SELECTED);
+  SnapshotsListView._onRecordButtonClick();
+
+  await recordingStarted;
+  ok(true, "Started recording a snapshot of the animation loop.");
+
+  const item = SnapshotsListView.getItemAtIndex(0);
+
+  is($(".snapshot-item-title", item.target).getAttribute("value"),
+    L10N.getFormatStr("snapshotsList.itemLabel", 1),
+    "The placeholder item's title label is correct.");
+
+  is($(".snapshot-item-calls", item.target).getAttribute("value"),
+    L10N.getStr("snapshotsList.loadingLabel"),
+    "The placeholder item's calls label is correct.");
+
+  is($(".snapshot-item-save", item.target).getAttribute("value"), "",
+    "The placeholder item's save label should not have a value yet.");
+
+  is($("#reload-notice").getAttribute("hidden"), "true",
+    "The reload notice should now be hidden.");
+  is($("#empty-notice").getAttribute("hidden"), "true",
+    "The empty notice should now be hidden.");
+  is($("#waiting-notice").hasAttribute("hidden"), false,
+    "The waiting notice should now be visible.");
+
+  is($("#screenshot-container").getAttribute("hidden"), "true",
+    "The screenshot container should still be hidden.");
+  is($("#snapshot-filmstrip").getAttribute("hidden"), "true",
+    "The snapshot filmstrip should still be hidden.");
+
+  is($("#debugging-pane-contents").getAttribute("hidden"), "true",
+    "The rest of the UI should still be hidden.");
+
+  await recordingFinished;
+  ok(true, "Finished recording a snapshot of the animation loop.");
+
+  await recordingSelected;
+  ok(true, "Finished selecting a snapshot of the animation loop.");
+
+  is($("#reload-notice").getAttribute("hidden"), "true",
+    "The reload notice should now be hidden.");
+  is($("#empty-notice").getAttribute("hidden"), "true",
+    "The empty notice should now be hidden.");
+  is($("#waiting-notice").getAttribute("hidden"), "true",
+    "The waiting notice should now be hidden.");
+
+  is($("#screenshot-container").hasAttribute("hidden"), false,
+    "The screenshot container should now be visible.");
+  is($("#snapshot-filmstrip").hasAttribute("hidden"), false,
+    "The snapshot filmstrip should now be visible.");
+
+  is($("#debugging-pane-contents").hasAttribute("hidden"), false,
+    "The rest of the UI should now be visible.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-03.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests whether the frontend displays the correct info for a snapshot
+ * after finishing recording.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  SnapshotsListView._onRecordButtonClick();
+
+  await recordingFinished;
+  ok(true, "Finished recording a snapshot of the animation loop.");
+
+  const item = SnapshotsListView.getItemAtIndex(0);
+
+  is(SnapshotsListView.selectedItem, item,
+    "The first item should now be selected in the snapshots list view (1).");
+  is(SnapshotsListView.selectedIndex, 0,
+    "The first item should now be selected in the snapshots list view (2).");
+
+  is($(".snapshot-item-calls", item.target).getAttribute("value"), "4 draws, 8 calls",
+    "The placeholder item's calls label is correct.");
+  is($(".snapshot-item-save", item.target).getAttribute("value"), "Save",
+    "The placeholder item's save label is correct.");
+  is($(".snapshot-item-save", item.target).getAttribute("disabled"), "false",
+    "The placeholder item's save label should be clickable.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-record-04.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1122766
+ * Tests that the canvas actor correctly returns from recordAnimationFrame
+ * in the scenario where a loop starts with rAF and has rAF in the beginning
+ * of its loop, when the recording starts before the rAFs start.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(RAF_BEGIN_URL);
+  const { window, EVENTS, gFront, SnapshotsListView } = panel.panelWin;
+  loadFrameScriptUtils();
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  SnapshotsListView._onRecordButtonClick();
+
+  // Wait until after the recording started to trigger the content.
+  // Use the gFront method rather than the SNAPSHOT_RECORDING_STARTED event
+  // which triggers before the underlying actor call
+  await waitUntil(async function() {
+    return !(await gFront.isRecording());
+  });
+
+  // Start animation in content
+  evalInDebuggee("start();");
+
+  await recordingFinished;
+  ok(true, "Finished recording a snapshot of the animation loop.");
+
+  await removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-01.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the frontend UI is properly reconfigured after reloading.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, EVENTS } = panel.panelWin;
+
+  const reset = once(window, EVENTS.UI_RESET);
+  const navigated = reload(target);
+
+  await reset;
+  ok(true, "The UI was reset after the refresh button was clicked.");
+
+  await navigated;
+  ok(true, "The target finished reloading.");
+
+  is($("#snapshots-pane").hasAttribute("hidden"), false,
+    "The snapshots pane should still be visible.");
+  is($("#debugging-pane").hasAttribute("hidden"), false,
+    "The debugging pane should still be visible.");
+
+  is($("#record-snapshot").hasAttribute("checked"), false,
+    "The 'record snapshot' button should not be checked.");
+  is($("#record-snapshot").hasAttribute("disabled"), false,
+    "The 'record snapshot' button should not be disabled.");
+
+  is($("#record-snapshot").hasAttribute("hidden"), false,
+    "The 'record snapshot' button should now be visible.");
+  is($("#import-snapshot").hasAttribute("hidden"), false,
+    "The 'import snapshot' button should still be visible.");
+  is($("#clear-snapshots").hasAttribute("hidden"), false,
+    "The 'clear snapshots' button should still be visible.");
+
+  is($("#reload-notice").getAttribute("hidden"), "true",
+    "The reload notice should now be hidden.");
+  is($("#empty-notice").hasAttribute("hidden"), false,
+    "The empty notice should now be visible.");
+  is($("#waiting-notice").getAttribute("hidden"), "true",
+    "The waiting notice should now be hidden.");
+
+  is($("#snapshot-filmstrip").getAttribute("hidden"), "true",
+    "The snapshot filmstrip should still be hidden.");
+  is($("#screenshot-container").getAttribute("hidden"), "true",
+    "The screenshot container should still be hidden.");
+
+  is($("#debugging-pane-contents").getAttribute("hidden"), "true",
+    "The rest of the UI should still be hidden.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-02.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the frontend UI is properly reconfigured after reloading.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  is(SnapshotsListView.itemCount, 0,
+    "There should be no snapshots initially displayed in the UI.");
+  is(CallsListView.itemCount, 0,
+    "There should be no function calls initially displayed in the UI.");
+
+  is($("#screenshot-container").hidden, true,
+    "The screenshot should not be initially displayed in the UI.");
+  is($("#snapshot-filmstrip").hidden, true,
+    "There should be no thumbnails initially displayed in the UI (1).");
+  is($all(".filmstrip-thumbnail").length, 0,
+    "There should be no thumbnails initially displayed in the UI (2).");
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  const thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+  const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([
+    recordingFinished,
+    callListPopulated,
+    thumbnailsDisplayed,
+    screenshotDisplayed,
+  ]);
+
+  is(SnapshotsListView.itemCount, 1,
+    "There should be one snapshot displayed in the UI.");
+  is(CallsListView.itemCount, 8,
+    "All the function calls should now be displayed in the UI.");
+
+  is($("#screenshot-container").hidden, false,
+    "The screenshot should now be displayed in the UI.");
+  is($("#snapshot-filmstrip").hidden, false,
+    "All the thumbnails should now be displayed in the UI (1).");
+  is($all(".filmstrip-thumbnail").length, 4,
+    "All the thumbnails should now be displayed in the UI (2).");
+
+  const reset = once(window, EVENTS.UI_RESET);
+  const navigated = reload(target);
+
+  await reset;
+  ok(true, "The UI was reset after the refresh button was clicked.");
+
+  is(SnapshotsListView.itemCount, 0,
+    "There should be no snapshots displayed in the UI after navigating.");
+  is(CallsListView.itemCount, 0,
+    "There should be no function calls displayed in the UI after navigating.");
+  is($("#snapshot-filmstrip").hidden, true,
+    "There should be no thumbnails displayed in the UI after navigating.");
+  is($("#screenshot-container").hidden, true,
+    "The screenshot should not be displayed in the UI after navigating.");
+
+  await navigated;
+  ok(true, "The target finished reloading.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-01.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if selecting snapshots in the frontend displays the appropriate data
+ * respective to their recorded animation frame.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  await reload(target);
+
+  await recordAndWaitForFirstSnapshot();
+  info("First snapshot recorded.");
+
+  is(SnapshotsListView.selectedIndex, 0,
+    "A snapshot should be automatically selected after first recording.");
+  is(CallsListView.selectedIndex, -1,
+    "There should be no call item automatically selected in the snapshot.");
+
+  await recordAndWaitForAnotherSnapshot();
+  info("Second snapshot recorded.");
+
+  is(SnapshotsListView.selectedIndex, 0,
+    "A snapshot should not be automatically selected after another recording.");
+  is(CallsListView.selectedIndex, -1,
+    "There should still be no call item automatically selected in the snapshot.");
+
+  const secondSnapshotTarget = SnapshotsListView.getItemAtIndex(1).target;
+  let snapshotSelected = waitForSnapshotSelection();
+  EventUtils.sendMouseEvent({ type: "mousedown" }, secondSnapshotTarget, window);
+
+  await snapshotSelected;
+  info("Second snapshot selected.");
+
+  is(SnapshotsListView.selectedIndex, 1,
+    "The second snapshot should now be selected.");
+  is(CallsListView.selectedIndex, -1,
+    "There should still be no call item automatically selected in the snapshot.");
+
+  const firstDrawCallContents = $(".call-item-contents", CallsListView.getItemAtIndex(2).target);
+  const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, firstDrawCallContents, window);
+
+  await screenshotDisplayed;
+  info("First draw call in the second snapshot selected.");
+
+  is(SnapshotsListView.selectedIndex, 1,
+    "The second snapshot should still be selected.");
+  is(CallsListView.selectedIndex, 2,
+    "The first draw call should now be selected in the snapshot.");
+
+  const firstSnapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
+  snapshotSelected = waitForSnapshotSelection();
+  EventUtils.sendMouseEvent({ type: "mousedown" }, firstSnapshotTarget, window);
+
+  await snapshotSelected;
+  info("First snapshot re-selected.");
+
+  is(SnapshotsListView.selectedIndex, 0,
+    "The first snapshot should now be re-selected.");
+  is(CallsListView.selectedIndex, -1,
+    "There should still be no call item automatically selected in the snapshot.");
+
+  function recordAndWaitForFirstSnapshot() {
+    const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+    const snapshotSelected = waitForSnapshotSelection();
+    SnapshotsListView._onRecordButtonClick();
+    return Promise.all([recordingFinished, snapshotSelected]);
+  }
+
+  function recordAndWaitForAnotherSnapshot() {
+    const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+    SnapshotsListView._onRecordButtonClick();
+    return recordingFinished;
+  }
+
+  function waitForSnapshotSelection() {
+    const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+    const thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
+    const screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
+    return Promise.all([
+      callListPopulated,
+      thumbnailsDisplayed,
+      screenshotDisplayed,
+    ]);
+  }
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-02.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if selecting snapshots in the frontend displays the appropriate data
+ * respective to their recorded animation frame.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  await reload(target);
+
+  SnapshotsListView._onRecordButtonClick();
+  const snapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
+  EventUtils.sendMouseEvent({ type: "mousedown" }, snapshotTarget, window);
+
+  ok(true, "clicking in-progress snapshot does not fail");
+
+  const finished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  SnapshotsListView._onRecordButtonClick();
+  await finished;
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stepping.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the stepping buttons in the call list toolbar work as advertised.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_URL);
+  const { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
+  SnapshotsListView._onRecordButtonClick();
+  await Promise.all([recordingFinished, callListPopulated]);
+
+  checkSteppingButtons(1, 1, 1, 1);
+  is(CallsListView.selectedIndex, -1,
+    "There should be no selected item in the calls list view initially.");
+
+  CallsListView._onResume();
+  checkSteppingButtons(1, 1, 1, 1);
+  is(CallsListView.selectedIndex, 0,
+    "The first draw call should now be selected.");
+
+  CallsListView._onResume();
+  checkSteppingButtons(1, 1, 1, 1);
+  is(CallsListView.selectedIndex, 2,
+    "The second draw call should now be selected.");
+
+  CallsListView._onStepOver();
+  checkSteppingButtons(1, 1, 1, 1);
+  is(CallsListView.selectedIndex, 3,
+    "The next context call should now be selected.");
+
+  CallsListView._onStepOut();
+  checkSteppingButtons(0, 0, 1, 0);
+  is(CallsListView.selectedIndex, 7,
+    "The last context call should now be selected.");
+
+  function checkSteppingButtons(resume, stepOver, stepIn, stepOut) {
+    if (!resume) {
+      is($("#resume").getAttribute("disabled"), "true",
+        "The resume button doesn't have the expected disabled state.");
+    } else {
+      is($("#resume").hasAttribute("disabled"), false,
+        "The resume button doesn't have the expected enabled state.");
+    }
+    if (!stepOver) {
+      is($("#step-over").getAttribute("disabled"), "true",
+        "The stepOver button doesn't have the expected disabled state.");
+    } else {
+      is($("#step-over").hasAttribute("disabled"), false,
+        "The stepOver button doesn't have the expected enabled state.");
+    }
+    if (!stepIn) {
+      is($("#step-in").getAttribute("disabled"), "true",
+        "The stepIn button doesn't have the expected disabled state.");
+    } else {
+      is($("#step-in").hasAttribute("disabled"), false,
+        "The stepIn button doesn't have the expected enabled state.");
+    }
+    if (!stepOut) {
+      is($("#step-out").getAttribute("disabled"), "true",
+        "The stepOut button doesn't have the expected disabled state.");
+    } else {
+      is($("#step-out").hasAttribute("disabled"), false,
+        "The stepOut button doesn't have the expected enabled state.");
+    }
+  }
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-01.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that you can stop a recording that does not have a rAF cycle.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(NO_CANVAS_URL);
+  const { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+  SnapshotsListView._onRecordButtonClick();
+
+  await recordingStarted;
+
+  is($("#empty-notice").hidden, true, "Empty notice not shown");
+  is($("#waiting-notice").hidden, false, "Waiting notice shown");
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+  SnapshotsListView._onRecordButtonClick();
+
+  await Promise.all([recordingFinished, recordingCancelled]);
+
+  ok(true, "Recording stopped and was considered failed.");
+
+  is(SnapshotsListView.itemCount, 0, "No snapshots in the list.");
+  is($("#empty-notice").hidden, false, "Empty notice shown");
+  is($("#waiting-notice").hidden, true, "Waiting notice not shown");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-02.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that a recording that does not have a rAF cycle fails after timeout.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(NO_CANVAS_URL);
+  const { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+  SnapshotsListView._onRecordButtonClick();
+
+  await recordingStarted;
+
+  is($("#empty-notice").hidden, true, "Empty notice not shown");
+  is($("#waiting-notice").hidden, false, "Waiting notice shown");
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+
+  await Promise.all([recordingFinished, recordingCancelled]);
+
+  ok(true, "Recording stopped and was considered failed.");
+
+  is(SnapshotsListView.itemCount, 0, "No snapshots in the list.");
+  is($("#empty-notice").hidden, false, "Empty notice shown");
+  is($("#waiting-notice").hidden, true, "Waiting notice not shown");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-03.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that a recording that has a rAF cycle, but no draw calls, fails
+ * after timeout.
+ */
+
+async function ifTestingSupported() {
+  const { target, panel } = await initCanvasDebuggerFrontend(RAF_NO_CANVAS_URL);
+  const { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
+
+  await reload(target);
+
+  const recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
+  SnapshotsListView._onRecordButtonClick();
+
+  await recordingStarted;
+
+  is($("#empty-notice").hidden, true, "Empty notice not shown");
+  is($("#waiting-notice").hidden, false, "Waiting notice shown");
+
+  const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
+  const recordingCancelled = once(window, EVENTS.SNAPSHOT_RECORDING_CANCELLED);
+
+  await Promise.all([recordingFinished, recordingCancelled]);
+
+  ok(true, "Recording stopped and was considered failed.");
+
+  is(SnapshotsListView.itemCount, 0, "No snapshots in the list.");
+  is($("#empty-notice").hidden, false, "Empty notice shown");
+  is($("#waiting-notice").hidden, true, "Waiting notice not shown");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_profiling-canvas.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if functions inside a single animation frame are recorded and stored
+ * for a canvas context profiling.
+ */
+
+async function ifTestingSupported() {
+  const currentTime = window.performance.now();
+  const { target, front } = await initCanvasDebuggerBackend(SIMPLE_CANVAS_URL);
+
+  const navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  const snapshotActor = await front.recordAnimationFrame();
+  ok(snapshotActor,
+    "A snapshot actor was sent after recording.");
+
+  const animationOverview = await snapshotActor.getOverview();
+  ok(animationOverview,
+    "An animation overview could be retrieved after recording.");
+
+  const functionCalls = animationOverview.calls;
+  ok(functionCalls,
+    "An array of function call actors was sent after recording.");
+  is(functionCalls.length, 8,
+    "The number of function call actors is correct.");
+
+  info("Check the timestamps of function calls");
+
+  for (let i = 0; i < functionCalls.length - 1; i += 2) {
+    ok(functionCalls[i].timestamp > 0, "The timestamp of the called function is larger than 0.");
+    ok(functionCalls[i].timestamp < currentTime, "The timestamp has been minus the frame start time.");
+    ok(functionCalls[i + 1].timestamp >= functionCalls[i].timestamp, "The timestamp of the called function is correct.");
+  }
+
+  await removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/browser_profiling-webgl.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if functions inside a single animation frame are recorded and stored
+ * for a canvas context profiling.
+ */
+
+async function ifTestingSupported() {
+  const currentTime = window.performance.now();
+  info("Start to estimate WebGL drawArrays function.");
+  var { target, front } = await initCanvasDebuggerBackend(WEBGL_DRAW_ARRAYS);
+
+  let navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  let snapshotActor = await front.recordAnimationFrame();
+  ok(snapshotActor,
+    "A snapshot actor was sent after recording.");
+
+  let animationOverview = await snapshotActor.getOverview();
+  ok(animationOverview,
+    "An animation overview could be retrieved after recording.");
+
+  let functionCalls = animationOverview.calls;
+  ok(functionCalls,
+    "An array of function call actors was sent after recording.");
+
+  testFunctionCallTimestamp(functionCalls, currentTime);
+
+  info("Check triangle and vertex counts in drawArrays()");
+  is(animationOverview.primitive.tris, 5, "The count of triangles is correct.");
+  is(animationOverview.primitive.vertices, 26, "The count of vertices is correct.");
+  is(animationOverview.primitive.points, 4, "The count of points is correct.");
+  is(animationOverview.primitive.lines, 8, "The count of lines is correct.");
+
+  await removeTab(target.tab);
+
+  info("Start to estimate WebGL drawElements function.");
+  const result = await initCanvasDebuggerBackend(WEBGL_DRAW_ELEMENTS);
+  target = result.target;
+  front = result.front;
+
+  navigated = once(target, "navigate");
+
+  await front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  await navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  snapshotActor = await front.recordAnimationFrame();
+  ok(snapshotActor,
+    "A snapshot actor was sent after recording.");
+
+  animationOverview = await snapshotActor.getOverview();
+  ok(animationOverview,
+    "An animation overview could be retrieved after recording.");
+
+  functionCalls = animationOverview.calls;
+  ok(functionCalls,
+    "An array of function call actors was sent after recording.");
+
+  testFunctionCallTimestamp(functionCalls, currentTime);
+
+  info("Check triangle and vertex counts in drawElements()");
+  is(animationOverview.primitive.tris, 5, "The count of triangles is correct.");
+  is(animationOverview.primitive.vertices, 26, "The count of vertices is correct.");
+  is(animationOverview.primitive.points, 4, "The count of points is correct.");
+  is(animationOverview.primitive.lines, 8, "The count of lines is correct.");
+
+  await removeTab(target.tab);
+  finish();
+}
+
+function testFunctionCallTimestamp(functionCalls, currentTime) {
+  info("Check the timestamps of function calls");
+
+  for (let i = 0; i < functionCalls.length - 1; i += 2) {
+    ok(functionCalls[i].timestamp > 0, "The timestamp of the called function is larger than 0.");
+    ok(functionCalls[i].timestamp < currentTime, "The timestamp has been minus the frame start time.");
+    ok(functionCalls[i + 1].timestamp >= functionCalls[i].timestamp, "The timestamp of the called function is correct.");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/call-watcher-actor.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const protocol = require("devtools/shared/protocol");
+const {
+  callWatcherSpec,
+} = require("chrome://mochitests/content/browser/devtools/client/canvasdebugger/test/call-watcher-spec");
+const {CallWatcher} = require("devtools/server/actors/utils/call-watcher");
+
+/**
+ * This actor observes function calls on certain objects or globals.
+ * It wraps the CallWatcher Helper so that it can be observed by tests
+ */
+exports.CallWatcherActor = protocol.ActorClassWithSpec(callWatcherSpec, {
+  initialize: function(conn, targetActor) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    CallWatcher.call(this, conn, targetActor);
+  },
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+    this.finalize();
+  },
+  ...CallWatcher.prototype,
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/call-watcher-front.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+  callWatcherSpec,
+} = require("chrome://mochitests/content/browser/devtools/client/canvasdebugger/test/call-watcher-spec");
+const protocol = require("devtools/shared/protocol");
+
+/**
+ * The corresponding Front object for the CallWatcherActor.
+ */
+class CallWatcherFront extends protocol.FrontClassWithSpec(callWatcherSpec) {
+  constructor(client) {
+    super(client);
+
+    // Attribute name from which to retrieve the actorID out of the target actor's form
+    this.formAttributeName = "callWatcherActor";
+  }
+}
+exports.CallWatcherFront = CallWatcherFront;
+protocol.registerFront(CallWatcherFront);
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/call-watcher-spec.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const protocol = require("devtools/shared/protocol");
+const { Arg, RetVal, Option, generateActorSpec } = protocol;
+
+const callWatcherSpec = generateActorSpec({
+  typeName: "call-watcher",
+
+  events: {
+    /**
+     * Events emitted when the `onCall` function isn't provided.
+     */
+    "call": {
+      type: "call",
+      function: Arg(0, "function-call"),
+    },
+  },
+
+  methods: {
+    setup: {
+      request: {
+        tracedGlobals: Option(0, "nullable:array:string"),
+        tracedFunctions: Option(0, "nullable:array:string"),
+        startRecording: Option(0, "boolean"),
+        performReload: Option(0, "boolean"),
+        holdWeak: Option(0, "boolean"),
+        storeCalls: Option(0, "boolean"),
+      },
+      oneway: true,
+    },
+    finalize: {
+      oneway: true,
+    },
+    isRecording: {
+      response: RetVal("boolean"),
+    },
+    initTimestampEpoch: {},
+    resumeRecording: {},
+    pauseRecording: {
+      response: { calls: RetVal("array:function-call") },
+    },
+    eraseRecording: {},
+  },
+});
+
+exports.callWatcherSpec = callWatcherSpec;
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_no-canvas.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Canvas inspector test page</title>
+  </head>
+
+  <body>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_raf-begin.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Canvas inspector test page</title>
+  </head>
+
+  <body>
+    <canvas width="128" height="128"></canvas>
+
+    <script type="text/javascript">
+      "use strict";
+
+      var ctx = document.querySelector("canvas").getContext("2d");
+
+      function drawRect(fill, size) {
+        ctx.fillStyle = fill;
+        ctx.fillRect(size[0], size[1], size[2], size[3]);
+      }
+
+      function drawScene() {
+        window.requestAnimationFrame(drawScene);
+        ctx.clearRect(0, 0, 128, 128);
+        drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
+        drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+        drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+      }
+
+      function start() {
+  window.requestAnimationFrame(drawScene);
+}
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_raf-no-canvas.html
@@ -0,0 +1,20 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Canvas inspector test page</title>
+  </head>
+
+  <body>
+    <script>
+      function render() {
+  window.requestAnimationFrame(render);
+}
+      window.requestAnimationFrame(render);
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_settimeout.html
@@ -0,0 +1,37 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Canvas inspector test page</title>
+  </head>
+
+  <body>
+    <canvas width="128" height="128"></canvas>
+
+    <script type="text/javascript">
+      "use strict";
+
+      var ctx = document.querySelector("canvas").getContext("2d");
+
+      function drawRect(fill, size) {
+        ctx.fillStyle = fill;
+        ctx.fillRect(size[0], size[1], size[2], size[3]);
+      }
+
+      function drawScene() {
+        ctx.clearRect(0, 0, 128, 128);
+        drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
+        drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+        drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+
+        window.setTimeout(drawScene, 50);
+      }
+
+      drawScene();
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html
@@ -0,0 +1,34 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Canvas inspector test page</title>
+  </head>
+
+  <body>
+    <canvas width="128" height="128"></canvas>
+
+    <script type="text/javascript">
+      "use strict";
+
+      var ctx = document.querySelector("canvas").getContext("2d");
+
+      function drawRect(fill, size) {
+        ctx.fillStyle = fill;
+        ctx.fillRect(size[0], size[1], size[2], size[3]);
+      }
+
+      function drawScene() {
+        ctx.clearRect(0, 0, 4, 4);
+        drawRect("rgb(192, 192, 192)", [0, 0, 1, 1]);
+        window.requestAnimationFrame(drawScene);
+      }
+
+      drawScene();
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html
@@ -0,0 +1,46 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Canvas inspector test page</title>
+  </head>
+
+  <body>
+    <canvas width="128" height="128"></canvas>
+
+    <script type="text/javascript">
+      "use strict";
+
+      var ctx = document.querySelector("canvas").getContext("2d");
+
+      function drawRect(fill, size) {
+        function A() {
+          function B() {
+            function C() {
+              ctx.fillStyle = fill;
+              ctx.fillRect(size[0], size[1], size[2], size[3]);
+            }
+            C();
+          }
+          B();
+        }
+        A();
+      }
+
+      function drawScene() {
+        ctx.clearRect(0, 0, 128, 128);
+        drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
+        drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+        drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+
+        window.requestAnimationFrame(drawScene);
+      }
+
+      drawScene();
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html
@@ -0,0 +1,37 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Canvas inspector test page</title>
+  </head>
+
+  <body>
+    <canvas width="128" height="128"></canvas>
+
+    <script type="text/javascript">
+      "use strict";
+
+      var ctx = document.querySelector("canvas").getContext("2d");
+
+      function drawRect(fill, size) {
+        ctx.fillStyle = fill;
+        ctx.fillRect(size[0], size[1], size[2], size[3]);
+      }
+
+      function drawScene() {
+        ctx.clearRect(0, 0, 128, 128);
+        drawRect("rgba(255, 255, 255, 0)", [0, 0, 128, 128]);
+        drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+        drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+
+        window.requestAnimationFrame(drawScene);
+      }
+
+      drawScene();
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_simple-canvas.html
@@ -0,0 +1,37 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Canvas inspector test page</title>
+  </head>
+
+  <body>
+    <canvas width="128" height="128"></canvas>
+
+    <script type="text/javascript">
+      "use strict";
+
+      var ctx = document.querySelector("canvas").getContext("2d");
+
+      function drawRect(fill, size) {
+        ctx.fillStyle = fill;
+        ctx.fillRect(size[0], size[1], size[2], size[3]);
+      }
+
+      function drawScene() {
+        ctx.clearRect(0, 0, 128, 128);
+        drawRect("rgb(192, 192, 192)", [0, 0, 128, 128]);
+        drawRect("rgba(0, 0, 192, 0.5)", [30, 30, 55, 50]);
+        drawRect("rgba(192, 0, 0, 0.5)", [10, 10, 55, 50]);
+
+        window.requestAnimationFrame(drawScene);
+      }
+
+      drawScene();
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_webgl-bindings.html
@@ -0,0 +1,61 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>WebGL editor test page</title>
+  </head>
+
+  <body>
+    <canvas id="canvas" width="1024" height="1024"></canvas>
+
+    <script type="text/javascript">
+      "use strict";
+
+      let canvas, gl;
+      let customFramebuffer;
+      let customRenderbuffer;
+      let customTexture;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+        gl.clearColor(1.0, 0.0, 0.0, 1.0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        customFramebuffer = gl.createFramebuffer();
+        gl.bindFramebuffer(gl.FRAMEBUFFER, customFramebuffer);
+
+        customRenderbuffer = gl.createRenderbuffer();
+        gl.bindRenderbuffer(gl.RENDERBUFFER, customRenderbuffer);
+        gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 1024, 1024);
+
+        customTexture = gl.createTexture();
+        gl.bindTexture(gl.TEXTURE_2D, customTexture);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1024, 1024, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
+
+        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, customTexture, 0);
+        gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, customRenderbuffer);
+
+        gl.viewport(128, 256, 384, 512);
+        gl.clearColor(0.0, 1.0, 0.0, 1.0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        drawScene();
+      };
+
+      function drawScene() {
+        gl.clearColor(0.0, 0.0, 1.0, 1.0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+        window.requestAnimationFrame(drawScene);
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html
@@ -0,0 +1,187 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>WebGL editor test page</title>
+  </head>
+
+  <body>
+    <canvas id="canvas" width="128" height="128"></canvas>
+    <script id="shader-fs" type="x-shader/x-fragment">
+      precision mediump float;
+      uniform vec4 mtrColor;
+
+      void main(void) {
+        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * mtrColor;
+      }
+    </script>
+    <script id="shader-vs" type="x-shader/x-vertex">
+      attribute vec3 aVertexPosition;
+
+      void main(void) {
+        gl_PointSize = 5.0;
+        gl_Position = vec4(aVertexPosition, 1.0);
+      }
+    </script>
+    <script type="text/javascript">
+      "use strict";
+
+      let canvas, gl, shaderProgram;
+      let triangleVertexPositionBuffer, squareVertexPositionBuffer;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+        gl.viewportWidth = canvas.width;
+        gl.viewportHeight = canvas.height;
+      
+        initShaders();
+        initBuffers();
+
+        gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
+        gl.clearColor(0.0, 0.0, 0.0, 1.0);
+        gl.disable(gl.DEPTH_TEST);
+        drawScene();
+      };
+
+      function getShader(gl, id) {
+        var shaderScript = document.getElementById(id);
+        if (!shaderScript) {
+          return null;
+        }
+
+        var str = "";
+        var k = shaderScript.firstChild;
+        while (k) {
+          if (k.nodeType == 3) {
+            str += k.textContent;
+          }
+          k = k.nextSibling;
+        }
+
+        var shader;
+        if (shaderScript.type == "x-shader/x-fragment") {
+          shader = gl.createShader(gl.FRAGMENT_SHADER);
+        } else if (shaderScript.type == "x-shader/x-vertex") {
+          shader = gl.createShader(gl.VERTEX_SHADER);
+        } else {
+          return null;
+        }
+
+        gl.shaderSource(shader, str);
+        gl.compileShader(shader);
+
+        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+          alert(gl.getShaderInfoLog(shader));
+          return null;
+        }
+
+        return shader;
+      }
+      
+      function initShaders() {
+        var fragmentShader = getShader(gl, "shader-fs");
+        var vertexShader = getShader(gl, "shader-vs");
+
+        shaderProgram = gl.createProgram();
+        gl.attachShader(shaderProgram, vertexShader);
+        gl.attachShader(shaderProgram, fragmentShader);
+        gl.linkProgram(shaderProgram);
+
+        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
+          alert("Could not initialise shaders");
+        }
+
+        gl.useProgram(shaderProgram);
+
+        shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
+        shaderProgram.pMaterialColor = gl.getUniformLocation(shaderProgram, "mtrColor");
+        gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
+      }
+
+      function initBuffers() {
+          // Create triangle vertex/index buffer
+        triangleVertexPositionBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+        var vertices = [
+               0.0,  0.5,  0.0,
+               -0.5, -0.5,  0.0,
+               0.5, -0.5,  0.0,
+        ];
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+        triangleVertexPositionBuffer.itemSize = 3;
+        triangleVertexPositionBuffer.numItems = 3;
+
+          // Create square vertex/index buffer
+        squareVertexPositionBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        vertices = [
+               0.8,  0.8,  0.0,
+               -0.8,  0.8,  0.0,
+               0.8, -0.8,  0.0,
+               -0.8, -0.8,  0.0,
+        ];
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+        squareVertexPositionBuffer.itemSize = 3;
+        squareVertexPositionBuffer.numItems = 4;
+      }
+
+      function drawScene() {
+        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
+
+        // DrawArrays
+        // --------------
+        // draw square - triangle strip
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 1, 1, 1, 1);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
+
+        // draw square - triangle fan
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 0, 1);
+        gl.drawArrays(gl.TRIANGLE_FAN, 0, squareVertexPositionBuffer.numItems);
+
+        // draw triangle
+        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 0, 1);
+        gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
+
+        // draw points
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 1, 1);
+        gl.drawArrays(gl.POINTS, 0, squareVertexPositionBuffer.numItems);
+
+        // draw lines
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 0, 0, 1, 1);
+        gl.lineWidth(8.0);
+        gl.drawArrays(gl.LINES, 0, squareVertexPositionBuffer.numItems);
+
+        // draw line strip
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 0.9, 0.6, 0, 1);
+        gl.lineWidth(3.0);
+        gl.drawArrays(gl.LINE_STRIP, 0, squareVertexPositionBuffer.numItems);
+
+        // draw line loop
+        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 1, 1);
+        gl.lineWidth(3.0);
+        gl.drawArrays(gl.LINE_LOOP, 0, triangleVertexPositionBuffer.numItems);
+
+        window.requestAnimationFrame(drawScene);
+      }
+    </script>
+  </body>
+
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_webgl-drawElements.html
@@ -0,0 +1,224 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>WebGL editor test page</title>
+  </head>
+
+  <body>
+    <canvas id="canvas" width="128" height="128"></canvas>
+    <script id="shader-fs" type="x-shader/x-fragment">
+      precision mediump float;
+      uniform vec4 mtrColor;
+
+      void main(void) {
+        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * mtrColor;
+      }
+    </script>
+    <script id="shader-vs" type="x-shader/x-vertex">
+      attribute vec3 aVertexPosition;
+
+      void main(void) {
+        gl_PointSize = 5.0;
+        gl_Position = vec4(aVertexPosition, 1.0);
+      }
+    </script>
+    <script type="text/javascript">
+      "use strict";
+
+      let canvas, gl, shaderProgram;
+      let triangleVertexPositionBuffer, squareVertexPositionBuffer;
+      let triangleIndexBuffer;
+      let squareIndexBuffer, squareStripIndexBuffer, squareFanIndexBuffer;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+        gl.viewportWidth = canvas.width;
+        gl.viewportHeight = canvas.height;
+      
+        initShaders();
+        initBuffers();
+
+        gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
+        gl.clearColor(0.0, 0.0, 0.0, 1.0);
+        gl.disable(gl.DEPTH_TEST);
+        drawScene();
+      };
+
+      function getShader(gl, id) {
+        var shaderScript = document.getElementById(id);
+        if (!shaderScript) {
+          return null;
+        }
+
+        var str = "";
+        var k = shaderScript.firstChild;
+        while (k) {
+          if (k.nodeType == 3) {
+            str += k.textContent;
+          }
+          k = k.nextSibling;
+        }
+
+        var shader;
+        if (shaderScript.type == "x-shader/x-fragment") {
+          shader = gl.createShader(gl.FRAGMENT_SHADER);
+        } else if (shaderScript.type == "x-shader/x-vertex") {
+          shader = gl.createShader(gl.VERTEX_SHADER);
+        } else {
+          return null;
+        }
+
+        gl.shaderSource(shader, str);
+        gl.compileShader(shader);
+
+        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+          alert(gl.getShaderInfoLog(shader));
+          return null;
+        }
+
+        return shader;
+      }
+      
+      function initShaders() {
+        var fragmentShader = getShader(gl, "shader-fs");
+        var vertexShader = getShader(gl, "shader-vs");
+
+        shaderProgram = gl.createProgram();
+        gl.attachShader(shaderProgram, vertexShader);
+        gl.attachShader(shaderProgram, fragmentShader);
+        gl.linkProgram(shaderProgram);
+
+        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
+          alert("Could not initialise shaders");
+        }
+
+        gl.useProgram(shaderProgram);
+
+        shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
+        shaderProgram.pMaterialColor = gl.getUniformLocation(shaderProgram, "mtrColor");
+        gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
+      }
+
+      function initBuffers() {
+          // Create triangle vertex/index buffer
+        triangleVertexPositionBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+        var vertices = [
+             0.0,  0.5,  0.0,
+             -0.5, -0.5,  0.0,
+             0.5, -0.5,  0.0,
+        ];
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+        triangleVertexPositionBuffer.itemSize = 3;
+        triangleVertexPositionBuffer.numItems = 3;
+
+        triangleIndexBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBuffer);
+        var indices = [
+              0, 1, 2,
+        ];
+        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
+        triangleIndexBuffer.itemSize = 1;
+        triangleIndexBuffer.numItems = 3;
+
+          // Create square vertex/index buffer
+        squareVertexPositionBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        vertices = [
+             0.8,  0.8,  0.0,
+             -0.8,  0.8,  0.0,
+             0.8, -0.8,  0.0,
+             -0.8, -0.8,  0.0,
+        ];
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+        squareVertexPositionBuffer.itemSize = 3;
+        squareVertexPositionBuffer.numItems = 4;
+
+        squareIndexBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareIndexBuffer);
+        indices = [
+            0, 1, 2,
+            1, 3, 2,
+        ];
+        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
+        squareIndexBuffer.itemSize = 1;
+        squareIndexBuffer.numItems = 6;
+
+        squareStripIndexBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+        indices = [
+            0, 1, 2, 3,
+        ];
+        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
+        squareStripIndexBuffer.itemSize = 1;
+        squareStripIndexBuffer.numItems = 4;
+      }
+
+      function drawScene() {
+        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
+
+        // DrawElements
+        // --------------
+        // draw triangle
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 1, 1, 1, 1);
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareIndexBuffer);
+        gl.drawElements(gl.TRIANGLES, squareIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+        // draw square - triangle strip
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 0, 1);
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+        gl.drawElements(gl.TRIANGLE_FAN, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+        // draw square - triangle fan
+        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 0, 1);
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBuffer);
+        gl.drawElements(gl.TRIANGLE_FAN, triangleIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+        // draw points
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 1, 0, 1, 1);
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+        gl.drawElements(gl.POINTS, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+        // draw lines
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 0, 0, 1, 1);
+        gl.lineWidth(8.0);
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+        gl.drawElements(gl.LINES, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+        // draw line strip
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 0.9, 0.6, 0, 1);
+        gl.lineWidth(3.0);
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareStripIndexBuffer);
+        gl.drawElements(gl.LINE_STRIP, squareStripIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+        // draw line loop
+        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
+        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
+        gl.uniform4f(shaderProgram.pMaterialColor, 0, 1, 1, 1);
+        gl.lineWidth(3.0);
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBuffer);
+        gl.drawElements(gl.LINE_LOOP, triangleIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+
+        window.requestAnimationFrame(drawScene);
+      }
+    </script>
+  </body>
+
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/doc_webgl-enum.html
@@ -0,0 +1,34 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>WebGL editor test page</title>
+  </head>
+
+  <body>
+    <canvas id="canvas" width="128" height="128"></canvas>
+
+    <script type="text/javascript">
+      "use strict";
+
+      let canvas, gl;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+        gl.clearColor(0.0, 0.0, 0.0, 1.0);
+        drawScene();
+      };
+
+      function drawScene() {
+        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
+        gl.bindTexture(gl.TEXTURE_2D, null);
+        window.requestAnimationFrame(drawScene);
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/canvasdebugger/test/head.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../shared/test/shared-head.js */
+/* import-globals-from ../../debugger/new/test/mochitest/helpers/context.js */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+  this);
+
+// Import helpers for the new debugger
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js",
+  this);
+
+var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+var { DebuggerClient } = require("devtools/shared/client/debugger-client");
+var { DebuggerServer } = require("devtools/server/main");
+var { METHOD_FUNCTION } = require("devtools/shared/fronts/function-call");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+var { isWebGLSupported } = require("devtools/client/shared/webgl-utils");
+
+const EXAMPLE_URL = "http://example.com/browser/devtools/client/canvasdebugger/test/";
+const SET_TIMEOUT_URL = EXAMPLE_URL + "doc_settimeout.html";
+const NO_CANVAS_URL = EXAMPLE_URL + "doc_no-canvas.html";
+const RAF_NO_CANVAS_URL = EXAMPLE_URL + "doc_raf-no-canvas.html";
+const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
+const SIMPLE_BITMASKS_URL = EXAMPLE_URL + "doc_simple-canvas-bitmasks.html";
+const SIMPLE_CANVAS_TRANSPARENT_URL = EXAMPLE_URL + "doc_simple-canvas-transparent.html";
+const SIMPLE_CANVAS_DEEP_STACK_URL = EXAMPLE_URL + "doc_simple-canvas-deep-stack.html";
+const WEBGL_ENUM_URL = EXAMPLE_URL + "doc_webgl-enum.html";
+const WEBGL_BINDINGS_URL = EXAMPLE_URL + "doc_webgl-bindings.html";
+const WEBGL_DRAW_ARRAYS = EXAMPLE_URL + "doc_webgl-drawArrays.html";
+const WEBGL_DRAW_ELEMENTS = EXAMPLE_URL + "doc_webgl-drawElements.html";
+const RAF_BEGIN_URL = EXAMPLE_URL + "doc_raf-begin.html";
+
+// Disable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+var gToolEnabled = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
+
+registerCleanupFunction(() => {
+  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+  Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", gToolEnabled);
+
+  // Some of yhese tests use a lot of memory due to GL contexts, so force a GC
+  // to help fragmentation.
+  info("Forcing GC after canvas debugger test.");
+  Cu.forceGC();
+});
+
+function handleError(aError) {
+  ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+  finish();
+}
+
+var gRequiresWebGL = false;
+
+function ifTestingSupported() {
+  ok(false, "You need to define a 'ifTestingSupported' function.");
+  finish();
+}
+
+function ifTestingUnsupported() {
+  todo(false, "Skipping test because some required functionality isn't supported.");
+  finish();
+}
+
+async function test() {
+  const generator = isTestingSupported() ? ifTestingSupported : ifTestingUnsupported;
+  try {
+    await generator();
+  } catch (e) {
+    handleError(e);
+  }
+}
+
+function createCanvas() {
+  return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+}
+
+function isTestingSupported() {
+  if (!gRequiresWebGL) {
+    info("This test does not require WebGL support.");
+    return true;
+  }
+
+  const supported = isWebGLSupported(document);
+
+  info("This test requires WebGL support.");
+  info("Apparently, WebGL is" + (supported ? "" : " not") + " supported.");
+  return supported;
+}
+
+function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") {
+  executeSoon(() => content.history[aDirection]());
+  return once(aTarget, aWaitForTargetEvent);
+}
+
+function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
+  executeSoon(() => aTarget.navigateTo({ url: aUrl }));
+  return once(aTarget, aWaitForTargetEvent);
+}
+
+function reload(aTarget, aWaitForTargetEvent = "navigate") {
+  executeSoon(() => aTarget.reload());
+  return once(aTarget, aWaitForTargetEvent);
+}
+
+function initServer() {
+  DebuggerServer.init();
+  DebuggerServer.registerAllActors();
+}
+
+function initCallWatcherBackend(aUrl) {
+  info("Initializing a call watcher front.");
+  initServer();
+
+  return (async function() {
+    const tab = await addTab(aUrl);
+
+    await registerActorInContentProcess("chrome://mochitests/content/browser/devtools/client/canvasdebugger/test/call-watcher-actor.js", {
+      prefix: "callWatcher",
+      constructor: "CallWatcherActor",
+      type: { target: true },
+    });
+
+    const target = await TargetFactory.forTab(tab);
+    await target.attach();
+
+    // Load the Front module in order to register it and have getFront to find it.
+    require("chrome://mochitests/content/browser/devtools/client/canvasdebugger/test/call-watcher-front.js");
+
+    const front = await target.getFront("call-watcher");
+    return { target, front };
+  })();
+}
+
+function initCanvasDebuggerBackend(aUrl) {
+  info("Initializing a canvas debugger front.");
+  initServer();
+
+  return (async function() {
+    const tab = await addTab(aUrl);
+    const target = await TargetFactory.forTab(tab);
+    await target.attach();
+
+    const front = await target.getFront("canvas");
+    return { target, front };
+  })();
+}
+
+function initCanvasDebuggerFrontend(aUrl) {
+  info("Initializing a canvas debugger pane.");
+
+  return (async function() {
+    const tab = await addTab(aUrl);
+    const target = await TargetFactory.forTab(tab);
+
+    await target.attach();
+
+    Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
+    const toolbox = await gDevTools.showToolbox(target, "canvasdebugger");
+    const panel = toolbox.getCurrentPanel();
+    return { target, panel };
+  })();
+}
+
+function teardown({target}) {
+  info("Destroying the specified canvas debugger.");
+
+  const {tab} = target;
+  return gDevTools.closeToolbox(target).then(() => {
+    removeTab(tab);
+  });
+}
+
+function getSourceActor(aSources, aURL) {
+  const item = aSources.getItemForAttachment(a => a.source.url === aURL);
+  return item ? item.value : null;
+}
+
+async function validateDebuggerLocation(dbg, url, line) {
+  const location = dbg.selectors.getSelectedLocation(dbg.getState());
+  const sourceUrl = dbg.selectors.getSelectedSource(dbg.getState()).url;
+
+  is(sourceUrl, url,
+    "The expected source was shown in the debugger.");
+  is(location.line, line,
+    "The expected source line is highlighted in the debugger.");
+}
--- a/devtools/client/debugger/new/src/client/firefox/types.js
+++ b/devtools/client/debugger/new/src/client/firefox/types.js
@@ -152,39 +152,44 @@ export type ResumedPacket = {
 export type FramesResponse = {
   frames: FramePacket[],
   from: ActorId
 };
 
 export type TabPayload = {
   actor: ActorId,
   animationsActor: ActorId,
+  callWatcherActor: ActorId,
+  canvasActor: ActorId,
   consoleActor: ActorId,
   cssPropertiesActor: ActorId,
   cssUsageActor: ActorId,
   directorManagerActor: ActorId,
   emulationActor: ActorId,
   eventLoopLagActor: ActorId,
   framerateActor: ActorId,
+  gcliActor: ActorId,
   inspectorActor: ActorId,
   memoryActor: ActorId,
   monitorActor: ActorId,
   outerWindowID: number,
   performanceActor: ActorId,
   performanceEntriesActor: ActorId,
   profilerActor: ActorId,
   promisesActor: ActorId,
   reflowActor: ActorId,
   storageActor: ActorId,
   styleEditorActor: ActorId,
   styleSheetsActor: ActorId,
   timelineActor: ActorId,
   title: string,
   url: URL,
-  webExtensionInspectedWindowActor: ActorId
+  webExtensionInspectedWindowActor: ActorId,
+  webaudioActor: ActorId,
+  webglActor: ActorId
 };
 
 /**
  * Actions
  * @memberof firefox
  * @static
  */
 export type Actions = {
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -1,23 +1,26 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Services = require("Services");
 const osString = Services.appinfo.OS;
+const { Cu } = require("chrome");
 
 // Panels
 loader.lazyGetter(this, "OptionsPanel", () => require("devtools/client/framework/toolbox-options").OptionsPanel);
 loader.lazyGetter(this, "InspectorPanel", () => require("devtools/client/inspector/panel").InspectorPanel);
 loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/client/webconsole/panel").WebConsolePanel);
 loader.lazyGetter(this, "NewDebuggerPanel", () => require("devtools/client/debugger/new/panel").DebuggerPanel);
 loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/client/styleeditor/panel").StyleEditorPanel);
+loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/client/canvasdebugger/panel").CanvasDebuggerPanel);
+loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/client/webaudioeditor/panel").WebAudioEditorPanel);
 loader.lazyGetter(this, "MemoryPanel", () => require("devtools/client/memory/panel").MemoryPanel);
 loader.lazyGetter(this, "PerformancePanel", () => require("devtools/client/performance/panel").PerformancePanel);
 loader.lazyGetter(this, "NewPerformancePanel", () => require("devtools/client/performance-new/panel").PerformancePanel);
 loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/client/netmonitor/panel").NetMonitorPanel);
 loader.lazyGetter(this, "StoragePanel", () => require("devtools/client/storage/panel").StoragePanel);
 loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/client/scratchpad/panel").ScratchpadPanel);
 loader.lazyGetter(this, "DomPanel", () => require("devtools/client/dom/panel").DomPanel);
 loader.lazyGetter(this, "AccessibilityPanel", () => require("devtools/client/accessibility/panel").AccessibilityPanel);
@@ -31,16 +34,19 @@ loader.lazyRequireGetter(this, "Responsi
 loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 
 const {MultiLocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new MultiLocalizationHelper(
   "devtools/client/locales/startup.properties",
   "devtools/startup/locales/key-shortcuts.properties"
 );
 
+// URL to direct people to the deprecated tools panel
+const DEPRECATION_URL = "https://developer.mozilla.org/en-US/docs/Tools/Deprecated_tools";
+
 var Tools = {};
 exports.Tools = Tools;
 
 // Definitions
 Tools.options = {
   id: "options",
   ordinal: 0,
   url: "chrome://devtools/content/framework/toolbox-options.xhtml",
@@ -166,16 +172,66 @@ Tools.styleEditor = {
     return target.hasActor("styleSheets");
   },
 
   build: function(iframeWindow, toolbox) {
     return new StyleEditorPanel(iframeWindow, toolbox);
   },
 };
 
+Tools.shaderEditor = {
+  id: "shadereditor",
+  deprecated: true,
+  deprecationURL: DEPRECATION_URL,
+  ordinal: 5,
+  visibilityswitch: "devtools.shadereditor.enabled",
+  icon: "chrome://devtools/skin/images/tool-shadereditor.svg",
+  url: "chrome://devtools/content/shadereditor/index.xul",
+  label: l10n("ToolboxShaderEditor.label"),
+  panelLabel: l10n("ToolboxShaderEditor.panelLabel"),
+  tooltip: l10n("ToolboxShaderEditor.tooltip"),
+
+  isTargetSupported: function(target) {
+    return target.hasActor("webgl") && !target.chrome;
+  },
+
+  build: function(iframeWindow, toolbox) {
+    const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+    const browserRequire = BrowserLoader({
+      baseURI: "resource://devtools/client/shadereditor/",
+      window: iframeWindow,
+    }).require;
+    const { ShaderEditorPanel } = browserRequire("devtools/client/shadereditor/panel");
+    return new ShaderEditorPanel(toolbox);
+  },
+};
+
+Tools.canvasDebugger = {
+  id: "canvasdebugger",
+  deprecated: true,
+  deprecationURL: DEPRECATION_URL,
+  ordinal: 6,
+  visibilityswitch: "devtools.canvasdebugger.enabled",
+  icon: "chrome://devtools/skin/images/tool-canvas.svg",
+  url: "chrome://devtools/content/canvasdebugger/index.xul",
+  label: l10n("ToolboxCanvasDebugger.label"),
+  panelLabel: l10n("ToolboxCanvasDebugger.panelLabel"),
+  tooltip: l10n("ToolboxCanvasDebugger.tooltip"),
+
+  // Hide the Canvas Debugger in the Add-on Debugger and Browser Toolbox
+  // (bug 1047520).
+  isTargetSupported: function(target) {
+    return target.hasActor("canvas") && !target.chrome;
+  },
+
+  build: function(iframeWindow, toolbox) {
+    return new CanvasDebuggerPanel(iframeWindow, toolbox);
+  },
+};
+
 Tools.performance = {
  id: "performance",
  ordinal: 7,
  icon: "chrome://devtools/skin/images/tool-profiler.svg",
  visibilityswitch: "devtools.performance.enabled",
  label: l10n("performance.label"),
  panelLabel: l10n("performance.panelLabel"),
  get tooltip() {
@@ -284,16 +340,37 @@ Tools.storage = {
            (target.hasActor("storage") && target.getTrait("storageInspector"));
   },
 
   build: function(iframeWindow, toolbox) {
     return new StoragePanel(iframeWindow, toolbox);
   },
 };
 
+Tools.webAudioEditor = {
+  id: "webaudioeditor",
+  deprecated: true,
+  deprecationURL: DEPRECATION_URL,
+  ordinal: 11,
+  visibilityswitch: "devtools.webaudioeditor.enabled",
+  icon: "chrome://devtools/skin/images/tool-webaudio.svg",
+  url: "chrome://devtools/content/webaudioeditor/index.xul",
+  label: l10n("ToolboxWebAudioEditor1.label"),
+  panelLabel: l10n("ToolboxWebAudioEditor1.panelLabel"),
+  tooltip: l10n("ToolboxWebAudioEditor1.tooltip"),
+
+  isTargetSupported: function(target) {
+    return !target.chrome && target.hasActor("webaudio");
+  },
+
+  build: function(iframeWindow, toolbox) {
+    return new WebAudioEditorPanel(iframeWindow, toolbox);
+  },
+};
+
 Tools.scratchpad = {
   id: "scratchpad",
   ordinal: 12,
   visibilityswitch: "devtools.scratchpad.enabled",
   icon: "chrome://devtools/skin/images/tool-scratchpad.svg",
   url: "chrome://devtools/content/scratchpad/index.xul",
   label: l10n("scratchpad.label"),
   panelLabel: l10n("scratchpad.panelLabel"),
@@ -384,16 +461,19 @@ Tools.application = {
 };
 
 var defaultTools = [
   Tools.options,
   Tools.webConsole,
   Tools.inspector,
   Tools.jsdebugger,
   Tools.styleEditor,
+  Tools.shaderEditor,
+  Tools.canvasDebugger,
+  Tools.webAudioEditor,
   Tools.performance,
   Tools.netMonitor,
   Tools.storage,
   Tools.scratchpad,
   Tools.memory,
   Tools.dom,
   Tools.accessibility,
   Tools.application,
--- a/devtools/client/framework/test/browser_target_support.js
+++ b/devtools/client/framework/test/browser_target_support.js
@@ -5,36 +5,36 @@
 
 // Test support methods on Target, such as `hasActor`, `getActorDescription`,
 // `actorHasMethod` and `getTrait`.
 
 async function testTarget(client, target) {
   await target.attach();
 
   is(target.hasActor("inspector"), true, "target.hasActor() true when actor exists.");
-  is(target.hasActor("storage"), true, "target.hasActor() true when actor exists.");
+  is(target.hasActor("webaudio"), true, "target.hasActor() true when actor exists.");
   is(target.hasActor("notreal"), false, "target.hasActor() false when actor does not exist.");
   // Create a front to ensure the actor is loaded
-  await target.getFront("storage");
+  await target.getFront("webaudio");
 
-  let desc = await target.getActorDescription("storage");
-  is(desc.typeName, "storage",
+  let desc = await target.getActorDescription("webaudio");
+  is(desc.typeName, "webaudio",
     "target.getActorDescription() returns definition data for corresponding actor");
-  is(desc.events["stores-update"].type, "storesUpdate",
+  is(desc.events["start-context"].type, "startContext",
     "target.getActorDescription() returns event data for corresponding actor");
 
   desc = await target.getActorDescription("nope");
   is(desc, undefined, "target.getActorDescription() returns undefined for non-existing actor");
   desc = await target.getActorDescription();
   is(desc, undefined, "target.getActorDescription() returns undefined for undefined actor");
 
-  let hasMethod = await target.actorHasMethod("storage", "listStores");
+  let hasMethod = await target.actorHasMethod("audionode", "getType");
   is(hasMethod, true,
     "target.actorHasMethod() returns true for existing actor with method");
-  hasMethod = await target.actorHasMethod("localStorage", "nope");
+  hasMethod = await target.actorHasMethod("audionode", "nope");
   is(hasMethod, false,
     "target.actorHasMethod() returns false for existing actor with no method");
   hasMethod = await target.actorHasMethod("nope", "nope");
   is(hasMethod, false,
     "target.actorHasMethod() returns false for non-existing actor with no method");
   hasMethod = await target.actorHasMethod();
   is(hasMethod, false,
     "target.actorHasMethod() returns false for undefined params");
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -244,16 +244,29 @@ OptionsPanel.prototype = {
       }
 
       checkboxInput.addEventListener("change",
         onCheckboxClick.bind(checkboxInput, this.telemetry, tool));
 
       checkboxLabel.appendChild(checkboxInput);
       checkboxLabel.appendChild(checkboxSpanLabel);
 
+      // TODO: remove in Firefox 68, with bug #1528296
+      if (tool.deprecated) {
+        const deprecationURL = this.panelDoc.createElement("a");
+        deprecationURL.title = deprecationURL.href = tool.deprecationURL;
+        deprecationURL.textContent = L10N.getFormatStr("options.deprecationNotice");
+        deprecationURL.target = "_blank";
+
+        const checkboxSpanDeprecated = this.panelDoc.createElement("span");
+        checkboxSpanDeprecated.className = "deprecation-notice";
+        checkboxLabel.appendChild(checkboxSpanDeprecated);
+        checkboxSpanDeprecated.appendChild(deprecationURL);
+      }
+
       return checkboxLabel;
     };
 
     // Clean up any existent default tools content.
     for (const label of defaultToolsBox.querySelectorAll("label")) {
       label.remove();
     }
 
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -1811,16 +1811,29 @@ Toolbox.prototype = {
 
       // If no parent yet, append the frame into default location.
       if (!iframe.parentNode) {
         const vbox = this.doc.getElementById("toolbox-panel-" + id);
         vbox.appendChild(iframe);
         vbox.visibility = "visible";
       }
 
+      // TODO: remove in Firefox 68, with bug #1528296
+      if (definition.deprecated) {
+        const deprecationURL = this.doc.createXULElement("label");
+        deprecationURL.textContent = L10N.getFormatStr("options.deprecationNotice");
+        deprecationURL.setAttribute("href", definition.deprecationURL);
+        deprecationURL.setAttribute("class", "text-link");
+
+        const deprecationNotice = this.doc.createXULElement("span");
+        deprecationNotice.className = "toolbox-panel_deprecation-notice";
+        deprecationNotice.appendChild(deprecationURL);
+        iframe.parentNode.prepend(deprecationNotice);
+      }
+
       const onLoad = async () => {
         if (id === "inspector") {
           await this._initInspector;
 
           // Stop loading the inspector if the toolbox is already being destroyed. This
           // can happen in unit tests where the tests are rapidly opening and closing the
           // toolbox and we encounter the scenario where the toolbox is closing as
           // the inspector is still loading.
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -21,16 +21,30 @@ devtools.jar:
     content/shared/sourceeditor/codemirror/keymap/emacs.js (shared/sourceeditor/codemirror/keymap/emacs.js)
     content/shared/sourceeditor/codemirror/keymap/vim.js (shared/sourceeditor/codemirror/keymap/vim.js)
     content/shared/sourceeditor/codemirror/keymap/sublime.js (shared/sourceeditor/codemirror/keymap/sublime.js)
     content/shared/sourceeditor/codemirror/codemirror.bundle.js (shared/sourceeditor/codemirror/codemirror.bundle.js)
     content/shared/sourceeditor/codemirror/lib/codemirror.css (shared/sourceeditor/codemirror/lib/codemirror.css)
     content/shared/sourceeditor/codemirror/mozilla.css (shared/sourceeditor/codemirror/mozilla.css)
     content/shared/sourceeditor/codemirror/cmiframe.html (shared/sourceeditor/codemirror/cmiframe.html)
     content/debugger/new/index.html (debugger/new/index.html)
+    content/shadereditor/index.xul (shadereditor/index.xul)
+    content/canvasdebugger/index.xul (canvasdebugger/index.xul)
+    content/canvasdebugger/canvasdebugger.js (canvasdebugger/canvasdebugger.js)
+    content/canvasdebugger/snapshotslist.js (canvasdebugger/snapshotslist.js)
+    content/canvasdebugger/callslist.js (canvasdebugger/callslist.js)
+    content/webaudioeditor/index.xul (webaudioeditor/index.xul)
+    content/webaudioeditor/includes.js (webaudioeditor/includes.js)
+    content/webaudioeditor/models.js (webaudioeditor/models.js)
+    content/webaudioeditor/controller.js (webaudioeditor/controller.js)
+    content/webaudioeditor/views/utils.js (webaudioeditor/views/utils.js)
+    content/webaudioeditor/views/context.js (webaudioeditor/views/context.js)
+    content/webaudioeditor/views/inspector.js (webaudioeditor/views/inspector.js)
+    content/webaudioeditor/views/properties.js (webaudioeditor/views/properties.js)
+    content/webaudioeditor/views/automation.js (webaudioeditor/views/automation.js)
     content/performance/index.xul (performance/index.xul)
     content/performance-new/index.xhtml (performance-new/index.xhtml)
     content/performance-new/frame-script.js (performance-new/frame-script.js)
     content/memory/index.xhtml (memory/index.xhtml)
     content/framework/toolbox-window.xul (framework/toolbox-window.xul)
     content/framework/toolbox-options.xhtml (framework/toolbox-options.xhtml)
     content/framework/toolbox.xul (framework/toolbox.xul)
     content/framework/toolbox-init.js (framework/toolbox-init.js)
@@ -71,16 +85,17 @@ devtools.jar:
     skin/images/arrow.svg (themes/images/arrow.svg)
     skin/images/arrow-big.svg (themes/images/arrow-big.svg)
     skin/images/arrowhead-left.svg (themes/images/arrowhead-left.svg)
     skin/images/arrowhead-right.svg (themes/images/arrowhead-right.svg)
     skin/images/arrowhead-down.svg (themes/images/arrowhead-down.svg)
     skin/images/arrowhead-up.svg (themes/images/arrowhead-up.svg)
     skin/images/breadcrumbs-divider.svg (themes/images/breadcrumbs-divider.svg)
     skin/images/checkbox.svg (themes/images/checkbox.svg)
+    skin/images/filters.svg (themes/images/filters.svg)
     skin/images/filter-swatch.svg (themes/images/filter-swatch.svg)
     skin/images/aboutdebugging-connect-icon.svg (themes/images/aboutdebugging-connect-icon.svg)
     skin/images/aboutdebugging-firefox-aurora.svg (themes/images/aboutdebugging-firefox-aurora.svg)
     skin/images/aboutdebugging-firefox-beta.svg (themes/images/aboutdebugging-firefox-beta.svg)
     skin/images/aboutdebugging-firefox-logo.svg (themes/images/aboutdebugging-firefox-logo.svg)
     skin/images/aboutdebugging-firefox-nightly.svg (themes/images/aboutdebugging-firefox-nightly.svg)
     skin/images/aboutdebugging-firefox-release.svg (themes/images/aboutdebugging-firefox-release.svg)
     skin/images/aboutdebugging-globe-icon.svg (themes/images/aboutdebugging-globe-icon.svg)
@@ -94,16 +109,17 @@ devtools.jar:
     skin/images/copy.svg (themes/images/copy.svg)
     skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg)
     skin/images/performance-details-waterfall.svg (themes/images/performance-details-waterfall.svg)
     skin/images/performance-details-call-tree.svg (themes/images/performance-details-call-tree.svg)
     skin/images/performance-details-flamegraph.svg (themes/images/performance-details-flamegraph.svg)
     skin/breadcrumbs.css (themes/breadcrumbs.css)
     skin/chart.css (themes/chart.css)
     skin/widgets.css (themes/widgets.css)
+    skin/images/power.svg (themes/images/power.svg)
     skin/rules.css (themes/rules.css)
     skin/images/command-paintflashing.svg (themes/images/command-paintflashing.svg)
     skin/images/command-screenshot.svg (themes/images/command-screenshot.svg)
     skin/images/command-responsivemode.svg (themes/images/command-responsivemode.svg)
     skin/images/command-replay.svg (themes/images/command-replay.svg)
     skin/images/command-pick.svg (themes/images/command-pick.svg)
     skin/images/command-pick-accessibility.svg (themes/images/command-pick-accessibility.svg)
     skin/images/command-frames.svg (themes/images/command-frames.svg)
@@ -116,23 +132,26 @@ devtools.jar:
     skin/markup.css (themes/markup.css)
     skin/webconsole.css (themes/webconsole.css)
     skin/images/webconsole/error.svg (themes/images/webconsole/error.svg)
     skin/images/webconsole/info.svg (themes/images/webconsole/info.svg)
     skin/images/webconsole/input.svg (themes/images/webconsole/input.svg)
     skin/images/webconsole/return.svg (themes/images/webconsole/return.svg)
     skin/images/breadcrumbs-scrollbutton.svg (themes/images/breadcrumbs-scrollbutton.svg)
     skin/animation.css (themes/animation.css)
+    skin/canvasdebugger.css (themes/canvasdebugger.css)
     skin/perf.css (themes/perf.css)
     skin/performance.css (themes/performance.css)
     skin/memory.css (themes/memory.css)
     skin/scratchpad.css (themes/scratchpad.css)
+    skin/shadereditor.css (themes/shadereditor.css)
     skin/storage.css (themes/storage.css)
     skin/splitview.css (themes/splitview.css)
     skin/styleeditor.css (themes/styleeditor.css)
+    skin/webaudioeditor.css (themes/webaudioeditor.css)
     skin/components-frame.css (themes/components-frame.css)
     skin/components-h-split-box.css (themes/components-h-split-box.css)
     skin/jit-optimizations.css (themes/jit-optimizations.css)
     skin/images/filter.svg (themes/images/filter.svg)
     skin/images/filter-small.svg (themes/images/filter-small.svg)
     skin/images/search.svg (themes/images/search.svg)
     skin/images/item-toggle.svg (themes/images/item-toggle.svg)
     skin/images/item-arrow-dark-rtl.svg (themes/images/item-arrow-dark-rtl.svg)
@@ -142,16 +161,19 @@ devtools.jar:
     skin/images/dropmarker.svg (themes/images/dropmarker.svg)
     skin/boxmodel.css (themes/boxmodel.css)
     skin/images/geometry-editor.svg (themes/images/geometry-editor.svg)
     skin/images/open-inspector.svg (themes/images/open-inspector.svg)
     skin/images/more.svg (themes/images/more.svg)
     skin/images/pause.svg (themes/images/pause.svg)
     skin/images/play.svg (themes/images/play.svg)
     skin/images/rewind.svg (themes/images/rewind.svg)
+    skin/images/canvasdebugger-step-in.svg (themes/images/canvasdebugger-step-in.svg)
+    skin/images/canvasdebugger-step-out.svg (themes/images/canvasdebugger-step-out.svg)
+    skin/images/canvasdebugger-step-over.svg (themes/images/canvasdebugger-step-over.svg)
     skin/images/dock-bottom.svg (themes/images/dock-bottom.svg)
     skin/images/dock-side-left.svg (themes/images/dock-side-left.svg)
     skin/images/dock-side-right.svg (themes/images/dock-side-right.svg)
     skin/images/dock-undock.svg (themes/images/dock-undock.svg)
     skin/floating-scrollbars-responsive-design.css (themes/floating-scrollbars-responsive-design.css)
     skin/badge.css (themes/badge.css)
     skin/inspector.css (themes/inspector.css)
     skin/images/profiler-stopwatch.svg (themes/images/profiler-stopwatch.svg)
@@ -162,23 +184,26 @@ devtools.jar:
     skin/images/globe.svg (themes/images/globe.svg)
     skin/images/globe-small.svg (themes/images/globe-small.svg)
     skin/images/next.svg (themes/images/next.svg)
     skin/images/next-circle.svg (themes/images/next-circle.svg)
     skin/images/folder.svg (themes/images/folder.svg)
     skin/images/sad-face.svg (themes/images/sad-face.svg)
     skin/images/shape-swatch.svg (themes/images/shape-swatch.svg)
     skin/images/tool-webconsole.svg (themes/images/tool-webconsole.svg)
+    skin/images/tool-canvas.svg (themes/images/tool-canvas.svg)
     skin/images/tool-debugger.svg (themes/images/tool-debugger.svg)
     skin/images/tool-inspector.svg (themes/images/tool-inspector.svg)
+    skin/images/tool-shadereditor.svg (themes/images/tool-shadereditor.svg)
     skin/images/tool-styleeditor.svg (themes/images/tool-styleeditor.svg)
     skin/images/tool-storage.svg (themes/images/tool-storage.svg)
     skin/images/tool-profiler.svg (themes/images/tool-profiler.svg)
     skin/images/tool-network.svg (themes/images/tool-network.svg)
     skin/images/tool-scratchpad.svg (themes/images/tool-scratchpad.svg)
+    skin/images/tool-webaudio.svg (themes/images/tool-webaudio.svg)
     skin/images/tool-memory.svg (themes/images/tool-memory.svg)
     skin/images/tool-dom.svg (themes/images/tool-dom.svg)
     skin/images/tool-accessibility.svg (themes/images/tool-accessibility.svg)
     skin/images/tool-application.svg (themes/images/tool-application.svg)
     skin/images/close.svg (themes/images/close.svg)
     skin/images/clear.svg (themes/images/clear.svg)
     skin/images/close-3-pane.svg (themes/images/close-3-pane.svg)
     skin/images/open-3-pane.svg (themes/images/open-3-pane.svg)
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/canvasdebugger.dtd
@@ -0,0 +1,45 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+  - keep it in English, or another language commonly spoken among web developers.
+  - You want to make that choice consistent across the developer tools.
+  - A good criteria is the language in which you'd find the best
+  - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.reloadNotice1): This is the label shown
+  -  on the button that triggers a page refresh. -->
+<!ENTITY canvasDebuggerUI.reloadNotice1   "Reload">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.reloadNotice2): This is the label shown
+  -  along with the button that triggers a page refresh. -->
+<!ENTITY canvasDebuggerUI.reloadNotice2   "the page to be able to debug &lt;canvas&gt; contexts.">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.emptyNotice1/2): This is the label shown
+  -  in the call list view when empty. -->
+<!ENTITY canvasDebuggerUI.emptyNotice1    "Click on the">
+<!ENTITY canvasDebuggerUI.emptyNotice2    "button to record an animation frame’s call stack.">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.waitingNotice): This is the label shown
+  -  in the call list view while recording a snapshot. -->
+<!ENTITY canvasDebuggerUI.waitingNotice   "Recording an animation cycle…">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.recordSnapshot): This string is displayed
+  -  on a button that starts a new snapshot. -->
+<!ENTITY canvasDebuggerUI.recordSnapshot.tooltip "Record the next frame in the animation loop.">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.importSnapshot): This string is displayed
+  -  on a button that opens a dialog to import a saved snapshot data file. -->
+<!ENTITY canvasDebuggerUI.importSnapshot "Import…">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.clearSnapshots): This string is displayed
+  -  on a button that remvoes all the snapshots. -->
+<!ENTITY canvasDebuggerUI.clearSnapshots "Clear">
+
+<!-- LOCALIZATION NOTE (canvasDebuggerUI.searchboxPlaceholder): This string is displayed
+  -  as a placeholder of the search box that filters the calls list. -->
+<!ENTITY canvasDebuggerUI.searchboxPlaceholder "Filter calls">
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/canvasdebugger.properties
@@ -0,0 +1,70 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE These strings are used inside the Canvas Debugger
+# which is available from the Web Developer sub-menu -> 'Canvas'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (noSnapshotsText): The text to display in the snapshots menu
+# when there are no recorded snapshots yet.
+noSnapshotsText=There are no snapshots yet.
+
+# LOCALIZATION NOTE (snapshotsList.itemLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# identifying a set of function calls of a recorded animation frame.
+snapshotsList.itemLabel=Snapshot #%S
+
+# LOCALIZATION NOTE (snapshotsList.loadingLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# for an item that has not finished loading.
+snapshotsList.loadingLabel=Loading…
+
+# LOCALIZATION NOTE (snapshotsList.saveLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# for saving an item to disk.
+snapshotsList.saveLabel=Save
+
+# LOCALIZATION NOTE (snapshotsList.savingLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# while saving an item to disk.
+snapshotsList.savingLabel=Saving…
+
+# LOCALIZATION NOTE (snapshotsList.loadedLabel):
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# for an item which was loaded from disk
+snapshotsList.loadedLabel=Loaded from disk
+
+# LOCALIZATION NOTE (snapshotsList.saveDialogTitle):
+# This string is displayed as a title for saving a snapshot to disk.
+snapshotsList.saveDialogTitle=Save animation frame snapshot…
+
+# LOCALIZATION NOTE (snapshotsList.saveDialogJSONFilter):
+# This string is displayed as a filter for saving a snapshot to disk.
+snapshotsList.saveDialogJSONFilter=JSON Files
+
+# LOCALIZATION NOTE (snapshotsList.saveDialogAllFilter):
+# This string is displayed as a filter for saving a snapshot to disk.
+snapshotsList.saveDialogAllFilter=All Files
+
+# LOCALIZATION NOTE (snapshotsList.drawCallsLabel):
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# as a generic description about how many draw calls were made.
+snapshotsList.drawCallsLabel=#1 draw;#1 draws
+
+# LOCALIZATION NOTE (snapshotsList.functionCallsLabel):
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# This string is displayed in the snapshots list of the Canvas Debugger,
+# as a generic description about how many function calls were made in total.
+snapshotsList.functionCallsLabel=#1 call;#1 calls
+
+# LOCALIZATION NOTE (recordingTimeoutFailure):
+# This notification alert is displayed when attempting to record a requestAnimationFrame
+# cycle in the Canvas Debugger and no cycles detected. This alerts the user that no
+# loops were found.
+recordingTimeoutFailure=Canvas Debugger could not find a requestAnimationFrame or setTimeout cycle.
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/shadereditor.dtd
@@ -0,0 +1,32 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+  - keep it in English, or another language commonly spoken among web developers.
+  - You want to make that choice consistent across the developer tools.
+  - A good criteria is the language in which you'd find the best
+  - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.vertexShader): This is the label for
+  -  the pane that displays a vertex shader's source. -->
+<!ENTITY shaderEditorUI.vertexShader    "Vertex Shader">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.fragmentShader): This is the label for
+  -  the pane that displays a fragment shader's source. -->
+<!ENTITY shaderEditorUI.fragmentShader  "Fragment Shader">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice1): This is the label shown
+  -  on the button that triggers a page refresh. -->
+<!ENTITY shaderEditorUI.reloadNotice1   "Reload">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice2): This is the label shown
+  -  along with the button that triggers a page refresh. -->
+<!ENTITY shaderEditorUI.reloadNotice2   "the page to be able to edit GLSL code.">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.emptyNotice): This is the label shown
+  -  while the page is refreshing and the tool waits for a WebGL context. -->
+<!ENTITY shaderEditorUI.emptyNotice     "Waiting for a WebGL context to be created…">
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/shadereditor.properties
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE These strings are used inside the Debugger
+# which is available from the Web Developer sub-menu -> 'Debugger'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (shadersList.programLabel):
+# This string is displayed in the programs list of the Shader Editor,
+# identifying a set of linked GLSL shaders.
+shadersList.programLabel=Program %S
+
+# LOCALIZATION NOTE (shadersList.blackboxLabel):
+# This string is displayed in the programs list of the Shader Editor, while
+# the user hovers over the checkbox used to toggle blackboxing of a program's
+# associated fragment shader.
+shadersList.blackboxLabel=Toggle geometry visibility
--- a/devtools/client/locales/en-US/shared.properties
+++ b/devtools/client/locales/en-US/shared.properties
@@ -1,8 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # LOCALIZATION NOTE (dimensions): This is used to display the dimensions
 # of a node or image, like 100×200.
 dimensions=%S\u00D7%S
 
+# LOCALIZATION NOTE (sideMenu.groupCheckbox.tooltip): This is used in the SideMenuWidget
+# as the default tooltip of a group checkbox
+sideMenu.groupCheckbox.tooltip=Toggle all checkboxes in this group
\ No newline at end of file
--- a/devtools/client/locales/en-US/startup.properties
+++ b/devtools/client/locales/en-US/startup.properties
@@ -93,16 +93,44 @@ ToolboxStyleEditor.panelLabel=Style Edit
 # displayed inside the developer tools window.
 # A keyboard shortcut for Stylesheet Editor will be shown inside the latter pair of brackets.
 ToolboxStyleEditor.tooltip3=Stylesheet Editor (CSS) (%S)
 
 # LOCALIZATION NOTE (open.accesskey): The access key used to open the style
 # editor.
 open.accesskey=l
 
+# LOCALIZATION NOTE (ToolboxShaderEditor.label):
+# This string is displayed in the title of the tab when the Shader Editor is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+ToolboxShaderEditor.label=Shader Editor
+
+# LOCALIZATION NOTE (ToolboxShaderEditor.panelLabel):
+# This is used as the label for the toolbox panel.
+ToolboxShaderEditor.panelLabel=Shader Editor Panel
+
+# LOCALIZATION NOTE (ToolboxShaderEditor.tooltip):
+# This string is displayed in the tooltip of the tab when the Shader Editor is
+# displayed inside the developer tools window.
+ToolboxShaderEditor.tooltip=Live GLSL shader language editor for WebGL
+
+# LOCALIZATION NOTE (ToolboxCanvasDebugger.label):
+# This string is displayed in the title of the tab when the Shader Editor is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+ToolboxCanvasDebugger.label=Canvas
+
+# LOCALIZATION NOTE (ToolboxCanvasDebugger.panelLabel):
+# This is used as the label for the toolbox panel.
+ToolboxCanvasDebugger.panelLabel=Canvas Panel
+
+# LOCALIZATION NOTE (ToolboxCanvasDebugger.tooltip):
+# This string is displayed in the tooltip of the tab when the Shader Editor is
+# displayed inside the developer tools window.
+ToolboxCanvasDebugger.tooltip=Tools to inspect and debug <canvas> contexts
+
 # LOCALIZATION NOTE (ToolboxWebAudioEditor1.label):
 # This string is displayed in the title of the tab when the Web Audio Editor
 # is displayed inside the developer tools window and in the Developer Tools Menu.
 ToolboxWebAudioEditor1.label=Web Audio
 
 # LOCALIZATION NOTE (ToolboxWebAudioEditor1.panelLabel):
 # This is used as the label for the toolbox panel.
 ToolboxWebAudioEditor1.panelLabel=Web Audio Panel
--- a/devtools/client/locales/en-US/toolbox.properties
+++ b/devtools/client/locales/en-US/toolbox.properties
@@ -22,16 +22,21 @@ toolbox.defaultTitle=Developer Tools
 toolbox.label=Developer Tools
 
 # LOCALIZATION NOTE (options.toolNotSupportedMarker): This is the template
 # used to add a * marker to the label for the Options Panel tool checkbox for the
 # tool which is not supported for the current toolbox target.
 # The name of the tool: %1$S.
 options.toolNotSupportedMarker=%1$S *
 
+# LOCALIZATION NOTE (options.deprecationNotice): This is the template
+# is used to generate a deprecation notice for a panel.
+# This entire text is treated as a link to an MDN page.
+options.deprecationNotice=Deprecated. Learn More…
+
 # LOCALIZATION NOTE (scratchpad.keycode)
 # Used for opening scratchpad from the detached toolbox window
 # Needs to match scratchpad.keycode from browser.dtd
 scratchpad.keycode=VK_F4
 
 # LOCALIZATION NOTE (toolbox.pickButton.tooltip)
 # This is the tooltip of the element picker button in the toolbox toolbar.
 # %S is the keyboard shortcut that toggles the element picker.
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/webaudioeditor.dtd
@@ -0,0 +1,53 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+  - keep it in English, or another language commonly spoken among web developers.
+  - You want to make that choice consistent across the developer tools.
+  - A good criteria is the language in which you'd find the best
+  - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.reloadNotice1): This is the label shown
+  -  on the button that triggers a page refresh. -->
+<!ENTITY webAudioEditorUI.reloadNotice1   "Reload">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.reloadNotice2): This is the label shown
+  -  along with the button that triggers a page refresh. -->
+<!ENTITY webAudioEditorUI.reloadNotice2   "the page to view and edit the audio context.">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.emptyNotice): This is the label shown
+  -  while the page is refreshing and the tool waits for a audio context. -->
+<!ENTITY webAudioEditorUI.emptyNotice     "Waiting for an audio context to be created…">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.tab.properties2): This is the label shown
+  -  for the properties tab view. -->
+<!ENTITY webAudioEditorUI.tab.properties2 "Properties">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.tab.automation): This is the label shown
+  -  for the automation tab view. -->
+<!ENTITY webAudioEditorUI.tab.automation  "Automation">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.inspectorTitle): This is the title for the
+  -  AudioNode inspector view. -->
+<!ENTITY webAudioEditorUI.inspectorTitle  "AudioNode Inspector">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.inspectorEmpty): This is the title for the
+  -  AudioNode inspector view empty message. -->
+<!ENTITY webAudioEditorUI.inspectorEmpty  "No AudioNode selected.">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.propertiesEmpty): This is the title for the
+  -  AudioNode inspector view properties tab empty message. -->
+<!ENTITY webAudioEditorUI.propertiesEmpty "Node does not have any properties.">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.automationEmpty): This is the title for the
+  -  AudioNode inspector view automation tab empty message. -->
+<!ENTITY webAudioEditorUI.automationEmpty "Node does not have any AudioParams.">
+
+<!-- LOCALIZATION NOTE (webAudioEditorUI.automationNoEvents): This is the title for the
+  -  AudioNode inspector view automation tab message when there are no automation
+  -  events. -->
+<!ENTITY webAudioEditorUI.automationNoEvents "AudioParam does not have any automation events.">
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/webaudioeditor.properties
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE These strings are used inside the Web Audio tool
+# which is available in the developer tools' toolbox, once
+# enabled in the developer tools' preference "Web Audio".
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (collapseInspector): This is the tooltip for the button
+# that collapses the inspector in the web audio tool UI.
+collapseInspector=Collapse inspector
+
+# LOCALIZATION NOTE (expandInspector): This is the tooltip for the button
+# that expands the inspector in the web audio tool UI.
+expandInspector=Expand inspector
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -6,33 +6,36 @@
 
 include('../templates.mozbuild')
 
 DIRS += [
     'aboutdebugging',
     'aboutdebugging-new',
     'accessibility',
     'application',
+    'canvasdebugger',
     'debugger',
     'dom',
     'framework',
     'inspector',
     'jsonview',
     'locales',
     'memory',
     'netmonitor',
     'performance',
     'performance-new',
     'preferences',
     'responsive.html',
     'scratchpad',
+    'shadereditor',
     'shared',
     'storage',
     'styleeditor',
     'themes',
+    'webaudioeditor',
     'webconsole',
     'webide',
     'webreplay',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
 DevToolsModules(
--- a/devtools/client/netmonitor/test/browser_net_accessibility-01.js
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-01.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
- * Tests if focus modifiers work for the Side Menu.
+ * Tests if focus modifiers work for the SideMenuWidget.
  */
 
 add_task(async function() {
   const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL);
   info("Starting test... ");
 
   // It seems that this test may be slow on Ubuntu builds running on ec2.
   requestLongerTimeout(2);
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -224,25 +224,37 @@ pref("devtools.styleeditor.showMediaSide
 pref("devtools.styleeditor.mediaSidebarWidth", 238);
 pref("devtools.styleeditor.navSidebarWidth", 245);
 pref("devtools.styleeditor.transitions", true);
 
 // Screenshot Option Settings.
 pref("devtools.screenshot.clipboard.enabled", false);
 pref("devtools.screenshot.audio.enabled", true);
 
+// Enable the Shader Editor.
+pref("devtools.shadereditor.enabled", false);
+
+// Enable the Canvas Debugger.
+pref("devtools.canvasdebugger.enabled", false);
+
+// Enable the Web Audio Editor
+pref("devtools.webaudioeditor.enabled", false);
+
 // Enable Scratchpad
 pref("devtools.scratchpad.enabled", false);
 
 // Make sure the DOM panel is hidden by default
 pref("devtools.dom.enabled", false);
 
 // Enable the Accessibility panel.
 pref("devtools.accessibility.enabled", true);
 
+// Web Audio Editor Inspector Width should be a preference
+pref("devtools.webaudioeditor.inspectorWidth", 300);
+
 // Web console filters
 pref("devtools.webconsole.filter.error", true);
 pref("devtools.webconsole.filter.warn", true);
 pref("devtools.webconsole.filter.info", true);
 pref("devtools.webconsole.filter.log", true);
 pref("devtools.webconsole.filter.debug", true);
 pref("devtools.webconsole.filter.css", false);
 pref("devtools.webconsole.filter.net", false);
new file mode 100644
--- /dev/null
+++ b/devtools/client/shadereditor/index.xul
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/shadereditor.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<!DOCTYPE window [
+  <!ENTITY % debuggerDTD SYSTEM "chrome://devtools/locale/shadereditor.dtd">
+  %debuggerDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/javascript"
+          src="chrome://devtools/content/shared/theme-switching.js"/>
+
+  <vbox class="theme-body" flex="1">
+    <hbox id="reload-notice"
+          class="notice-container"
+          align="center"
+          pack="center"
+          flex="1">
+      <button id="requests-menu-reload-notice-button"
+              class="devtools-toolbarbutton"
+              standalone="true"
+              label="&shaderEditorUI.reloadNotice1;"/>
+      <label id="requests-menu-reload-notice-label"
+             class="plain"
+             value="&shaderEditorUI.reloadNotice2;"/>
+    </hbox>
+    <hbox id="waiting-notice"
+          class="notice-container devtools-throbber"
+          align="center"
+          pack="center"
+          flex="1"
+          hidden="true">
+      <label id="requests-menu-waiting-notice-label"
+             class="plain"
+             value="&shaderEditorUI.emptyNotice;"/>
+    </hbox>
+
+    <box id="content"
+         class="devtools-responsive-container"
+         flex="1"
+         hidden="true">
+      <vbox id="shaders-pane"/>
+      <splitter class="devtools-side-splitter"/>
+      <box id="shaders-editors" class="devtools-responsive-container" flex="1">
+        <vbox flex="1">
+          <vbox id="vs-editor" flex="1"/>
+          <label id="vs-editor-label"
+                 class="plain editor-label"
+                 value="&shaderEditorUI.vertexShader;"/>
+        </vbox>
+        <splitter id="editors-splitter" class="devtools-side-splitter"/>
+        <vbox flex="1">
+          <vbox id="fs-editor" flex="1"/>
+          <label id="fs-editor-label"
+                 class="plain editor-label"
+                 value="&shaderEditorUI.fragmentShader;"/>
+        </vbox>
+      </box>
+    </box>
+  </vbox>
+
+</window>
new file mode 100644
--- /dev/null
+++ b/devtools/client/shadereditor/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'panel.js',
+    'shadereditor.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+with Files('**'):
+    BUG_COMPONENT = ('DevTools', 'WebGL Shader Editor')
new file mode 100644
--- /dev/null
+++ b/devtools/client/shadereditor/panel.js
@@ -0,0 +1,71 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { EventsHandler, ShadersListView, ShadersEditorsView, EVENTS, $, L10N } =
+  require("./shadereditor");
+
+function ShaderEditorPanel(toolbox) {
+  this._toolbox = toolbox;
+  this._destroyer = null;
+  this.panelWin = window;
+
+  EventEmitter.decorate(this);
+}
+
+exports.ShaderEditorPanel = ShaderEditorPanel;
+
+ShaderEditorPanel.prototype = {
+
+  // Expose symbols for tests:
+  EVENTS,
+  $,
+  L10N,
+
+  /**
+   * Open is effectively an asynchronous constructor.
+   *
+   * @return object
+   *         A promise that is resolved when the Shader Editor completes opening.
+   */
+  async open() {
+    this.front = await this.target.getFront("webgl");
+    this.shadersListView = new ShadersListView();
+    this.eventsHandler = new EventsHandler();
+    this.shadersEditorsView = new ShadersEditorsView();
+    await this.shadersListView.initialize(this._toolbox, this.shadersEditorsView);
+    await this.eventsHandler.initialize(this, this._toolbox, this.target, this.front,
+                                        this.shadersListView);
+    await this.shadersEditorsView.initialize(this, this.shadersListView);
+
+    this.isReady = true;
+    this.emit("ready");
+    return this;
+  },
+
+  // DevToolPanel API
+
+  get target() {
+    return this._toolbox.target;
+  },
+
+  destroy() {
+    // Make sure this panel is not already destroyed.
+    if (this._destroyer) {
+      return this._destroyer;
+    }
+
+    return (this._destroyer = (async () => {
+      await this.shadersListView.destroy();
+      await this.eventsHandler.destroy();
+      await this.shadersEditorsView.destroy();
+      // Destroy front to ensure packet handler is removed from client
+      this.front.destroy();
+      this.emit("destroyed");
+    })());
+  },
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/shadereditor/shadereditor.js
@@ -0,0 +1,625 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const {SideMenuWidget} = require("devtools/client/shared/widgets/SideMenuWidget.jsm");
+const promise = require("promise");
+const {Task} = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const Editor = require("devtools/client/shared/sourceeditor/editor");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const {extend} = require("devtools/shared/extend");
+const {WidgetMethods, setNamedTimeout} =
+  require("devtools/client/shared/widgets/view-helpers");
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+  // When new programs are received from the server.
+  NEW_PROGRAM: "ShaderEditor:NewProgram",
+  PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded",
+
+  // When the vertex and fragment sources were shown in the editor.
+  SOURCES_SHOWN: "ShaderEditor:SourcesShown",
+
+  // When a shader's source was edited and compiled via the editor.
+  SHADER_COMPILED: "ShaderEditor:ShaderCompiled",
+
+  // When the UI is reset from tab navigation
+  UI_RESET: "ShaderEditor:UIReset",
+
+  // When the editor's error markers are all removed
+  EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned",
+};
+exports.EVENTS = EVENTS;
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const STRINGS_URI = "devtools/client/locales/shadereditor.properties";
+const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba
+const TYPING_MAX_DELAY = 500; // ms
+const SHADERS_AUTOGROW_ITEMS = 4;
+const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px
+const GUTTER_ERROR_PANEL_DELAY = 100; // ms
+const DEFAULT_EDITOR_CONFIG = {
+  gutters: ["errors"],
+  lineNumbers: true,
+  showAnnotationRuler: true,
+};
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+class EventsHandler {
+  /**
+   * Listen for events emitted by the current tab target.
+   */
+  initialize(panel, toolbox, target, front, shadersListView) {
+    this.panel = panel;
+    this.toolbox = toolbox;
+    this.target = target;
+    this.front = front;
+    this.shadersListView = shadersListView;
+
+    this._onHostChanged = this._onHostChanged.bind(this);
+    this._onTabNavigated = this._onTabNavigated.bind(this);
+    this._onTabWillNavigate = this._onTabWillNavigate.bind(this);
+    this._onProgramLinked = this._onProgramLinked.bind(this);
+    this._onProgramsAdded = this._onProgramsAdded.bind(this);
+
+    this.toolbox.on("host-changed", this._onHostChanged);
+    this.target.on("will-navigate", this._onTabWillNavigate);
+    this.target.on("navigate", this._onTabNavigated);
+    this.front.on("program-linked", this._onProgramLinked);
+    this.reloadButton = $("#requests-menu-reload-notice-button");
+    this._onReloadCommand = this._onReloadCommand.bind(this);
+    this.reloadButton.addEventListener("command", this._onReloadCommand);
+  }
+
+  /**
+   * Remove events emitted by the current tab target.
+   */
+  destroy() {
+    this.toolbox.off("host-changed", this._onHostChanged);
+    this.target.off("will-navigate", this._onTabWillNavigate);
+    this.target.off("navigate", this._onTabNavigated);
+    this.front.off("program-linked", this._onProgramLinked);
+    this.reloadButton.removeEventListener("command", this._onReloadCommand);
+  }
+
+  /**
+   * Handles a command event on reload button
+   */
+  _onReloadCommand() {
+    this.front.setup({ reload: true });
+  }
+
+  /**
+   * Handles a host change event on the parent toolbox.
+   */
+  _onHostChanged() {
+    if (this.toolbox.hostType == "right" || this.toolbox.hostType == "left") {
+      $("#shaders-pane").removeAttribute("height");
+    }
+  }
+
+  _onTabWillNavigate({isFrameSwitching}) {
+    // Make sure the backend is prepared to handle WebGL contexts.
+    if (!isFrameSwitching) {
+      this.front.setup({ reload: false });
+    }
+
+    // Reset UI.
+    this.shadersListView.empty();
+    // When switching to an iframe, ensure displaying the reload button.
+    // As the document has already been loaded without being hooked.
+    if (isFrameSwitching) {
+      $("#reload-notice").hidden = false;
+      $("#waiting-notice").hidden = true;
+    } else {
+      $("#reload-notice").hidden = true;
+      $("#waiting-notice").hidden = false;
+    }
+
+    $("#content").hidden = true;
+    this.panel.emit(EVENTS.UI_RESET);
+  }
+
+  /**
+   * Called for each location change in the debugged tab.
+   */
+  _onTabNavigated() {
+    // Manually retrieve the list of program actors known to the server,
+    // because the backend won't emit "program-linked" notifications
+    // in the case of a bfcache navigation (since no new programs are
+    // actually linked).
+    this.front.getPrograms().then(this._onProgramsAdded);
+  }
+
+  /**
+   * Called every time a program was linked in the debugged tab.
+   */
+  _onProgramLinked(programActor) {
+    this._addProgram(programActor);
+    this.panel.emit(EVENTS.NEW_PROGRAM);
+  }
+
+  /**
+   * Callback for the front's getPrograms() method.
+   */
+  _onProgramsAdded(programActors) {
+    programActors.forEach(this._addProgram.bind(this));
+    this.panel.emit(EVENTS.PROGRAMS_ADDED);
+  }
+
+  /**
+   * Adds a program to the shaders list and unhides any modal notices.
+   */
+  _addProgram(programActor) {
+    $("#waiting-notice").hidden = true;
+    $("#reload-notice").hidden = true;
+    $("#content").hidden = false;
+    this.shadersListView.addProgram(programActor);
+  }
+}
+exports.EventsHandler = EventsHandler;
+
+/**
+ * Functions handling the sources UI.
+ */
+function WidgetMethodsClass() {
+}
+WidgetMethodsClass.prototype = WidgetMethods;
+class ShadersListView extends WidgetMethodsClass {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize(toolbox, shadersEditorsView) {
+    this.toolbox = toolbox;
+    this.shadersEditorsView = shadersEditorsView;
+    this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), {
+      showArrows: true,
+      showItemCheckboxes: true,
+    });
+
+    this._onProgramSelect = this._onProgramSelect.bind(this);
+    this._onProgramCheck = this._onProgramCheck.bind(this);
+    this._onProgramMouseOver = this._onProgramMouseOver.bind(this);
+    this._onProgramMouseOut = this._onProgramMouseOut.bind(this);
+
+    this.widget.addEventListener("select", this._onProgramSelect);
+    this.widget.addEventListener("check", this._onProgramCheck);
+    this.widget.addEventListener("mouseover", this._onProgramMouseOver, true);
+    this.widget.addEventListener("mouseout", this._onProgramMouseOut, true);
+  }
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy() {
+    this.widget.removeEventListener("select", this._onProgramSelect);
+    this.widget.removeEventListener("check", this._onProgramCheck);
+    this.widget.removeEventListener("mouseover", this._onProgramMouseOver, true);
+    this.widget.removeEventListener("mouseout", this._onProgramMouseOut, true);
+  }
+
+  /**
+   * Adds a program to this programs container.
+   *
+   * @param object programActor
+   *        The program actor coming from the active thread.
+   */
+  addProgram(programActor) {
+    if (this.hasProgram(programActor)) {
+      return;
+    }
+
+    // Currently, there's no good way of differentiating between programs
+    // in a way that helps humans. It will be a good idea to implement a
+    // standard of allowing debuggees to add some identifiable metadata to their
+    // program sources or instances.
+    const label = L10N.getFormatStr("shadersList.programLabel", this.itemCount);
+    const contents = document.createElement("label");
+    contents.className = "plain program-item";
+    contents.setAttribute("value", label);
+    contents.setAttribute("crop", "start");
+    contents.setAttribute("flex", "1");
+
+    // Append a program item to this container.
+    this.push([contents], {
+      index: -1, /* specifies on which position should the item be appended */
+      attachment: {
+        label: label,
+        programActor: programActor,
+        checkboxState: true,
+        checkboxTooltip: L10N.getStr("shadersList.blackboxLabel"),
+      },
+    });
+
+    // Make sure there's always a selected item available.
+    if (!this.selectedItem) {
+      this.selectedIndex = 0;
+    }
+
+    // Prevent this container from growing indefinitely in height when the
+    // toolbox is docked to the side.
+    if ((this.toolbox.hostType == "left" || this.toolbox.hostType == "right") &&
+        this.itemCount == SHADERS_AUTOGROW_ITEMS) {
+      this._pane.setAttribute("height", this._pane.getBoundingClientRect().height);
+    }
+  }
+
+  /**
+   * Returns whether a program was already added to this programs container.
+   *
+   * @param object programActor
+   *        The program actor coming from the active thread.
+   * @param boolean
+   *        True if the program was added, false otherwise.
+   */
+  hasProgram(programActor) {
+    return !!this.attachments.filter(e => e.programActor == programActor).length;
+  }
+
+  /**
+   * The select listener for the programs container.
+   */
+  _onProgramSelect({ detail: sourceItem }) {
+    if (!sourceItem) {
+      return;
+    }
+    // The container is not empty and an actual item was selected.
+    const attachment = sourceItem.attachment;
+
+    function getShaders() {
+      return promise.all([
+        attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()),
+        attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader()),
+      ]);
+    }
+    function getSources([vertexShaderActor, fragmentShaderActor]) {
+      return promise.all([
+        vertexShaderActor.getText(),
+        fragmentShaderActor.getText(),
+      ]);
+    }
+    const showSources = ([vertexShaderText, fragmentShaderText]) => {
+      return this.shadersEditorsView.setText({
+        vs: vertexShaderText,
+        fs: fragmentShaderText,
+      });
+    };
+
+    getShaders()
+      .then(getSources)
+      .then(showSources)
+      .catch(console.error);
+  }
+
+  /**
+   * The check listener for the programs container.
+   */
+  _onProgramCheck({ detail: { checked }, target }) {
+    const sourceItem = this.getItemForElement(target);
+    const attachment = sourceItem.attachment;
+    attachment.isBlackBoxed = !checked;
+    attachment.programActor[checked ? "unblackbox" : "blackbox"]();
+  }
+
+  /**
+   * The mouseover listener for the programs container.
+   */
+  _onProgramMouseOver(e) {
+    const sourceItem = this.getItemForElement(e.target, { noSiblings: true });
+    if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
+      sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT);
+
+      if (e instanceof Event) {
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    }
+  }
+
+  /**
+   * The mouseout listener for the programs container.
+   */
+  _onProgramMouseOut(e) {
+    const sourceItem = this.getItemForElement(e.target, { noSiblings: true });
+    if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
+      sourceItem.attachment.programActor.unhighlight();
+
+      if (e instanceof Event) {
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    }
+  }
+}
+exports.ShadersListView = ShadersListView;
+
+/**
+ * Functions handling the editors displaying the vertex and fragment shaders.
+ */
+class ShadersEditorsView {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize(panel, shadersListView) {
+    this.panel = panel;
+    this.shadersListView = shadersListView;
+    XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map());
+    this._vsFocused = this._onFocused.bind(this, "vs", "fs");
+    this._fsFocused = this._onFocused.bind(this, "fs", "vs");
+    this._vsChanged = this._onChanged.bind(this, "vs");
+    this._fsChanged = this._onChanged.bind(this, "fs");
+
+    this._errors = {
+      vs: [],
+      fs: [],
+    };
+  }
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  async destroy() {
+    this._destroyed = true;
+    await this._toggleListeners("off");
+    for (const p of this._editorPromises.values()) {
+      const editor = await p;
+      editor.destroy();
+    }
+  }
+
+  /**
+   * Sets the text displayed in the vertex and fragment shader editors.
+   *
+   * @param object sources
+   *        An object containing the following properties
+   *          - vs: the vertex shader source code
+   *          - fs: the fragment shader source code
+   * @return object
+   *        A promise resolving upon completion of text setting.
+   */
+  setText(sources) {
+    const view = this;
+    function setTextAndClearHistory(editor, text) {
+      editor.setText(text);
+      editor.clearHistory();
+    }
+
+    return (async function() {
+      await view._toggleListeners("off");
+      await promise.all([
+        view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)),
+        view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs)),
+      ]);
+      await view._toggleListeners("on");
+    })().then(() => this.panel.emit(EVENTS.SOURCES_SHOWN, sources));
+  }
+
+  /**
+   * Lazily initializes and returns a promise for an Editor instance.
+   *
+   * @param string type
+   *        Specifies for which shader type should an editor be retrieved,
+   *        either are "vs" for a vertex, or "fs" for a fragment shader.
+   * @return object
+   *        Returns a promise that resolves to an editor instance
+   */
+  _getEditor(type) {
+    if (this._editorPromises.has(type)) {
+      return this._editorPromises.get(type);
+    }
+
+    const promise = new Promise(resolve =>{
+      // Initialize the source editor and store the newly created instance
+      // in the ether of a resolved promise's value.
+      const parent = $("#" + type + "-editor");
+      const editor = new Editor(DEFAULT_EDITOR_CONFIG);
+      editor.config.mode = Editor.modes[type];
+
+      if (this._destroyed) {
+        resolve(editor);
+      } else {
+        editor.appendTo(parent).then(() => resolve(editor));
+      }
+    });
+    this._editorPromises.set(type, promise);
+    return promise;
+  }
+
+  /**
+   * Toggles all the event listeners for the editors either on or off.
+   *
+   * @param string flag
+   *        Either "on" to enable the event listeners, "off" to disable them.
+   * @return object
+   *        A promise resolving upon completion of toggling the listeners.
+   */
+  _toggleListeners(flag) {
+    return promise.all(["vs", "fs"].map(type => {
+      return this._getEditor(type).then(editor => {
+        editor[flag]("focus", this["_" + type + "Focused"]);
+        editor[flag]("change", this["_" + type + "Changed"]);
+      });
+    }));
+  }
+
+  /**
+   * The focus listener for a source editor.
+   *
+   * @param string focused
+   *        The corresponding shader type for the focused editor (e.g. "vs").
+   * @param string focused
+   *        The corresponding shader type for the other editor (e.g. "fs").
+   */
+  _onFocused(focused, unfocused) {
+    $("#" + focused + "-editor-label").setAttribute("selected", "");
+    $("#" + unfocused + "-editor-label").removeAttribute("selected");
+  }
+
+  /**
+   * The change listener for a source editor.
+   *
+   * @param string type
+   *        The corresponding shader type for the focused editor (e.g. "vs").
+   */
+  _onChanged(type) {
+    setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type));
+
+    // Remove all the gutter markers and line classes from the editor.
+    this._cleanEditor(type);
+  }
+
+  /**
+   * Recompiles the source code for the shader being edited.
+   * This function is fired at a certain delay after the user stops typing.
+   *
+   * @param string type
+   *        The corresponding shader type for the focused editor (e.g. "vs").
+   */
+  _doCompile(type) {
+    (async function() {
+      const editor = await this._getEditor(type);
+      const shaderActor = await this.shadersListView.selectedAttachment[type];
+
+      try {
+        await shaderActor.compile(editor.getText());
+        this._onSuccessfulCompilation();
+      } catch (e) {
+        this._onFailedCompilation(type, editor, e);
+      }
+    }.bind(this))();
+  }
+
+  /**
+   * Called uppon a successful shader compilation.
+   */
+  _onSuccessfulCompilation() {
+    // Signal that the shader was compiled successfully.
+    this.panel.emit(EVENTS.SHADER_COMPILED, null);
+  }
+
+  /**
+   * Called uppon an unsuccessful shader compilation.
+   */
+  _onFailedCompilation(type, editor, errors) {
+    const lineCount = editor.lineCount();
+    const currentLine = editor.getCursor().line;
+    const listeners = { mouseover: this._onMarkerMouseOver };
+
+    function matchLinesAndMessages(string) {
+      return {
+        // First number that is not equal to 0.
+        lineMatch: string.match(/\d{2,}|[1-9]/),
+        // The string after all the numbers, semicolons and spaces.
+        textMatch: string.match(/[^\s\d:][^\r\n|]*/),
+      };
+    }
+    function discardInvalidMatches(e) {
+      // Discard empty line and text matches.
+      return e.lineMatch && e.textMatch;
+    }
+    function sanitizeValidMatches(e) {
+      return {
+        // Drivers might yield confusing line numbers under some obscure
+        // circumstances. Don't throw the errors away in those cases,
+        // just display them on the currently edited line.
+        line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1,
+        // Trim whitespace from the beginning and the end of the message,
+        // and replace all other occurences of double spaces to a single space.
+        text: e.textMatch[0].trim().replace(/\s{2,}/g, " "),
+      };
+    }
+    function sortByLine(first, second) {
+      // Sort all the errors ascending by their corresponding line number.
+      return first.line > second.line ? 1 : -1;
+    }
+    function groupSameLineMessages(accumulator, current) {
+      // Group errors corresponding to the same line number to a single object.
+      const previous = accumulator[accumulator.length - 1];
+      if (!previous || previous.line != current.line) {
+        return [...accumulator, {
+          line: current.line,
+          messages: [current.text],
+        }];
+      }
+      previous.messages.push(current.text);
+      return accumulator;
+    }
+    function displayErrors({ line, messages }) {
+      // Add gutter markers and line classes for every error in the source.
+      editor.addMarker(line, "errors", "error");
+      editor.setMarkerListeners(line, "errors", "error", listeners, messages);
+      editor.addLineClass(line, "error-line");
+    }
+
+    (this._errors[type] = errors.link
+      .split("ERROR")
+      .map(matchLinesAndMessages)
+      .filter(discardInvalidMatches)
+      .map(sanitizeValidMatches)
+      .sort(sortByLine)
+      .reduce(groupSameLineMessages, []))
+      .forEach(displayErrors);
+
+    // Signal that the shader wasn't compiled successfully.
+    this.panel.emit(EVENTS.SHADER_COMPILED, errors);
+  }
+
+  /**
+   * Event listener for the 'mouseover' event on a marker in the editor gutter.
+   */
+  _onMarkerMouseOver(line, node, messages) {
+    if (node._markerErrorsTooltip) {
+      return;
+    }
+
+    const tooltip = node._markerErrorsTooltip = new HTMLTooltip(document, {
+      type: "arrow",
+      useXulWrapper: true,
+    });
+
+    const div = document.createElementNS(XHTML_NS, "div");
+    div.className = "devtools-shader-tooltip-container";
+    for (const message of messages) {
+      const messageDiv = document.createElementNS(XHTML_NS, "div");
+      messageDiv.className = "devtools-tooltip-simple-text";
+      messageDiv.textContent = message;
+      div.appendChild(messageDiv);
+    }
+    tooltip.panel.appendChild(div);
+
+    tooltip.startTogglingOnHover(node, () => true, {
+      toggleDelay: GUTTER_ERROR_PANEL_DELAY,
+    });
+  }
+
+  /**
+   * Removes all the gutter markers and line classes from the editor.
+   */
+  _cleanEditor(type) {
+    this._getEditor(type).then(editor => {
+      editor.removeAllMarkers("errors");
+      this._errors[type].forEach(e => editor.removeLineClass(e.line));
+      this._errors[type].length = 0;
+      this.panel.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
+    });
+  }
+}
+exports.ShadersEditorsView = ShadersEditorsView;
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(STRINGS_URI);
+exports.L10N = L10N;
+
+/**
+ * DOM query helper.
+ */
+var $ = (selector, target = document) => target.querySelector(selector);
+exports.$ = $;
new file mode 100644
--- /dev/null
+++ b/devtools/client/shadereditor/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+  // Extend from the shared list of defined globals for mochitests.
+  "extends": "../../../.eslintrc.mochitests.js"
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser.ini
@@ -0,0 +1,59 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+  doc_blended-geometry.html
+  doc_multiple-contexts.html
+  doc_overlapping-geometry.html
+  doc_shader-order.html
+  doc_simple-canvas.html
+  head.js
+  !/devtools/client/shared/test/frame-script-utils.js
+  !/devtools/client/shared/test/shared-head.js
+  !/devtools/client/shared/test/telemetry-test-helpers.js
+
+[browser_se_aaa_run_first_leaktest.js]
+[browser_se_bfcache.js]
+skip-if = true # Bug 942473, caused by Bug 940541
+[browser_se_editors-contents.js]
+skip-if = (verify && (os == 'win'))
+[browser_se_editors-error-gutter.js]
+skip-if = (verify && !debug && (os == 'win'))
+[browser_se_editors-error-tooltip.js]
+skip-if = (verify && (os == 'win' || os == 'linux'))
+[browser_se_editors-lazy-init.js]
+[browser_se_first-run.js]
+[browser_se_navigation.js]
+[browser_se_programs-blackbox-01.js]
+[browser_se_programs-blackbox-02.js]
+[browser_se_programs-cache.js]
+[browser_se_programs-highlight-01.js]
+skip-if = (verify && debug && (os == 'win'))
+[browser_se_programs-highlight-02.js]
+[browser_se_programs-list.js]
+[browser_se_shaders-edit-01.js]
+[browser_se_shaders-edit-02.js]
+[browser_se_shaders-edit-03.js]
+skip-if = (verify && (os == 'win'))
+[browser_webgl-actor-test-01.js]
+[browser_webgl-actor-test-02.js]
+[browser_webgl-actor-test-03.js]
+[browser_webgl-actor-test-04.js]
+[browser_webgl-actor-test-05.js]
+skip-if = (verify && !debug && (os == 'linux'))
+[browser_webgl-actor-test-06.js]
+skip-if = (verify && (os == 'linux'))
+[browser_webgl-actor-test-07.js]
+[browser_webgl-actor-test-08.js]
+skip-if = (verify && debug && (os == 'win'))
+[browser_webgl-actor-test-09.js]
+[browser_webgl-actor-test-10.js]
+[browser_webgl-actor-test-11.js]
+[browser_webgl-actor-test-12.js]
+[browser_webgl-actor-test-13.js]
+[browser_webgl-actor-test-14.js]
+[browser_webgl-actor-test-15.js]
+[browser_webgl-actor-test-16.js]
+[browser_webgl-actor-test-17.js]
+skip-if = (verify && debug && (os == 'win'))
+[browser_webgl-actor-test-18.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_aaa_run_first_leaktest.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shader editor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+async function ifWebGLSupported() {
+  const { target, panel } = await initShaderEditor(SIMPLE_CANVAS_URL);
+
+  ok(target, "Should have a target available.");
+  ok(panel, "Should have a panel available.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_bfcache.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shader editor works with bfcache.
+ */
+async function ifWebGLSupported() {
+  const { target, panel } = await initShaderEditor(SIMPLE_CANVAS_URL);
+  const { front, $, EVENTS, shadersListView, shadersEditorsView } = panel;
+
+  // Attach frame scripts if in e10s to perform
+  // history navigation via the content
+  loadFrameScripts();
+
+  const reloaded = reload(target);
+  const firstProgram = await once(front, "program-linked");
+  await reloaded;
+
+  const navigated = navigate(target, MULTIPLE_CONTEXTS_URL);
+  const [secondProgram, thirdProgram] = await getPrograms(front, 2);
+  await navigated;
+
+  const vsEditor = await shadersEditorsView._getEditor("vs");
+  const fsEditor = await shadersEditorsView._getEditor("fs");
+
+  await navigateInHistory(target, "back", "will-navigate");
+  await once(panel, EVENTS.PROGRAMS_ADDED);
+  await once(panel, EVENTS.SOURCES_SHOWN);
+
+  is($("#content").hidden, false,
+    "The tool's content should not be hidden.");
+  is(shadersListView.itemCount, 1,
+    "The shaders list contains one entry after navigating back.");
+  is(shadersListView.selectedIndex, 0,
+    "The shaders list has a correct selection after navigating back.");
+
+  is(vsEditor.getText().indexOf("gl_Position"), 170,
+    "The vertex shader editor contains the correct text.");
+  is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+    "The fragment shader editor contains the correct text.");
+
+  await navigateInHistory(target, "forward", "will-navigate");
+  await once(panel, EVENTS.PROGRAMS_ADDED);
+  await once(panel, EVENTS.SOURCES_SHOWN);
+
+  is($("#content").hidden, false,
+    "The tool's content should not be hidden.");
+  is(shadersListView.itemCount, 2,
+    "The shaders list contains two entries after navigating forward.");
+  is(shadersListView.selectedIndex, 0,
+    "The shaders list has a correct selection after navigating forward.");
+
+  is(vsEditor.getText().indexOf("gl_Position"), 100,
+    "The vertex shader editor contains the correct text.");
+  is(fsEditor.getText().indexOf("gl_FragColor"), 89,
+    "The fragment shader editor contains the correct text.");
+
+  await teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/shadereditor/test/browser_se_editors-contents.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://cre