Skip to content

Instantly share code, notes, and snippets.

@mihaisucan
Created September 10, 2012 15:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mihaisucan/3691691 to your computer and use it in GitHub Desktop.
Save mihaisucan/3691691 to your computer and use it in GitHub Desktop.
Description of the Web Console remoting work. See Mozilla bug 768096

Web Console remoting, bug 768096

This document describes the ongoing work for the Web Console remoting patches. I also include questions, concerns for problems encountered and proposed changes. Feedback is requested.

Bug 768096 has landed. Questions and concerns that have remained open will probably be addressed at a later point in time. Thanks to everyone who provided feedback!

Introduction

Goals:

  • Make the web console usable over the remote debugging protocol, such that console API logging, network logging, object inspection and JS evaluation work with remote servers (b2g, fennec, etc).
  • Keep changes to a minimum, minimum user-impact. Any user-facing changes with potential impact to how various functionality works shall be postponed. Such work shall be done in separate bugs.
  • Complete this work in time for Firefox 18, the 8th of October.
    • Which for all practical purposes shall be completed by the 28th of September - the remaining days will be vacation time - until the 4th of October. I do not expect too much work to happen from the 4th to the 8th.

Decisions made upfront - in agreement with Panos and Rob:

  • Initially we will not attach to the ThreadActor to avoid causing any performance impact for page scripts - deoptimization of JS execution due to the debugger.
  • For the use of the WebConsoleActor we will not require the client to attach to the TabActor.

In bug 673148 a lot of work was done to split the UI away from the various listeners used by the Web Console. Tests have also been updated accordingly.

Today, the impact of that work is highly visible: most of the work consists of shuffling listeners around, making things work over the RDP and designing actors and the new protocol packets for reusability.

Questions and concerns are marked like this. Please address my questions in your feedback. Thank you!

The WebConsoleActor and the WebConsoleClient

The new WebConsoleActor lives in dbg-webconsole-actors.js, in the new toolkit/devtools/webconsole folder.

The new WebConsoleClient lives in WebConsoleClient.jsm (in toolkit) and it used by the Web Console when working with the Web Console actor.

To see how the debugger is used in the Web Console code, look in browser/devtools/webconsole/webconsole.js, search for WebConsoleConnectionProxy.

Do note that we created only one Web Console actor for the general needs of the Web Console. New actors:

  • The WebConsoleActor allows JS evaluation, autocomplete, start/stop listeners, etc.
  • The WebConsoleObjectActor allows us to marshal content objects from the server to the client.
  • The NetworkEventActor is used for each new network request. The client can request further network event details - like response body or request headers.

In the future we might want to split things even further.

Voice your concerns. Shall we split the Web Console actor? Please make your suggestions on how we should split things, if you want so.

It has been suggested that we split out JS evaluation and autocomplete into a separate actor, but I am unsure how we should do that. Maybe it's too soon? We did not yet clearly define requirements for a split...

To attach to the WebConsoleActor one follows these steps:

connectToServer() // the usual
listTabs()
pickTheTabYouWant()
debuggerClient.attachConsole(tab.consoleActor, listeners, onAttachConsole)

The listeners arguments tells which listeners you want to start in the web console. These can be: page errors, window.console API messages, network activity and file activity.

The onAttachConsole callback receives a new instance of the WebConsoleClient object. This object provides methods that abstract away protocol packets, things like startListeners(), stopListeners(), etc.

Protocol packets look as follows:

{
  "to": "root",
  "type": "listTabs"
}
{
  "from": "root",
  "consoleActor": "conn0.console9",
  "selected": 2,
  "tabs": [
    {
      "actor": "conn0.tab2",
      "consoleActor": "conn0.console7",
      "title": "",
      "url": "https://tbpl.mozilla.org/?tree=Fx-Team"
    },
...
  ]
}

Notice that the consoleActor is also available as a global actor. When you attach to the global consoleActor you receive all of the network requests, page errors, and all of the other events from all of the tabs and windows, including chrome errors and network events. This allows us to implement a Global Console or to debug remote Firefox/B2G instances.

Any concerns with the added consoleActor?

Currently we are missing a way to send the source of a file from the server. For example, if a page error happens in some script the client will fetch its own copy of the script from the given URL. It may work, or it may not. However, in the case of chrome debugging this always fails - the client cannot access chrome files. See bug 799198.

startListeners(listeners, onResponse)

The new startListeners packet:

