XML + Code + Good times = RSG Application

Plex Engineering
Plex Labs
Published in
6 min readApr 6, 2018

Written by John Zolezzi — April 6th 2018

Roku SceneGraph (RSG) brings modern user interface (UI) capabilities to Roku. It allows an extensible XML based UI layout, bitmap masks (circular user avatars!), and animation storyboard support.

Previously, Plex has used Roku’s 2d API to render the UI, which involves a lot of boilerplate code to handle layout, focus, animation, and resource cleanup. RSG now handles most of this code for us.

RSG is node and XML based and has the following node or component types:

  • Abstract node: Node. The base node for all of the following types.
  • Data model node: ContentNode. These nodes build the data used by Roku controls for displaying lists and grids.
  • Task worker nodes (for background thread work): The only way to make HTTP requests. Task run functions allow you to call any Roku API. Tasks are used to convert HTTP responses to ContentNodes, long running work, or to call additional Roku APIs.
  • Renderable + layout nodes: Define the layout, controls, and renderable items like images and text.
  • Video and Audio nodes: Only one can be actively playing at a time. Used to play video and audio.

RSG allows you to customize any node by extending it; however, nodes have certain characteristics:

  • They are initialized by a separate thread. Roku’s CPU profiler shows separate threads calling the node’s init function.
  • They cannot access code or data directly in other custom nodes.
  • Every custom node must define all script files that it uses. This is burdensome for large applications. Imagine having to remember all the dependencies for every script you include. If any one script is missing, the application can crash during runtime when it runs into an undefined function.
  • They can only access another node via its interface, which includes fields and functions.
  • Most fields are deep copied when assigned or read, except for fields with a node data type. You can set multiple fields at once for performance reasons.
  • When calling interface functions, all data passed in or returned is deep copied, even nodes!

Here is a basic RSG custom node: a clock!

Use the clock in another node, like a screen, for displaying the current time.

Threading context matters a lot

Before RSG, Roku application code always ran under a single thread context: the main application thread. RSG introduces (and forces you to have) multiple thread contexts. With different thread contexts, care must be taken to ensure a high-performance application.

But first, here are the three different thread contexts:

  • Main application thread: “Legacy” Roku application thread used to launch RSG scene.
  • RSG render owned thread context: Typically RSG nodes run under this context. You can only call a very limited set of Roku APIs, you can’t call HTTP or file system APIs for example. If you spend too long processing a field change you’ll run into a execution timeout and your application will crash. If this timeout occurs, consider optimizing or moving work into a task.
  • Task background thread context: Allows you to call any Roku API in the task run function.

Crossing these thread boundaries is slow, since it involves a rendezvous for data fields set or read or a node function call. Rendezvous involves thread synchronization and data marshaling. Data is deep copied to ensure thread safety. This has performance consequences, so we try to limit rendezvous as much as possible. However some rendezvous operations are expected as you need to pass data from a Task to RSG render owned nodes. See the examples below.

Task nodes have both a background thread context and a render thread context. Both contexts have a copy of any data defined in the Task node’s init function:

  • Render owned context: Task interface functions or functions used with ObserveField or ObserveFieldScoped are under the render owned context. Basically any function not directly called by the Task’s run function is also under render owned context.
  • Task background thread context: All code that runs inside the thread’s run function is under the background thread context. This typically also includes message processing with message ports. Here you can call any Roku API, including HTTP requests. Any Nodes or ContentNodes created inside this context are owned by the background thread until transferred to render context via setting a field in the Task’s interface. m.top.someResult = myData.

How we made Roku development even more awesome!

A lot of lessons were learned while building Plex News, Live TV, and the new RSG video player. We developed a number of good practices and enhancements to help us out with building the next Plex application with RSG.

Create larger custom nodes instead of many smaller ones

Instead of creating multiple smaller nodes, we create custom nodes that contain more internal code. Each custom node has its own script engine instance and passing data between nodes is often deep copied. We create multiple internal scripts that work together within the same node and easily share data by reference. Our large custom nodes are logically grouped and expose an interface to handle major groups of application functionality. Examples include Playback, ProviderMgr, and MyPlexMgr. Larger nodes improve application performance by reducing the overall number of custom nodes and amount of data sent between these nodes.

Listen to application state changes

We also listen to application state changes and then cache within the custom node. Services and any helper scripts don’t call out, but rather get notified of changes. In other words, “Don’t call us, we call you.” This cache avoids extra trips (and deep copies) to fetch application state outside of the current custom node.

Improved performance of background tasks and HTTP requests

  • We’ve added an extensible pattern to efficiently transform data into UI-consumable ContentNodes within the Task thread. Any number of data transforms functions can be registered. These functions are given helpers to parse XML or JSON data and process our Plex data model. When making an HTTP request, you can then give the name of the transform function you wish to use for that request.
  • We transform HTTP response data into UI consumable data as single task operations as much as possible to avoid multiple trips between render thread and task thread.
  • We limit calling to render thread context via m.top and m.global. A task listens for changes via its message port instead of calling out for this information. Again, don’t call us, we’ll call you.
  • We’ve created an extensible GeneralTask for performing registered work inside a Task thread. This always runs and listens for work to do. GeneralTask also reduces the number of always running background tasks.

Created build preprocessor

We also created a build preprocessor. Having to include and know script dependencies for all scripts used by the custom node is just too cumbersome for a large Roku application, so we created this preprocessor to improve Roku development. This preprocessor generates code during build time to support easier script referencing and data binding described below.

Easier script referencing via @require

We developed @require to make referencing scripts within custom node easier. When our preprocessor sees @require in the source file, it adds that script to the current custom node along with walking dependencies of the referenced scripts. Example below.

The preprocessor sees the @require statements and updates the AppScene.xml in a build directory to add the referenced scripts above along with their dependent scripts. Here you can see it finds quite a bit of required scripts. The scripts can also @require scripts of their own. It would be painful to have to add these manually and remember their dependencies.

Support data binding and observing with UI node fields

We adopted a view + view model pattern with our Roku application. This helps us separate view logic with application logic. To keep us from having to manually hook up view model changes and assign UI fields, we updated our preprocessor again to support binding and observer functionality.

We developed non-node view model BrightScript “classes” that can have their data bound to UI node fields. Inside the node’s xml, you can bind UI node fields from a model via {Binding} and also invoke functions when a UI node field changes via {Observe}. The preprocessor generates a lot of boilerplate code for us. Our view models classes can also be used by other script files within the same custom node. It doesn’t have to be consumed by UI nodes.

When the processor sees {Binding} or {Observe} in the node’s xml, it generates a script file [NodeFileName].g.brs. The generated file provides two functions to setup or clear bindings: _ApplyBindings or _ClearBindings. The node’s script calls _ApplyBindings() to setup the generated bindings and observers referencing the given view model.

Below is an snippet showing sample Binding and Observe use inside our home screen xml. It shows handling a button invoke and updating a list with data.

Snippet showing _ApplyBindings() call to setup our data bindings within our home screen’s BrightScript file.

Finally a snippet of HomeScreenViewModel.brs used by the home screen xml above. You can see it handling the search button and also updating UI within the view model.

Despite some performance gotchas and scripting limitations, we were able to develop best practices and leverage the power of RSG to produce a modern and rich application for Roku.

Thanks to my fellow teammates Rob Reed and Rick Phillips!

--

--