Author: Reuben Kan (reuben@google.com), David Hearnden (hearnden@google.com), David Wang (zdwang@google.com)Date: 19-Oct-2010ObjectiveThis design's goal is to create a wave panel that behaves like the one in Google Wave with the important improvement of lowering user-perceived latency when opening a wave, especially when opening a wave via a URL. This design aims to achieve this by performing incremental loading of a wave. The display of a wave must be fast even in the case when the bulk of the javascript is not yet available (either because it hasn't been downloaded yet, or not yet compiled/evaluated). This wave panel will also support IE 7+, Firefox 3+, Chrome, Safari 4+, and mobile browsers (Android, iPhone, and iPad) with varying degree of functionality.BackgroundAfter spending nearly twelve months improving the speed of the wave panel in the Google Wave client, the ratio of performance increase to engineering effort of profiling and refactoring was significantly diminishing for each new round of optimizations. This wave panel has a number of architectural constraints that were limiting the scope of needed optimizations. For example, like a typical GWT application, it constructs the rendering of a wave client-side. This requires downloading all the javascript code for rendering, along with the code for many rarely used features, before rendering even begins. Additionally, it uses a stateful presnetation model, where an in-memory representation of the wave's presentation is constructed before any of that state is pushed into the DOM. These architectural properties impose a significant lower bound on the latency for showing a wave.Undercurrent is a complete redesign of the wave panel's architecture, based on the lessons learned from the Google Wave client's original approach. Undercurrent uses a pipelined architecture that optimizes for the initial display speed of a wave, rather than the speed to load all the javascript and data necessary to have a fully functional wave panel. In particular, unlike a regular GWT application, this design explicitly supports (but does not require) server-side rendering of wave content, attaching client-side behaviour to server-supplied HTML. Server-side rendering is the primary tool for reducing the latency of displaying waves. OverviewThis design uses a pipelined process that will reduce perceived latency by showing the wave content to the user as soon as possible. The pipelined rendering process allows the client to scale gracefully across different browser platform by trading functionality against computation power. On low-end devices the panel will offer a cut-down experience by exiting the pipeline early. The pipeline consists of four stages.
Stage 1. Server-supplied renderingWith server-side rendering enabled, the server has already supplied a rendering of wave content, so it has already been displayed on screen. Note that this rendering does not have to be complete. It only needs to have rendered enough content (blips) to fill a browser's screen; other blips can be rendered blank. This rendering also includes all the user-specific information, such as unread status and diff highlighting. The HTML DOM holds attributes that allow dynamic behaviour to be added later. In addition, the HTML holds some JSON that describes the blip structure of the wave.Stage one is instantiated on this existing HTML content, and installs a minimal set of features, such as the reading frame that focuses the user's attention on a particular blip. Stage 2. Live model (read only)The defining component of the second stage is the wave model code, including the operation-based infrastructure. Any feature that requires interaction with the wave model must be delayed until at least this stage.Client-side rendering, which is based on the wave model, is also included in this stage. If server-side rendering is not enabled, then the client renders the wave from scratch. If server-side rendering is enabled, then the client can complete the rendering of any part of the wave not rendered by the server. This stage also includes liveness, meaning the rendering is updated as the model changes due to incoming operations. The client can now also render items in the wave that are not capable of being rendered on the server, such as gadgets (blip components defined by external javascript) and doodads (blip components defined by plug-in GWT code). For robustness, this is achieved by making the client re-render every blip in the wave. Although this is redundant work in the overwhelming majority of cases, it is a transparent process if the rendering server supplied a correct rendering. It should be possible in the future to optimize this process not to re-render every blip. Other features for interacting with a wave in a reading context are installed in this stage, such as read/unread highlighting and thread collapsing. Stage 3. WriteThe defining component of the third stage is the installation of writing features, such as replying, editing, and deleting wave content. Since the code for these features is quite large, environments that have already exited the pipeline (such as a read-only mobile client) will save a significant download.Stage 4. Feature completenessThe code for virtually all other features is included in this stage, and those features are installed on demand. This stage is not a strict part of Undercurrent's pipeline: it could be divided into more stages if necessary.Design principlesAdditional considerations for performance:Detailed designInitial HTML / bootstrap pageIt is likely that the render server as mentioned in this part of the design will not be implemented immediately in the Wave in a Box project. However, this "optional" server was one of the driving motivations of the overall design. It is described in detail amongst this section. The system functions even though this component is missing. In order to show the content of the wave as quickly as possible, the first screenful of wave content is rendered as HTML into the bootstrap page (the startup page for the client). The bootstrap page is also cacheable by wave-id so that the rendered HTML can be shown as quickly as possible for recently viewed waves. We are willing to accept some cache staleness (60 seconds right now, can be reduced to 0 if we want to be conservative) by setting the expiry time of the bootstrap page. After the page's expiry time passes, the browser's cache validation mechanism kicks in (HTTP 304, see http://en.wikipedia.org/wiki/HTTP_ETag for details on HTTP cache validation). Depending on which level of the cache the client hits, the client pays a different price The skeleton bootstrap page looks like the following: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html> <meta http-equiv="X-UA-Compatible" content="IE=8"> 1 <style type="text/css"> ... 2</style> <script type="text/javascript" defer="defer"> 3 function loadScript() { 4</script> <body> ... <div id="initialHtml"> </div>... 6 </body> </html> AlternativeAn alternative approach is to have the bootstrap page as non-cacheable, with an iframe pointing to a permanently cacheable page with unique url for a wavelet with version, user id and language information encoded. It has the advantage of allowing the content cache to be infinitely cacheable at the proxy level, but has the additional cost of an extra round trip time.HTML formatIn order to allow the Javascript to attach behavior to the server-supplied DOM efficiently, the HTML must be generated with annotations containing information needed to find appropriate elements quickly. HTML IDs are used for these annotations, illustrated by the following snippet.<div> The rendering server that produces this DOM HTML shares the same HTML producing Java code with the GWT client. This means that if the prerendered HTML is missing, the client is able to produce it itself. Also, this code is necessary for the client to update the DOM in a way that remains consistent with what the rendering server produces. Event handling system - O(1) upfront setupThe canonical approach for handling browser events in a GWT application is to construct the UI using GWT widget library. However, GWT widgets are relatively heavyweight compared to plain HTML (both in terms of memory usage and execution speed), and the widget library's architecture requires that every event-handling element be a widget. This requires an initial setup cost that is linear in the number of event-handling UI components.This design uses an alternative event architecture that does not require a GWT widget for every UI component, providing constant-time setup cost, as well as saving other costs inherent to widgets. In its most extreme form, this architecture has only a single widget for the entire application. This is achieved by taking advantage of the browser's underlying event bubbling mechanism. A single event listener on the top-level DOM element receives all events on the page. Since the rendering pattern is to annotate the HTML of UI components with their kind (HTML attribute), this top-level event handler can trace up the DOM from the target of a browser event, and dispatch the event to application-level event handlers registered against those kinds. This bottom-up dispatch mechanism is analogous to the browser's native event bubbling mechanism. The kind-based dispatch model means that all the contextual information for delivering an event to the appropriate handler is part of the HTML, which can be rendered on the server and delivered to the client. For example:
In comparison with the traditional GWT Widget approach, this dispatch mechanism trades upfront setup cost against runtime cost of event delivery: In code, this event system is implemented by EventDispatcherPanel. AlternativesThis algorithm requires upwards DOM traversal from an event's target element to find an appropriate handler, which is bounded only by the depth of the element (O(n)). An alternative is to dispatch the event directly from elements with kinds. For example:<div kind="thread" onclick="window.__handle('click', 'thread', this);"> This has the advantage that no explicit JS router is needed to bubble the javascript event. However, it has the disadvantages of making the HTML larger, and requires a named, global, dispatcher object. Model/view/presenter and flyweight patterns - O(1) start-upThis section assumes the reader understands the MVP pattern. In order to support server-side rendering, the view state needs to be represented entirely by the DOM, disallowing stateful view objects. This allows Undercurrent to use flyweight objects for views. These flyweights are associated dynamically on-demand with DOM elements of interest, in order to interpret the DOM fragments meaningfully, and may be pooled and re-used. Furthermore, rather than fine-grained presenters for each UI component on screen, there is a single presenter object per wave for each category of presentation (one presenter for conversation structure, one presenter for profile information, one presenter for read state, etc). This keeps the startup cost for attaching behaviour to an existing HTML rendering minimal, and does not grow with the size of the wave.The following snippet illustrates the essential structure of these view classes: public final class BlipMetaDomImpl implements ... {Note: The transfer of model state into view state is performed by 'live renderers', which play the role of presenters. The following fragment (simplified from the actual code) illustrates the control flow for updating a blip's timestamp. public final class LiveConversationViewRenderer implements ObservableConversation.Listener, ... { 1 Note: In particular, note that the presentation logic has no direct dependencies on GWT, DOM, or HTML: it simply defines a mapping from model state to view state, expressed using abstract view interfaces. This keeps the presentation logic portable and easily testable. Updated concurrency control (CC) stackThe spirit of this design is to stage the code downloaded to the client so that content is shown immediately, and functionality is loaded in the order it is likely to be used. With that spirit, it's desirable to be able to show live streaming updates to the wave without having to load the entire operation transport stack, which includes much code that is not needed for unless writing (e.g., the operational transform code). To do this, this design introduces an empty CC stack which simply passes through any operations from the server to the client. If the client generates any operations on a wavelet, before the proper CC stack is downloaded and installed, the empty CC stack is paused. All operations, both client operations going out to the server, and server operations coming in to the client, are buffered and not processed. Once the real CC stack is in place, and the client has the ability to do operational transformation (OT), those buffered streams are flushed through transformation, and the CC stack resumes.Code and Package StructureThe client package in the wave libraries is primarily structured around Undercurrent's UI architecture for events, rendering, and staged loading. It also includes a wave panel with a core set of features, built in a plug-in style. In org.waveprotocol.wave.client. Since the staged loading is a top-level concern (in order to use GWT's runAsync capabilities effectively), the stages and their loading sequence are part of the top-level client package. The stage implementations are designed to be configurable, so that in different client environments, arbitrary components within each stage can be substituted for alternative implementations. Using Undercurrent |
