Skip to content

Commit

Permalink
Revised options based on changes on Colab
Browse files Browse the repository at this point in the history
  • Loading branch information
stephanwlee committed May 17, 2019
1 parent 8420e54 commit 6fd3bdb
Showing 1 changed file with 32 additions and 53 deletions.
85 changes: 32 additions & 53 deletions rfcs/20190411-tensorboard-improved-plugin-ext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
:-------------- |:---------------------------------------------------- |
| **Author(s)** | Stephan Lee (Google), William Chargin (Google) |
| **Sponsor** | Mani Varadarajan (Google) |
| **Updated** | 2019-04-11 |
| **Updated** | 2019-05-16 |

## Overview

Expand Down Expand Up @@ -72,59 +72,16 @@ One of our soft goals is to enable existing visualization suite to integrate wit
#### Requirement: Notebook Integration
TensorBoard recently added support for Jupyter and Colab and the feature is well received by the community. The new plugin frontend design should be compatible with the notebook integration.

Of two prominent notebooks, Colab imposes greater limitations to our designs due to its security model: running in google.com domain, Colab sandbox and isolate an output cell from the main process using various techniques. Colab, specifically, imposes restriction on usage of iframe in an output cell and it is related to how Colab routes network requests. To make it transparent to TensorBoard, Colab uses a ServiceWorker to intercept and reroute requests to localhost via its internal API. However, due to the implementation of Colab's ServiceWorker that is designed to intercept only requests to localhost (cannot use a relative path), one cannot render an iframe in an output cell.
Of two prominent notebooks, Colab imposes greater limitations to our designs due to its security model: running in google.com domain, Colab sandbox and isolate an output cell from the main process using various techniques. Colab, specifically, employs ServiceWorker to proxy network request originating from output cell to a server running on the kernel but this imposes restriction the iframe usage; network request to fetch the document of the iframe is not intercepted by the ServiceWorker.

This restriction is relevant for *all solutions* below since a plugin can render an iframe within a plugin frame.
#### Iframe based binary loading
When a plugin is activated by a user, TensorBoard will create an iframe and point the iframe to the endpoint that plugin defined as an entry point.

#### Non-iframe
Generally speaking, there are largely two types of solutions: one using iframe and one without using an iframe. Within the non-iframe based solution, there are several ways to achieve the goal, and they are depicted below.
Iframe sandboxes JavaScript context and it gives plugin developers a lot of freedom in terms of frameworks -- with context disjoint from others, each plugin binary can incorporate as many dependencies as it would like to use without worrying about the correctness. This property is especially useful for Polymer-based applications because Polymer uses a global state, `CustomElementRegistry` (in v1, `document.registerElement`), and is not possible to load components with the same component name more than once. Loading plugins in its own separate frame will let plugin authors develop with ease and potentially have a standalone widgets for use outside of TensorBoard.

One critical drawback of all non-iframe approaches is that TensorBoard cannot enforce plugin authors from inadvertently mutating the global object. For instance, if a library that a plugin transitively depends polyfills and monkey-patches a global object, it can influence how other plugins behave.

Within the non-iframe based solution, there are about two options to achieve load plugins. Two solutions share strengths and weaknesses and they are:

**Pro**
- easy to support
- supports Colab as long as plugins do not use iframes

**Con**
- global mutating libraries like Polymer app will not work
- can lead to an awkward UX: e.g., different version of a visualization library have different UI/UX

##### Option 1: Require plugins to bundle all dependencies into one binary
Modern web binaries/bundles are often created with Webpack, Browserify, or Rollup and they often leverage techniques like vendoring or code-splitting to optimize above-fold-time. Though details vary by bundlers, it is hard to guarantee correctness depending on a shim from CommonJS: some shims use a global name as identifier for finding a module across bundles (e.g., 'react') instead of hashed unique name. This creates non-zero chance of collision and causes a plugin to load a different module than expected. As such, TensorBoard mandate plugins to bundle all transitive dependencies binary.

