broadway: Reintroduce smarter diffing

We now send very minimal diff operations.
This commit is contained in:
Alexander Larsson 2019-03-28 19:17:01 +01:00
parent 4dfe2e6e2f
commit f30ba56323
5 changed files with 219 additions and 87 deletions

View File

@ -8,6 +8,7 @@
#include "broadway-output.h"
//#define DEBUG_NODE_SENDING
//#define DEBUG_NODE_SENDING_REMOVE
/************************************************************************
* Basic I/O primitives *
@ -317,9 +318,9 @@ static void
append_type (BroadwayOutput *output, guint32 type, BroadwayNode *node)
{
#ifdef DEBUG_NODE_SENDING
g_print ("%*s%s", append_node_depth*2, "", broadway_node_type_names[type]);
g_print ("%*s%s(%d/%d)", append_node_depth*2, "", broadway_node_type_names[type], node->id, node->output_id);
if (type == BROADWAY_NODE_TEXTURE)
g_print (" %u", node->data[4]);
g_print (" tx=%u", node->data[4]);
g_print ("\n");
#endif
@ -367,13 +368,14 @@ append_node (BroadwayOutput *output,
reused_node = lookup_old_node (old_node_lookup, node->id);
if (reused_node)
{
broadway_node_mark_deep_consumed (reused_node, TRUE);
append_type (output, BROADWAY_NODE_REUSE, node);
append_uint32 (output, node->id);
append_uint32 (output, node->output_id);
}
else
{
append_type (output, node->type, node);
append_uint32 (output, node->id);
append_uint32 (output, node->output_id);
for (i = 0; i < node->n_data; i++)
append_uint32 (output, node->data[i]);
for (i = 0; i < node->n_children; i++)
@ -386,36 +388,130 @@ append_node (BroadwayOutput *output,
}
static void
static BroadwayNode *
append_node_ops (BroadwayOutput *output,
BroadwayNode *node,
BroadwayNode *parent,
BroadwayNode *previous_sibling,
BroadwayNode *old_node,
GHashTable *old_node_lookup)
{
GPtrArray *to_remove = g_ptr_array_new ();
BroadwayNode *reused_node;
guint32 i;
if (old_node != NULL)
g_ptr_array_add (to_remove, old_node);
/* Maybe can be reused from the last tree. */
reused_node = lookup_old_node (old_node_lookup, node->id);
if (reused_node)
{
g_assert (node == reused_node);
g_assert (reused_node->reused);
g_assert (!reused_node->consumed); /* Should only be once in the tree, and not consumed otherwise */
append_uint32 (output, BROADWAY_NODE_OP_APPEND_NODE);
append_uint32 (output, 0); /* Parent */
broadway_node_mark_deep_consumed (reused_node, TRUE);
if (node == old_node)
{
/* The node in the old tree at the current position is the same, so
we need to do nothing, just don't delete it (which we won't since
its marked used) */
}
else
{
/* We can reuse it, bu it comes from a different place or
order, if so we need to move it in place */
#ifdef DEBUG_NODE_SENDING
g_print ("Move old node %d/%d to parent %d/%d after %d/%d\n",
reused_node->id, reused_node->output_id,
parent ? parent->id : 0,
parent ? parent->output_id : 0,
previous_sibling ? previous_sibling->id : 0,
previous_sibling ? previous_sibling->output_id : 0);
#endif
append_uint32 (output, BROADWAY_NODE_OP_MOVE_AFTER_CHILD);
append_uint32 (output, parent ? parent->output_id : 0);
append_uint32 (output, previous_sibling ? previous_sibling->output_id : 0);
append_uint32 (output, reused_node->output_id);
}
return reused_node;
}
/* If the next node in place is shallowly equal (but not necessarily
* deep equal) we reuse it and tweak its children as needed.
* Except we avoid this for reused node as those make more sense to reuse deeply.
*/
if (old_node && broadway_node_equal (node, old_node) && !old_node->reused)
{
int old_i = 0;
BroadwayNode *last_child = NULL;
old_node->consumed = TRUE; // Don't reuse again
// We rewrite this new node as it now represents the old node in the browser
node->output_id = old_node->output_id;
/* However, we might need to rewrite then children of old_node */
for (i = 0; i < node->n_children; i++)
{
BroadwayNode *child = node->children[i];
/* Find the next (or first) non-consumed old child, if any */
while (old_i < old_node->n_children &&
old_node->children[old_i]->consumed)
old_i++;
last_child =
append_node_ops (output,
child,
node, /* parent */
last_child,
(old_i < old_node->n_children) ? old_node->children[old_i] : NULL,
old_node_lookup);
}
/* Remaining old nodes are either reused elsewhere, or end up marked not consumed so are deleted at the end */
return old_node;
}
/* Fallback to create a new tree */
#ifdef DEBUG_NODE_SENDING
g_print ("Insert nodes in parent %d/%d, after sibling %d/%d\n",
parent ? parent->id : 0,
parent ? parent->output_id : 0,
previous_sibling ? previous_sibling->id : 0,
previous_sibling ? previous_sibling->output_id : 0);
#endif
append_uint32 (output, BROADWAY_NODE_OP_INSERT_NODE);
append_uint32 (output, parent ? parent->output_id : 0);
append_uint32 (output, previous_sibling ? previous_sibling->output_id : 0);
append_node(output, node, old_node_lookup);
for (i = 0; i < to_remove->len; i++)
{
BroadwayNode *removed_node = g_ptr_array_index (to_remove, i);
if (!removed_node->used)
{
// TODO: Use an array of nodes instead
append_uint32 (output, BROADWAY_NODE_OP_REMOVE_NODE);
append_uint32 (output, old_node->id);
}
}
g_ptr_array_unref (to_remove);
return node;
}
/* Remove non-consumed nodes */
static void
append_node_removes (BroadwayOutput *output,
BroadwayNode *node)
{
// TODO: Use an array of nodes instead
if (!node->consumed)
{
#ifdef DEBUG_NODE_SENDING_REMOVE
g_print ("Remove old node non-consumed node %d/%d\n",
node->id, node->output_id);
#endif
append_uint32 (output, BROADWAY_NODE_OP_REMOVE_NODE);
append_uint32 (output, node->output_id);
}
for (int i = 0; i < node->n_children; i++)
append_node_removes (output, node->children[i]);
}
void
broadway_output_surface_set_nodes (BroadwayOutput *output,
@ -426,14 +522,14 @@ broadway_output_surface_set_nodes (BroadwayOutput *output,
{
gsize size_pos, start, end;
/* Mark old nodes as unused, so we can use them once */
if (old_root)
broadway_node_mark_deep_used (old_root, FALSE);
/* Mark new nodes as used, causing any shared nodes to be marked as
unused. This way we won't accidentally reuse them for something
else. However, this means that is safe to reuse nodes marked as
used for the specific case of where the id matches. */
broadway_node_mark_deep_used (root, TRUE);
{
broadway_node_mark_deep_consumed (old_root, FALSE);
broadway_node_mark_deep_reused (old_root, FALSE);
/* This will modify children of old_root if any are shared */
broadway_node_mark_deep_reused (root, TRUE);
}
write_header (output, BROADWAY_OP_SET_NODES);
@ -444,9 +540,11 @@ broadway_output_surface_set_nodes (BroadwayOutput *output,
start = output->buf->len;
#ifdef DEBUG_NODE_SENDING
g_print ("====== node tree for %d =======\n", id);
g_print ("====== node ops for surface %d =======\n", id);
#endif
append_node_ops (output, root, old_root, old_node_lookup);
append_node_ops (output, root, NULL, NULL, old_root, old_node_lookup);
if (old_root)
append_node_removes (output, old_root);
end = output->buf->len;
patch_uint32 (output, (end - start) / 4, size_pos);
}

View File

@ -26,8 +26,9 @@ typedef enum { /* Sync changes with broadway.js */
} BroadwayNodeType;
typedef enum { /* Sync changes with broadway.js */
BROADWAY_NODE_OP_APPEND_NODE = 0,
BROADWAY_NODE_OP_INSERT_NODE = 0,
BROADWAY_NODE_OP_REMOVE_NODE = 1,
BROADWAY_NODE_OP_MOVE_AFTER_CHILD = 2,
} BroadwayNodeOpType;
static const char *broadway_node_type_names[] G_GNUC_UNUSED = {

View File

@ -183,10 +183,6 @@ broadway_node_equal (BroadwayNode *a,
{
int i;
// Early fast return for reused nodes
if (a == b)
return TRUE;
if (a->type != b->type)
return FALSE;
@ -211,10 +207,6 @@ broadway_node_deep_equal (BroadwayNode *a,
{
int i;
// Early fast return for reused nodes
if (a == b)
return TRUE;
if (a->hash != b->hash)
return FALSE;
@ -233,12 +225,21 @@ broadway_node_deep_equal (BroadwayNode *a,
void
broadway_node_mark_deep_used (BroadwayNode *node,
gboolean used)
broadway_node_mark_deep_reused (BroadwayNode *node,
gboolean reused)
{
node->used = used;
node->reused = reused;
for (int i = 0; i < node->n_children; i++)
broadway_node_mark_deep_used (node->children[i], used);
broadway_node_mark_deep_reused (node->children[i], reused);
}
void
broadway_node_mark_deep_consumed (BroadwayNode *node,
gboolean consumed)
{
node->consumed = consumed;
for (int i = 0; i < node->n_children; i++)
broadway_node_mark_deep_consumed (node->children[i], consumed);
}
void
@ -1779,6 +1780,7 @@ decode_nodes (BroadwayServer *server,
g_ref_count_init (&node->refcount);
node->type = type;
node->id = id;
node->output_id = id;
node->texture_id = 0;
node->n_children = n_children;
node->children = (BroadwayNode **)((char *)node + sizeof(BroadwayNode) + (size - 1) * sizeof(guint32));

View File

@ -25,13 +25,15 @@ struct _BroadwayNode {
grefcount refcount;
guint32 type;
guint32 id;
guint32 output_id;
guint32 hash; /* deep hash */
guint32 n_children;
BroadwayNode **children;
guint32 texture_id;
/* Scratch stuff used during diff */
gboolean used;
gboolean reused;
gboolean consumed;
guint32 n_data;
guint32 data[1];
@ -41,8 +43,10 @@ gboolean broadway_node_equal (BroadwayNode *
BroadwayNode *b);
gboolean broadway_node_deep_equal (BroadwayNode *a,
BroadwayNode *b);
void broadway_node_mark_deep_used (BroadwayNode *node,
gboolean used);
void broadway_node_mark_deep_reused (BroadwayNode *node,
gboolean reused);
void broadway_node_mark_deep_consumed (BroadwayNode *node,
gboolean consumed);
void broadway_node_add_to_lookup (BroadwayNode *node,
GHashTable *node_lookup);
BroadwayServer *broadway_server_new (char *address,

View File

@ -15,8 +15,9 @@ const BROADWAY_NODE_TRANSFORM = 11;
const BROADWAY_NODE_DEBUG = 12;
const BROADWAY_NODE_REUSE = 13;
const BROADWAY_NODE_OP_APPEND_NODE = 0;
const BROADWAY_NODE_OP_INSERT_NODE = 0;
const BROADWAY_NODE_OP_REMOVE_NODE = 1;
const BROADWAY_NODE_OP_MOVE_AFTER_CHILD = 2;
const BROADWAY_OP_GRAB_POINTER = 'g';
const BROADWAY_OP_UNGRAB_POINTER = 'u';
@ -236,7 +237,7 @@ function cmdCreateSurface(id, x, y, width, height, isTemp)
surface.transientParent = 0;
surface.visible = false;
surface.imageData = null;
surface.old_nodes = {};
surface.nodes = {};
var div = document.createElement('div');
div.surface = surface;
@ -299,14 +300,13 @@ function cmdLowerSurface(id)
moveToHelper(surface, 0);
}
function TransformNodes(node_data, div, old_nodes, display_commands) {
function TransformNodes(node_data, div, nodes, display_commands) {
this.node_data = node_data;
this.display_commands = display_commands;
this.data_pos = 0;
this.div = div;
this.outstanding = 1;
this.old_nodes = old_nodes;
this.new_nodes = {};
this.nodes = nodes;
}
TransformNodes.prototype.decode_uint32 = function() {
@ -479,16 +479,21 @@ function set_rrect_style (div, rrect) {
div.style["border-bottom-left-radius"] = args(px(rrect.sizes[3].width), px(rrect.sizes[3].height));
}
function addOldIds(node, node_map) {
node_map[node.node_id] = node;
var child = node.firstChild;
while (child) {
addOldIds(child, node_map);
child = child.nextSibling;
}
TransformNodes.prototype.createDiv = function(id)
{
var div = document.createElement('div');
div.node_id = id;
this.nodes[id] = div;
return div;
}
TransformNodes.prototype.createImage = function(id)
{
var image = new Image();
image.node_id = id;
this.nodes[id] = image;
return image;
}
TransformNodes.prototype.insertNode = function(parent, previousSibling, is_toplevel)
{
@ -502,7 +507,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
/* Reuse divs from last frame */
case BROADWAY_NODE_REUSE:
{
oldNode = this.old_nodes[id];
oldNode = this.nodes[id];
}
break;
/* Leaf nodes */
@ -511,7 +516,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
{
var rect = this.decode_rect();
var texture_id = this.decode_uint32();
var image = new Image();
var image = this.createImage(id);
image.width = rect.width;
image.height = rect.height;
image.style["position"] = "absolute";
@ -528,7 +533,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
{
var rect = this.decode_rect();
var c = this.decode_color ();
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["position"] = "absolute";
set_rect_style(div, rect);
div.style["background-color"] = c;
@ -546,7 +551,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
for (var i = 0; i < 4; i++)
border_colors[i] = this.decode_color();
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["position"] = "absolute";
rrect.bounds.width -= border_widths[1] + border_widths[3];
rrect.bounds.height -= border_widths[0] + border_widths[2];
@ -573,7 +578,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
var spread = this.decode_float();
var blur = this.decode_float();
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["position"] = "absolute";
set_rrect_style(div, rrect);
div.style["box-shadow"] = args(px(dx), px(dy), px(blur), px(spread), color);
@ -590,7 +595,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
var spread = this.decode_float();
var blur = this.decode_float();
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["position"] = "absolute";
set_rrect_style(div, rrect);
div.style["box-shadow"] = args("inset", px(dx), px(dy), px(blur), px(spread), color);
@ -605,7 +610,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
var start = this.decode_point ();
var end = this.decode_point ();
var stops = this.decode_color_stops ();
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["position"] = "absolute";
set_rect_style(div, rect);
@ -669,7 +674,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
m[12] + "," + m[13] + "," + m[14] + "," + m[15] + ")";
}
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["transform"] = transform_string;
div.style["transform-origin"] = "0px 0px";
@ -681,7 +686,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
case BROADWAY_NODE_CLIP:
{
var rect = this.decode_rect();
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["position"] = "absolute";
set_rect_style(div, rect);
div.style["overflow"] = "hidden";
@ -693,7 +698,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
case BROADWAY_NODE_ROUNDED_CLIP:
{
var rrect = this.decode_rounded_rect();
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["position"] = "absolute";
set_rrect_style(div, rrect);
div.style["overflow"] = "hidden";
@ -705,7 +710,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
case BROADWAY_NODE_OPACITY:
{
var opacity = this.decode_float();
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["position"] = "absolute";
div.style["left"] = px(0);
div.style["top"] = px(0);
@ -727,7 +732,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
var blur = this.decode_float();
filters = filters + "drop-shadow(" + args (px(dx), px(dy), px(blur), color) + ")";
}
var div = document.createElement('div');
var div = this.createDiv(id);
div.style["position"] = "absolute";
div.style["left"] = px(0);
div.style["top"] = px(0);
@ -741,7 +746,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
case 14: // DEBUG
{
var str = this.decode_string();
var div = document.createElement('div');
var div = this.createDiv(id);
div.setAttribute('debug', str);
this.insertNode(div, null, false);
newNode = div;
@ -752,7 +757,7 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
case BROADWAY_NODE_CONTAINER:
{
var div = document.createElement('div');
var div = this.createDiv(id);
var len = this.decode_uint32();
var lastChild = null;
for (var i = 0; i < len; i++) {
@ -767,15 +772,12 @@ TransformNodes.prototype.insertNode = function(parent, previousSibling, is_tople
}
if (newNode) {
newNode.node_id = id;
this.new_nodes[id] = newNode;
if (is_toplevel)
this.display_commands.push([DISPLAY_OP_APPEND_CHILD, parent, newNode]);
else // Here it is safe to display directly because we have not added the toplevel to the document yet
this.display_commands.push([DISPLAY_OP_INSERT_AFTER_CHILD, parent, previousSibling, newNode]);
else // It is safe to display directly because we have not added the toplevel to the document yet
parent.appendChild(newNode);
return newNode;
} else if (oldNode) {
addOldIds(oldNode, this.new_nodes);
// This must be delayed until display ops, because it will remove from the old parent
this.display_commands.push([DISPLAY_OP_INSERT_AFTER_CHILD, parent, previousSibling, oldNode]);
return oldNode;
@ -788,22 +790,48 @@ TransformNodes.prototype.execute = function(display_commands)
while (this.data_pos < this.node_data.byteLength) {
var op = this.decode_uint32();
var parentId, parent;
switch (op) {
case BROADWAY_NODE_OP_APPEND_NODE:
var parentId = this.decode_uint32();
var parent;
case BROADWAY_NODE_OP_INSERT_NODE:
parentId = this.decode_uint32();
if (parentId == 0)
parent = root;
else
parent = this.old_nodes[parentId];
this.insertNode(parent, null, true);
else {
parent = this.nodes[parentId];
if (parent == null)
console.log("Wanted to insert into parent " + parentId + " but it is unknown");
}
var previousChildId = this.decode_uint32();
var previousChild = null;
if (previousChildId != 0)
previousChild = this.nodes[previousChildId];
this.insertNode(parent, previousChild, true);
break;
case BROADWAY_NODE_OP_REMOVE_NODE:
var removeId = this.decode_uint32();
var remove = this.old_nodes[removeId];
var remove = this.nodes[removeId];
delete this.nodes[removeId];
if (remove == null)
console.log("Wanted to delete node " + removeId + " but it is unknown");
this.display_commands.push([DISPLAY_OP_DELETE_NODE, remove]);
break;
case BROADWAY_NODE_OP_MOVE_AFTER_CHILD:
parentId = this.decode_uint32();
if (parentId == 0)
parent = root;
else
parent = this.nodes[parentId];
var previousChildId = this.decode_uint32();
var previousChild = null;
if (previousChildId != 0)
previousChild = this.nodes[previousChildId];
var toMoveId = this.decode_uint32();
var toMove = this.nodes[toMoveId];
this.display_commands.push([DISPLAY_OP_INSERT_AFTER_CHILD, parent, previousChild, toMove]);
break;
}
}
}
@ -1028,9 +1056,8 @@ function handleCommands(cmd, display_commands, new_textures, modified_trees)
var node_data = cmd.get_nodes ();
surface = surfaces[id];
var transform_nodes = new TransformNodes (node_data, surface.div, surface.old_nodes, display_commands);
var transform_nodes = new TransformNodes (node_data, surface.div, surface.nodes, display_commands);
transform_nodes.execute();
surface.old_nodes = transform_nodes.new_nodes;
}
break;