Bug 917226 - Build a canvas inspection tool, r=rcampbell, jryans

This commit is contained in:
Victor Porof 2014-03-29 13:01:37 -04:00
parent 96b586969b
commit 41f23a846e
70 changed files with 5761 additions and 101 deletions

View File

@ -1205,6 +1205,9 @@ pref("devtools.styleeditor.autocompletion-enabled", true);
// Enable the Shader Editor.
pref("devtools.shadereditor.enabled", false);
// Enable the Canvas Debugger.
pref("devtools.canvasdebugger.enabled", false);
// Enable tools for Chrome development.
pref("devtools.chrome.enabled", false);

File diff suppressed because it is too large Load Diff

View File

@ -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://browser/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/canvasdebugger.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % canvasDebuggerDTD SYSTEM "chrome://browser/locale/devtools/canvasdebugger.dtd">
%canvasDebuggerDTD;
]>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://browser/content/devtools/theme-switching.js"/>
<script type="application/javascript" src="canvasdebugger.js"/>
<hbox class="theme-body" flex="1">
<vbox id="snapshots-pane">
<toolbar id="snapshots-toolbar"
class="devtools-toolbar">
<hbox id="snapshots-controls"
class="devtools-toolbarbutton-group">
<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()"
label="&canvasDebuggerUI.importSnapshot;"/>
<toolbarbutton id="clear-snapshots"
class="devtools-toolbarbutton"
oncommand="SnapshotsListView._onClearButtonClick()"
label="&canvasDebuggerUI.clearSnapshots;"/>
</hbox>
</toolbar>
<vbox id="snapshots-list" flex="1"/>
</vbox>
<vbox id="debugging-pane" flex="1">
<hbox id="reload-notice"
class="notice-container"
align="center"
pack="center"
flex="1">
<button id="reload-notice-button"
class="devtools-toolbarbutton"
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"
oncommand="SnapshotsListView._onRecordButtonClick()"/>
<label value="&canvasDebuggerUI.emptyNotice2;"/>
</hbox>
<hbox id="import-notice"
class="notice-container"
align="center"
pack="center"
flex="1"
hidden="true">
<label value="&canvasDebuggerUI.importNotice;"/>
</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=""/>
<scale id="calls-slider"
movetoclick="true"
flex="100"/>
<textbox id="calls-searchbox"
class="devtools-searchinput"
placeholder="&canvasDebuggerUI.searchboxPlaceholder;"
type="search"
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>

View File

@ -0,0 +1,12 @@
# 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/.
TEST_DIRS += ['test']
JS_MODULES_PATH = 'modules/devtools/canvasdebugger'
EXTRA_JS_MODULES += [
'panel.js'
]

View File

@ -0,0 +1,72 @@
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const EventEmitter = require("devtools/toolkit/event-emitter");
const { CanvasFront } = require("devtools/server/actors/canvas");
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
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: function() {
let targetPromise;
// Local debugging needs to make the target remote.
if (!this.target.isRemote) {
targetPromise = this.target.makeRemote();
} else {
targetPromise = promise.resolve(this.target);
}
return targetPromise
.then(() => {
this.panelWin.gToolbox = this._toolbox;
this.panelWin.gTarget = this.target;
this.panelWin.gFront = new CanvasFront(this.target.client, this.target.form);
return this.panelWin.startupCanvasDebugger();
})
.then(() => {
this.isReady = true;
this.emit("ready");
return this;
})
.then(null, function onError(aReason) {
DevToolsUtils.reportException("CanvasDebuggerPanel.prototype.open", aReason);
});
},
// DevToolPanel API
get target() 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(() => {
this.emit("destroyed");
});
}
};

View File

@ -0,0 +1,34 @@
[DEFAULT]
support-files =
doc_simple-canvas.html
doc_simple-canvas-deep-stack.html
doc_simple-canvas-transparent.html
head.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-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-reload-01.js]
[browser_canvas-frontend-reload-02.js]
[browser_canvas-frontend-slider-01.js]
[browser_canvas-frontend-slider-02.js]
[browser_canvas-frontend-snapshot-select.js]
[browser_canvas-frontend-stepping.js]

View File

@ -0,0 +1,18 @@
/* 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.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCallWatcherBackend(SIMPLE_CANVAS_URL);
ok(target, "Should have a target available.");
ok(debuggee, "Should have a debuggee available.");
ok(front, "Should have a protocol front available.");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,77 @@
/* 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.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCallWatcherBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({
tracedGlobals: ["CanvasRenderingContext2D", "WebGLRenderingContext"],
startRecording: true,
performReload: true
});
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
// Allow the content to execute some functions.
yield waitForTick();
let functionCalls = yield 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, CallWatcherFront.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, "ctx",
"The called function's caller preview is correct.");
is(functionCalls[0].argsPreview, "0, 0, 128, 128",
"The called function's args preview is correct.");
let details = yield 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).");
yield removeTab(target.tab);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
ok(snapshotActor,
"A snapshot actor was sent after recording.");
let animationOverview = yield snapshotActor.getOverview();
ok(snapshotActor,
"An animation overview could be retrieved after recording.");
let 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, CallWatcherFront.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, "ctx",
"The first called function's caller preview is correct.");
is(functionCalls[6].type, CallWatcherFront.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, "ctx",
"The penultimate called function's caller preview is correct.");
is(functionCalls[7].type, CallWatcherFront.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.contains("Function"),
"The last called function's args preview is correct.");
is(functionCalls[7].callerPreview, "",
"The last called function's caller preview is correct.");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,80 @@
/* 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.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
ok(snapshotActor,
"A snapshot actor was sent after recording.");
let animationOverview = yield snapshotActor.getOverview();
ok(snapshotActor,
"An animation overview could be retrieved after recording.");
let 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(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(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(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(thumbnails[3].pixels, e => e > 0), 4290822336,
"The fourth thumbnail's pixels seem to not be completely transparent.");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,45 @@
/* 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.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
ok(snapshotActor,
"A snapshot actor was sent after recording.");
let animationOverview = yield snapshotActor.getOverview();
ok(snapshotActor,
"An animation overview could be retrieved after recording.");
let 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(screenshot.pixels, e => e > 0), 4290822336,
"The screenshot's pixels seem to not be completely transparent.");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,95 @@
/* 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.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_TRANSPARENT_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
let animationOverview = yield snapshotActor.getOverview();
let 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.");
let firstDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[0]);
let secondDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[2]);
let thirdDrawCallScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[4]);
let fourthDrawCallScreenshot = yield 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(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(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(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(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).");
yield removeTab(target.tab);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(SIMPLE_CANVAS_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
let animationOverview = yield snapshotActor.getOverview();
let 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.");
let firstNonDrawCall = yield functionCalls[1].getDetails();
let secondNonDrawCall = yield functionCalls[3].getDetails();
let lastNonDrawCall = yield 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.");
let firstScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[1]);
let secondScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[3]);
let lastScreenshot = yield 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.");
let firstActualScreenshot = yield 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.");
let secondActualScreenshot = yield 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.");
let lastActualScreenshot = yield 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).");
yield 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;
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield 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.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
is(CallsListView.itemCount, 8,
"All the function calls should now be displayed in the UI.");
testItem(CallsListView.getItemAtIndex(0),
"1", "ctx", "clearRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:25");
testItem(CallsListView.getItemAtIndex(1),
"2", "ctx", "fillStyle", " = rgb(192, 192, 192)", "doc_simple-canvas.html:20");
testItem(CallsListView.getItemAtIndex(2),
"3", "ctx", "fillRect", "(0, 0, 128, 128)", "doc_simple-canvas.html:21");
testItem(CallsListView.getItemAtIndex(3),
"4", "ctx", "fillStyle", " = rgba(0, 0, 192, 0.5)", "doc_simple-canvas.html:20");
testItem(CallsListView.getItemAtIndex(4),
"5", "ctx", "fillRect", "(30, 30, 55, 50)", "doc_simple-canvas.html:21");
testItem(CallsListView.getItemAtIndex(5),
"6", "ctx", "fillStyle", " = rgba(192, 0, 0, 0.5)", "doc_simple-canvas.html:20");
testItem(CallsListView.getItemAtIndex(6),
"7", "ctx", "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) {
let 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), null,
"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.");
}
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
let searchbox = $("#calls-searchbox");
yield reload(target);
let firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield 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, CallWatcherFront.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, "ctx",
"The visible item's caller has the expected value.");
let secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield secondRecordingFinished;
SnapshotsListView.selectedIndex = 1;
yield 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.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
let callItem = CallsListView.getItemAtIndex(2);
let 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.");
let callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window);
yield callStackDisplayed;
isnot($(".call-item-stack", callItem.target), null,
"There should be a stack container available now for the draw call.");
is($all(".call-item-stack-fn", callItem.target).length, 4,
"There should be 4 functions on the stack for the draw call.");
ok($all(".call-item-stack-fn-name", callItem.target)[0].getAttribute("value")
.contains("C()"),
"The first function on the stack has the correct name.");
ok($all(".call-item-stack-fn-name", callItem.target)[1].getAttribute("value")
.contains("B()"),
"The second function on the stack has the correct name.");
ok($all(".call-item-stack-fn-name", callItem.target)[2].getAttribute("value")
.contains("A()"),
"The third function on the stack has the correct name.");
ok($all(".call-item-stack-fn-name", callItem.target)[3].getAttribute("value")
.contains("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.");
let jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-stack-fn-location", callItem.target));
yield jumpedToSource;
let toolbox = yield gDevTools.getToolbox(target);
let { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger");
is(view.Sources.selectedValue, SIMPLE_CANVAS_DEEP_STACK_URL,
"The expected source was shown in the debugger.");
is(view.editor.getCursor().line, 25,
"The expected source line is highlighted in the debugger.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
let callItem = CallsListView.getItemAtIndex(2);
let 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.");
let callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
EventUtils.sendMouseEvent({ type: "mousedown" }, locationLink, window);
yield callStackDisplayed;
isnot($(".call-item-stack", callItem.target), null,
"There should be a stack container available now for the draw call.");
is($all(".call-item-stack-fn", callItem.target).length, 4,
"There should be 4 functions on the stack for the draw call.");
let jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-location", callItem.target));
yield jumpedToSource;
let toolbox = yield gDevTools.getToolbox(target);
let { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger");
is(view.Sources.selectedValue, SIMPLE_CANVAS_DEEP_STACK_URL,
"The expected source was shown in the debugger.");
is(view.editor.getCursor().line, 23,
"The expected source line is highlighted in the debugger.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,61 @@
/* 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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
let callItem = CallsListView.getItemAtIndex(2);
let view = $(".call-item-view", callItem.target);
let 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.");
let callStackDisplayed = once(window, EVENTS.CALL_STACK_DISPLAYED);
EventUtils.sendMouseEvent({ type: "dblclick" }, contents, window);
yield 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.");
is($all(".call-item-stack-fn", callItem.target).length, 4,
"There should be 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.");
is($all(".call-item-stack-fn", callItem.target).length, 4,
"There should still be 4 functions on the stack for the draw call.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, SnapshotsListView } = panel.panelWin;
yield reload(target);
let firstRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
yield 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.");
let secondRecordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
yield 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.");
let clearingFinished = once(window, EVENTS.SNAPSHOTS_LIST_CLEARED);
SnapshotsListView._onClearButtonClick();
yield clearingFinished;
ok(true, "Finished recording all snapshots.");
is(SnapshotsListView.itemCount, 0,
"There should be no items available in the snapshots list.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated, screenshotDisplayed]);
is($("#screenshot-container").hidden, false,
"The screenshot container should now be visible.");
is($("#screenshot-dimensions").getAttribute("value"), "128 x 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.contains("#screenshot-rendering"),
"The screenshot element should have an offscreen canvas element as a background.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, $all, EVENTS, SnapshotsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated, thumbnailsDisplayed]);
is($all(".filmstrip-thumbnail").length, 4,
"There should be 4 thumbnails displayed in the UI.");
let 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.");
let 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.");
let 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.");
let 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.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield 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);
yield 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);
yield 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);
yield 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.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { $ } = 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($("#import-notice").getAttribute("hidden"), "true",
"The import 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.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,62 @@
/* 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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
yield 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.");
let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
yield 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").getAttribute("disabled"), "true",
"The 'record snapshot' button should now be disabled.");
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.");
yield 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.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, L10N, $, SnapshotsListView } = panel.panelWin;
yield reload(target);
let recordingStarted = once(window, EVENTS.SNAPSHOT_RECORDING_STARTED);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let recordingSelected = once(window, EVENTS.SNAPSHOT_RECORDING_SELECTED);
SnapshotsListView._onRecordButtonClick();
yield recordingStarted;
ok(true, "Started recording a snapshot of the animation loop.");
let 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($("#import-notice").hasAttribute("hidden"), false,
"The import 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.");
yield recordingFinished;
ok(true, "Finished recording a snapshot of the animation loop.");
yield 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($("#import-notice").getAttribute("hidden"), "true",
"The import 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.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, EVENTS, $, SnapshotsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
yield recordingFinished;
ok(true, "Finished recording a snapshot of the animation loop.");
let 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.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS } = panel.panelWin;
let reset = once(window, EVENTS.UI_RESET);
let navigated = reload(target);
yield reset;
ok(true, "The UI was reset after the refresh button was clicked.");
yield 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($("#import-notice").getAttribute("hidden"), "true",
"The import 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.");
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { 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).");
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield 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).");
let reset = once(window, EVENTS.UI_RESET);
let navigated = reload(target);
yield 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.");
yield navigated;
ok(true, "The target finished reloading.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the slider in the calls list view works as advertised.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated]);
is(CallsListView.selectedIndex, -1,
"No item in the function calls list should be initially selected.");
is($("#calls-slider").value, 0,
"The slider should be moved all the way to the start.");
is($("#calls-slider").min, 0,
"The slider minimum value should be 0.");
is($("#calls-slider").max, 7,
"The slider maximum value should be 7.");
CallsListView.selectedIndex = 1;
is($("#calls-slider").value, 1,
"The slider should be changed according to the current selection.");
$("#calls-slider").value = 2;
is(CallsListView.selectedIndex, 2,
"The calls selection should be changed according to the current slider value.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,97 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the slider in the calls list view works as advertised.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, gFront, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
SnapshotsListView._onRecordButtonClick();
yield promise.all([recordingFinished, callListPopulated, thumbnailsDisplayed]);
let firstSnapshot = SnapshotsListView.getItemAtIndex(0);
let firstSnapshotOverview = yield firstSnapshot.attachment.actor.getOverview();
let thumbnails = firstSnapshotOverview.thumbnails;
is(thumbnails.length, 4,
"There should be 4 thumbnails cached for the snapshot item.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 1;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[0].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 1.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 2;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[1].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 2.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 7;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[3].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 7.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 4;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[2].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 4.");
let thumbnailImageElementSet = waitForMozSetImageElement(window);
$("#calls-slider").value = 0;
let thumbnailPixels = yield thumbnailImageElementSet;
ok(sameArray(thumbnailPixels, thumbnails[0].pixels),
"The screenshot element should have a thumbnail as an immediate background.");
yield once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
ok(true, "The full-sized screenshot was displayed for the item at index 0.");
yield teardown(panel);
finish();
}
function waitForMozSetImageElement(panel) {
let deferred = promise.defer();
panel._onMozSetImageElement = deferred.resolve;
return deferred.promise;
}
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;
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
yield 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.");
yield 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.");
let secondSnapshotTarget = SnapshotsListView.getItemAtIndex(1).target;
let snapshotSelected = waitForSnapshotSelection();
EventUtils.sendMouseEvent({ type: "mousedown" }, secondSnapshotTarget, window);
yield 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.");
let firstDrawCallContents = $(".call-item-contents", CallsListView.getItemAtIndex(2).target);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
EventUtils.sendMouseEvent({ type: "mousedown" }, firstDrawCallContents, window);
yield 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.");
let firstSnapshotTarget = SnapshotsListView.getItemAtIndex(0).target;
let snapshotSelected = waitForSnapshotSelection();
EventUtils.sendMouseEvent({ type: "mousedown" }, firstSnapshotTarget, window);
yield 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() {
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let snapshotSelected = waitForSnapshotSelection();
SnapshotsListView._onRecordButtonClick();
return promise.all([recordingFinished, snapshotSelected]);
}
function recordAndWaitForAnotherSnapshot() {
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
SnapshotsListView._onRecordButtonClick();
return recordingFinished;
}
function waitForSnapshotSelection() {
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
let thumbnailsDisplayed = once(window, EVENTS.THUMBNAILS_DISPLAYED);
let screenshotDisplayed = once(window, EVENTS.CALL_SCREENSHOT_DISPLAYED);
return promise.all([
callListPopulated,
thumbnailsDisplayed,
screenshotDisplayed
]);
}
yield teardown(panel);
finish();
}

View File

@ -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.
*/
function ifTestingSupported() {
let [target, debuggee, panel] = yield initCanavsDebuggerFrontend(SIMPLE_CANVAS_URL);
let { window, $, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin;
yield reload(target);
let recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED);
let callListPopulated = once(window, EVENTS.CALL_LIST_POPULATED);
SnapshotsListView._onRecordButtonClick();
yield 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.");
}
}
yield teardown(panel);
finish();
}

