Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

WeakRefGroups

Dean Tribble edited this page Feb 9, 2018 · 1 revision

Summary

An enhancement to the weak ref proposal to enable efficient in-turn finalization by e.g. a bridge comm system to interface to WASM, and enable/encourage sub-system level finalization.

Motivation

Most uses of finalization have numerous object that should be managed consistently. In the weakref proposal, for example, all remote reference objects in a comm system issue dropRef messages on their shared network connection, all unclosed filestreams issue a warning and close, etc. Handling finalization consistently for similar objects in a subsystem generally requires less complexity and less overhead, and results thus in more reliable code.

Additionally, having an object that represents a related group of weak references provides a basis for additional operations to support use cases like application-controlled finalization within turns, within separate WASM threads, etc.

Finally, the additions here gracefully address or simplify some open questions in the original proposal.

Manual Finalization

A key aspect of post-mortem finalization is that it is very easy for convenient automated finalization to exist with a manual finalization process. The GC runs on it's own schedule, references to no-longer-reachable objects. Finalization could sort of be done by polling that weak cells and executing the cleanup code. For example, the RemoteConnection example in the proposal could be enhanced with sendDrops() to eagerly batch up not-yet-reported references to collection:

  // finalize any dropped references
  sendDrops() {
    const drops = [];
    this.remotes.forEach((weakref, remoteId) => {
      if (weakref.deref() === undefined) {
        drops.push(remoteId);
      }
    }
    if (drops.length > 0) {
        this.dropMany(drops);
    }
  }

This kind of polling is expensive in several ways however, but could be very convenient with the right support.

Weak Reference Collections

Designing a new weak reference collection type seems on the surface to be a good direction to pursue. In exploring examples using it, however, it was only good for very simple use cases. Further, it provides the attractive nuisance of looking like something that a novice developer could easily misuse. The approach here provides the tools need, can be efficiently implemented, and encourages design patterns for library implementors.

Approach

The basic weak reference approach is mostly unchanged. Instead of creating each WeakRef individually, the library user creates a new WeakRefGroup with it executor, and then creates individual WeakRefs in that group using that WeakRefGroup object.

The usage API is just:

class WeakRefGroup {
    constructor(executorFn) { ... }

    makeRef(target, holdings) { ... }
}

class WeakRef {

    // Return the target
    deref() { ... }

    // Drop the weak reference to the target and prevent finalization
    clear() { ... }

    // Return the holdings associated with the target
    get holdings() { ... }
    
}

Manual Finalization

There are several potential variations for using the group object to support manual finalization; below are a couple of options: apply the already-attached executor or apply a supplied function.

class WeakRefGroup {
    constructor(executorFn) { ... }

    makeRef(target, holdings) { ... }

// options for manual finalization

    // Apply the existing finalizer to some finalizable
    // WeakRefs in this group
    finalizationSome() { ... }

    // Apply a user-supplied executor to some finalizable 
    // WeakRefs in this group 
    manuallyFinalize(executorFn) { ... }
  }
}

The specifics here are still open to discussion, but are not crucial to the basic idea. These operations must be specified to work well with incremental GC. Thus, they may not find some cells that were already collected, and after returning, another call might immediately be productive (e.g., might "find" new finalizable WeakRefs). This ensures that the GC can run at full efficient and as needed.

RemoteConnection example

Here is the RenmoteConnection example from the proposal, updated for groups.

class RemoteConnection {

  constructor(transport) {
    this.transport = transport; // the low-level communication channel
    const executor = remoteId => this.dropRef(remoteId);
    this.remotesGroup = new WeakReferenceGroup(executor);
    this.remotes = new Map();  // map from id to weakRef
    // ... more construction
  }

  // Create a remote reference for a remote return result. Setup  
  // finalization to clean up after it if it is garbge collected.
  makeResultRemoteRef(remoteId) {
    let remoteRef = ???; // remoteRef construction elided
    this.remotes.set(remoteId, this.remotesGroup.makeRef(remoteRef, remoteId));
    return remoteRef;
  }

  // The remote reference object has been garbage collected. Discard  
  // the associated bookeeping and notify the server that it is no 
  // longer referenced.
  dropRef(remoteId) {
    this.transport.send("DROP", remoteId);
    this.remotes.delete(remoteId);
  }

  // Send a batch of drop refs all at once.
  // Called every 100 message or some such.
  sendDrops() {
    const drops = [];
    this.remotesGroup.manuallyFinalize((weakref, remoteId) => {
      drops.push(remoteId);
      this.remotes.delete(remoteId);
    }
    if (drops.length > 0) {
      this.transport.send("DROP_MANY", drops);
    }
  }
}

Implementation Note

Thsi approach also addresses the security issue around the WeakRef constructor avaialbility, without requiring additional support in class construction. Pseudo-code that specifies the semantics of WeakRef constructor:

class WeakRef {
  ...
}

const realWeakRef = WeakRef;

// Hide the WeakRef constructor so that WeakRefs can only 
// be created a part of a group
function WeakRef() { throw ... }
WeakRef.prototype = realWeakRef.prototype;
WeakRef.prototype.constructor = WeakRef;

class WeakRefGroup {
    constructor(executor, optName) {
        this.executor = executor;
        this.name = optName;
    }

    makeRef(target, holdings) {
        return realWeakRef(target, this, holdings);
    }

    ...
}

// export only the WeakRefGroup
// the WeakRef class is available but not interesting
export default WeakRefGroup;