Bug 1204595 - Store audionode properties once via server rather than async fetching the unchanging properties in the tool. r=jryans
authorJordan Santell <jsantell@mozilla.com>
Mon, 14 Sep 2015 16:04:54 -0700
changeset 295243 93a6681ccc23d1766f9b06977060f15a97f3eaef
parent 295242 5f69abfe8d2ec1feafe3ad12f4edcdb7c274943b
child 295244 7400c2750b7b0617363e2df7f0ac1c27c57032b2
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1204595
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1204595 - Store audionode properties once via server rather than async fetching the unchanging properties in the tool. r=jryans
browser/devtools/webaudioeditor/controller.js
browser/devtools/webaudioeditor/models.js
browser/devtools/webaudioeditor/test/browser.ini
browser/devtools/webaudioeditor/test/browser_audionode-actor-bypassable.js
browser/devtools/webaudioeditor/test/browser_audionode-actor-get-type.js
browser/devtools/webaudioeditor/test/browser_audionode-actor-is-source.js
browser/devtools/webaudioeditor/test/browser_audionode-actor-source.js
browser/devtools/webaudioeditor/test/browser_audionode-actor-type.js
browser/devtools/webaudioeditor/test/browser_wa_inspector-bypass-01.js
browser/devtools/webaudioeditor/test/browser_webaudio-actor-destroy-node.js
browser/devtools/webaudioeditor/test/browser_webaudio-actor-simple.js
browser/devtools/webaudioeditor/views/inspector.js
toolkit/devtools/server/actors/utils/audionodes.json
toolkit/devtools/server/actors/webaudio.js
toolkit/devtools/server/docs/protocol.js.md
toolkit/devtools/server/protocol.js
toolkit/devtools/server/tests/unit/test_protocol_children.js
--- a/browser/devtools/webaudioeditor/controller.js
+++ b/browser/devtools/webaudioeditor/controller.js
@@ -180,19 +180,19 @@ let WebAudioEditorController = {
     $("#content").hidden = false;
     window.emit(EVENTS.START_CONTEXT);
   },
 
   /**
    * Called when a new node is created. Creates an `AudioNodeView` instance
    * for tracking throughout the editor.
    */
-  _onCreateNode: Task.async(function* (nodeActor) {
-    yield gAudioNodes.add(nodeActor);
-  }),
+  _onCreateNode: function (nodeActor) {
+    gAudioNodes.add(nodeActor);
+  },
 
   /**
    * Called on `destroy-node` when an AudioNode is GC'd. Removes
    * from the AudioNode array and fires an event indicating the removal.
    */
   _onDestroyNode: function (nodeActor) {
     gAudioNodes.remove(gAudioNodes.get(nodeActor.actorID));
   },