View File

@ -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;version=1.8">
"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>

View File

@ -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;version=1.8">
"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>

View File

@ -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;version=1.8">
"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>

View File

@ -0,0 +1,234 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
// Disable logging for all the tests. Both the debugger server and frontend will
// be affected by this pref.
let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
Services.prefs.setBoolPref("devtools.debugger.log", false);
let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
let { CallWatcherFront } = devtools.require("devtools/server/actors/call-watcher");
let { CanvasFront } = devtools.require("devtools/server/actors/canvas");
let TiltGL = devtools.require("devtools/tilt/tilt-gl");
let TargetFactory = devtools.TargetFactory;
let Toolbox = devtools.Toolbox;
const EXAMPLE_URL = "http://example.com/browser/browser/devtools/canvasdebugger/test/";
const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.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";
// All tests are asynchronous.
waitForExplicitFinish();
let gToolEnabled = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
registerCleanupFunction(() => {
info("finish() was called, cleaning up...");
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 addTab(aUrl, aWindow) {
info("Adding tab: " + aUrl);
let deferred = promise.defer();
let targetWindow = aWindow || window;
let targetBrowser = targetWindow.gBrowser;
targetWindow.focus();
let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
let linkedBrowser = tab.linkedBrowser;
linkedBrowser.addEventListener("load", function onLoad() {
linkedBrowser.removeEventListener("load", onLoad, true);
info("Tab added and finished loading: " + aUrl);
deferred.resolve(tab);
}, true);
return deferred.promise;
}
function removeTab(aTab, aWindow) {
info("Removing tab.");
let deferred = promise.defer();
let targetWindow = aWindow || window;
let targetBrowser = targetWindow.gBrowser;
let tabContainer = targetBrowser.tabContainer;
tabContainer.addEventListener("TabClose", function onClose(aEvent) {
tabContainer.removeEventListener("TabClose", onClose, false);
info("Tab removed and finished closing.");
deferred.resolve();
}, false);
targetBrowser.removeTab(aTab);
return deferred.promise;
}
function handleError(aError) {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
}
let 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();
}
function test() {
let generator = isTestingSupported() ? ifTestingSupported : ifTestingUnsupported;
Task.spawn(generator).then(null, handleError);
}
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;
}
let supported =
!TiltGL.isWebGLForceEnabled() &&
TiltGL.isWebGLSupported() &&
TiltGL.create3DContext(createCanvas());
info("This test requires WebGL support.");
info("Apparently, WebGL is" + (supported ? "" : " not") + " supported.");
return supported;
}
function once(aTarget, aEventName, aUseCapture = false) {
info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
let deferred = promise.defer();
for (let [add, remove] of [
["on", "off"], // Use event emitter before DOM events for consistency
["addEventListener", "removeEventListener"],
["addListener", "removeListener"]
]) {
if ((add in aTarget) && (remove in aTarget)) {
aTarget[add](aEventName, function onEvent(...aArgs) {
aTarget[remove](aEventName, onEvent, aUseCapture);
deferred.resolve(...aArgs);
}, aUseCapture);
break;
}
}
return deferred.promise;
}
function waitForTick() {
let deferred = promise.defer();
executeSoon(deferred.resolve);
return deferred.promise;
}
function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") {
executeSoon(() => content.history[aDirection]());
return once(aTarget, aWaitForTargetEvent);
}
function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
return once(aTarget, aWaitForTargetEvent);
}
function reload(aTarget, aWaitForTargetEvent = "navigate") {
executeSoon(() => aTarget.activeTab.reload());
return once(aTarget, aWaitForTargetEvent);
}
function initServer() {
if (!DebuggerServer.initialized) {
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
}
}
function initCallWatcherBackend(aUrl) {
info("Initializing a call watcher front.");
initServer();
return Task.spawn(function*() {
let tab = yield addTab(aUrl);
let target = TargetFactory.forTab(tab);
let debuggee = target.window.wrappedJSObject;
yield target.makeRemote();
let front = new CallWatcherFront(target.client, target.form);
return [target, debuggee, front];
});
}
function initCanavsDebuggerBackend(aUrl) {
info("Initializing a canvas debugger front.");
initServer();
return Task.spawn(function*() {
let tab = yield addTab(aUrl);
let target = TargetFactory.forTab(tab);
let debuggee = target.window.wrappedJSObject;
yield target.makeRemote();
let front = new CanvasFront(target.client, target.form);
return [target, debuggee, front];
});
}
function initCanavsDebuggerFrontend(aUrl) {
info("Initializing a canvas debugger pane.");
return Task.spawn(function*() {
let tab = yield addTab(aUrl);
let target = TargetFactory.forTab(tab);
let debuggee = target.window.wrappedJSObject;
yield target.makeRemote();
Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
let toolbox = yield gDevTools.showToolbox(target, "canvasdebugger");
let panel = toolbox.getCurrentPanel();
return [target, debuggee, panel];
});
}
function teardown(aPanel) {
info("Destroying the specified canvas debugger.");
return promise.all([
once(aPanel, "destroyed"),
removeTab(aPanel.target.tab)
]);
}

View File

@ -0,0 +1,6 @@
# 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/.
BROWSER_CHROME_MANIFESTS += ['browser.ini']

View File

