R Object Memory Management
References to R objects
When webR provides a reference to an R object on the main thread, such as the result of R code evaluation with WebR.evalR()
, it is provided in the form of an RObject
proxy.
Technically, an RObject
is a JavaScript object that holds a reference to an R object in WebAssembly memory and forwards requests relating to that object to the webR worker thread. The forwarding between the main and worker threads is implemented with the JavaScript Proxy
mechanism, which is why RObject
instances print as objects of type Proxy
in the console:
Proxy {obj: { type: 'double', ptr: 2184504, ... }, payloadType: 'ptr'}
RObject
proxys can be passed to webR API calls that manipulate or access R objects. For instance, an RObject
representing an environment can be passed to WebR.evalR()
to evaluate code in a particular environment.
A proxy is parameterised by an R type, e.g. a proxy that wraps a character vector is implemented as RProxy<RWorker.RCharacter>
. The RMain
module provides convenient type aliases for each R type. For instance, RCharacter
stands for RProxy<RWorker.RCharacter>
. Each of these types supports specific methods related to the kind of data they represent. For instance RCharacter
supports conversion to a JS string with toString()
whereas RDouble
supports toNumber()
.
Under the hood
Each type of RObject
proxy is associated with a corresponding subclass of RWorker.RObject
with the same name, e.g. RMain.RCharacter
has a corresponding RWorker.RCharacter
type that is it proxied to. The RWorker
module implements the methods for interacting with R objects on the webR worker thread, and the methods are made available to the main thread through the RObject
proxies.
Invoking a method on an RObject
proxy is automatically handled by webR by issuing a request to the worker thread over the established communication channel. RProxy
method invocation returns a JavaScript Promise that resolves to the result of invoking the corresponding RWorker.RObject
method for the R object associated with the proxy.
Memory management
Both the R interpreter and the JS environment include a built-in garbage collector (GC) that deletes objects from memory when they are no longer required. Unfortunately, there is no way to integrate these garbage collectors in an automated way 1. This is why a Javascript wrapper can’t keep an R object alive on its own and it needs some manual work from the developer who must ensure that any R objects targeted by an RObject
reference are not deleted by the R GC while they are still in use.
Sheltering R object references
WebR solves the problem of keeping R objects alive through a sheltering mechanism. When an RObject
reference is created on the main thread, the object referenced is automatically sheltered from R’s GC. The object will not be deleted by R until it has been removed from the shelter.
Once an R object is no longer protected by a shelter, there are no longer any guarantees about the state of the referenced object – it may have already been deleted by the GC. In effect the RObject
reference is no longer valid, and so the process of removing an object from the shelter is referred to as destroying the reference.
The global shelter
As webR initialises it creates a default shelter for R objects. When R object references are created without specifying a particular shelter, when running WebR.evalR()
for example, they are sheltered by the global shelter.
Unlike normal shelters, the global shelter can’t be purged all at once. A purge would not generally be safe because this shelter is likely shared among different concurrent routines. Instead, objects sheltered globally must be individually destroyed by passing them to the WebR.destroy()
method.
Using try
/finally
with the global shelter
To avoid memory leaks, all RObject
instances should be destroyed once they are no longer in use. This requires precautions to ensure destruction when a JavaScript error is thrown and interrupts execution. A useful pattern is to wrap code manipulating an RObject
within a JavaScript try
/finally
block to ensure that the R object reference is always destroyed once the work is complete.
const obj = await webR.evalR('42 + 123');
try {
const result = await obj.toNumber();
console.log(result);
finally {
} .destroy(obj);
webR }
However this pattern quickly shows its limits when multiple objects are transferred from R because each object requires its own try
/ finally
blocks.
const obj1 = await webR.evalR('42 + 123');
try {
let result = await obj1.toNumber();
const obj2 = await webR.evalR('42 * 123');
try {
= result + await obj2.toNumber();
result finally {
} .destroy(obj2);
webR
}
console.log(result);
finally {
} .destroy(obj1);
webR }
Using try
/finally
with a local shelter
To avoid increasingly nested blocks, it is often preferable to work with a shelter instead. A new shelter can be created using the Shelter
class. An asynchronous constructor for this class is available in the WebR.Shelter
property for convenient instantiation, e.g. const myShelter = await new myWebR.Shelter()
.
The objects referenced in a shelter can be destroyed all at once using the
purge()
method. They can also be destroyed individually with thedestroy()
method, just like you would do with the global shelter.The
Shelter.evalR()
andShelter.captureR()
methods automatically protect resultingRObject
references within the shelter instance instead of the default shelter.
The nested try
/ finally
blocks example can be refactored as:
const myShelter = await new webR.Shelter();
try {
const obj1 = await myShelter.evalR('42 + 123');
const obj2 = await webR.evalR('42 * 123');
const result = await obj1.toNumber() + await obj2.toNumber();
console.log(result);
finally {
} .purge();
myShelter }
A list of available methods can be found in the methods section of the Shelter
class reference. This includes similar methods for interacting with R or evaluating code to those provided on the WebR
class.
Create new objects protected by a shelter
Shelters also provide a range of R object class proxies which can be used to create a new R object by providing a JavaScript object. This works in the same way as when creating R objects protected by the global shelter, except that the resulting R objects will automatically be protected using this shelter, rather than the default shelter.
const obj = await new myShelter.RDouble([1, 1.5, 2, 2.5, 3]);
Footnotes
While the JavaScript specification does provide a finaliser mechanism to signal when objects are no longer in use, it cannot be relied upon. Even a fully conforming JavaScript implementation is not required to call the cleanup callbacks.↩︎