On 11/04/2015 08:09 AM, Jussi Kalliokoski wrote:
It provides the needed interface and the unused child revisions get cleaned up properly. However:

* This is a complete nightmare for GC performance because of cyclical weak references.

Not necessarily. Current Spidermonkey should handle it ok, using a linear-time algorithm for weakmap marking. (I think an inverted representation would also be good for this? I haven't thought it through.)

* Any reference to a child will maintain references to all its parents.

However this doesn't necessarily need to be the case because the stored ancestry is not observable to anything that creates a WeakGraph, except to the oldest ancestor that has a reference elsewhere.

I'm not sure if this use case alone warrants adding a new feature to the language, or if I'm just missing something and it can be implemented with existing constructs or if there should be some other lower level primitive that would allow building a WeakGraph on the user level.

The brute-force approach would be to give each node its own weakmap, and add an entry for *every* ancestor:

```JS
this.getLineage() = function (node, ancestor) {
  const lineage = [];
  while (node) {
    lineage.push(node);
    if (node == ancestor)
      return lineage;
    node = node.parentForAncestor.get(ancestor);
  }
}

this.addNode = function (node, parent, ancestor) {
  node.parentForAncestor = new WeakMap();
  for (let key of this.getLineage(parent, ancestor))
    node.parentForAncestor.set(key, parent);
};
```

...but notice that addNode now requires an ancestor to be specified, and it will only work as far back as that. And of course, this defeats the whole point of the exercise, in that it requires space quadratic in the number of live nodes. And insertion is linear in the distance from the node to the chosen ancestor. Which could still be a win in rare cases, I guess, but my point is only to show that the leak could be "fixed".

I should note that in your example, you have a WeakMap with basically infinite lifetime. That means that keys always hold values live, in which case you might as well store them directly as properties on the key (node.parent = parent).

I also pondered what language primitives would fix this case. The straightforward approach is to expose something tailored specifically to this use case -- call it a WeakList. You cannot look up elements by index. You can call WeakList.prototype.get(begin, end) and it will return an Array of elements from begin..end (where 'begin' and 'end' are actual elements of the list), or undefined if either begin or end is not present in the list. Internally, the implementation would allowed to discard all leading and trailing dead elements. It would be stored as a dag to share space with overlapping lists.

A totally different option is something I'm calling a VagueMap. I should mention here that I don't know of any prior art, nor have I looked for any, so my apologies if this is known by another name. A VagueMap is sort of the dual of a WeakMap, where the value keeps the entry (and key) alive rather than the key keeping the value alive. But to avoid exposing GC behavior, you cannot just look up values by key (since then if you wanted to know when something gets GC'ed, you'd just stick it in a VagueMap under a well-known key and look it up periodically.) Instead, get(key) returns a "vague value", which can only be used in two ways: for equality testing, and as a VagueMap key. VagueMap lookups would treat vague values and their equivalent non-vague values identically. VagueMap would also support has().

Note that VagueMap does not automatically keep its keys live (as in, only the ones with live values will be kept live.) So you still can't iterate over the keys.

This still doesn't fix the original example. The simple replacement of WeakMap with VagueMap will "work", but getLineage() will return an array of vague values, which aren't much use. So I'll need to add another funky feature to VagueMap: if you give it a (non-vague) value, it will hand you back the non-vague, guaranteed live, key. I'll call it VagueMap.prototype.getKey(vkey, value) where vkey is a possibly-vague key that maps to value.

We get back very close to the original code, with an added loop to reify the vague nodes (oh, and I include the ancestor in the lineage -- which means the do/while loop could now easily be a for loop, but I'll stick close to the original):

```JS
function WeakGraph () {
    const parentByNode = new VagueMap();

    this.getLineage = function (node, ancestor) {
        const lineage = [];

        let currentNode = node;
        do {
            lineage.push(currentNode);
if ( !parentByNode.has(currentNode) ) { throw new Error("node is not a descendant of ancestor"); }
            currentNode = parentByNode.get(currentNode);
        } while ( currentNode !== ancestor );
        lineage.push(ancestor);

        for (let i = lineage.length; i > 1; i--) {
            lineage[i-2] = parentByNode.getKey(lineage[i-2], lineage[i-1]);
        }

        return lineage;
    };

    this.addNode = function (node, parent) {
        parentByNode.set(node, parent);
    };
}
```

Given the number of failed attempts I've taken at this today, there's probably some very obvious problem with all of this. But I *think* this all checks out now?

_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to