{
  "to": "conn0.console9",
  "type": "startListeners",
  "listeners": [
    "PageError",
    "ConsoleAPI",
    "NetworkActivity",
    "FileActivity",
    "LocationChange"
  ]
}

The idea with the startListeners packet is that you can start/stop listeners as needed. You do not need to attach and start all listeners at once. You get only what you need. The reply is:

{
  "startedListeners": [
    "PageError",
    "ConsoleAPI",
    "NetworkActivity",
    "FileActivity",
    "LocationChange"
  ],
  "nativeConsoleAPI": true,
  "from": "conn0.console9"
}

The reply tells which listeners were started and it includes a flag nativeConsoleAPI which tells if the window.console object was overridden by the scripts in the page or not.

getCachedMessages(types, onResponse)

One can do webConsoleClient.getCachedMessages(types, onResponse). This method sends the following packet to the server:

{
  "to": "conn0.console9",
  "type": "getCachedMessages",
  "messageTypes": [
    "PageError",
    "ConsoleAPI"
  ]
}

The getCachedMessages packet allows one to retrieve the cached messages from before the Web Console was open. You can only get cached messages for page errors and console API calls. The reply looks like this:

{
  "messages": [ ... ],
  "from": "conn0.console9"
}

Each message in the array is of the same type as when we send typical page errors and console API calls. These will be explained in the following sections of this document.

setPreferences(prefs, onResponse)

To allow the Web Console to configure logging options while it is running we have added the setPreferences packet:

{
  "to": "conn0.console9",
  "type": "setPreferences",
  "preferences": {
    "NetworkMonitor.saveRequestAndResponseBodies": false
  }
}

Reply:

{
  "updated": [
    "NetworkMonitor.saveRequestAndResponseBodies"
  ],
  "from": "conn0.console10"
}

For convenience you can use webConsoleClient.setPreferences(prefs, onResponse).

Any concerns here? With the new packets or with the overall proposed workflow.

Patch 1: page errors

Page errors come from the nsIConsoleService. Each allowed page error is an nsIScriptError object.

The new pageError packet is:

{
  "from": "conn0.console9",
  "type": "pageError",
  "pageError": {
    "message": "[JavaScript Error: \"ReferenceError: foo is not defined\" {file: \"http://localhost/~mihai/mozilla/test.js\" line: 6}]",
    "errorMessage": "ReferenceError: foo is not defined",
    "sourceName": "http://localhost/~mihai/mozilla/test.js",
    "lineText": "",
    "lineNumber": 6,
    "columnNumber": 0,
    "category": "content javascript",
    "timeStamp": 1347294508210,
    "error": false,
    "warning": false,
    "exception": true,
    "strict": false
  }
}

The packet is simply a JSON-ification of the nsIScriptError - for simplicity. I only removed several unneeded properties and changed how flags work.

Thoughts? Different/better proposals?

Patch 2: Console API messages and JavaScript evaluation

Introduction (to problems)

Given the decision to avoid the use of the Debugger API, to not cause performance regressions, we cannot use the ObjectActor to pass objects from the server to the client. We cannot make real object grips, and we cannot work with the Debuggee.Object API.

Beyond that, the lifetime of ObjectActors in the debugger is much different to the lifetime of objects in the Web Console: if my understanding is correct, the ObjectActor existence is assumed to be while the debugger is paused. The Web Console needs to work with live objects.

Also, a WeakMap is used to map between each ObjectActor that wraps Debuggee.Object. This means that UI can release the object actor only once for each unique object. If the client UI references the same object in multiple places, then it needs to avoid releasing the object actor too soon. Example: call console.log(window) and inspect the object. The Property Panel needs to track the object actors it received for each object on the window object. When closed, the property panel releases the object actors. At this point the console.log output message reference to the window object actor is lost. You can no longer use it.

The only way to make it semi-work is to add reference counting in the Web Console code, which is basically a way to do footguns. It works until you take into consideration how the Web Console output works: for performance reasons we output only 200 messages, every few milliseconds. We prune the output queue for cases when we receive many requests for output, like thousands of window.console API calls - we do not display them, we only display the last 200 messages. The thing with pruning is that we need to release object actors. Given 1000 calls to console.log(window) we would release the same object actor 800 times. The last 200 messages we receive would point to a stale object actor. Ref counting on the client does not work because the Web Console does not know all of the incoming at once - it can't tell if it releases an object actor too early.