##### Option 2: Default libraries
TensorBoard can add React to its front-end dependencies and attach the symbol to the global object, i.e., `window.React`, akin to d3, Plottable, and dagre in TensorBoard today. Plugin authors will assume presence when developing for TensorBoard. Otherwise simple, this solution comes with several drawbacks.

If React (or any library) makes a backwards incompatible change, TensorBoard upgrading it will break plugins. This puts maintenance burden on plugin authors and, when not properly maintained, broken plugins will provide janky experience for users.

Another disadvantage of this solution is that TensorBoard will become a gatekeeper of dependencies. In a fast changing frontend ecosystem, a library can easily replace another. If, for example, Redux becomes obsolete in favor a new framework, a developer will have to raise an issue to TensorBoard and go through the process to get it added to the global.

#### Option 3: iframe
**Pro**
- ease of development. Can bring your own version of Polymer, React, and etc...
- encapsulation and containment of plugins accidentally doing wrong things

**Con**
- does not work in Colab
- generally larger binaries
- Cannot reuse components across plugins
- can lead to an awkward UX: e.g., different version of a visualization library have different UI/UX

When a plugin is activated by a user, TensorBoard will dynamically create an iframe and point the iframe to the endpoint that plugin defined as an entry point.

The solution is the most favorable because it gives plugin developers a lot of freedom in terms of frameworks. With global state disjoint from others, each plugin binary can incorporate as many dependencies as it would like to use without worrying about the correctness. This is quite appealing especially for Polymer-based applications because Polymer uses a global state, `CustomElementRegistry` (in v1, `document.registerElement`), and is not possible to load components with the same component name more than once. Loading plugins in its own separate frame will let plugin authors develop with ease and potentially have a standalone widgets for use outside of TensorBoard.
It must be clearly stated that the use of global state is not unique to Polymer. Although JavaScript bundles use Immediately Invoked Function Expression (IIFE) and use closure to encapsulate dependencies, a bundler like Browserify or Webpack _can_ uses global states when shimming CommonJS `window.require` and use non-unique identifier for a global module.

It must be clearly stated that the use of global state is not unique to Polymer. Although JavaScript bundles use Immediately Invoked Function Expression (IIFE) and use closure to encapsulate dependencies, a bundler like Browserify or Webpack can uses global states when shimming CommonJS `window.require` and use non-unique identifier for a global module.

Despite the benefits, iframe can be difficult to work with under Colab. Colab, running a VM under google.com domain, makes sure ports opened by user instantiated processes are not accessible publicly. Under this limitation, TensorBoard operates using a ServiceWorker based network proxy installed by Colab that routes a request to _localhost_ to an endpoint running in VM. This proxy is mostly transparent from TensorBoard as long as we make requests to the localhost.

A difficulty arises with how ServiceWorker behave with respect to an iframe. A ServiceWorker can only intercept requests that originates from a document within its scope. A ServiceWorker, derived from its module source, is associated to a scope (for instance, if the ServiceWorker module originates from tensorflow.org/foo/bar/sw.js, it will have a scope of "tensorflow.org/foo/bar" and it can only intercept request from a document whose host starts with "tensorflow.org/foo/bar/"). Now, to render an iframe whose content source is backed by an endpoint running in a VM, we need to go through the Colab proxy by setting the source of the iframe as `https://localhost/*` (untrue; will be discussed below). This, though, is a problem to a request that originates within the iframe -- since iframe's origin is localhost, only ServiceWorkers with scope of "localhost" will be active and Colab's proxy will not be effective.

In addition to the origin problem, defining a source to an iframe creates a new document whose request will have scope defined by the source of the iframe. In other words, if TensorBoard renders an iframe "https://localhost:6006/foo.html", a request to fetch the content of the foo.html can be intercepted by a ServiceWorker registered with a scope of "localhost:6006" but not by the one created by Colab. As a result, in Colab, requests to fetch content of iframes will be made to users' local machine.

