Bug 980506 - Emit destruction events on AudioNodes in the WebAudioActor. r=vp

This commit is contained in:
Jordan Santell 2014-06-20 12:05:00 -04:00
parent 2265f9a900
commit cf47d8ecf6
6 changed files with 140 additions and 3 deletions

View File

@ -7,6 +7,7 @@ support-files =
doc_simple-node-creation.html
doc_buffer-and-array.html
doc_media-node-creation.html
doc_destroy-nodes.html
440hz_sine.ogg
head.js
@ -17,6 +18,7 @@ support-files =
[browser_audionode-actor-get-param-flags.js]
[browser_audionode-actor-is-source.js]
[browser_webaudio-actor-simple.js]
[browser_webaudio-actor-destroy-node.js]
[browser_wa_first-run.js]
[browser_wa_reset-01.js]

View File

@ -0,0 +1,41 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test `destroy-node` event on WebAudioActor.
*/
function spawnTest () {
let [target, debuggee, front] = yield initBackend(DESTROY_NODES_URL);
let waitUntilDestroyed = getN(front, "destroy-node", 10);
let [_, _, created] = yield Promise.all([
front.setup({ reload: true }),
once(front, "start-context"),
// Should create 1 destination node and 10 disposable buffer nodes
getN(front, "create-node", 13)
]);
// Force CC so we can ensure it's run to clear out dead AudioNodes
forceCC();
let destroyed = yield waitUntilDestroyed;
let destroyedTypes = yield Promise.all(destroyed.map(actor => actor.getType()));
destroyedTypes.forEach((type, i) => {
ok(type, "AudioBufferSourceNode", "Only buffer nodes are destroyed");
ok(actorIsInList(created, destroyed[i]),
"`destroy-node` called only on AudioNodes in current document.");
});
yield removeTab(target.tab);
finish();
}
function actorIsInList (list, actor) {
for (let i = 0; i < list.length; i++) {
if (list[i].actorID === actor.actorID)
return list[i];
}
return null;
}

View File

@ -0,0 +1,32 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Web Audio Editor test page</title>
</head>
<body>
<script type="text/javascript;version=1.8">
"use strict";
(function () {
let ctx = new AudioContext();
let osc = ctx.createOscillator();
let gain = ctx.createGain();
for (let i = 0; i < 10; i++) {
ctx.createBufferSource();
}
osc.connect(gain);
gain.connect(ctx.destination);
gain.gain.value = 0;
osc.start();
})();
</script>
</body>
</html>

View File

@ -26,6 +26,7 @@ const COMPLEX_CONTEXT_URL = EXAMPLE_URL + "doc_complex-context.html";
const SIMPLE_NODES_URL = EXAMPLE_URL + "doc_simple-node-creation.html";
const MEDIA_NODES_URL = EXAMPLE_URL + "doc_media-node-creation.html";
const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html";
const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html";
// All tests are asynchronous.
waitForExplicitFinish();
@ -372,6 +373,15 @@ function countGraphObjects (win) {
}
}
/**
* Forces cycle collection and GC, used in AudioNode destruction tests.
*/
function forceCC () {
SpecialPowers.DOMWindowUtils.cycleCollect();
SpecialPowers.DOMWindowUtils.garbageCollect();
SpecialPowers.DOMWindowUtils.garbageCollect();
}
/**
* List of audio node properties to test against expectations of the AudioNode actor
*/

View File

@ -68,8 +68,11 @@ let FunctionCallActor = protocol.ActorClass({
* The called function's arguments.
* @param any result
* The value returned by the function call.
* @param boolean holdWeak
* Determines whether or not FunctionCallActor stores a weak reference
* to the underlying objects.
*/
initialize: function(conn, [window, global, caller, type, name, stack, args, result]) {
initialize: function(conn, [window, global, caller, type, name, stack, args, result], holdWeak) {
protocol.Actor.prototype.initialize.call(this, conn);
this.details = {
@ -81,7 +84,7 @@ let FunctionCallActor = protocol.ActorClass({
// Store a weak reference to all objects so we don't
// prevent natural GC if `holdWeak` was passed into
// setup as truthy. Used in the Web Audio Editor.
if (this._holdWeak) {
if (holdWeak) {
let weakRefs = {
window: Cu.getWeakReference(window),
caller: Cu.getWeakReference(caller),
@ -526,7 +529,7 @@ let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
* Invoked whenever an instrumented function is called.
*/
_onContentFunctionCall: function(...details) {
let functionCall = new FunctionCallActor(this.conn, details);
let functionCall = new FunctionCallActor(this.conn, details, this._holdWeak);
this._functionCalls.push(functionCall);
this.onCall(functionCall);
}

View File

@ -9,6 +9,7 @@ const Services = require("Services");
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const events = require("sdk/event/core");
const { on: systemOn, off: systemOff } = require("sdk/system/events");
const protocol = require("devtools/server/protocol");
const { CallWatcherActor, CallWatcherFront } = require("devtools/server/actors/call-watcher");
const { ThreadActor } = require("devtools/server/actors/script");
@ -289,6 +290,9 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
// to the associated actorID, so we don't have to expose `nativeID`
// to the client in any way.
this._nativeToActorID = new Map();
this._onDestroyNode = this._onDestroyNode.bind(this);
this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
},
destroy: function(conn) {
@ -326,6 +330,11 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
performReload: reload,
holdWeak: true
});
// Bind to the `global-destroyed` event on the content observer so we can
// unbind events between the global destruction and the `finalize` cleanup
// method on the actor.
// TODO expose these events on CallWatcherActor itself, bug 1021321
on(this._callWatcher._contentObserver, "global-destroyed", this._onGlobalDestroyed);
}, {
request: { reload: Option(0, "boolean") },
oneway: true
@ -395,6 +404,7 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
}
this.tabActor = null;
this._initialized = false;
off(this._callWatcher._contentObserver, "global-destroyed", this._onGlobalDestroyed);
this._nativeToActorID = null;
this._callWatcher.eraseRecording();
this._callWatcher.finalize();
@ -433,6 +443,10 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
"create-node": {
type: "createNode",
source: Arg(0, "audionode")
},
"destroy-node": {
type: "destroyNode",
source: Arg(0, "audionode")
}
},
@ -471,6 +485,7 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
* Called on first audio node creation, signifying audio context usage
*/
_onStartContext: function () {
systemOn("webaudio-node-demise", this._onDestroyNode);
emit(this, "start-context");
},
@ -520,6 +535,40 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
_onCreateNode: function (node) {
let actor = this._constructAudioNode(node);
emit(this, "create-node", actor);
},
/** Called when `webaudio-node-demise` is triggered,
* and emits the associated actor to the front if found.
*/
_onDestroyNode: function ({data}) {
// Cast to integer.
let nativeID = ~~data;
let actor = this._getActorByNativeID(nativeID);
// If actorID exists, emit; in the case where we get demise
// notifications for a document that no longer exists,
// the mapping should not be found, so we do not emit an event.
if (actor) {
this._nativeToActorID.delete(nativeID);
emit(this, "destroy-node", actor);
}
},
/**
* Called when the underlying ContentObserver fires `global-destroyed`
* so we can cleanup some things between the global being destroyed and
* when the actor's `finalize` method gets called.
*/
_onGlobalDestroyed: function (id) {
if (this._callWatcher._tracedWindowId !== id) {
return;
}
if (this._nativeToActorID) {
this._nativeToActorID.clear();
}
systemOff("webaudio-node-demise", this._onDestroyNode);
}
});