Liveness Scopes
This guide discusses liveness scopes. It covers why queries can benefit from a liveness scope, how liveness scopes work, and how to create and use one.
Why use a liveness scope?
Deephaven's engine runs in Java, where the JVM handles garbage collection (GC). Users have little control over when GC runs, because the JVM decides when to collect based on memory demand.
A liveness scope lets your code choose when to release the query engine objects it no longer needs, instead of waiting for the JVM. Releasing a scope cleans up:
- Tables and plots.
- Other query engine objects, such as the listeners that connect tables in the update graph.
Without a scope of their own, queries rely on JVM garbage collection, which is fine in most cases. A scope helps when a query repeatedly creates ticking tables that it needs only for a while, such as a script that rebuilds its tables each time its parameters change. Releasing the scope cleans up the old tables right away. Otherwise they keep processing updates until garbage collection runs.
How liveness scopes work
Deephaven uses reference counting to decide when to clean up tables, plots, and many other query engine objects. Each object counts the references to it, and Deephaven cleans the object up as soon as the count drops to zero. An object tracked this way is a liveness referent. A referent is live while its count is above zero. A liveness scope is an object that holds references to referents. Deephaven tracks references as follows:
- Open liveness scopes form a stack on each thread. When a query creates a referent, the scope at the top of the stack manages it, which adds a reference. While the Groovy console runs your code, the console's own liveness scope is open, so by default it manages the tables and other referents your script creates.
- A refreshing (ticking) table holds a reference to each refreshing parent table it depends on, so a parent stays live as long as one of its children does.
- When a referent's reference count drops to zero, Deephaven releases it immediately. A released refreshing table stops processing updates and drops its references to its parents, which can in turn be released.
- Releasing a scope drops its references to everything it manages. Referents that nothing else references are released. The rest, such as a result you manage in an outer scope (one lower on the stack), stay live.
Reference counting does not replace GC. A variable that refers to a table is not one of these counted references. Suppose no variable refers to a refreshing table, no live table depends on it, and no scope that manages it has been released. The table keeps processing updates until the JVM garbage collector reclaims it.
How to use a LivenessScope
In Groovy, you work with two classes. LivenessScope is the scope itself. LivenessScopeStack holds each thread's stack of open scopes.
Create a LivenessScope, then enclose your query in a try-with-resources block that calls LivenessScopeStack.open(scope, releaseOnClose). While it is the innermost open scope, the scope manages the objects your query creates inside the block. When the block exits:
- If
releaseOnCloseistrue, closing the block pops the scope from the stack and releases it. - If
releaseOnCloseisfalse, closing the block only pops the scope from the stack. Callscope.releaseyourself when you no longer need its objects.
In the following example, each call to makeTableAndScope creates a filtered ticking table inside a new scope and returns both. Because releaseOnClose is false, the scope stays unreleased after the block, so the table keeps ticking. Calling release stops the table, even though t1 still refers to it. Two things keep it updating after the release:
- Another scope, or a refreshing child table, that holds a reference to it.
- The UI, while the table is open there.
Call LivenessScopeStack.open with no arguments to create an anonymous scope, one that open creates for you and that you hold no variable for. It is released when the block exits.
Push and pop a scope manually
The following example calls LivenessScopeStack.push(scope) and LivenessScopeStack.pop(scope) directly to manage a LivenessScope. This does by hand what LivenessScopeStack.open(scope, true) does in a try-with-resources block:
Note
This example shows how the LivenessScopeStack manages LivenessScope objects. In practice, use try-with-resources blocks, which pop the scope even if the query throws an exception.
Multiple liveness scopes
Liveness scopes can be nested. Because open scopes form a stack, the scope at the top of the LivenessScopeStack manages new objects. When that scope is popped, the scope below it is at the top again. Nested try-with-resources blocks push and pop scopes in the right order.
In the following example, the inner anonymous scope manages ticks and evens. Before each block opens, LivenessScopeStack.peek gets the scope that encloses it: consoleScope for the outer block and outerScope for the inner block. Inside the inner block, outerScope.manage keeps ticks live in the outer scope. Inside the outer block, consoleScope.manage keeps it live in the console's scope.
evens is released when the inner block exits, so it stops updating. ticks keeps ticking, because outerScope manages it before the inner scope is released, and the console's scope manages it before outerScope is released.
Scope and stack methods
LivenessScopeStack methods control which scope manages new objects on the current thread:
LivenessScopeStack.push(scope)pushes a scope onto the current thread's scope stack, which makes it the scope that manages new objects.LivenessScopeStack.pop(scope)pops the scope from the top of the current thread's scope stack. The scope must be at the top of the stack. Popping a scope does not release it.LivenessScopeStack.open(scope, releaseOnClose)pushes a scope onto the scope stack and returns aSafeCloseablethat pops it. IfreleaseOnCloseistrue, closing theSafeCloseablealso releases the scope.LivenessScopeStack.openwith no arguments pushes an anonymous scope onto the scope stack and returns aSafeCloseablethat pops and releases it.LivenessScopeStack.peekgets the scope that manages new objects on the current thread, which is the scope at the top of the stack. If the thread has no open scope, it returns the thread's base manager, which never releases the objects it manages.
Scope methods control which objects a scope keeps live:
scope.manage(referent)adds a reference fromscopeto the referent. To keep a result live after an inner scope is released, call it on an outer scope, such as the oneLivenessScopeStack.peekreturns before the inner scope opens.scope.unmanage(referent)drops one reference fromscopeto the referent.scope.releasereleases the scope, dropping its references to everything it manages.
Example: release a query's intermediate tables
Queries often create intermediate tables that you don't need to refer to once the query has run. Without a liveness scope of their own, those tables stay managed by the console's scope. Let's look at the problem first, and then solve it with a scope.
This query uses readCsv, merge, and tree to create a simple tree table grouped by Instrument. Two tables remain in the console: crypto and comboTree.
The above example works, but the query creates many objects that the console doesn't need to refer to by name once the query finishes: data, combo, and the unnamed table that each chained operation produces. Setting data and combo to null removes their names from the console, but it doesn't release the tables.
The following example runs the same query inside a LivenessScope that is released when the block exits. Before the block, LivenessScopeStack.peek gets the console's scope. Inside the block, enclosing.manage keeps crypto and comboTree live in that scope. When the block exits, the scope is released, and so are the intermediate tables, because no other scope or table manages them. The last two lines remove the released tables' names from the console.
The release succeeds, but it frees no memory here. Every intermediate table is an ancestor of comboTree, which still reads their data. Because readCsv reads the file once, these tables are static and have no updates to stop. If they were refreshing, comboTree would keep them live and updating. A scope frees resources only for tables that nothing you keep depends on.
The next example shows that case with ticking data. latest is the result you keep, and it reads from trades. everyThird also reads from trades, but nothing you keep reads from everyThird.
After release, everyThird stops updating, because nothing you kept depends on it. latest and its parent trades keep ticking, because the console's scope manages latest, which depends on trades. As in the makeTableAndScope example, a table that is open in the UI keeps updating until you close it.