This brings us to the second problem with the current ObjectActor and how things work. Potential solutions:

  • Reference counting on the server.
    • ... different kind of footgunn, but better than doing it client-side.
    • We can do this in two ways:
      • use a map, the debugger code does, and refcount each use. Do actual object actor release only when the count goes to 0.
      • don't a map, just create a new object actor each time when an actor is needed, even if there already exists one object actor for the given object.
  • Actor pools: create object actor pools based on some magical criteria?

At this point, in agreement with Rob, we did a WebConsoleObjectActor that wraps every content object we need to send to the client over RDP. We don't use a map.

Please voice your concerns loud and clear about all of this stuff. If something from the above list of concerns is unclear/confusing, please ask for a better explanation.

The WebConsoleObjectActor

The actor grip looks as follows, for two different objects:

{
  "type": "object",
  "className": "HTMLDivElement",
  "displayString": "[object HTMLDivElement]",
  "inspectable": true,
  "actor": "conn0.consoleObj12"
}
{
  "type": "object",
  "className": "Object",
  "displayString": "({a:1, b:2, c:3})",
  "inspectable": true,
  "actor": "conn0.consoleObj16"
}

The above packet is the minimal information we send to the web console, such that the object can be displayed in the output and in the property panel. Unfortunately, we have some "weird" ways on how we display objects - and these are different in the property panel.

This is the actor grip for functions:

{
  "type": "function",
  "className": "function",
  "displayString": "function myOnPaste(e)\n{\n  console.log(\"onpaste!\");\n}",
  "inspectable": false,
  "functionName": "myOnPaste",
  "functionArguments": [
    "e"
  ],
  "actor": "conn0.consoleObj15"
}

This object actor does not need the debugger API, nor does it need the ThreadActor. It does not implement any of the request types from ObjectActor since they do not suit the needs of the Web Console client, except the release request which releases the object actor.

Comments and questions about these packets are very much welcome.

The inspectProperties request

The Web Console object actor implements the inspectProperties request type:

{
  "to": "conn0.consoleObj17",
  "type": "inspectProperties"
}

Example reply:

{
  "from": "conn0.consoleObj17",
  "properties": [
    {
      "name": "duron",
      "configurable": true,
      "enumerable": true,
      "writable": true,
      "value": {
        "type": "object",
        "className": "Object",
        "displayString": "({opteron:\"amd\", athlon:\"amd\", core2duo:\"intel\", nehalem:\"intel\"})",
        "inspectable": true,
        "actor": "conn0.consoleObj18"
      }
    },
    {
      "name": "foobar",
      "configurable": true,
      "enumerable": true,
      "writable": true,
      "value": "omg"
    },
    {
      "name": "zuzu",
      "configurable": true,
      "enumerable": true,
      "writable": true,
      "value": "boom"
    }
  ]
}

TODO: see bug 787981

  • we need to use the LongStringActor for strings, when strings are too long.

For each enumerable property on the object we send a property descriptor. The properties array is sorted by property name. In each descriptor, for set, get and value we create object actors, if needed.

The way the ObjectActor is implemented now only allows separate retrieval of object own properties, then prototype, and so on. Additionally, it is missing a good number of important properties on objects, see bug 788148. For the Web Console we really need a new type of request.

Comments/concerns about inspectProperties?

Console API messages, the consoleAPICall packet

Console API messages come through the nsIObserverService - the console object implementation lives in dom/base/ConsoleAPI.js.

For each console message we receive in the server, we send something similar to the following packet to the client:

{
  "from": "conn0.console9",
  "type": "consoleAPICall",
  "message": {
    "level": "error",
    "filename": "http://localhost/~mihai/mozilla/test.html",
    "lineNumber": 149,
    "functionName": "",
    "timeStamp": 1347302713771,
    "arguments": [
      "error omg aloha ",
      {
        "type": "object",
        "className": "HTMLBodyElement",
        "displayString": "[object HTMLBodyElement]",
        "inspectable": true,
        "actor": "conn0.consoleObj20"
      },
      " 960 739 3.141592653589793 %a",
      "zuzu",
      {
        "type": "null"
      },
      {
        "type": "undefined"
      }
    ]
  }
}

Similar to how we send the page errors, here we send the actual console event received from the nsIObserverService. We change the arguments array - we create WebConsoleObjectActors for each object passed as an argument - and, lastly, we remove some unneeded properties (like window IDs). The Web Console can then inspect the arguments.

We have small variations for the object, depending on the console API call method - just like there are small differences in the console event object received from the observer service.

Should we make any changes? Other thoughts?

JavaScript evaluation

Introduction (and concerns)