@ -390,8 +390,7 @@ let DebuggerView = {
this._setEditorText(L10N.getStr("loadingText"));
this._editorSource = { url: aSource.url, promise: deferred.promise };
DebuggerController.SourceScripts.getText(aSource)
.then(([, aText, aContentType]) => {
DebuggerController.SourceScripts.getText(aSource).then(([, aText, aContentType]) => {
// Avoid setting an unexpected source. This may happen when switching
// very fast between sources that haven't been fetched yet.
if (this._editorSource.url != aSource.url) {
@ -469,8 +468,7 @@ let DebuggerView = {
// Make sure the requested source client is shown in the editor, then
// update the source editor's caret position and debug location.
return this._setEditorSource(sourceForm, aFlags)
.then(([,, aContentType]) => {
return this._setEditorSource(sourceForm, aFlags).then(([,, aContentType]) => {
// Record the contentType learned from fetching
sourceForm.contentType = aContentType;
// Line numbers in the source editor should start from 1. If invalid

View File

@ -434,7 +434,7 @@
<tabpanels flex="1">
<tabpanel id="variables-tabpanel">
<vbox id="expressions"/>
<splitter class="devtools-horizontal-splitter devtools-invisible splitter"/>
<splitter class="devtools-horizontal-splitter devtools-invisible-splitter"/>
<vbox id="variables" flex="1"/>
</tabpanel>
<tabpanel id="events-tabpanel">

View File

@ -8,7 +8,6 @@
const { Cc, Ci, Cu, Cr } = require("chrome");
const promise = require("sdk/core/promise");
const EventEmitter = require("devtools/toolkit/event-emitter");
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
function DebuggerPanel(iframeWindow, toolbox) {
@ -60,7 +59,7 @@ DebuggerPanel.prototype = {
return this;
})
.then(null, function onError(aReason) {
DevToolsUtils.reportException("DebuggerPane.prototype.open", aReason);
DevToolsUtils.reportException("DebuggerPanel.prototype.open", aReason);
});
},

View File

@ -245,8 +245,7 @@ function waitForSourceShown(aPanel, aUrl) {
}
function waitForEditorLocationSet(aPanel) {
return waitForDebuggerEvents(aPanel,
aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET);
return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET);
}
function ensureSourceIs(aPanel, aUrl, aWaitFlag = false) {

View File

@ -60,6 +60,8 @@ browser.jar:
content/browser/devtools/debugger-panes.js (debugger/debugger-panes.js)
content/browser/devtools/shadereditor.xul (shadereditor/shadereditor.xul)
content/browser/devtools/shadereditor.js (shadereditor/shadereditor.js)
content/browser/devtools/canvasdebugger.xul (canvasdebugger/canvasdebugger.xul)
content/browser/devtools/canvasdebugger.js (canvasdebugger/canvasdebugger.js)
content/browser/devtools/profiler.xul (profiler/profiler.xul)
content/browser/devtools/cleopatra.html (profiler/cleopatra/cleopatra.html)
content/browser/devtools/profiler/cleopatra/css/ui.css (profiler/cleopatra/css/ui.css)

View File

@ -28,6 +28,7 @@ loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/pa
loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/panel").DebuggerPanel);
loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/styleeditor/styleeditor-panel").StyleEditorPanel);
loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel);
loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/canvasdebugger/panel").CanvasDebuggerPanel);
loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel"));
loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel);
loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
@ -38,6 +39,7 @@ const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties";
const canvasDebuggerProps = "chrome://browser/locale/devtools/canvasdebugger.properties";
const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
@ -47,6 +49,7 @@ loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle
loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps));
loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBundle(shaderEditorProps));
loader.lazyGetter(this, "canvasDebuggerStrings", () => Services.strings.createBundle(canvasDebuggerProps));
loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps));
loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
@ -200,11 +203,31 @@ Tools.shaderEditor = {
}
};
Tools.canvasDebugger = {
id: "canvasdebugger",
ordinal: 6,
visibilityswitch: "devtools.canvasdebugger.enabled",
icon: "chrome://browser/skin/devtools/tool-styleeditor.svg",
invertIconForLightTheme: true,
url: "chrome://browser/content/devtools/canvasdebugger.xul",
label: l10n("ToolboxCanvasDebugger.label", canvasDebuggerStrings),
tooltip: l10n("ToolboxCanvasDebugger.tooltip", canvasDebuggerStrings),
isTargetSupported: function(target) {
return true;
},
build: function(iframeWindow, toolbox) {
let panel = new CanvasDebuggerPanel(iframeWindow, toolbox);
return panel.open();
}
};
Tools.jsprofiler = {
id: "jsprofiler",
accesskey: l10n("profiler.accesskey", profilerStrings),
key: l10n("profiler2.commandkey", profilerStrings),
ordinal: 6,
ordinal: 7,
modifiers: "shift",
visibilityswitch: "devtools.profiler.enabled",
icon: "chrome://browser/skin/devtools/tool-profiler.svg",
@ -228,7 +251,7 @@ Tools.netMonitor = {
id: "netmonitor",
accesskey: l10n("netmonitor.accesskey", netMonitorStrings),
key: l10n("netmonitor.commandkey", netMonitorStrings),
ordinal: 7,
ordinal: 8,
modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
visibilityswitch: "devtools.netmonitor.enabled",
icon: "chrome://browser/skin/devtools/tool-network.svg",
@ -251,7 +274,7 @@ Tools.netMonitor = {
Tools.scratchpad = {
id: "scratchpad",
ordinal: 8,
ordinal: 9,
visibilityswitch: "devtools.scratchpad.enabled",
icon: "chrome://browser/skin/devtools/tool-scratchpad.svg",
invertIconForLightTheme: true,
@ -277,6 +300,7 @@ let defaultTools = [
Tools.jsdebugger,
Tools.styleEditor,
Tools.shaderEditor,
Tools.canvasDebugger,
Tools.jsprofiler,
Tools.netMonitor,
Tools.scratchpad

View File

@ -6,6 +6,7 @@
DIRS += [
'app-manager',
'canvasdebugger',
'commandline',
'debugger',
'fontinspector',

View File

@ -117,6 +117,9 @@ const {Tooltip} = require("devtools/shared/widgets/Tooltip");
XPCOMUtils.defineLazyModuleGetter(this, "Chart",
"resource:///modules/devtools/Chart.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Curl",
"resource:///modules/devtools/Curl.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
@ -126,23 +129,17 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
"resource://gre/modules/devtools/DevToolsUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "devtools",
"resource://gre/modules/devtools/Loader.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
Object.defineProperty(this, "NetworkHelper", {
get: function() {
return devtools.require("devtools/toolkit/webconsole/network-helper");
return require("devtools/toolkit/webconsole/network-helper");
},
configurable: true,
enumerable: true
});
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
XPCOMUtils.defineLazyModuleGetter(this, "Curl",
"resource:///modules/devtools/Curl.jsm");
/**
* Object defining the network monitor controller components.
*/

View File

@ -8,6 +8,7 @@
const { Cc, Ci, Cu, Cr } = require("chrome");
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const EventEmitter = require("devtools/toolkit/event-emitter");
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
function NetMonitorPanel(iframeWindow, toolbox) {
this.panelWin = iframeWindow;
@ -49,8 +50,7 @@ NetMonitorPanel.prototype = {
return this;
})
.then(null, function onError(aReason) {
Cu.reportError("NetMonitorPanel open failed. " +
aReason.error + ": " + aReason.message);
DevToolsUtils.reportException("NetMonitorPanel.prototype.open", aReason);
});
},

View File

@ -10,4 +10,3 @@ JS_MODULES_PATH = 'modules/devtools/shadereditor'
EXTRA_JS_MODULES += [
'panel.js'
]

View File

@ -9,6 +9,7 @@ const { Cc, Ci, Cu, Cr } = require("chrome");
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const EventEmitter = require("devtools/toolkit/event-emitter");
const { WebGLFront } = require("devtools/server/actors/webgl");
const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
function ShaderEditorPanel(iframeWindow, toolbox) {
this.panelWin = iframeWindow;
@ -21,6 +22,12 @@ function ShaderEditorPanel(iframeWindow, toolbox) {
exports.ShaderEditorPanel = ShaderEditorPanel;
ShaderEditorPanel.prototype = {
/**
* Open is effectively an asynchronous constructor.
*
* @return object
* A promise that is resolved when the Shader Editor completes opening.
*/
open: function() {
let targetPromise;
@ -44,8 +51,7 @@ ShaderEditorPanel.prototype = {
return this;
})
.then(null, function onError(aReason) {
Cu.reportError("ShaderEditorPanel open failed. " +
aReason.error + ": " + aReason.message);
DevToolsUtils.reportException("ShaderEditorPanel.prototype.open", aReason);
});
},

View File

@ -8,7 +8,6 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");

View File

@ -20,7 +20,8 @@ Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
this.EXPORTED_SYMBOLS = [
"Heritage", "ViewHelpers", "WidgetMethods",
"setNamedTimeout", "clearNamedTimeout"
"setNamedTimeout", "clearNamedTimeout",
"setConditionalTimeout", "clearConditionalTimeout",
];
/**
@ -57,7 +58,7 @@ this.Heritage = {
* @param function aCallback
* Invoked when no more events are fired after the specified time.
*/
this.setNamedTimeout = function(aId, aWait, aCallback) {
this.setNamedTimeout = function setNamedTimeout(aId, aWait, aCallback) {
clearNamedTimeout(aId);
namedTimeoutsStore.set(aId, setTimeout(() =>
@ -71,7 +72,7 @@ this.setNamedTimeout = function(aId, aWait, aCallback) {
* @param string aId
* A string identifier for the named timeout.
*/
this.clearNamedTimeout = function(aId) {
this.clearNamedTimeout = function clearNamedTimeout(aId) {
if (!namedTimeoutsStore) {
return;
}
@ -79,6 +80,41 @@ this.clearNamedTimeout = function(aId) {
namedTimeoutsStore.delete(aId);
};
/**
* Same as `setNamedTimeout`, but invokes the callback only if the provided
* predicate function returns true. Otherwise, the timeout is re-triggered.
*
* @param string aId
* A string identifier for the conditional timeout.
* @param number aWait
* The amount of milliseconds to wait after no more events are fired.
* @param function aPredicate
* The predicate function used to determine whether the timeout restarts.
* @param function aCallback
* Invoked when no more events are fired after the specified time, and
* the provided predicate function returns true.
*/
this.setConditionalTimeout = function setConditionalTimeout(aId, aWait, aPredicate, aCallback) {
setNamedTimeout(aId, aWait, function maybeCallback() {
if (aPredicate()) {
aCallback();
return;
}
setConditionalTimeout(aId, aWait, aPredicate, aCallback);
});
};
/**
* Clears a conditional timeout.
* @see setConditionalTimeout
*
* @param string aId
* A string identifier for the conditional timeout.
*/
this.clearConditionalTimeout = function clearConditionalTimeout(aId) {
clearNamedTimeout(aId);
};
XPCOMUtils.defineLazyGetter(this, "namedTimeoutsStore", () => new Map());
/**

View File

@ -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.reloadNotice1): This is the label shown
- in the call list view while loading a snapshot. -->
<!ENTITY canvasDebuggerUI.importNotice "Loading…">
<!-- 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">

View File

@ -0,0 +1,74 @@
# 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 (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.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 (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

View File

@ -32,6 +32,8 @@
locale/browser/devtools/netmonitor.properties (%chrome/browser/devtools/netmonitor.properties)
locale/browser/devtools/shadereditor.dtd (%chrome/browser/devtools/shadereditor.dtd)
locale/browser/devtools/shadereditor.properties (%chrome/browser/devtools/shadereditor.properties)
locale/browser/devtools/canvasdebugger.dtd (%chrome/browser/devtools/canvasdebugger.dtd)
locale/browser/devtools/canvasdebugger.properties (%chrome/browser/devtools/canvasdebugger.properties)
locale/browser/devtools/gcli.properties (%chrome/browser/devtools/gcli.properties)
locale/browser/devtools/gclicommands.properties (%chrome/browser/devtools/gclicommands.properties)
locale/browser/devtools/webconsole.properties (%chrome/browser/devtools/webconsole.properties)

View File

@ -0,0 +1,5 @@
/* 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/. */
%include ../../shared/devtools/canvasdebugger.inc.css

View File

@ -209,6 +209,7 @@ browser.jar:
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css)

View File

@ -0,0 +1,6 @@
/* 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/. */
%include ../shared.inc
%include ../../shared/devtools/canvasdebugger.inc.css

View File

@ -330,6 +330,7 @@ browser.jar:
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css)

View File

@ -0,0 +1,501 @@
/* 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/. */
%filter substitution
%define darkCheckerboardBackground #000
%define lightCheckerboardBackground #fff
%define checkerboardCell rgba(128,128,128,0.2)
%define checkerboardPattern linear-gradient(45deg, @checkerboardCell@ 25%, transparent 25%, transparent 75%, @checkerboardCell@ 75%, @checkerboardCell@), linear-gradient(45deg, @checkerboardCell@ 25%, transparent 25%, transparent 75%, @checkerboardCell@ 75%, @checkerboardCell@)
%define gutterWidth 3em
%define gutterPaddingStart 22px
/* Reload and waiting notices */
.notice-container {
margin-top: -50vh;
font-size: 120%;
}
.theme-dark .notice-container {
background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
color: #f5f7fa; /* Light foreground text */
}
.theme-light .notice-container {
background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
color: #585959; /* Grey foreground text */
}
#reload-notice > button {
min-height: 2em;
}
#empty-notice > button {
min-width: 30px;
min-height: 28px;
margin: 0;
list-style-image: url(profiler-stopwatch.png);
-moz-image-region: rect(0px,16px,16px,0px);
}
#empty-notice > button .button-text {
display: none;
}
.theme-dark #import-notice {
font-size: 250%;
color: rgba(255,255,255,0.2);
}
.theme-light #import-notice {
font-size: 250%;
color: rgba(0,0,0,0.2);
}
/* Snapshots pane */
#snapshots-pane > tabs {
-moz-border-end: 1px solid;
}
#snapshots-pane .devtools-toolbar {
-moz-border-end: 1px solid;
}
.theme-dark #snapshots-pane > tabs,
.theme-dark #snapshots-pane .devtools-toolbar {
-moz-border-end-color: black; /* Match the splitter color. */
}
.theme-light #snapshots-pane > tabs,
.theme-light #snapshots-pane .devtools-toolbar {
-moz-border-end-color: #aaa; /* Match the splitter color. */
}
#record-snapshot {
list-style-image: url("chrome://browser/skin/devtools/profiler-stopwatch.png");
-moz-image-region: rect(0px,16px,16px,0px);
}
#record-snapshot[checked] {
-moz-image-region: rect(0px,32px,16px,16px);
}
/* Snapshots items */
.snapshot-item-thumbnail {
image-rendering: -moz-crisp-edges;
background-image: @checkerboardPattern@;
background-size: 12px 12px, 12px 12px;
background-position: 0px 0px, 6px 6px;
background-repeat: repeat, repeat;
}
.snapshot-item-thumbnail[flipped=true] {
transform: scaleY(-1);
}
.theme-dark .snapshot-item-thumbnail {
background-color: @darkCheckerboardBackground@;
}
.theme-light .snapshot-item-thumbnail {
background-color: @lightCheckerboardBackground@;
}
.snapshot-item-details {
-moz-padding-start: 6px;
}
.snapshot-item-calls {
padding-top: 4px;
font-size: 80%;
}
.snapshot-item-save {
padding-bottom: 2px;
font-size: 90%;
}
.theme-dark .snapshot-item-calls,
.theme-dark .snapshot-item-save {
color: #b6babf; /* Foreground (Text) - Grey */
}
.theme-light .snapshot-item-calls,
.theme-light .snapshot-item-save {
color: #585959; /* Foreground (Text) - Grey */
}
.snapshot-item-save {
text-decoration: underline;
cursor: pointer;
}
.snapshot-item-save[disabled=true] {
text-decoration: none;
pointer-events: none;
}
.snapshot-item-footer[saving]::before {
display: inline-block;
content: "";
background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
width: 16px;
height: 16px;
margin-top: -2px;
-moz-margin-end: 4px;
}
#snapshots-list .selected label {
/* Text inside a selected item should not be custom colored. */
color: inherit !important;
}
/* Debugging pane controls */
#resume {
list-style-image: url(debugger-play.png);
-moz-image-region: rect(0px,32px,16px,16px);
}
#step-over {
list-style-image: url(debugger-step-over.png);
}
#step-in {
list-style-image: url(debugger-step-in.png);
}
#step-out {
list-style-image: url(debugger-step-out.png);
}
#debugging-controls > toolbarbutton {
transition: opacity 0.15s ease-in-out;
}
#debugging-controls > toolbarbutton[disabled=true] {
opacity: 0.5;
}
#calls-slider {
-moz-padding-end: 24px;
}
#calls-slider .scale-slider {
margin: 0;
}
#debugging-toolbar-sizer-button {
/* This button's only purpose in life is to make the
container .devtools-toolbar have the right height. */
visibility: hidden;
min-width: 1px;
}
/* Calls list pane */
#calls-list .side-menu-widget-container {
background: transparent;
}
#calls-list .side-menu-widget-item {
padding: 0;
}
/* Calls list items */
.theme-dark #calls-list .side-menu-widget-item {
border-color: #111;
border-bottom-color: transparent;
}
.theme-light #calls-list .side-menu-widget-item {
border-color: #eee;
border-bottom-color: transparent;
}
.theme-dark .call-item-view:hover {
background-color: rgba(255,255,255,.025);
}
.theme-light .call-item-view:hover {
background-color: rgba(0,0,0,.025);
}
.theme-dark .call-item-view[draw-call] {
background-color: rgba(112,191,83,0.15);
}
.theme-light .call-item-view[draw-call] {
background-color: rgba(44,187,15,0.1);
}
.theme-dark .call-item-view[interesting-call] {
background-color: rgba(223,128,255,0.15);
}
.theme-light .call-item-view[interesting-call] {
background-color: rgba(184,46,229,0.1);
}
.call-item-gutter {
width: calc(@gutterWidth@ + @gutterPaddingStart@);
-moz-padding-start: @gutterPaddingStart@;
-moz-padding-end: 4px;
padding-top: 2px;
padding-bottom: 2px;
-moz-border-end: 1px solid;
-moz-margin-end: 6px;
}
.selected .call-item-gutter {
background-image: url("editor-debug-location.png");
background-repeat: no-repeat;
background-position: 6px center;
background-size: 12px;
}
.theme-dark .call-item-gutter {
background-color: #181d20;
color: #5f7387;
border-color: #000;
}
.theme-light .call-item-gutter {
background-color: #f7f7f7;
color: #667380;
border-color: #aaa;
}
.call-item-index {
text-align: end;
}
.theme-dark .call-item-context {
color: #eb5368; /* Highlight Orange */
}
.theme-light .call-item-context {
color: #f13c00; /* Highlight Orange */
}
.theme-dark .call-item-name {
color: #46afe3; /* Highlight Blue */
}
.theme-light .call-item-name {
color: #0088cc; /* Highlight Blue */
}
.call-item-location {
-moz-padding-start: 2px;
-moz-padding-end: 6px;
text-align: end;
cursor: pointer;
}
.theme-dark .call-item-location:hover {
color: #0088cc; /* Highlight Blue */
}
.theme-light .call-item-location:hover {
color: #46afe3; /* Highlight Blue */
}
.call-item-view:hover .call-item-location,
.call-item-view[expanded] .call-item-location {
text-decoration: underline;
}
.theme-dark .call-item-location {
border-color: #111;
color: #5e88b0; /* Highlight Blue-Grey */
}
.theme-light .call-item-location {
border-color: #eee;
color: #5f88b0; /* Highlight Blue-Grey */
}
.call-item-stack {
-moz-padding-start: calc(@gutterWidth@ + @gutterPaddingStart@);
padding-bottom: 10px;
}
.theme-dark .call-item-stack {
background: rgba(0,0,0,0.9);
}
.theme-light .call-item-stack {
background: rgba(255,255,255,0.9);
}
.call-item-stack-fn {
padding-top: 2px;
padding-bottom: 2px;
}
.call-item-stack-fn-location {
-moz-padding-start: 2px;
-moz-padding-end: 6px;
text-align: end;
cursor: pointer;
text-decoration: underline;
}
.theme-dark .call-item-stack-fn-name {
color: #a9bacb; /* Content (Text) - Light */
}
.theme-light .call-item-stack-fn-name {
color: #667380; /* Content (Text) - Dark Grey */
}
.theme-dark .call-item-stack-fn-location {
color: #5e88b0; /* Highlight Blue-Grey */
}
.theme-light .call-item-stack-fn-location {
color: #5e88b0; /* Highlight Blue-Grey */
}
.theme-dark .call-item-stack-fn-location:hover {
color: #0088cc; /* Highlight Blue */
}
.theme-light .call-item-stack-fn-location:hover {
color: #46afe3; /* Highlight Blue */
}
#calls-list .selected .call-item-contents > label:not(.call-item-gutter) {
/* Text inside a selected item should not be custom colored. */
color: inherit !important;
}
/* Rendering preview */
#screenshot-container {
background-image: @checkerboardPattern@;
background-size: 30px 30px, 30px 30px;
background-position: 0px 0px, 15px 15px;
background-repeat: repeat, repeat;
}
.theme-dark #screenshot-container {
background-color: @darkCheckerboardBackground@;
}
.theme-light #screenshot-container {
background-color: @lightCheckerboardBackground@;
}
@media (min-width: 701px) {
#screenshot-container {
width: 30vw;
max-width: 50vw;
min-width: 100px;
}
}
@media (max-width: 700px) {
#screenshot-container {
height: 40vh;
max-height: 70vh;
min-height: 100px;
}
}
#screenshot-image {
background-image: -moz-element(#screenshot-rendering);
background-size: contain;
background-position: center, center;
background-repeat: no-repeat;
}
#screenshot-image[flipped=true] {
transform: scaleY(-1);
}
#screenshot-dimensions {
padding-top: 4px;
padding-bottom: 4px;
text-align: center;
}
.theme-dark #screenshot-dimensions {
background-color: rgba(0,0,0,0.4);
}
.theme-light #screenshot-dimensions {
background-color: rgba(255,255,255,0.8);
}
/* Snapshot filmstrip */
#snapshot-filmstrip {
overflow: hidden;
}
.theme-dark #snapshot-filmstrip {
border-top: 1px solid #000;
background-image: url(background-noise-toolbar.png);
color: #f5f7fa; /* Light foreground text */
}
.theme-light #snapshot-filmstrip {
border-top: 1px solid #aaa;
background-image: url(background-noise-toolbar.png);
color: #585959; /* Grey foreground text */
}
.filmstrip-thumbnail {
image-rendering: -moz-crisp-edges;
background-image: @checkerboardPattern@;
background-size: 12px 12px, 12px 12px;
background-position: 0px -1px, 6px 5px;
background-repeat: repeat, repeat;
background-origin: content-box;
cursor: pointer;
padding-top: 1px;
padding-bottom: 1px;
transition: opacity 0.1s ease-in-out;
}
.filmstrip-thumbnail[flipped=true] {
transform: scaleY(-1);
}
.theme-dark .filmstrip-thumbnail {
background-color: @darkCheckerboardBackground@;
}
.theme-light .filmstrip-thumbnail {
background-color: @lightCheckerboardBackground@;
}
.theme-dark .filmstrip-thumbnail {
-moz-border-end: 1px solid #000;
}
.theme-light .filmstrip-thumbnail {
-moz-border-end: 1px solid #aaa;
}
.theme-dark #snapshot-filmstrip > .filmstrip-thumbnail:hover,
.theme-dark #snapshot-filmstrip:not(:hover) > .filmstrip-thumbnail[highlighted] {
border: 1px solid #46afe3; /* Highlight Blue */
margin: 0 0 0 -1px;
padding: 0;
opacity: 0.66;
}
.theme-light #snapshot-filmstrip > .filmstrip-thumbnail:hover,
.theme-light #snapshot-filmstrip:not(:hover) > .filmstrip-thumbnail[highlighted] {
border: 1px solid #0088cc; /* Highlight Blue */
margin: 0 0 0 -1px;
padding: 0;
opacity: 0.66;
}