--- a/browser/devtools/webaudioeditor/models.js
+++ b/browser/devtools/webaudioeditor/models.js
@@ -19,47 +19,23 @@ const AudioNodeModel = Class({
   extends: EventTarget,
 
   // Will be added via AudioNodes `add`
   collection: null,
 
   initialize: function (actor) {
     this.actor = actor;
     this.id = actor.actorID;
+    this.type = actor.type;
+    this.bypassable = actor.bypassable;
+    this._bypassed = false;
     this.connections = [];
   },
 
   /**
-   * After instantiating the AudioNodeModel, calling `setup` caches values
-   * from the actor onto the model. In this case, only the type of audio node.
-   *
-   * @return promise
-   */
-  setup: Task.async(function* () {
-    yield this.getType();
-
-    // Query bypass status on start up
-    this._bypassed = yield this.isBypassed();
-
-    // Store whether or not this node is bypassable in the first place
-    this.bypassable = !AUDIO_NODE_DEFINITION[this.type].unbypassable;
-  }),
-
-  /**
-   * A proxy for the underlying AudioNodeActor to fetch its type
-   * and subsequently assign the type to the instance.
-   *
-   * @return Promise->String
-   */
-  getType: Task.async(function* () {
-    this.type = yield this.actor.getType();
-    return this.type;
-  }),
-
-  /**
    * Stores connection data inside this instance of this audio node connecting
    * to another node (destination). If connecting to another node's AudioParam,
    * the second argument (param) must be populated with a string.
    *
    * Connecting nodes is idempotent. Upon new connection, emits "connect" event.
    *
    * @param AudioNodeModel destination
    * @param String param
@@ -79,20 +55,20 @@ const AudioNodeModel = Class({
   disconnect: function () {
     this.connections.length = 0;
     coreEmit(this, "disconnect", this);
   },
 
   /**
    * Gets the bypass status of the audio node.
    *
-   * @return Promise->Boolean
+   * @return Boolean
    */
   isBypassed: function () {
-    return this.actor.isBypassed();
+    return this._bypassed;
   },
 
   /**
    * Sets the bypass value of an AudioNode.
    *
    * @param Boolean enable
    * @return Promise
    */
@@ -157,17 +133,19 @@ const AudioNodeModel = Class({
       // and `param` is just more explicitly the param, ignoring
       // implementation details.
       if (edge.param) {
         options.label = options.param = edge.param;
       }
 
       graph.addEdge(null, this.id, edge.destination, options);
     }
-  }
+  },
+
+  toString: () => "[object AudioNodeModel]",
 });
 
 
 /**
  * Constructor for a Collection of `AudioNodeModel` models.
  *
  * Events:
  * - `add`: node
@@ -195,35 +173,31 @@ const AudioNodesCollection = Class({
     this.models.forEach(fn);
   },
 
   /**
    * Creates a new AudioNodeModel, passing through arguments into the AudioNodeModel
    * constructor, and adds the model to the internal collection store of this
    * instance.
    *
-   * Also calls `setup` on the model itself, and sets up event piping, so that
-   * events emitted on each model propagate to the collection itself.
-   *
    * Emits "add" event on instance when completed.
    *
    * @param Object obj
-   * @return Promise->AudioNodeModel
+   * @return AudioNodeModel
    */
-  add: Task.async(function* (obj) {
+  add: function (obj) {
     let node = new this.model(obj);
     node.collection = this;
-    yield node.setup();
 
     this.models.add(node);
 
     node.on("*", this._onModelEvent);
     coreEmit(this, "add", node);
     return node;
-  }),
+  },
 
   /**
    * Removes an AudioNodeModel from the internal collection. Calls `delete` method
    * on the model, and emits "remove" on this instance.
    *
    * @param AudioNodeModel node
    */
   remove: function (node) {
@@ -303,10 +277,12 @@ const AudioNodesCollection = Class({
       // If a `remove` event from the model, remove it
       // from the collection, and let the method handle the emitting on
       // the collection
       this.remove(node);
     } else {
       // Pipe the event to the collection
       coreEmit(this, eventName, node, ...args);
     }
-  }
+  },
+
+  toString: () => "[object AudioNodeCollection]",
 });
--- a/browser/devtools/webaudioeditor/test/browser.ini
+++ b/browser/devtools/webaudioeditor/test/browser.ini
@@ -17,19 +17,20 @@ support-files =
   doc_bug_1112378.html
   440hz_sine.ogg
   head.js
 
 [browser_audionode-actor-get-param-flags.js]
 [browser_audionode-actor-get-params-01.js]
 [browser_audionode-actor-get-params-02.js]
 [browser_audionode-actor-get-set-param.js]
-[browser_audionode-actor-get-type.js]
-[browser_audionode-actor-is-source.js]
+[browser_audionode-actor-type.js]
+[browser_audionode-actor-source.js]
 [browser_audionode-actor-bypass.js]