For evaluation I still use the Sandbox object. I did not make changes mainly to avoid potential regressions. I would like to use the API discussed by bz and bholley in bug 774753, but bug 785174 came along, then Thaddee's patch came in - confusion was bound to happen. :)

My thoughts:

  • We need Debugger.Object.evalInGlobalWithBindings(). This is for use with the debugger.
  • We need a windowUtils.executeInWindow(). This should be the way to do it without the heavy debugger machinery, without causing any deoptimizations in page scripts.

It might be that, in the end, if the debugger can avoid deoptimizing JS execution in pages, then Debugger.Object.evalInGlobalWithBindings() shall remain the only way to eval JS in content. Until then... ?

Can we ever expect that a JS debugger will not cause script execution deoptimizations?

Thaddee's patch experiments with making the Web Console use the Debugger.Object.evalInGlobalWithBindings()...

Thoughts? Anything I should do at this point? I feel like I should just continue with using the Sandbox object until things settle.

The evaluateJS request and response packets

The Web Console client provides the evaluateJS(requestId, string, onResponse) method which sends the following packet:

{
  "to": "conn0.console9",
  "type": "evaluateJS",
  "text": "document"
}

Response packet:

{
  "from": "conn0.console9",
  "input": "document",
  "result": {
    "type": "object",
    "className": "HTMLDocument",
    "displayString": "[object HTMLDocument]",
    "inspectable": true,
    "actor": "conn0.consoleObj20"
  },
  "timestamp": 1347306273605,
  "error": null,
  "errorMessage": null,
  "helperResult": null
}
  • error holds the JSON-ification of the exception thrown during evaluation;
  • errorMessage holds the error.toString() result.
  • result has the result object actor.
  • helperResult is anything that might come from a JSTerm helper result, JSON stuff (not content objects!).

Questions? Concerns?

Autocomplete and more

The autocomplete request packet:

{
  "to": "conn0.console9",
  "type": "autocomplete",
  "text": "d",
  "cursor": 1
}

The response packet:

{
  "from": "conn0.console9",
  "matches": [
    "decodeURI",
    "decodeURIComponent",
    "defaultStatus",
    "devicePixelRatio",
    "disableExternalCapture",
    "dispatchEvent",
    "doMyXHR",
    "document",
    "dump"
  ],
  "matchProp": "d"
}

There's also the clearMessagesCache request packet that has no response. This clears the console API calls cache and should clear the page errors cache - see bug 717611.

Patch 3: network logging

The networkEvent packet

Whenever a new network request starts being logged the networkEvent packet is sent:

{
  "from": "conn0.console10",
  "type": "networkEvent",
  "eventActor": {
    "actor": "conn0.netEvent14",
    "startedDateTime": "2012-09-17T19:50:03.699Z",
    "url": "http://localhost/~mihai/mozilla/test2.css",
    "method": "GET"
  }
}

This packet is used to inform the Web Console of a new network event. For each request a new NetworkEventActor instance is created.

The NetworkEventActor

The new network event actor stores further request and response information.

The networkEventUpdate packet

The Web Console UI needs to be kept up-to-date when changes happen, when new stuff is added. The new networkEventUpdate packet is sent for this purpose. Examples:

{
  "from": "conn0.netEvent14",
  "type": "networkEventUpdate",
  "updateType": "requestHeaders",
  "headers": 10,
  "headersSize": 425
}
{
  "from": "conn0.netEvent14",
  "type": "networkEventUpdate",
  "updateType": "requestCookies",
  "cookies": 0
}
{
  "from": "conn0.netEvent14",
  "type": "networkEventUpdate",
  "updateType": "responseStart",
  "response": {
    "httpVersion": "HTTP/1.1",
    "status": "304",
    "statusText": "Not Modified",
    "headersSize": 194,
    "discardResponseBody": true
  }
}
{
  "from": "conn0.netEvent14",
  "type": "networkEventUpdate",
  "updateType": "eventTimings",
  "totalTime": 1
}
{
  "from": "conn0.netEvent14",
  "type": "networkEventUpdate",
  "updateType": "responseHeaders",
  "headers": 6,
  "headersSize": 194
}
{
  "from": "conn0.netEvent14",
  "type": "networkEventUpdate",
  "updateType": "responseCookies",
  "cookies": 0
}
{
  "from": "conn0.netEvent14",
  "type": "networkEventUpdate",
  "updateType": "responseContent",
  "mimeType": "text/css",
  "contentSize": 0,
  "discardResponseBody": true
}