View File

@ -774,6 +774,7 @@
.theme-light .scrollbutton-up > .toolbarbutton-icon,
.theme-light .scrollbutton-down > .toolbarbutton-icon,
.theme-light #black-boxed-message-button .button-icon,
.theme-light #canvas-debugging-empty-notice-button .button-icon,
.theme-light #requests-menu-perf-notice-button .button-icon,
.theme-light #requests-menu-network-summary-button .button-icon {
filter: url(filters.svg#invert);

View File

@ -564,10 +564,12 @@
}
.theme-dark .side-menu-widget-empty-text {
background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
color: #b6babf; /* Foreground (Text) - Grey */
}
.theme-light .side-menu-widget-empty-text {
background: #f7f7f7; /* Toolbars */
color: #585959; /* Grey foreground text */
}

View File

@ -0,0 +1,5 @@
/* 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/. */
%include ../../shared/devtools/canvasdebugger.inc.css

View File

@ -242,6 +242,7 @@ browser.jar:
* skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
* skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
* skin/classic/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css)
@ -589,6 +590,7 @@ browser.jar:
* skin/classic/aero/browser/devtools/splitview.css (../shared/devtools/splitview.css)
skin/classic/aero/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css)
* skin/classic/aero/browser/devtools/shadereditor.css (devtools/shadereditor.css)
* skin/classic/aero/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
* skin/classic/aero/browser/devtools/debugger.css (devtools/debugger.css)
* skin/classic/aero/browser/devtools/profiler.css (devtools/profiler.css)
* skin/classic/aero/browser/devtools/netmonitor.css (devtools/netmonitor.css)