TensorBoard team is engaging with the Colab team to resolve the issue as this is the most favorable one of three options. However, because it is unclear when and how it will be resolved, there is a risk associated with this solution.
Despite the benefits and its simplicity, Colab does make this solution a bit trickier. Because frames with different origin cannot be intercepted by Colab's ServiceWorker, it would require some mechanism to route request to the non-publicly exposed server running the kernel. The TensorBoard team is working with Colab to introduce new API that would allows a port to be "exposed" to a stable URL (actual behavior is a lot more complex and is subject to changes). The team is confident thatTensorBoard can work around the iframe issue using this URL.

### Frontend binary
Vulcanization today does not support CommonJS or packages from NPM. The team recognizes it to be an impediment for a plugin authorship and is unnatural for developing a frontend. TensorBoard team will provide a canonical build configuration that uses bazelbuild/rules_nodejs and Rollup but will not inhibit one from using Webpack and other bundlers.
Expand All @@ -145,9 +102,31 @@ A plugin may decide to put instrumentations and there is no way for TensorBoard
How can a plugin show a helpful manual without asking user to visit the repository to review the README? Can TensorBoard assist by showing HTMLfied README in the browser?

## Alternatives considered
### Iframe and Inter-frame communication
Using XMLHttpRequest, TensorBoard can fetch the content of an iframe as text and use `iframe.srcdoc` to render the content. This iframe, however, will have origin of `null` and its requests will not be intercepted by any ServiceWorker.

Instead, TensorBoard can provide a library one of which will be a `request`. The method will use `window.postMessage` to route the network request to the main frame and hide away the fact that it is using inter-frame communication at all. This solution, though easy for XMLHttpRequest based request, is difficult to apply for, for example, image, audio, video, CSS, web-fonts, EsModule, and WebSocket requests. To illustrate the problem more concretely, when `<img src="foo.png">` is created, a browser initiates the requests and it is not possible to route that request via the library. This will make a plugin like Images plugin difficult to write.
#### Non-iframe based plugin loading
Generally speaking, there are largely two types of solutions for loading plugin frontend: one using iframe and one without using an iframe. Within the non-iframe based solution, there are several ways to achieve the goal, and they are depicted below.

One critical drawback of all non-iframe approaches is that TensorBoard cannot enforce plugin authors from inadvertently mutating the global object. For instance, if a library that a plugin transitively depends polyfills and monkey-patches a global object, it can influence how other plugins behave.

Within the non-iframe based solution, there are about two options to achieve load plugins. Two solutions share strengths and weaknesses and they are:

**Pro**
- easy to support
- supports Colab as long as plugins do not use iframes

**Con**
- global mutating libraries like Polymer app will not work
- can lead to an awkward UX: e.g., different version of a visualization library have different UI/UX

##### Option A: Require plugins to bundle all dependencies into one binary
Modern web binaries/bundles are often created with Webpack, Browserify, or Rollup and they often leverage techniques like vendoring or code-splitting to optimize above-fold-time. Though details vary by bundlers, it is hard to guarantee correctness depending on a shim from CommonJS: some shims use a global name as identifier for finding a module across bundles (e.g., 'react') instead of hashed unique name. This creates non-zero chance of collision and causes a plugin to load a different module than expected. As such, TensorBoard mandate plugins to bundle all transitive dependencies binary.

##### Option B: Default libraries
TensorBoard can add React to its front-end dependencies and attach the symbol to the global object, i.e., `window.React`, akin to d3, Plottable, and dagre in TensorBoard today. Plugin authors will assume presence when developing for TensorBoard. Otherwise simple, this solution comes with several drawbacks.

If React (or any library) makes a backwards incompatible change, TensorBoard upgrading it will break plugins. This puts maintenance burden on plugin authors and, when not properly maintained, broken plugins will provide janky experience for users.

Another disadvantage of this solution is that TensorBoard will become a gatekeeper of dependencies. In a fast changing frontend ecosystem, a library can easily replace another. If, for example, Redux becomes obsolete in favor a new framework, a developer will have to raise an issue to TensorBoard and go through the process to get it added to the global.


## Questions and Discussion Topics

0 comments on commit 6fd3bdb

Please sign in to comment.