+[browser_audionode-actor-bypassable.js]
 [browser_audionode-actor-connectnode-disconnect.js]
 [browser_audionode-actor-connectparam.js]
 skip-if = true # bug 1092571
 # [browser_audionode-actor-add-automation-event.js] bug 1134036
 # [browser_audionode-actor-get-automation-data-01.js] bug 1134036
 # [browser_audionode-actor-get-automation-data-02.js] bug 1134036
 # [browser_audionode-actor-get-automation-data-03.js] bug 1134036
 [browser_callwatcher-01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-bypassable.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test AudioNode#bypassable
+ */
+
+add_task(function*() {
+  let { target, front } = yield initBackend(SIMPLE_NODES_URL);
+  let [_, nodes] = yield Promise.all([
+    front.setup({ reload: true }),
+    getN(front, "create-node", 14)
+  ]);
+
+  let actualBypassability = nodes.map(node => node.bypassable);
+  let expectedBypassability = [
+    false, // AudioDestinationNode
+    true, // AudioBufferSourceNode
+    true, // ScriptProcessorNode
+    true, // AnalyserNode
+    true, // GainNode
+    true, // DelayNode
+    true, // BiquadFilterNode
+    true, // WaveShaperNode
+    true, // PannerNode
+    true, // ConvolverNode
+    false, // ChannelSplitterNode
+    false, // ChannelMergerNode
+    true, // DynamicsCompressNode
+    true, // OscillatorNode
+  ];
+
+  expectedBypassability.forEach((bypassable, i) => {
+    is(actualBypassability[i], bypassable, `${nodes[i].type} has correct ".bypassable" status`);
+  });
+
+  yield removeTab(target.tab);
+});
rename from browser/devtools/webaudioeditor/test/browser_audionode-actor-is-source.js
rename to browser/devtools/webaudioeditor/test/browser_audionode-actor-source.js
--- a/browser/devtools/webaudioeditor/test/browser_audionode-actor-is-source.js
+++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-source.js
@@ -1,27 +1,27 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Test AudioNode#isSource()
+ * Test AudioNode#source
  */
 
 add_task(function*() {
   let { target, front } = yield initBackend(SIMPLE_NODES_URL);
   let [_, nodes] = yield Promise.all([
     front.setup({ reload: true }),
     getN(front, "create-node", 14)
   ]);
 
-  let actualTypes = yield Promise.all(nodes.map(node => node.getType()));
-  let isSourceResult = yield Promise.all(nodes.map(node => node.isSource()));
+  let actualTypes = nodes.map(node => node.type);
+  let isSourceResult = nodes.map(node => node.source);
 
   actualTypes.forEach((type, i) => {
     let shouldBeSource = type === "AudioBufferSourceNode" || type === "OscillatorNode";
     if (shouldBeSource)
-      is(isSourceResult[i], true, type + "'s isSource() yields into `true`");
+      is(isSourceResult[i], true, type + "'s `source` is `true`");
     else
-      is(isSourceResult[i], false, type + "'s isSource() yields into `false`");
+      is(isSourceResult[i], false, type + "'s `source` is `false`");
   });
 
   yield removeTab(target.tab);
 });
rename from browser/devtools/webaudioeditor/test/browser_audionode-actor-get-type.js
rename to browser/devtools/webaudioeditor/test/browser_audionode-actor-type.js
--- a/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-type.js
+++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-type.js
@@ -1,23 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Test AudioNode#getType()
+ * Test AudioNode#type
  */
 
 add_task(function*() {
   let { target, front } = yield initBackend(SIMPLE_NODES_URL);
   let [_, nodes] = yield Promise.all([
     front.setup({ reload: true }),
     getN(front, "create-node", 14)
   ]);
 
-  let actualTypes = yield Promise.all(nodes.map(node => node.getType()));
+  let actualTypes = nodes.map(node => node.type);
   let expectedTypes = [
     "AudioDestinationNode",
     "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode",
     "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode",
     "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode"
   ];
 
   expectedTypes.forEach((type, i) => {
--- a/browser/devtools/webaudioeditor/test/browser_wa_inspector-bypass-01.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_inspector-bypass-01.js
@@ -13,22 +13,18 @@ add_task(function*() {
   reload(target);
 
   let [actors] = yield Promise.all([
     get3(gFront, "create-node"),
     waitForGraphRendered(panelWin, 3, 2)
   ]);
   let nodeIds = actors.map(actor => actor.actorID);
 
-  click(panelWin, findGraphNode(panelWin, nodeIds[1]));
   // Wait for the node to be set as well as the inspector to come fully into the view
-  yield Promise.all([
-    waitForInspectorRender(panelWin, EVENTS),
-    once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED)
-  ]);
+  yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[1]), true);
 
   let $bypass = $("toolbarbutton.bypass");
 
   is((yield actors[1].isBypassed()), false, "AudioNodeActor is not bypassed by default.")
   is($bypass.checked, true, "Button is 'on' for normal nodes");
   is($bypass.disabled, false, "Bypass button is not disabled for normal nodes");
 
   command($bypass);
@@ -44,19 +40,17 @@ add_task(function*() {
   yield once(gAudioNodes, "bypass");
 
   is((yield actors[1].isBypassed()), false, "AudioNodeActor is no longer bypassed.")
   is($bypass.checked, true, "Button is back on when clicked");
   is($bypass.disabled, false, "Bypass button is not disabled after click");
   ok(!findGraphNode(panelWin, nodeIds[1]).classList.contains("bypassed"),
     "AudioNode no longer has 'bypassed' class.");
 
-  click(panelWin, findGraphNode(panelWin, nodeIds[0]));
-
-  yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
+  yield clickGraphNode(panelWin, findGraphNode(panelWin, nodeIds[0]));
 
   is((yield actors[0].isBypassed()), false, "Unbypassable AudioNodeActor is not bypassed.");
   is($bypass.checked, false, "Button is 'off' for unbypassable nodes");
   is($bypass.disabled, true, "Bypass button is disabled for unbypassable nodes");
 
   command($bypass);
   is((yield actors[0].isBypassed()), false,
     "Clicking button on unbypassable node does not change bypass state on actor.");
--- a/browser/devtools/webaudioeditor/test/browser_webaudio-actor-destroy-node.js
+++ b/browser/devtools/webaudioeditor/test/browser_webaudio-actor-destroy-node.js
@@ -16,19 +16,18 @@ add_task(function*() {
     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");
+  destroyed.forEach((node, i) => {
+    ok(node.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);
 });
 
 function actorIsInList (list, actor) {
--- a/browser/devtools/webaudioeditor/test/browser_webaudio-actor-simple.js
+++ b/browser/devtools/webaudioeditor/test/browser_webaudio-actor-simple.js
@@ -9,23 +9,19 @@ add_task(function*() {
   let { target, front } = yield initBackend(SIMPLE_CONTEXT_URL);
   let [_, __, [destNode, oscNode, gainNode], [connect1, connect2]] = yield Promise.all([
     front.setup({ reload: true }),
     once(front, "start-context"),
     get3(front, "create-node"),
     get2(front, "connect-node")
   ]);
 
-  let destType = yield destNode.getType();
-  let oscType = yield oscNode.getType();
-  let gainType = yield gainNode.getType();
-
-  is(destType, "AudioDestinationNode", "WebAudioActor:create-node returns AudioNodeActor for AudioDestination");
-  is(oscType, "OscillatorNode", "WebAudioActor:create-node returns AudioNodeActor");
-  is(gainType, "GainNode", "WebAudioActor:create-node returns AudioNodeActor");
+  is(destNode.type, "AudioDestinationNode", "WebAudioActor:create-node returns AudioNodeActor for AudioDestination");
+  is(oscNode.type, "OscillatorNode", "WebAudioActor:create-node returns AudioNodeActor");
+  is(gainNode.type, "GainNode", "WebAudioActor:create-node returns AudioNodeActor");
 
   let { source, dest } = connect1;
   is(source.actorID, oscNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on source (osc->gain)");
   is(dest.actorID, gainNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on dest (osc->gain)");
 
   ({ source, dest } = connect2);
   is(source.actorID, gainNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on source (gain->dest)");
   is(dest.actorID, destNode.actorID, "WebAudioActor:connect-node returns correct actor with ID on dest (gain->dest)");
--- a/browser/devtools/webaudioeditor/views/inspector.js
+++ b/browser/devtools/webaudioeditor/views/inspector.js
@@ -83,17 +83,17 @@ let InspectorView = {
       $("#web-audio-editor-details-pane-empty").removeAttribute("hidden");
       $("#web-audio-editor-tabs").setAttribute("hidden", "true");
       window.emit(EVENTS.UI_INSPECTOR_NODE_SET, null);
     }
     // Otherwise load up the tabs view and hide the empty placeholder
     else {
       $("#web-audio-editor-details-pane-empty").setAttribute("hidden", "true");
       $("#web-audio-editor-tabs").removeAttribute("hidden");
-      yield this._buildToolbar();
+      this._buildToolbar();
       window.emit(EVENTS.UI_INSPECTOR_NODE_SET, this._currentNode.id);
     }
   }),
 
   /**
    * Returns the current AudioNodeView.
    */
   getCurrentAudioNode: function () {
@@ -106,35 +106,35 @@ let InspectorView = {
   resetUI: function () {
     // Set current node to empty to load empty view
     this.setCurrentAudioNode();
 
     // Reset AudioNode inspector and hide
     this.hideImmediately();
   },
 
-  _buildToolbar: Task.async(function* () {
+  _buildToolbar: function () {
     let node = this.getCurrentAudioNode();
 
     let bypassable = node.bypassable;
-    let bypassed = yield node.isBypassed();
+    let bypassed = node.isBypassed();
     let button = $("#audio-node-toolbar .bypass");
 
     if (!bypassable) {
       button.setAttribute("disabled", true);
     } else {
       button.removeAttribute("disabled");
     }
 
     if (!bypassable || bypassed) {
       button.removeAttribute("checked");
     } else {
       button.setAttribute("checked", true);
     }
-  }),
+  },
 
   /**
    * Event handlers
    */
 
   /**
    * Called on EVENTS.UI_SELECT_NODE, and takes an actorID `id`
    * and calls `setCurrentAudioNode` to scaffold the inspector view.
--- a/toolkit/devtools/server/actors/utils/audionodes.json
+++ b/toolkit/devtools/server/actors/utils/audionodes.json
@@ -1,10 +1,11 @@
 {
   "OscillatorNode": {
+    "source": true,
     "properties": {
       "type": {},
       "frequency": {
         "param": true
       },
       "detune": {
         "param": true
       }
@@ -12,16 +13,17 @@
   },
   "GainNode": {
     "properties": { "gain": { "param": true }}
   },
   "DelayNode": {
     "properties": { "delayTime": { "param": true }}
   },
   "AudioBufferSourceNode": {
+    "source": true,
     "properties": {
       "buffer": { "Buffer": true },
       "playbackRate": {
         "param": true
       },
       "loop": {},
       "loopStart": {},
       "loopEnd": {}
@@ -86,18 +88,22 @@
     "unbypassable": true
   },
   "ChannelSplitterNode": {
     "unbypassable": true
   },
   "ChannelMergerNode": {
     "unbypassable": true
   },
-  "MediaElementAudioSourceNode": {},
-  "MediaStreamAudioSourceNode": {},
+  "MediaElementAudioSourceNode": {
+    "source": true
+  },
+  "MediaStreamAudioSourceNode": {
+    "source": true
+  },
   "MediaStreamAudioDestinationNode": {
     "unbypassable": true,
     "properties": {
       "stream": { "MediaStream": true }
     }
   },
   "StereoPannerNode": {
     "properties": {
--- a/toolkit/devtools/server/actors/webaudio.js
+++ b/toolkit/devtools/server/actors/webaudio.js
@@ -3,23 +3,24 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 const Services = require("Services");
 
 const events = require("sdk/event/core");
+const promise = require("promise");
 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 { createValueGrip } = require("devtools/server/actors/object");
 const AutomationTimeline = require("./utils/automation-timeline");
 const { on, once, off, emit } = events;
-const { types, method, Arg, Option, RetVal } = protocol;
+const { types, method, Arg, Option, RetVal, preEvent } = protocol;
 const AUDIO_NODE_DEFINITION = require("devtools/server/actors/utils/audionodes.json");
 const ENABLE_AUTOMATION = false;
 const AUTOMATION_GRANULARITY = 2000;
 const AUTOMATION_GRANULARITY_MAX = 6000;
 
 const AUDIO_GLOBALS = [
   "AudioContext", "AudioNode", "AudioParam"
 ];
@@ -44,16 +45,29 @@ const NODE_ROUTING_METHODS = [
 /**
  * An Audio Node actor allowing communication to a specific audio node in the
  * Audio Context graph.
  */
 types.addActorType("audionode");
 let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
   typeName: "audionode",
 
+  form: function (detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
+
+    return {
+      actor: this.actorID, // actorID is set when this is added to a pool
+      type: this.type,
+      source: this.source,
+      bypassable: this.bypassable,
+    };
+  },
+
   /**
    * Create the Audio Node actor.
    *
    * @param DebuggerServerConnection conn
    *        The server connection.
    * @param AudioNode node
    *        The AudioNode that was created.
    */
@@ -70,43 +84,35 @@ let AudioNodeActor = exports.AudioNodeAc
     this.automation = {};
 
     try {
       this.type = getConstructorName(node);
     } catch (e) {
       this.type = "";
     }
 
+    this.source = !!AUDIO_NODE_DEFINITION[this.type].source;
+    this.bypassable = !AUDIO_NODE_DEFINITION[this.type].unbypassable;
+
     // Create automation timelines for all AudioParams
     Object.keys(AUDIO_NODE_DEFINITION[this.type].properties || {})
       .filter(isAudioParam.bind(null, node))
       .forEach(paramName => {
         this.automation[paramName] = new AutomationTimeline(node[paramName].defaultValue);
       });
   },
 
   /**
-   * Returns the name of the audio type.
-   * Examples: "OscillatorNode", "MediaElementAudioSourceNode"
+   * Returns the string name of the audio type.
+   *
+   * DEPRECATED: Use `audionode.type` instead, left here for legacy reasons.
    */
   getType: method(function () {
     return this.type;
-  }, {
-    response: { type: RetVal("string") }
-  }),
-
-  /**
-   * Returns a boolean indicating if the node is a source node,
-   * like BufferSourceNode, MediaElementAudioSourceNode, OscillatorNode, etc.
-   */
-  isSource: method(function () {
-    return !!~this.type.indexOf("Source") || this.type === "OscillatorNode";
-  }, {
-    response: { source: RetVal("boolean") }
-  }),
+  }, { response: { type: RetVal("string") }}),
 
   /**
    * Returns a boolean indicating if the AudioNode has been "bypassed",
    * via `AudioNodeActor#bypass` method.
    *
    * @return Boolean
    */
   isBypassed: method(function () {
@@ -134,18 +140,17 @@ let AudioNodeActor = exports.AudioNodeAc
    */
   bypass: method(function (enable) {
     let node = this.node.get();
 
     if (node === null) {
       return;
     }
 
-    let bypassable = !AUDIO_NODE_DEFINITION[this.type].unbypassable;
-    if (bypassable) {
+    if (this.bypassable) {
       node.passThrough = enable;
     }
 
     return this.isBypassed();
   }, {
     request: { enable: Arg(0, "boolean") },
     response: { bypassed: RetVal("boolean") }
   }),
@@ -450,18 +455,39 @@ let AudioNodeActor = exports.AudioNodeAc
   _recordAutomationEvent: function (paramName, eventName, args) {
     let timeline = this.automation[paramName];
     timeline[eventName].apply(timeline, args);
   }
 });
 
 /**
  * The corresponding Front object for the AudioNodeActor.
+ *
+ * @attribute {String} type
+ *            The type of audio node, like "OscillatorNode", "MediaElementAudioSourceNode"
+ * @attribute {Boolean} source
+ *            Boolean indicating if the node is a source node, like BufferSourceNode,
+ *            MediaElementAudioSourceNode, OscillatorNode, etc.
+ * @attribute {Boolean} bypassable
+ *            Boolean indicating if the audio node is bypassable (splitter,
+ *            merger and destination nodes, for example, are not)
  */
 let AudioNodeFront = protocol.FrontClass(AudioNodeActor, {
+  form: function (form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+
+    this.actorID = form.actor;
+    this.type = form.type;
+    this.source = form.source;
+    this.bypassable = form.bypassable;
+  },
+
   initialize: function (client, form) {
     protocol.Front.prototype.initialize.call(this, client, form);
     // if we were manually passed a form, this was created manually and
     // needs to own itself for now.
     if (form) {
       this.manage(this);
     }
   }
@@ -859,17 +885,32 @@ let WebAudioActor = exports.WebAudioActo
 
 /**
  * The corresponding Front object for the WebAudioActor.
  */
 let WebAudioFront = exports.WebAudioFront = protocol.FrontClass(WebAudioActor, {
   initialize: function(client, { webaudioActor }) {
     protocol.Front.prototype.initialize.call(this, client, { actor: webaudioActor });
     this.manage(this);
-  }
+  },
+
+  /**
+   * If connecting to older geckos (<Fx43), where audio node actor's do not
+   * contain `type`, `source` and `bypassable` properties, fetch
+   * them manually here.
+   */
+  _onCreateNode: preEvent("create-node", function (audionode) {
+    if (!audionode.type) {
+      return audionode.getType().then(type => {
+        audionode.type = type;
+        audionode.source = !!AUDIO_NODE_DEFINITION[type].source;
+        audionode.bypassable = !AUDIO_NODE_DEFINITION[type].unbypassable;
+      });
+    }
+  }),
 });
 
 WebAudioFront.AUTOMATION_METHODS = new Set(AUTOMATION_METHODS);
 WebAudioFront.NODE_CREATION_METHODS = new Set(NODE_CREATION_METHODS);
 WebAudioFront.NODE_ROUTING_METHODS = new Set(NODE_ROUTING_METHODS);
 
 /**
  * Determines whether or not property is an AudioParam.
--- a/toolkit/devtools/server/docs/protocol.js.md
+++ b/toolkit/devtools/server/docs/protocol.js.md
@@ -418,16 +418,22 @@ And now you can listen to events on a fr
     front.giveGoodNews().then(() => { console.log("request returned.") });
 
 You might want to update your front's state when an event is fired, before emitting it against the front.  You can use `preEvent` in the front definition for that:
 
     countGoodNews: protocol.preEvent("good-news", function(news) {
         this.amountOfGoodNews++;
     });
 
+You can have events wait until an asynchronous action completes before firing by returning a promise. If you have multiple preEvents defined for a specific event, and atleast one fires asynchronously, then all preEvents most resolve before all events are fired.
+
+    countGoodNews: protocol.preEvent("good-news", function(news) {
+        return this.updateGoodNews().then(() => this.amountOfGoodNews++);
+    });
+
 On a somewhat related note, not every method needs to be request/response.  Just like an actor can emit a one-way event, a method can be marked as a one-way request.  Maybe we don't care about giveGoodNews returning anything:
 
     giveGoodNews: method(function(news) {
         emit(this, "good-news", news);
     }, {
         request: { news: Arg(0, "string") },
         oneway: true
     });
--- a/toolkit/devtools/server/protocol.js
+++ b/toolkit/devtools/server/protocol.js
@@ -1193,18 +1193,26 @@ let Front = Class({
       try {
         args = event.request.read(packet, this);
       } catch(ex) {
         console.error("Error reading event: " + packet.type);
         console.exception(ex);
         throw ex;
       }
       if (event.pre) {
-        event.pre.forEach((pre) => pre.apply(this, args));
+        let results = event.pre.map(pre => pre.apply(this, args));
+
+        // Check to see if any of the preEvents returned a promise -- if so,
+        // wait for their resolution before emitting. Otherwise, emit synchronously.
+        if (results.some(result => result && typeof result.then === "function")) {
+          promise.all(results).then(() => events.emit.apply(null, [this, event.name].concat(args)));
+          return;
+        }
       }
+
       events.emit.apply(null, [this, event.name].concat(args));
       return;
     }
 
     // Remaining packets must be responses.
     if (this._requests.length === 0) {
       let msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
       let err = Error(msg);
--- a/toolkit/devtools/server/tests/unit/test_protocol_children.js
+++ b/toolkit/devtools/server/tests/unit/test_protocol_children.js
@@ -99,31 +99,37 @@ let ChildActor = protocol.ActorClass({
     return this.parent().getChild(id);
   }, {
     request: { id: Arg(0) },
     response: { sibling: RetVal("childActor") }
   }),
 
   emitEvents: method(function() {
     events.emit(this, "event1", 1, 2, 3);
+    events.emit(this, "event2", 4, 5, 6);
     events.emit(this, "named-event", 1, 2, 3);
     events.emit(this, "object-event", this);
     events.emit(this, "array-object-event", [this]);
   }, {
     response: { value: "correct response" },
   }),
 
   release: method(function() { }, { release: true }),
 
   events: {
     "event1" : {
       a: Arg(0),
       b: Arg(1),
       c: Arg(2)
     },
+    "event2" : {
+      a: Arg(0),
+      b: Arg(1),
+      c: Arg(2)
+    },
     "named-event": {
       type: "namedEvent",
       a: Arg(0),
       b: Arg(1),
       c: Arg(2)
     },
     "object-event": {
       type: "objectEvent",
@@ -156,16 +162,24 @@ let ChildFront = protocol.FrontClass(Chi
     }
     this.childID = form.childID;
     this.detail = form.detail;
   },
 
   onEvent1: preEvent("event1", function(a, b, c) {
     this.event1arg3 = c;
   }),
+
+  onEvent2a: preEvent("event2", function(a, b, c) {
+    return promise.resolve().then(() => this.event2arg3 = c);
+  }),
+
+  onEvent2b: preEvent("event2", function(a, b, c) {
+    this.event2arg2 = b;
+  }),
 });
 
 types.addDictType("manyChildrenDict", {
   child5: "childActor",
   more: "array:childActor",
 });
 
 types.addLifetime("temp", "_temporaryHolder");
@@ -404,26 +418,38 @@ function run_test()
       do_check_true(ret[0] === childFront);
       do_check_true(ret[1] !== childFront);
       do_check_true(ret[1] instanceof ChildFront);
 
       // On both children, listen to events.  We're only
       // going to trigger events on the first child, so an event
       // triggered on the second should cause immediate failures.
 
-      let set = new Set(["event1", "named-event", "object-event", "array-object-event"]);
+      let set = new Set(["event1", "event2", "named-event", "object-event", "array-object-event"]);
 
       childFront.on("event1", (a, b, c) => {
         do_check_eq(a, 1);
         do_check_eq(b, 2);
         do_check_eq(c, 3);
         // Verify that the pre-event handler was called.
         do_check_eq(childFront.event1arg3, 3);
         set.delete("event1");
       });
+      childFront.on("event2", (a, b, c) => {
+        do_check_eq(a, 4);
+        do_check_eq(b, 5);
+        do_check_eq(c, 6);
+        // Verify that the async pre-event handler was called,
+        // setting the property before this handler was called.
+        do_check_eq(childFront.event2arg3, 6);
+        // And check that the sync preEvent with the same name is also
+        // executed
+        do_check_eq(childFront.event2arg2, 5);
+        set.delete("event2");
+      });
       childFront.on("named-event", (a, b, c) => {
         do_check_eq(a, 1);
         do_check_eq(b, 2);
         do_check_eq(c, 3);
         set.delete("named-event");
       });
       childFront.on("object-event", (obj) => {
         do_check_true(obj === childFront);
@@ -435,23 +461,25 @@ function run_test()
         do_check_eq(childFront.detail, "detail2");
         set.delete("array-object-event");
       });
 
       let fail = function() {
         do_throw("Unexpected event");
       }
       ret[1].on("event1", fail);
+      ret[1].on("event2", fail);
       ret[1].on("named-event", fail);
       ret[1].on("object-event", fail);
       ret[1].on("array-object-event", fail);
 
       return childFront.emitEvents().then(() => {
         trace.expectSend({"type":"emitEvents","to":"<actorid>"});
         trace.expectReceive({"type":"event1","a":1,"b":2,"c":3,"from":"<actorid>"});
+        trace.expectReceive({"type":"event2","a":4,"b":5,"c":6,"from":"<actorid>"});
         trace.expectReceive({"type":"namedEvent","a":1,"b":2,"c":3,"from":"<actorid>"});
         trace.expectReceive({"type":"objectEvent","detail":{"actor":"<actorid>","childID":"child1","detail":"detail1"},"from":"<actorid>"});
         trace.expectReceive({"type":"arrayObjectEvent","detail":[{"actor":"<actorid>","childID":"child1","detail":"detail2"}],"from":"<actorid>"});
         trace.expectReceive({"value":"correct response","from":"<actorid>"});
 
 
         do_check_eq(set.size, 0);
       });