Liveness scoping

Deephaven's Liveness Scoping allows the "liveness" (whether or not they are active) of nodes in a refreshing query's update propagation graph to be updated proactively, rather than only via actions of the Java garbage collector. This does not replace garbage collection (GC), but it does allow liveness-related cleanup to happen immediately when objects in the GUI are not needed. This is accomplished internally via reference counting, and works automatically for all users. For developers building new functionality using the Deephaven query engine, the LivenessScope and LivenessScopeStack classes allow a finer degree of control over the reference counts of various query engine artifacts. Constructing an external scope before running a refreshing query will hold together the related artifacts created in its query update propagation graph. Releasing the scope after the query runs will release any referents that are no longer "live".

The reference counting instrumentation includes the following components:

  • Liveness Referents are objects that are reference countable, and are included in Deephaven's process of tracking refreshing items.
  • Liveness Managers are objects that are able to keep track of their outstanding references to Liveness Referents.
  • A Liveness Scope is primarily a Liveness Manager, but it may be a referent in certain use cases. When the scope is released, any subsequently added Liveness Referents are cleaned up.
  • A Liveness Artifact is both a manager and a referent, and its implementation is used for most Internal Deephaven Objects (including Tables, Listeners, and so on) created as part of the query update propagation graph.

When a table is open in Deephaven, the update propagation graph accumulates parent nodes that produce data (data sources) at the top, and all the child nodes that stream down the graph. Deephaven's Liveness Scoping system keeps track of these referents: when child objects cease to be referenced and the parents' liveness count goes down, they will be cleaned up immediately without having to wait for GC.

These counts are logged in the LivenessCount table within the performanceOverview() method. When a query executes, the referent count for that worker increases as artifacts are created for operations such creating empty tables and timetables, performing selection methods, sorts, filters, etc. Each one of these processes generates listeners, sub-tables and related artifacts that are included in the referent count. A simple query on regular tables would not create new referents. However, a complex query may require many objects. A high referent count is not a concern in and of itself unless the count does not consistently go back down to a baseline number.

Using a LivenessScope

Deephaven's reference counting instrumentation will only clean up objects created purely for the GUI. This can be augmented by creating a LivenessScope for your query. When the scope is released, any objects that are no longer needed or are not refreshing are let go.

The following syntax creates a LivenessScope that can preface any Deephaven query:

import com.illumon.iris.db.util.liveness.*
scope = new LivenessScope()
LivenessScopeStack.push(scope)

[your query]

First, import the Liveness class, then create the new LivenessScope and push it onto the stack (the stack dictates which scope automatically manages new query artifacts). After you run your query in the console, release it from the stack.

On a separate line, run:

scope.release()

This clears the scope's references to the Liveness Referents it manages. When the query is run, the reference count will quickly increase from 0 as objects are created. The scope holds all of these related artifacts together, and when it is released, the query update propagation graph immediately drops any objects that are not refreshing.

The act of viewing a table in the console causes it to be exported. Within a Deephaven worker, the act of exporting a table to a client adds the table to a scope associated with that client connection. Liveness is transitive: when a Table (or other LivenessArtifact) depends on other tables, those tables are in turn retained by the Liveness reference counting system. When the client connection is terminated, all exported tables will have their Liveness count decreased.

Of special note, if you were to call the release() method before the tables are visible in your console (e.g., in the same execution as your query), then the liveness count of the tables would have been decreased to zero before the export operation had an opportunity to increase them. Once an artifact has a reference count of zero, it may no longer be used for further Deephaven operations - even if it is reachable in your binding. The export operation would thus fail, and you would not have access to the tables that were created within the query. Tables may be preserved indefinitely by managing them with a new scope that is retained instead of being released.

Let's compare two examples, with and without the LivenessScope. The performanceOverview(worker_number) method allows us to easily track the referent count for each query in its LivenessCount table.

Example 1

Open a new console in Deephaven and name it "Console 1". Then run the query below. The query creates a simple Tree Table grouped by Sym. Two tables will open in Console 1: syms and data.

