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.

Note

It is the user’s responsibility to signal to webR when they have finished working with an RObject reference.

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.

Warning

An RObject reference must not be used after it has been destroyed. Using a destroyed RObject results in undefined behaviour in the worker thread running R, most likely a crash.

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 {
  webR.destroy(obj);
}

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 = result + await obj2.toNumber();
  } finally {
    webR.destroy(obj2);
  }

  console.log(result);
} finally {
  webR.destroy(obj1);
}

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 the destroy() method, just like you would do with the global shelter.

  • The Shelter.evalR() and Shelter.captureR() methods automatically protect resulting RObject 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 {
  myShelter.purge();
}

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

  1. 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.↩︎