Actual headers, cookies and bodies are not sent.

The getRequestHeaders and other packets

To get more details about a network event you can use the following packet requests (and replies).

The getRequestHeaders packet:

{
  "to": "conn0.netEvent15",
  "type": "getRequestHeaders"
}
{
  "from": "conn0.netEvent15",
  "headers": [
    {
      "name": "Host",
      "value": "localhost"
    }, ...
  ],
  "headersSize": 350
}

The getRequestCookies packet:

{
  "to": "conn0.netEvent15",
  "type": "getRequestCookies"
}
{
  "from": "conn0.netEvent15",
  "cookies": []
}

The getResponseHeaders packet:

{
  "to": "conn0.netEvent15",
  "type": "getResponseHeaders"
}
{
  "from": "conn0.netEvent15",
  "headers": [
    {
      "name": "Date",
      "value": "Mon, 17 Sep 2012 20:05:27 GMT"
    }, ...
  ],
  "headersSize": 320
}

The getResponseCookies packet:

{
  "to": "conn0.netEvent15",
  "type": "getResponseCookies"
}
{
  "from": "conn0.netEvent15",
  "cookies": []
}

The getRequestPostData packet:

{
  "to": "conn0.netEvent15",
  "type": "getRequestPostData"
}
{
  "from": "conn0.netEvent15",
  "postData": { text: "foobar" },
  "postDataDiscarded": false
}

The getResponseContent packet:

{
  "to": "conn0.netEvent15",
  "type": "getResponseContent"
}
{
  "from": "conn0.netEvent15",
  "content": {
    "mimeType": "text/css",
    "text": "\n@import \"test.css\";\n\n.foobar { color: green }\n\n"
  },
  "contentDiscarded": false
}

The getEventTimings packet:

{
  "to": "conn0.netEvent15",
  "type": "getEventTimings"
}
{
  "from": "conn0.netEvent15",
  "timings": {
    "blocked": 0,
    "dns": 0,
    "connect": 0,
    "send": 0,
    "wait": 16,
    "receive": 0
  },
  "totalTime": 16
}

TODO: see bug 787981

  • we need to use the LongStringActor for strings - header values, request and response bodies, etc.

I would like comments about the new network event actor and its packets.

The fileActivity packet

When a file load is observed the following fileActivity packet is sent to the client:

{
  "from": "conn0.console9",
  "type": "fileActivity",
  "uri": "file:///home/mihai/public_html/mozilla/test2.css"
}

The locationChange packet

I couldn't reuse the tabNavigated packet because it only fires at the end of the page load. For the Web Console we need to be notified about when loading starts, and when it ends - see bug 792062.

The locationChange packets:

{
  "from": "conn0.console9",
  "type": "locationChange",
  "uri": "http://localhost/~mihai/mozilla/test.html",
  "title": "",
  "state": "start",
  "nativeConsoleAPI": true
}

{
  "from": "conn0.console9",
  "type": "locationChange",
  "uri": "http://localhost/~mihai/mozilla/test.html",
  "title": "foobar",
  "state": "stop",
  "nativeConsoleAPI": true
}

The nativeConsoleAPI API flag is included such that the user is correctly informed if the window.console API is overridden on the new page.

Other changes

  • WebConsoleUtils.jsm and NetworkHelper.jsm have moved to toolkit/devtools/webconsole from browser/devtools/webconsole.
  • HUDService-content.js has been removed, and now HUDService.jsm no longer has any code related to the message manager.

Follow-up bugs

Results and conclusions

Initially performance went down from ~14 seconds to ~3m40s, when I did 50000 calls to console.log("foo" + i). Panos quickly fixed this in bug 790202.

The use/don't use debugger API and the object actors "conundrum" are the biggest issues. I would like us to address as many issues as soon as possible, but let's decide on what's very important first.

I want your feedback on how to do things The Right Way and the pragmatic way (due to deadlines). Thank you!

@janodvarko
Copy link

setPreferences packet type; the chapter says that there is not reply from the server. But, the RDP doc says:
In this protocol description, a request is a packet sent from the client which always elicits a single packet from the recipient, the reply.

See Requests and Replies

I think there should always be a reply.

Honza

@mihaisucan
Copy link
Author

Jan: there is a reply, yes, but the reply is empty/undefined. What do you propose we should reply?

@janodvarko
Copy link

E.g. it could be just a confirmation response.
Honza

@mihaisucan
Copy link
Author

Jan: fixed that. thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment