anchor.diff:    return reader.done;
   }
   
  -String makeJson(root) {
  -  // 'root' can be arbitrarily deep, so we need to use an explicit stack
  -  // instead of the call stack. We also must avoid using a JSON expression to
  -  // avoid stack overflow in the JS engine parser.
  -  final ids = new Map<dynamic, int>();
  -  final sb = new StringBuffer();
  -  final worklist = new List<dynamic>();
  -  worklist.add(root);
  -
  -  var id = 0;
  -  while (id < worklist.length) {
  -    var object = worklist[id];
  -    ids[object] = id++;
  -    worklist.addAll(object.children);
  -  }
  -
  -  while (!worklist.isEmpty) {
  -    var object = worklist.removeLast();
  -    var id = ids[object];
  -
  -    sb.write('var node');
  -    sb.write(id.toString());
  -    sb.write(' = {"name":"');
  -    sb.write(object.description);
  -    sb.write('","type":"');
  -    sb.write(object.klass.name);
  -    sb.write('","size":');
  -    sb.write(object.retainedSize.toString());
  -    sb.write(',"children":[');
  -
  -    bool first = true;
  -    for (var child in object.children) {
  -      if (!first) {
  -        sb.write(',');
  -      }
  -      first = false;
  -      sb.write('node');
  -      sb.write(ids[child].toString());
  -    }
  -
  -    sb.write(']};\n');
  -  }
  -
  -  return sb.toString();
  +String makeData(dynamic root) {
  +  // 'root' can be arbitrarily deep, so we can't directly represent it as a
  +  // JSON tree, which cause a stack overflow here encoding it and in the JS
  +  // engine decoding it. Instead we flatten the tree into a list of tuples with
  +  // a parent pointer and re-inflate it in JS.
  +  final indices = <dynamic, int>{};
  +  final preorder = <dynamic>[];
  +  preorder.add(root);
  +
  +  for (var index = 0; index < preorder.length; index++) {
  +    final object = preorder[index];
  +    preorder.addAll(object.children);
  +  }
  +
  +  final flattened = <dynamic>[];
  +  for (var index = 0; index < preorder.length; index++) {
  +    final object = preorder[index];
  +    indices[object] = index;
  +
  +    flattened.add(object.description);
  +    flattened.add(object.klass.name);
  +    flattened.add(object.retainedSize);
  +    if (index == 0) {
  +      flattened.add(null);
  +    } else {
  +      flattened.add(indices[object.parent] as int);
  +    }
  +  }
  +
  +  return json.encode(flattened);
   }
   
   var css = '''
  
positive.code:   content.appendChild(topTile);
  }
  
  function linkParents(root) {
    // 'root' can be arbitrarily deep, so we need to use an explicit stack
    // instead of the call stack.
    let worklist = new Array();
    worklist.push(root);
  
    while (worklist.length > 0) {
      let node = worklist.pop();
      node.children.forEach(function (child) {
        child["parent"] = node;
        worklist.push(child);
      });
    }
  }
  
  var root = null;
  (function() {
    __JSON__
    root = node0;
  })();
  
  linkParents(root);
  showDominatorTree(root);
  ''';
  
  
negative.code:   final dir = await Directory.systemTemp.createTemp('heap-snapshot');
    final path = dir.path + '/merged-dominator.html';
    final file = await File(path).create();
    final tree = makeData(snapshot.mergedRoot);
    await file.writeAsString(html.replaceAll('__DATA__', tree));
    print('Wrote file://' + path);
  }
  