View File

@ -8,6 +8,7 @@
const { Ci, Cu } = require("chrome");
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
let { setTimeout, clearTimeout } = Cu.import("resource://gre/modules/Timer.jsm", {});
/**
* Turn the error |aError| into a string, without fail.
@ -27,6 +28,8 @@ exports.safeErrorString = function safeErrorString(aError) {
}
} catch (ee) { }
// Append additional line and column number information to the output,
// since it might not be part of the stringified error.
if (typeof aError.lineNumber == "number" && typeof aError.columnNumber == "number") {
errorString += "Line: " + aError.lineNumber + ", column: " + aError.columnNumber;
}
@ -113,12 +116,41 @@ exports.zip = function zip(a, b) {
return pairs;
};
const executeSoon = aFn => {
/**
* Waits for the next tick in the event loop to execute a callback.
*/
exports.executeSoon = function executeSoon(aFn) {
Services.tm.mainThread.dispatch({
run: exports.makeInfallible(aFn)
}, Ci.nsIThread.DISPATCH_NORMAL);
};
/**
* Waits for the next tick in the event loop.
*
* @return Promise
* A promise that is resolved after the next tick in the event loop.
*/
exports.waitForTick = function waitForTick() {
let deferred = promise.defer();
exports.executeSoon(deferred.resolve);
return deferred.promise;
};
/**
* Waits for the specified amount of time to pass.
*
* @param number aDelay
* The amount of time to wait, in milliseconds.
* @return Promise
* A promise that is resolved after the specified amount of time passes.
*/
exports.waitForTime = function waitForTime(aDelay) {
let deferred = promise.defer();
setTimeout(deferred.resolve, aDelay);
return deferred.promise;
};
/**
* 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
@ -127,16 +159,19 @@ const executeSoon = aFn => {
* @param Array aArray
* The array being iterated over.
* @param Function aFn
* The function called on each item in the array.
* 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.
* over, and all promises returned by the aFn callback are resolved.
*/
exports.yieldingEach = function yieldingEach(aArray, aFn) {
const deferred = promise.defer();
let i = 0;
let len = aArray.length;
let outstanding = [deferred.promise];
(function loop() {
const start = Date.now();
@ -147,12 +182,12 @@ exports.yieldingEach = function yieldingEach(aArray, aFn) {
// aren't including time spent in non-JS here, but this is Good
// Enough(tm).
if (Date.now() - start > 16) {
executeSoon(loop);
exports.executeSoon(loop);
return;
}
try {
aFn(aArray[i++]);
outstanding.push(aFn(aArray[i], i++));
} catch (e) {
deferred.reject(e);
return;
@ -162,10 +197,9 @@ exports.yieldingEach = function yieldingEach(aArray, aFn) {
deferred.resolve();
}());
return deferred.promise;
return promise.all(outstanding);
}
/**
* Like XPCOMUtils.defineLazyGetter, but with a |this| sensitive getter that
* allows the lazy getter to be defined on a prototype and work correctly with
@ -266,4 +300,3 @@ exports.isSafeJSObject = function isSafeJSObject(aObj) {
return Cu.isXrayWrapper(aObj);
};

View File

@ -71,6 +71,7 @@ BuiltinProvider.prototype = {
"devtools/client": "resource://gre/modules/devtools/client",
"devtools/pretty-fast": "resource://gre/modules/devtools/pretty-fast.js",
"devtools/async-utils": "resource://gre/modules/devtools/async-utils",
"devtools/content-observer": "resource://gre/modules/devtools/content-observer",
"gcli": "resource://gre/modules/devtools/gcli",
"acorn": "resource://gre/modules/devtools/acorn",
"acorn/util/walk": "resource://gre/modules/devtools/acorn/walk.js",
@ -120,6 +121,7 @@ SrcdirProvider.prototype = {
let clientURI = this.fileURI(OS.Path.join(toolkitDir, "client"));
let prettyFastURI = this.fileURI(OS.Path.join(toolkitDir), "pretty-fast.js");
let asyncUtilsURI = this.fileURI(OS.Path.join(toolkitDir), "async-utils.js");
let contentObserverURI = this.fileURI(OS.Path.join(toolkitDir), "content-observer.js");
let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli", "source", "lib", "gcli"));
let acornURI = this.fileURI(OS.Path.join(toolkitDir, "acorn"));
let acornWalkURI = OS.Path.join(acornURI, "walk.js");
@ -144,6 +146,7 @@ SrcdirProvider.prototype = {
"devtools/client": clientURI,
"devtools/pretty-fast": prettyFastURI,
"devtools/async-utils": asyncUtilsURI,
"devtools/content-observer": contentObserverURI,
"gcli": gcliURI,
"acorn": acornURI,
"acorn/util/walk": acornWalkURI

View File

@ -0,0 +1,72 @@
/* 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 {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
const events = require("sdk/event/core");
const promise = require("sdk/core/promise");
/**
* Handles adding an observer for the creation of content document globals,
* event sent immediately after a web content document window has been set up,
* but before any script code has been executed.
*/
function ContentObserver(tabActor) {
this._contentWindow = tabActor.window;
this._onContentGlobalCreated = this._onContentGlobalCreated.bind(this);
this._onInnerWindowDestroyed = this._onInnerWindowDestroyed.bind(this);
this.startListening();
}
module.exports.ContentObserver = ContentObserver;
ContentObserver.prototype = {
/**
* Starts listening for the required observer messages.
*/
startListening: function() {
Services.obs.addObserver(
this._onContentGlobalCreated, "content-document-global-created", false);
Services.obs.addObserver(
this._onInnerWindowDestroyed, "inner-window-destroyed", false);
},
/**
* Stops listening for the required observer messages.
*/
stopListening: function() {
Services.obs.removeObserver(
this._onContentGlobalCreated, "content-document-global-created", false);
Services.obs.removeObserver(
this._onInnerWindowDestroyed, "inner-window-destroyed", false);
},
/**
* Fired immediately after a web content document window has been set up.
*/
_onContentGlobalCreated: function(subject, topic, data) {
if (subject == this._contentWindow) {
events.emit(this, "global-created", subject);
}
},
/**
* Fired when an inner window is removed from the backward/forward cache.
*/
_onInnerWindowDestroyed: function(subject, topic, data) {
let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
events.emit(this, "global-destroyed", id);
}
};
// Utility functions.
ContentObserver.GetInnerWindowID = function(window) {
return window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
};

View File

@ -0,0 +1,559 @@
/* 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 events = require("sdk/event/core");
const promise = require("sdk/core/promise");
const protocol = require("devtools/server/protocol");
const {ContentObserver} = require("devtools/content-observer");
const {on, once, off, emit} = events;
const {method, Arg, Option, RetVal} = protocol;
exports.register = function(handle) {
handle.addTabActor(CallWatcherActor, "callWatcherActor");
};
exports.unregister = function(handle) {
handle.removeTabActor(CallWatcherActor);
};
/**
* Type describing a single function call in a stack trace.
*/
protocol.types.addDictType("call-stack-item", {
name: "string",
file: "string",
line: "number"
});
/**
* Type describing an overview of a function call.
*/
protocol.types.addDictType("call-details", {
type: "number",
name: "string",
stack: "array:call-stack-item"
});
/**
* This actor contains information about a function call, like the function
* type, name, stack, arguments, returned value etc.
*/
let FunctionCallActor = protocol.ActorClass({
typeName: "function-call",
/**
* Creates the function call actor.
*
* @param DebuggerServerConnection conn
* The server connection.
* @param DOMWindow window
* The content window.
* @param string global
* The name of the global object owning this function, like
* "CanvasRenderingContext2D" or "WebGLRenderingContext".
* @param object caller
* The object owning the function when it was called.
* For example, in `foo.bar()`, the caller is `foo`.
* @param number type
* Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER.
* @param string name
* The called function's name.
* @param array stack
* The called function's stack, as a list of { name, file, line } objects.
* @param array args
* The called function's arguments.
* @param any result
* The value returned by the function call.
*/
initialize: function(conn, [window, global, caller, type, name, stack, args, result]) {
protocol.Actor.prototype.initialize.call(this, conn);
this.details = {
window: window,
caller: caller,
type: type,
name: name,
stack: stack,
args: args,
return: result
};
this.meta = {
global: -1,
previews: { caller: "", args: "" }
};
if (global == "WebGLRenderingContext") {
this.meta.global = CallWatcherFront.CANVAS_WEBGL_CONTEXT;
} else if (global == "CanvasRenderingContext2D") {
this.meta.global = CallWatcherFront.CANVAS_2D_CONTEXT;
} else if (global == "window") {
this.meta.global = CallWatcherFront.UNKNOWN_SCOPE;
} else {
this.meta.global = CallWatcherFront.GLOBAL_SCOPE;
}
this.meta.previews.caller = this._generateCallerPreview();
this.meta.previews.args = this._generateArgsPreview();
},
/**
* Customize the marshalling of this actor to provide some generic information
* directly on the Front instance.
*/
form: function() {
return {
actor: this.actorID,
type: this.details.type,
name: this.details.name,
file: this.details.stack[0].file,
line: this.details.stack[0].line,
callerPreview: this.meta.previews.caller,
argsPreview: this.meta.previews.args
};
},
/**
* Gets more information about this function call, which is not necessarily
* available on the Front instance.
*/
getDetails: method(function() {
let { type, name, stack } = this.details;
// Since not all calls on the stack have corresponding owner files (e.g.
// callbacks of a requestAnimationFrame etc.), there's no benefit in
// returning them, as the user can't jump to the Debugger from them.
for (let i = stack.length - 1;;) {
if (stack[i].file) {
break;
}
stack.pop();
i--;
}
// XXX: Use grips for objects and serialize them properly, in order
// to add the function's caller, arguments and return value. Bug 978957.
return {
type: type,
name: name,
stack: stack
};
}, {
response: { info: RetVal("call-details") }
}),
/**
* Serializes the caller's name so that it can be easily be transferred
* as a string, but still be useful when displayed in a potential UI.
*
* @return string
* The caller's name as a string.
*/
_generateCallerPreview: function() {
let global = this.meta.global;
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
return "gl";
}
if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
return "ctx";
}
return "";
},
/**
* Serializes the arguments so that they can be easily be transferred
* as a string, but still be useful when displayed in a potential UI.
*
* @return string
* The arguments as a string.
*/
_generateArgsPreview: function() {
let { caller, args } = this.details;
let { global } = this.meta;
// XXX: All of this sucks. Make this smarter, so that the frontend
// can inspect each argument, be it object or primitive. Bug 978960.
let serializeArgs = () => args.map(arg => {
if (typeof arg == "undefined") {
return "undefined";
}
if (typeof arg == "function") {
return "Function";
}
if (typeof arg == "object") {
return "Object";
}
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
// XXX: This doesn't handle combined bitmasks. Bug 978964.
return getEnumsLookupTable("webgl", caller)[arg] || arg;
}
if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
return getEnumsLookupTable("2d", caller)[arg] || arg;
}
return arg;
});
return serializeArgs().join(", ");
}
});
/**
* The corresponding Front object for the FunctionCallActor.
*/
let FunctionCallFront = protocol.FrontClass(FunctionCallActor, {
initialize: function(client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
},
/**
* Adds some generic information directly to this instance,
* to avoid extra roundtrips.
*/
form: function(form) {
this.actorID = form.actor;
this.type = form.type;
this.name = form.name;
this.file = form.file;
this.line = form.line;
this.callerPreview = form.callerPreview;
this.argsPreview = form.argsPreview;
}
});
/**
* This actor observes function calls on certain objects or globals.
*/
let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
typeName: "call-watcher",
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._onGlobalCreated = this._onGlobalCreated.bind(this);
this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
this.finalize();
},
/**
* Starts waiting for the current tab actor's document global to be
* created, in order to instrument the specified objects and become
* aware of everything the content does with them.
*/
setup: method(function({ tracedGlobals, tracedFunctions, startRecording, performReload }) {
if (this._initialized) {
return;
}
this._initialized = true;
this._functionCalls = [];
this._tracedGlobals = tracedGlobals || [];
this._tracedFunctions = tracedFunctions || [];
this._contentObserver = new ContentObserver(this.tabActor);
on(this._contentObserver, "global-created", this._onGlobalCreated);
on(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
if (startRecording) {
this.resumeRecording();
}
if (performReload) {
this.tabActor.window.location.reload();
}
}, {
request: {
tracedGlobals: Option(0, "nullable:array:string"),
tracedFunctions: Option(0, "nullable:array:string"),
startRecording: Option(0, "boolean"),
performReload: Option(0, "boolean")
},
oneway: true
}),
/**
* Stops listening for document global changes and puts this actor
* to hibernation. This method is called automatically just before the
* actor is destroyed.
*/
finalize: method(function() {
if (!this._initialized) {
return;
}
this._initialized = false;
this._contentObserver.stopListening();
off(this._contentObserver, "global-created", this._onGlobalCreated);
off(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
this._tracedGlobals = null;
this._tracedFunctions = null;
this._contentObserver = null;
}, {
oneway: true
}),
/**
* Returns whether the instrumented function calls are currently recorded.
*/
isRecording: method(function() {
return this._recording;
}, {
response: RetVal("boolean")
}),
/**
* Starts recording function calls.
*/
resumeRecording: method(function() {
this._recording = true;
}),
/**
* Stops recording function calls.
*/
pauseRecording: method(function() {
this._recording = false;
return this._functionCalls;
}, {
response: { calls: RetVal("array:function-call") }
}),
/**
* Erases all the recorded function calls.
* Calling `resumeRecording` or `pauseRecording` does not erase history.
*/
eraseRecording: method(function() {
this._functionCalls = [];
}),
/**
* Lightweight listener invoked whenever an instrumented function is called
* while recording. We're doing this to avoid the event emitter overhead,
* since this is expected to be a very hot function.
*/
onCall: function() {},
/**
* Invoked whenever the current tab actor's document global is created.
*/
_onGlobalCreated: function(window) {
let self = this;
this._tracedWindowId = ContentObserver.GetInnerWindowID(window);
let unwrappedWindow = XPCNativeWrapper.unwrap(window);
let callback = this._onContentFunctionCall;
for (let global of this._tracedGlobals) {
let prototype = unwrappedWindow[global].prototype;
let properties = Object.keys(prototype);
properties.forEach(name => overrideSymbol(global, prototype, name, callback));
}
for (let name of this._tracedFunctions) {
overrideSymbol("window", unwrappedWindow, name, callback);
}
/**
* Instruments a method, getter or setter on the specified target object to
* invoke a callback whenever it is called.
*/
function overrideSymbol(global, target, name, callback) {
let propertyDescriptor = Object.getOwnPropertyDescriptor(target, name);
if (propertyDescriptor.get || propertyDescriptor.set) {
overrideAccessor(global, target, name, propertyDescriptor, callback);
return;
}
if (propertyDescriptor.writable && typeof propertyDescriptor.value == "function") {
overrideFunction(global, target, name, propertyDescriptor, callback);
return;
}
}
/**
* Instruments a function on the specified target object.
*/
function overrideFunction(global, target, name, descriptor, callback) {
let originalFunc = target[name];
Object.defineProperty(target, name, {
value: function(...args) {
let result = originalFunc.apply(this, args);
if (self._recording) {
let stack = getStack(name);
let type = CallWatcherFront.METHOD_FUNCTION;
callback(unwrappedWindow, global, this, type, name, stack, args, result);
}
return result;
},
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
writable: true
});
}
/**
* Instruments a getter or setter on the specified target object.
*/
function overrideAccessor(global, target, name, descriptor, callback) {
let originalGetter = target.__lookupGetter__(name);
let originalSetter = target.__lookupSetter__(name);
Object.defineProperty(target, name, {
get: function(...args) {
if (!originalGetter) return undefined;
let result = originalGetter.apply(this, args);
if (self._recording) {
let stack = getStack(name);
let type = CallWatcherFront.GETTER_FUNCTION;
callback(unwrappedWindow, global, this, type, name, stack, args, result);
}
return result;
},
set: function(...args) {
if (!originalSetter) return;
originalSetter.apply(this, args);
if (self._recording) {
let stack = getStack(name);
let type = CallWatcherFront.SETTER_FUNCTION;
callback(unwrappedWindow, global, this, type, name, stack, args, undefined);
}
},
configurable: descriptor.configurable,
enumerable: descriptor.enumerable
});
}
/**
* Stores the relevant information about calls on the stack when
* a function is called.
*/
function getStack(caller) {
try {
// Using Components.stack wouldn't be a better idea, since it's
// much slower because it attempts to retrieve the C++ stack as well.
throw new Error();
} catch (e) {
var stack = e.stack;
}
// Of course, using a simple regex like /(.*?)@(.*):(\d*):\d*/ would be
// much prettier, but this is a very hot function, so let's sqeeze
// every drop of performance out of it.
let calls = [];
let callIndex = 0;
let currNewLinePivot = stack.indexOf("\n") + 1;
let nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
while (nextNewLinePivot > 0) {
let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot);
let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1);
let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1);
if (!calls[callIndex]) {
calls[callIndex] = { name: "", file: "", line: 0 };
}
if (!calls[callIndex + 1]) {
calls[callIndex + 1] = { name: "", file: "", line: 0 };
}
if (callIndex > 0) {
let file = stack.substring(nameDelimiterIndex + 1, lineDelimiterIndex);
let line = stack.substring(lineDelimiterIndex + 1, columnDelimiterIndex);
let name = stack.substring(currNewLinePivot, nameDelimiterIndex);
calls[callIndex].name = name;
calls[callIndex - 1].file = file;
calls[callIndex - 1].line = line;
} else {
// Since the topmost stack frame is actually our overwritten function,
// it will not have the expected name.
calls[0].name = caller;
}
currNewLinePivot = nextNewLinePivot + 1;
nextNewLinePivot = stack.indexOf("\n", currNewLinePivot);
callIndex++;
}
return calls;
}
},
/**
* Invoked whenever the current tab actor's inner window is destroyed.
*/
_onGlobalDestroyed: function(id) {
if (this._tracedWindowId == id) {
this.pauseRecording();
this.eraseRecording();
}
},
/**
* Invoked whenever an instrumented function is called.
*/
_onContentFunctionCall: function(...details) {
let functionCall = new FunctionCallActor(this.conn, details);
this._functionCalls.push(functionCall);
this.onCall(functionCall);
}
});
/**
* The corresponding Front object for the CallWatcherActor.
*/
let CallWatcherFront = exports.CallWatcherFront = protocol.FrontClass(CallWatcherActor, {
initialize: function(client, { callWatcherActor }) {
protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor });
client.addActorPool(this);
this.manage(this);
}
});
/**
* Constants.
*/
CallWatcherFront.METHOD_FUNCTION = 0;
CallWatcherFront.GETTER_FUNCTION = 1;
CallWatcherFront.SETTER_FUNCTION = 2;
CallWatcherFront.GLOBAL_SCOPE = 0;
CallWatcherFront.UNKNOWN_SCOPE = 1;
CallWatcherFront.CANVAS_WEBGL_CONTEXT = 2;
CallWatcherFront.CANVAS_2D_CONTEXT = 3;
/**
* A lookup table for cross-referencing flags or properties with their name
* assuming they look LIKE_THIS most of the time.
*
* For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed
* argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT".
*/
var gEnumRegex = /^[A-Z_]+$/;
var gEnumsLookupTable = {};
function getEnumsLookupTable(type, object) {
let cachedEnum = gEnumsLookupTable[type];
if (cachedEnum) {
return cachedEnum;
}
let table = gEnumsLookupTable[type] = {};
for (let key in object) {
if (key.match(gEnumRegex)) {
table[object[key]] = key;
}
}
return table;
}

