andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 1 | # Using the Chrome Devtools JavaScript preprocessing feature |
| 2 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 3 | The Chrome Devtools JavaScript preprocessor intercepts JavaScript just before it |
| 4 | enters V8, the Chrome JS system, allowing the JS to be transcoded before |
| 5 | compilation. In combination with page injected JavaScript, the preprocessor |
| 6 | allows a complete synthetic runtime to be constructed in JavaScript. Combined |
| 7 | with other functions in the `chrome.devtools` extension API, the preprocessor |
| 8 | allows new more sophisticated JavaScript-related developer tools to be created. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 9 | |
| 10 | ## API |
| 11 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 12 | To use the script preprocessor, write a |
| 13 | [chrome devtools extension](http://developer.chrome.com/extensions/devtools.inspectedWindow.html#method-reload) |
| 14 | that reloads the Web page with the preprocessor installed: |
| 15 | |
| 16 | ```javascript |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 17 | chrome.devtools.inspectedWindow.reload({ |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 18 | ignoreCache: true, |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 19 | injectedScript: runThisFirst, |
| 20 | preprocessorScript: preprocessor |
| 21 | }); |
| 22 | ``` |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 23 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 24 | where `preprocessorScript` is source code (string) for a JavaScript function |
| 25 | taking three string arguments, the source to preprocess, the URL of the source, |
| 26 | and a function name if the source is an DOM event handler. The |
| 27 | `preprocessorerScript` function should return a string to be compiled by Chrome |
| 28 | in place of the input source. In the case that the source is a DOM event |
| 29 | handler, the returned source must compile to a single JS function. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 30 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 31 | The |
| 32 | [Chrome Preprocessor Example](http://developer.chrome.com/extensions/samples.html) |
| 33 | illustrates the API call in a simple chrome devtools extension. Download and |
| 34 | unpack the .zip file, use `chrome://extensions` in Developer Mode and load the |
| 35 | unpacked extension. Then open or reopen devtools. The Preprocessor panel has a |
| 36 | **reload** button that triggers a simple preprocessor. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 37 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 38 | The preprocessor runs in an isolated world similar to the environment of Chrome |
| 39 | content scripts. A `window` object is available but it shares no properties with |
| 40 | the Web page `window` object. DOM calls in the preprocessor environment will |
| 41 | operate on the Web page, but developers should be cautious about operating on |
| 42 | the DOM in the preprocessor. We do not test such operations though we expect the |
| 43 | result to resemble calls from the outer function of `<script>` tags. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 44 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 45 | In some applications the developer may coordinate runtime initialization using |
| 46 | the `injectedScript` property in the object passed to the `reload()` call. This |
| 47 | is also JavaScript source code; it is compiled into the page ahead of any Web |
| 48 | page scripts and thus before any JavaScript is preprocessed. |
| 49 | |
| 50 | The preprocessor is compiled once just before the first JavaScript appears. It |
| 51 | remains active until the page is reloaded or otherwise navigated. Navigating the |
| 52 | Web page back and then forward will result in no preprocessing. Closing devtools |
| 53 | will leave the preprocessor in place. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 54 | |
| 55 | ## Use Cases |
| 56 | |
| 57 | The script preprocessor supports transcoding input source to JavaScript. Use cases include: |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 58 | |
| 59 | * Adding write barriers for Querypoint debugging, |
| 60 | * Supporting feature-specific debugging of next generation EcmaScript using eg Traceur, |
| 61 | * Integration of development tools like coverage analysis. |
| 62 | * Analysis of call sequences for performance tuning. |
| 63 | |
| 64 | Several JavaScript compilers support transcoding, including |
| 65 | [Traceur](https://github.com/google/traceur-compiler#readme) and |
| 66 | [Esprima](http://esprima.org/). |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 67 | |
| 68 | ## Implementation |
| 69 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 70 | The implementation relies on the Devtools front-end hosting an extension |
| 71 | supplying the preprocessor script; the front end communicates with the browser |
| 72 | backend over eg web sockets. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 73 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 74 | The devtools extension function call issues a postMessage() event from the |
| 75 | devtools extension iframe to the devtools main frame. The event is handled in |
| 76 | `ExtensionServer.js` which forwards it over the |
| 77 | [devtools remote debug protocol](https://developers.google.com/chrome-developer-tools/docs/protocol/1.0/page#command-reload). |
| 78 | (See [Bug 229971](https://crbug.com/229971) for this part of the implementation |
| 79 | and its status). |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 80 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 81 | When the preprocessor script arrives in the back end, |
| 82 | `InspectorPageAgent::reload` stores the preprocessor script in |
| 83 | `m_pendingScriptPreprocessor`. After the browser begins the reload operation, it |
| 84 | calls `PageDebuggerAgent::didClearWindowObjectInWorld` which moves the processor |
| 85 | source into the `scriptDebugServer()`. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 86 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 87 | Next the browser prepares the page environment and calls |
| 88 | `PageDebuggerAgent::didClearWindowObjectInWorld`. This function clears the |
| 89 | preprocessor object pointer and if it is not recreated during the page load, no |
| 90 | scripts will be preprocessed. At this point we only store the preprocessor |
| 91 | source, delaying the compilation of the preprocessor until just before its first |
| 92 | use. This helps ensure that the JS environment we use is fully initialized. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 93 | |
| 94 | Source to be preprocessed comes from three different places: |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 95 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 96 | 1. Web page `<script>` tags, |
| 97 | 1. DOM event-listener attributes, eg `onload`, |
| 98 | 1. JS `eval()` or `new Function()` calls. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 99 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 100 | When the browser encounters either a `<script>` tag |
| 101 | (`ScriptController::executeScriptInMainWorld`) or an element attribute script |
| 102 | (`V8LazyEventListener::prepareListenerObject`) we call a corresponding function |
| 103 | in InspectorInstrumentation. This function has a fast inlined return path in the |
| 104 | case that the debugger is not attached. |
| 105 | |
| 106 | If the debugger is attached, InspectorInstrumentation will call the matching |
| 107 | function in PageDebuggerAgent (see core/inspector/InspectorInstrumentation.idl). |
| 108 | It checks to see if the preprocessor is installed. If not, it returns. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 109 | |
| 110 | The preprocessor source is stored in PageScriptDebugServer. |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 111 | If the preprocessor is installed, we check to see if it is compiled. If not, we |
| 112 | create a new `ScriptPreprocessor` object. The constructor uses |
| 113 | `ScriptController::executeScriptInIsolatedWorld` to compile the preprocessor in |
| 114 | a new isolated world associated with the Web page's main world. If the |
| 115 | compilation and outer script execution succeed and if the result is a JavaScript |
| 116 | function, we store the resulting function as a `ScopedPersistent<v8::Function>` |
| 117 | member of the preprocessor. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 118 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 119 | If the `PageScriptDebugServer::preprocess()` has a value for the preprocessor |
| 120 | function, it applies the function to the web page source using |
| 121 | `V8ScriptRunner::callAsFunction()`. This calls the compiled JS function in the |
| 122 | ScriptPreprocessor's isolated world and retrieves the resulting string. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 123 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 124 | When the preprocessed JavaScript source runs it may call `eval()` or |
| 125 | `new Function()`. These calls cause the V8 runtime to compile source. |
| 126 | Immediately before compiling, V8 issues a beforeCompile event which triggers |
| 127 | `ScriptDebugServer::handleV8DebugEvent()`. This code is only called if the |
| 128 | debugger is active. In the handler we call `ScriptDebugServer::preprocessEval()` |
| 129 | to examine the ScriptCompilationTypeInfo, a marker set by V8, to see if we are |
| 130 | compiling dynamic code. Only dynamic code is preprocessed in this function and |
| 131 | only if we are not executing the preprocessor itself. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 132 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 133 | During the browser operation, API generation code, debugger console |
| 134 | initialization code, injected page script code, debugger information extraction |
| 135 | code, and regular web page code enter this function. There is currently no way |
| 136 | to distinguish internal or system code from the web page code. However the |
| 137 | internal code is all static. By limiting our preprocessing to dynamic code in |
| 138 | the beforeCompile handler, we know we are only operating on Web page code. The |
| 139 | static Web page code is preprocessed as described above. |
andybons | 3322f76 | 2015-08-24 21:37:09 | [diff] [blame] | 140 | |
| 141 | ## Limitations |
| 142 | |
andybons | ad92aa3 | 2015-08-31 02:27:44 | [diff] [blame^] | 143 | We currently do not support preprocessing of WebWorker source code. |