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!
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 theTabActor
.
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 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.
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.
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.
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.
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?
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 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 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 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?
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 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 theerror.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?
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.
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 new network event actor stores further request and response information.
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.
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.
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"
}
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.
WebConsoleUtils.jsm
andNetworkHelper.jsm
have moved totoolkit/devtools/webconsole
frombrowser/devtools/webconsole
.HUDService-content.js
has been removed, and nowHUDService.jsm
no longer has any code related to the message manager.
- Bug 787361 - Improve sorting for the object inspector
- Bug 787975 - Do not cross the client/server boundaries in the JSTerm $0 helper
- Bug 787981 - Use LongStringActor in the Web Console actors
- Bug 787985 - Console API messages are not consistent
- Bug 787986 - Throttle/disable autocomplete in the Web Console when connected to a remote server
- Bug 790016 - Web console remote protocol sends more data than needed for completion
- Bug 790202 - Protocol layer performance overhead is too much in some cases
- Bug 792043 - Network panel response image is loaded locally, not from the server
- Bug 792049 - Network panel should only display/fetch the request/response bodies on demand
- Bug 792062 - Make the tabNavigated notification reusable by the Web Console
- Bug 799198 - Provide a way to retrieve file:// and http:// resources from the server
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!
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