View File

@ -0,0 +1,759 @@
/* 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 events = require("sdk/event/core");
const promise = require("sdk/core/promise");
const protocol = require("devtools/server/protocol");
const {CallWatcherActor, CallWatcherFront} = require("devtools/server/actors/call-watcher");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js");
const {on, once, off, emit} = events;
const {method, custom, Arg, Option, RetVal} = protocol;
const CANVAS_CONTEXTS = [
"CanvasRenderingContext2D",
"WebGLRenderingContext"
];
const ANIMATION_GENERATORS = [
"requestAnimationFrame",
"mozRequestAnimationFrame"
];
const DRAW_CALLS = [
// 2D canvas
"fill",
"stroke",
"clearRect",
"fillRect",
"strokeRect",
"fillText",
"strokeText",
"drawImage",
// WebGL
"clear",
"drawArrays",
"drawElements",
"finish",
"flush"
];
const INTERESTING_CALLS = [
// 2D canvas
"save",
"restore",
// WebGL
"useProgram"
];
exports.register = function(handle) {
handle.addTabActor(CanvasActor, "canvasActor");
};
exports.unregister = function(handle) {
handle.removeTabActor(CanvasActor);
};
/**
* Type representing an Uint32Array buffer, serialized fast(er).
*
* XXX: It would be nice if on local connections (only), we could just *give*
* the buffer directly to the front, instead of going through all this
* serialization redundancy.
*/
protocol.types.addType("uint32-array", {
write: (v) => "[" + Array.join(v, ",") + "]",
read: (v) => new Uint32Array(JSON.parse(v))
});
/**
* Type describing a thumbnail or screenshot in a recorded animation frame.
*/
protocol.types.addDictType("snapshot-image", {
index: "number",
width: "number",
height: "number",
flipped: "boolean",
pixels: "uint32-array"
});
/**
* Type describing an overview of a recorded animation frame.
*/
protocol.types.addDictType("snapshot-overview", {
calls: "array:function-call",
thumbnails: "array:snapshot-image",
screenshot: "snapshot-image"
});
/**
* This actor represents a recorded animation frame snapshot, along with
* all the corresponding canvas' context methods invoked in that frame,
* thumbnails for each draw call and a screenshot of the end result.
*/
let FrameSnapshotActor = protocol.ActorClass({
typeName: "frame-snapshot",
/**
* Creates the frame snapshot call actor.
*
* @param DebuggerServerConnection conn
* The server connection.
* @param HTMLCanvasElement canvas
* A reference to the content canvas.
* @param array calls
* An array of "function-call" actor instances.
* @param object screenshot
* A single "snapshot-image" type instance.
*/
initialize: function(conn, { canvas, calls, screenshot }) {
protocol.Actor.prototype.initialize.call(this, conn);
this._contentCanvas = canvas;
this._functionCalls = calls;
this._lastDrawCallScreenshot = screenshot;
},
/**
* Gets as much data about this snapshot without computing anything costly.
*/
getOverview: method(function() {
return {
calls: this._functionCalls,
thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e),
screenshot: this._lastDrawCallScreenshot
};
}, {
response: { overview: RetVal("snapshot-overview") }
}),
/**
* Gets a screenshot of the canvas's contents after the specified
* function was called.
*/
generateScreenshotFor: method(function(functionCall) {
let caller = functionCall.details.caller;
let global = functionCall.meta.global;
let canvas = this._contentCanvas;
let calls = this._functionCalls;
let index = calls.indexOf(functionCall);
// To get a screenshot, replay all the steps necessary to render the frame,
// by invoking the context calls up to and including the specified one.
// This will be done in a custom framebuffer in case of a WebGL context.
let { replayContext, lastDrawCallIndex } = ContextUtils.replayAnimationFrame({
contextType: global,
canvas: canvas,
calls: calls,
first: 0,
last: index
});
// To keep things fast, generate an image that's relatively small.
let dimensions = Math.min(CanvasFront.SCREENSHOT_HEIGHT_MAX, canvas.height);
let screenshot;
// Depending on the canvas' context, generating a screenshot is done
// in different ways. In case of the WebGL context, we also need to reset
// the framebuffer binding to the default value.
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
screenshot = ContextUtils.getPixelsForWebGL(replayContext);
replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, null);
screenshot.flipped = true;
}
// In case of 2D contexts, no additional special treatment is necessary.
else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
screenshot = ContextUtils.getPixelsFor2D(replayContext);
screenshot.flipped = false;
}
screenshot.index = lastDrawCallIndex;
return screenshot;
}, {
request: { call: Arg(0, "function-call") },
response: { screenshot: RetVal("snapshot-image") }
})
});
/**
* The corresponding Front object for the FrameSnapshotActor.
*/
let FrameSnapshotFront = protocol.FrontClass(FrameSnapshotActor, {
initialize: function(client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
this._lastDrawCallScreenshot = null;
this._cachedScreenshots = new WeakMap();
},
/**
* This implementation caches the last draw call screenshot to optimize
* frontend requests to `generateScreenshotFor`.
*/
getOverview: custom(function() {
return this._getOverview().then(data => {
this._lastDrawCallScreenshot = data.screenshot;
return data;
});
}, {
impl: "_getOverview"
}),
/**
* This implementation saves a roundtrip to the backend if the screenshot
* was already generated and retrieved once.
*/
generateScreenshotFor: custom(function(functionCall) {
if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name)) {
return promise.resolve(this._lastDrawCallScreenshot);
}
let cachedScreenshot = this._cachedScreenshots.get(functionCall);
if (cachedScreenshot) {
return cachedScreenshot;
}
let screenshot = this._generateScreenshotFor(functionCall);
this._cachedScreenshots.set(functionCall, screenshot);
return screenshot;
}, {
impl: "_generateScreenshotFor"
})
});
/**
* This Canvas Actor handles simple instrumentation of all the methods
* of a 2D or WebGL context, to provide information regarding all the calls
* made when drawing frame inside an animation loop.
*/
let CanvasActor = exports.CanvasActor = protocol.ActorClass({
typeName: "canvas",
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
this.finalize();
},
/**
* Starts listening for function calls.
*/
setup: method(function({ reload }) {
if (this._initialized) {
return;
}
this._initialized = true;
this._callWatcher = new CallWatcherActor(this.conn, this.tabActor);
this._callWatcher.onCall = this._onContentFunctionCall;
this._callWatcher.setup({
tracedGlobals: CANVAS_CONTEXTS,
tracedFunctions: ANIMATION_GENERATORS,
performReload: reload
});
}, {
request: { reload: Option(0, "boolean") },
oneway: true
}),
/**
* Stops listening for function calls.
*/
finalize: method(function() {
if (!this._initialized) {
return;
}
this._initialized = false;
this._callWatcher.finalize();
this._callWatcher = null;
}, {
oneway: true
}),
/**
* Returns whether this actor has been set up.
*/
isInitialized: method(function() {
return !!this._initialized;
}, {
response: { initialized: RetVal("boolean") }
}),
/**
* Records a snapshot of all the calls made during the next animation frame.
* The animation should be implemented via the de-facto requestAnimationFrame
* utility, not inside a `setInterval` or recursive `setTimeout`.
*
* XXX: Currently only supporting requestAnimationFrame. When this isn't used,
* it'd be a good idea to display a huge red flashing banner telling people to
* STOP USING `setInterval` OR `setTimeout` FOR ANIMATION. Bug 978948.
*/
recordAnimationFrame: method(function() {
if (this._callWatcher.isRecording()) {
return this._currentAnimationFrameSnapshot.promise;
}
this._callWatcher.eraseRecording();
this._callWatcher.resumeRecording();
let deferred = this._currentAnimationFrameSnapshot = promise.defer();
return deferred.promise;
}, {
response: { snapshot: RetVal("frame-snapshot") }
}),
/**
* Invoked whenever an instrumented function is called, be it on a
* 2d or WebGL context, or an animation generator like requestAnimationFrame.
*/
_onContentFunctionCall: function(functionCall) {
let { window, name, args } = functionCall.details;
// The function call arguments are required to replay animation frames,
// in order to generate screenshots. However, simply storing references to
// every kind of object is a bad idea, since their properties may change.
// Consider transformation matrices for example, which are typically
// Float32Arrays whose values can easily change across context calls.
// They need to be cloned.
inplaceShallowCloneArrays(args, window);
if (CanvasFront.ANIMATION_GENERATORS.has(name)) {
this._handleAnimationFrame(functionCall);
return;
}
if (CanvasFront.DRAW_CALLS.has(name) && this._animationStarted) {
this._handleDrawCall(functionCall);
return;
}
},
/**
* Handle animations generated using requestAnimationFrame.
*/
_handleAnimationFrame: function(functionCall) {
if (!this._animationStarted) {
this._handleAnimationFrameBegin();
} else {
this._handleAnimationFrameEnd(functionCall);
}
},
/**
* Called whenever an animation frame rendering begins.
*/
_handleAnimationFrameBegin: function() {
this._callWatcher.eraseRecording();
this._animationStarted = true;
},
/**
* Called whenever an animation frame rendering ends.
*/
_handleAnimationFrameEnd: function() {
// Get a hold of all the function calls made during this animation frame.
// Since only one snapshot can be recorded at a time, erase all the
// previously recorded calls.
let functionCalls = this._callWatcher.pauseRecording();
this._callWatcher.eraseRecording();
// Since the animation frame finished, get a hold of the (already retrieved)
// canvas pixels to conveniently create a screenshot of the final rendering.
let index = this._lastDrawCallIndex;
let width = this._lastContentCanvasWidth;
let height = this._lastContentCanvasHeight;
let flipped = this._lastThumbnailFlipped;
let pixels = ContextUtils.getPixelStorage()["32bit"];
let lastDrawCallScreenshot = {
index: index,
width: width,
height: height,
flipped: flipped,
pixels: pixels.subarray(0, width * height)
};
// Wrap the function calls and screenshot in a FrameSnapshotActor instance,
// which will resolve the promise returned by `recordAnimationFrame`.
let frameSnapshot = new FrameSnapshotActor(this.conn, {
canvas: this._lastDrawCallCanvas,
calls: functionCalls,
screenshot: lastDrawCallScreenshot
});
this._currentAnimationFrameSnapshot.resolve(frameSnapshot);
this._currentAnimationFrameSnapshot = null;
this._animationStarted = false;
},
/**
* Invoked whenever a draw call is detected in the animation frame which is
* currently being recorded.
*/
_handleDrawCall: function(functionCall) {
let functionCalls = this._callWatcher.pauseRecording();
let caller = functionCall.details.caller;
let global = functionCall.meta.global;
let contentCanvas = this._lastDrawCallCanvas = caller.canvas;
let index = this._lastDrawCallIndex = functionCalls.indexOf(functionCall);
let w = this._lastContentCanvasWidth = contentCanvas.width;
let h = this._lastContentCanvasHeight = contentCanvas.height;
// To keep things fast, generate images of small and fixed dimensions.
let dimensions = CanvasFront.THUMBNAIL_HEIGHT;
let thumbnail;
// Create a thumbnail on every draw call on the canvas context, to augment
// the respective function call actor with this additional data.
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
// Check if drawing to a custom framebuffer (when rendering to texture).
// Don't create a thumbnail in this particular case.
let framebufferBinding = caller.getParameter(caller.FRAMEBUFFER_BINDING);
if (framebufferBinding == null) {
thumbnail = ContextUtils.getPixelsForWebGL(caller, 0, 0, w, h, dimensions);
thumbnail.flipped = this._lastThumbnailFlipped = true;
thumbnail.index = index;
}
} else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
thumbnail = ContextUtils.getPixelsFor2D(caller, 0, 0, w, h, dimensions);
thumbnail.flipped = this._lastThumbnailFlipped = false;
thumbnail.index = index;
}
functionCall._thumbnail = thumbnail;
this._callWatcher.resumeRecording();
}
});
/**
* A collection of methods for manipulating canvas contexts.
*/
let ContextUtils = {
/**
* WebGL contexts are sensitive to how they're queried. Use this function
* to make sure the right context is always retrieved, if available.
*
* @param HTMLCanvasElement canvas
* The canvas element for which to get a WebGL context.
* @param WebGLRenderingContext gl
* The queried WebGL context, or null if unavailable.
*/
getWebGLContext: function(canvas) {
return canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl");
},
/**
* Gets a hold of the rendered pixels in the most efficient way possible for
* a canvas with a WebGL context.
*
* @param WebGLRenderingContext gl
* The WebGL context to get a screenshot from.
* @param number srcX [optional]
* The first left pixel that is read from the framebuffer.
* @param number srcY [optional]
* The first top pixel that is read from the framebuffer.
* @param number srcWidth [optional]
* The number of pixels to read on the X axis.
* @param number srcHeight [optional]
* The number of pixels to read on the Y axis.
* @param number dstHeight [optional]
* The desired generated screenshot height.
* @return object
* An objet containing the screenshot's width, height and pixel data.
*/
getPixelsForWebGL: function(gl,
srcX = 0, srcY = 0,
srcWidth = gl.canvas.width,
srcHeight = gl.canvas.height,
dstHeight = srcHeight)
{
let contentPixels = ContextUtils.getPixelStorage(srcWidth, srcHeight);
let { "8bit": charView, "32bit": intView } = contentPixels;
gl.readPixels(srcX, srcY, srcWidth, srcHeight, gl.RGBA, gl.UNSIGNED_BYTE, charView);
return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
},
/**
* Gets a hold of the rendered pixels in the most efficient way possible for
* a canvas with a 2D context.
*
* @param CanvasRenderingContext2D ctx
* The 2D context to get a screenshot from.
* @param number srcX [optional]
* The first left pixel that is read from the canvas.
* @param number srcY [optional]
* The first top pixel that is read from the canvas.
* @param number srcWidth [optional]
* The number of pixels to read on the X axis.
* @param number srcHeight [optional]
* The number of pixels to read on the Y axis.
* @param number dstHeight [optional]
* The desired generated screenshot height.
* @return object
* An objet containing the screenshot's width, height and pixel data.
*/
getPixelsFor2D: function(ctx,
srcX = 0, srcY = 0,
srcWidth = ctx.canvas.width,
srcHeight = ctx.canvas.height,
dstHeight = srcHeight)
{
let { data } = ctx.getImageData(srcX, srcY, srcWidth, srcHeight);
let { "32bit": intView } = ContextUtils.usePixelStorage(data.buffer);
return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
},
/**
* Resizes the provided pixels to fit inside a rectangle with the specified
* height and the same aspect ratio as the source.
*
* @param Uint32Array srcPixels
* The source pixel data, assuming 32bit/pixel and 4 color components.
* @param number srcWidth
* The source pixel data width.
* @param number srcHeight
* The source pixel data height.
* @param number dstHeight [optional]
* The desired resized pixel data height.
* @return object
* An objet containing the resized pixels width, height and data.
*/
resizePixels: function(srcPixels, srcWidth, srcHeight, dstHeight) {
let screenshotRatio = dstHeight / srcHeight;
let dstWidth = Math.floor(srcWidth * screenshotRatio);
// Use a plain array instead of a Uint32Array to make serializing faster.
let dstPixels = new Array(dstWidth * dstHeight);
// If the resized image ends up being completely transparent, returning
// an empty array will skip some redundant serialization cycles.
let isTransparent = true;
for (let dstX = 0; dstX < dstWidth; dstX++) {
for (let dstY = 0; dstY < dstHeight; dstY++) {
let srcX = Math.floor(dstX / screenshotRatio);
let srcY = Math.floor(dstY / screenshotRatio);
let cPos = srcX + srcWidth * srcY;
let dPos = dstX + dstWidth * dstY;
let color = dstPixels[dPos] = srcPixels[cPos];
if (color) {
isTransparent = false;
}
}
}
return {
width: dstWidth,
height: dstHeight,
pixels: isTransparent ? [] : dstPixels
};
},
/**
* Invokes a series of canvas context calls, to "replay" an animation frame
* and generate a screenshot.
*
* In case of a WebGL context, an offscreen framebuffer is created for
* the respective canvas, and the rendering will be performed into it.
* This is necessary because some state (like shaders, textures etc.) can't
* be shared between two different WebGL contexts.
* Hopefully, once SharedResources are a thing this won't be necessary:
* http://www.khronos.org/webgl/wiki/SharedResouces
*
* In case of a 2D context, a new canvas is created, since there's no
* intrinsic state that can't be easily duplicated.
*
* @param number contexType
* The type of context to use. See the CallWatcherFront scope types.
* @param HTMLCanvasElement canvas
* The canvas element which is the source of all context calls.
* @param array calls
* An array of function call actors.
* @param number first
* The first function call to start from.
* @param number last
* The last (inclusive) function call to end at.
* @return object
* The context on which the specified calls were invoked and the
* last registered draw call's index.
*/
replayAnimationFrame: function({ contextType, canvas, calls, first, last }) {
let w = canvas.width;
let h = canvas.height;
let replayCanvas;
let replayContext;
let customFramebuffer;
let lastDrawCallIndex = -1;
// In case of WebGL contexts, rendering will be done offscreen, in a
// custom framebuffer, but on the provided canvas context.
if (contextType == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
replayCanvas = canvas;
replayContext = this.getWebGLContext(replayCanvas);
customFramebuffer = this.createBoundFramebuffer(replayContext, w, h);
}
// In case of 2D contexts, draw everything on a separate canvas context.
else if (contextType == CallWatcherFront.CANVAS_2D_CONTEXT) {
let contentDocument = canvas.ownerDocument;
replayCanvas = contentDocument.createElement("canvas");
replayCanvas.width = w;
replayCanvas.height = h;
replayContext = replayCanvas.getContext("2d");
replayContext.clearRect(0, 0, w, h);
}
// Replay all the context calls up to and including the specified one.
for (let i = first; i <= last; i++) {
let { type, name, args } = calls[i].details;
// Prevent WebGL context calls that try to reset the framebuffer binding
// to the default value, since we want to perform the rendering offscreen.
if (name == "bindFramebuffer" && args[1] == null) {
replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer);
} else {
if (type == CallWatcherFront.METHOD_FUNCTION) {
replayContext[name].apply(replayContext, args);
} else if (type == CallWatcherFront.SETTER_FUNCTION) {
replayContext[name] = args;
} else {
// Ignore getter calls.
}
if (CanvasFront.DRAW_CALLS.has(name)) {
lastDrawCallIndex = i;
}
}
}
return {
replayContext: replayContext,
lastDrawCallIndex: lastDrawCallIndex
};
},
/**
* Gets an object containing a buffer large enough to hold width * height
* pixels, assuming 32bit/pixel and 4 color components.
*
* This method avoids allocating memory and tries to reuse a common buffer
* as much as possible.
*
* @param number w
* The desired pixel array storage width.
* @param number h
* The desired pixel array storage height.
* @return object
* The requested pixel array buffer.
*/
getPixelStorage: function(w = 0, h = 0) {
let storage = this._currentPixelStorage;
if (storage && storage["32bit"].length >= w * h) {
return storage;
}
return this.usePixelStorage(new ArrayBuffer(w * h * 4));
},
/**
* Creates and saves the array buffer views used by `getPixelStorage`.
*
* @param ArrayBuffer buffer
* The raw buffer used as storage for various array buffer views.
*/
usePixelStorage: function(buffer) {
let array8bit = new Uint8Array(buffer);
let array32bit = new Uint32Array(buffer);
return this._currentPixelStorage = {
"8bit": array8bit,
"32bit": array32bit
};
},
/**
* Creates a framebuffer of the specified dimensions for a WebGL context,
* assuming a RGBA color buffer, a depth buffer and no stencil buffer.
*
* @param WebGLRenderingContext gl
* The WebGL context to create and bind a framebuffer for.
* @param number width
* The desired width of the renderbuffers.
* @param number height
* The desired height of the renderbuffers.
* @return WebGLFramebuffer
* The generated framebuffer object.
*/
createBoundFramebuffer: function(gl, width, height) {
let framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// Use a texture as the color rendebuffer attachment, since consumenrs of
// this function will most likely want to read the rendered pixels back.
let colorBuffer = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, colorBuffer);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
let depthBuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorBuffer, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
return framebuffer;
}
};
/**
* The corresponding Front object for the CanvasActor.
*/
let CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, {
initialize: function(client, { canvasActor }) {
protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor });
client.addActorPool(this);
this.manage(this);
}
});
/**
* Constants.
*/
CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS);
CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS);
CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS);
CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS);
CanvasFront.THUMBNAIL_HEIGHT = 50; // px
CanvasFront.SCREENSHOT_HEIGHT_MAX = 256; // px
CanvasFront.INVALID_SNAPSHOT_IMAGE = {
index: -1,
width: 0,
height: 0,
pixels: []
};
/**
* Goes through all the arguments and creates a one-level shallow copy
* of all arrays and array buffers.
*/
function inplaceShallowCloneArrays(functionArguments, contentWindow) {
let { Object, Array, ArrayBuffer } = contentWindow;
functionArguments.forEach((arg, index, store) => {
if (arg instanceof Array) {
store[index] = arg.slice();
}
if (arg instanceof Object && arg.buffer instanceof ArrayBuffer) {
store[index] = new arg.constructor(arg);
}
});
}

View File

@ -4,9 +4,9 @@
"use strict";
const {Cc, Ci, Cu, Cr} = require("chrome");
const Services = require("Services");
const events = require("sdk/event/core");
const protocol = require("devtools/server/protocol");
const { ContentObserver } = require("devtools/content-observer");
const { on, once, off, emit } = events;
const { method, Arg, Option, RetVal } = protocol;
@ -293,7 +293,7 @@ let WebGLActor = exports.WebGLActor = protocol.ActorClass({
* This is useful for dealing with bfcache, when no new programs are linked.
*/
getPrograms: method(function() {
let id = getInnerWindowID(this.tabActor.window);
let id = ContentObserver.GetInnerWindowID(this.tabActor.window);
return this._programActorsCache.filter(e => e.ownerWindow == id);
}, {
response: { programs: RetVal("array:gl-program") }
@ -346,58 +346,6 @@ let WebGLFront = exports.WebGLFront = protocol.FrontClass(WebGLActor, {
}
});
/**
* Handles adding an observer for the creation of content document globals,
* event sent immediately after a web content document window has been set up,
* but before any script code has been executed. This will allow us to
* instrument the HTMLCanvasElement with the appropriate inspection methods.
*/
function ContentObserver(tabActor) {
this._contentWindow = tabActor.window;
this._onContentGlobalCreated = this._onContentGlobalCreated.bind(this);
this._onInnerWindowDestroyed = this._onInnerWindowDestroyed.bind(this);
this.startListening();
}
ContentObserver.prototype = {
/**
* Starts listening for the required observer messages.
*/
startListening: function() {
Services.obs.addObserver(
this._onContentGlobalCreated, "content-document-global-created", false);
Services.obs.addObserver(
this._onInnerWindowDestroyed, "inner-window-destroyed", false);
},
/**
* Stops listening for the required observer messages.
*/
stopListening: function() {
Services.obs.removeObserver(
this._onContentGlobalCreated, "content-document-global-created", false);
Services.obs.removeObserver(
this._onInnerWindowDestroyed, "inner-window-destroyed", false);
},
/**
* Fired immediately after a web content document window has been set up.
*/
_onContentGlobalCreated: function(subject, topic, data) {
if (subject == this._contentWindow) {
emit(this, "global-created", subject);
}
},
/**
* Fired when an inner window is removed from the backward/forward cache.
*/
_onInnerWindowDestroyed: function(subject, topic, data) {
let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
emit(this, "global-destroyed", id);
}
};
/**
* Instruments a HTMLCanvasElement with the appropriate inspection methods.
*/
@ -413,7 +361,7 @@ let WebGLInstrumenter = {
handle: function(window, observer) {
let self = this;
let id = getInnerWindowID(window);
let id = ContentObserver.GetInnerWindowID(window);
let canvasElem = XPCNativeWrapper.unwrap(window.HTMLCanvasElement);
let canvasPrototype = canvasElem.prototype;
let originalGetContext = canvasPrototype.getContext;
@ -1354,13 +1302,6 @@ WebGLProxy.prototype = {
// Utility functions.
function getInnerWindowID(window) {
return window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
}
function removeFromMap(map, predicate) {
for (let [key, value] of map) {
if (predicate(value)) {

View File

@ -393,6 +393,8 @@ var DebuggerServer = {
this.addActors("resource://gre/modules/devtools/server/actors/script.js");
this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
this.registerModule("devtools/server/actors/inspector");
this.registerModule("devtools/server/actors/call-watcher");
this.registerModule("devtools/server/actors/canvas");
this.registerModule("devtools/server/actors/webgl");
this.registerModule("devtools/server/actors/stylesheets");
this.registerModule("devtools/server/actors/styleeditor");
@ -401,8 +403,9 @@ var DebuggerServer = {
this.registerModule("devtools/server/actors/tracer");
this.registerModule("devtools/server/actors/memory");
this.registerModule("devtools/server/actors/eventlooplag");
if ("nsIProfiler" in Ci)
if ("nsIProfiler" in Ci) {
this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
}
},
/**