syms = db.t("LearnDeephaven","StockTrades")
     .firstBy("Sym").update("ID=Sym","Parent=(String)null")
     .update("Date=(String) null",
        "Timestamp=(com.illumon.iris.db.tables.utils.DBDateTime) null",
        "SecurityType = (String) null",
        "Exchange=(String)null",
        "Last=(Double) null",
        "Size=(Integer)null",
        "Source=(String)null",
        "ExchangeId=(Long) null",
        "ExchangeTimestamp=(DBDateTime)null",
        "SaleCondition=(String)null")

data = db.t("LearnDeephaven","StockTrades")
        .update("ID=Long.toString(ii)","Parent=Sym")

combo = merge(syms,data)

comboTree = combo.treeTable("ID","Parent")

data=null

combo=null

Note

Liveness to the displayed tables is maintained by the console viewing them. The tables that are not needed in the console have their binding variables set to "null".

Open a new Deephaven console and name it "Console 2". (Don't close Console 1.) In Console 2, execute the performanceOverview() method using the worker_number of Console 1 as the argument - in this case, worker_1. For example:

performanceOverview(1)

Tip

The worker number of your console can be found underneath the Show Table button:

img

After executing the query above, select the tab in Console 2 for the LivenessCount table. Once the query initializes, the count shown rises to 58, as shown below:

img

If you return to Console 1 and sort the table, clear a filter, apply a new filter, etc., these activities will create new rows in the LivenessCount table (in Console 2), and the referent counts will increase and then decrease accordingly. See below:

img

The count keeps returning to 58. However, closing all the open tables brings the referent count to 55. This final count of 55 (in this example) results because the LivenessScope has not released the objects.

Example 2

Example 2 executes the same core query as Example 1. However, this example first implements the LivenessScope method, which enables unused objects/referents to be released, and thus improve performance.

Open a new console window and name it "Console 3". Then, run the query below. Two new tables will be generated: syms2 and data2.

import com.illumon.iris.db.util.liveness.*
scope = new LivenessScope()
LivenessScopeStack.push(scope)

syms2 = db.t("LearnDeephaven","StockTrades")
     .firstBy("Sym").update("ID=Sym","Parent=(String)null")
     .update("Date=(String) null",
        "Timestamp=(com.illumon.iris.db.tables.utils.DBDateTime) null",
        "SecurityType = (String) null",
        "Exchange=(String)null",
        "Last=(Double) null",
        "Size=(Integer)null",
        "Source=(String)null",
        "ExchangeId=(Long) null",
        "ExchangeTimestamp=(DBDateTime)null",
        "SaleCondition=(String)null")

data2 = db.t("LearnDeephaven","StockTrades")
        .update("ID=Long.toString(ii)","Parent=Sym")

combo2 = merge(syms2,data2)

comboTree2 = combo2.treeTable("ID","Parent")

LivenessScopeStack.pop(scope)

data2=null

combo2=null

Then, on a separate line, run the following:

scope.release()

Note

Liveness to the displayed tables is maintained by the console viewing them. The tables that are not needed in the console have their binding variables set to "null".

Open a new Deephaven console and name it "Console 4". Then execute the performanceOverview() method using the applicable worker number for Console 3 - in this case worker_1:

performanceOverview(1)

After executing the query, select the tab in Console 4 for the LivenessCount table. As shown below, the query referent count climbs to 27, then drops to 5. This is because the LivenessScope was released.

img

If we again perform operations on the table in Console 3 (such as filters, sorts, etc.), the count climbs, then consistently returns to 6, and finally drops to 2 when the tables are closed. Compared to the count of 55 in Example 1, the final referent count for Example 2 is very low.

img

Advanced Methods

There are additional methods available to control how the reference counts of various query engine artifacts are managed, such as the order of a scope on the stack.

  • peek() - Get the scope at the top of the current thread's scope stack, or the base manager if no scopes have been pushed but not popped on this thread. This determines which scope automatically manages new query artifacts.
  • pop(scope) - Pop the scope from the top of the current thread's scope stack.
  • open(scope, true) - Push a scope onto the scope stack, and get a SafeCloseable that pops it. The first parameter specifies the scope; the second boolean parameter determines whether the scope should release when the result is closed. This is useful for enclosing scope usage in a try-with-resources block.
  • open() - Push an anonymous scope onto the scope stack, and get a SafeCloseable that pops it and then uses LivenessScope.release(). This is useful enclosing a series of query engine actions whose results must be explicitly retained externally in order to preserve liveness.