DOM inspector: incremental rendering when inspected DOM changes

This commit is contained in:
gorhill 2015-06-27 13:32:10 -04:00
parent f12bbd4703
commit f988d74b4d
2 changed files with 360 additions and 71 deletions

View File

@ -144,8 +144,9 @@ var tabIdFromClassName = function(className) {
var tabSelector = uDom.nodeFromId('pageSelector'); var tabSelector = uDom.nodeFromId('pageSelector');
var nodeFromDomEntry = function(entry) { var nodeFromDomEntry = function(entry) {
var node; var node, value;
var li = document.createElement('li'); var li = document.createElement('li');
li.setAttribute('id', entry.nid);
// expander/collapser // expander/collapser
node = document.createElement('span'); node = document.createElement('span');
li.appendChild(node); li.appendChild(node);
@ -154,11 +155,11 @@ var tabIdFromClassName = function(className) {
node.textContent = entry.sel; node.textContent = entry.sel;
li.appendChild(node); li.appendChild(node);
// descendant count // descendant count
if ( entry.cnt !== 0 ) { value = entry.cnt || 0;
node = document.createElement('span'); node = document.createElement('span');
node.textContent = entry.cnt.toLocaleString(); node.textContent = value !== 0 ? value.toLocaleString() : '';
node.setAttribute('data-cnt', value);
li.appendChild(node); li.appendChild(node);
}
// cosmetic filter // cosmetic filter
if ( entry.filter !== undefined ) { if ( entry.filter !== undefined ) {
node = document.createElement('code'); node = document.createElement('code');
@ -186,10 +187,10 @@ var tabIdFromClassName = function(className) {
} }
}; };
var renderDOM = function(response) { var renderDOMFull = function(response) {
var ul = document.createElement('ul'); var ul = document.createElement('ul');
var lvl = 0; var lvl = 0;
var entries = response; var entries = response.layout;
var n = entries.length; var n = entries.length;
var li, entry; var li, entry;
for ( var i = 0; i < n; i++ ) { for ( var i = 0; i < n; i++ ) {
@ -229,6 +230,85 @@ var tabIdFromClassName = function(className) {
inspector.appendChild(ul); inspector.appendChild(ul);
}; };
var patchIncremental = function(from, delta) {
var span, cnt;
var li = from.parentElement.parentElement;
var patchCosmeticHide = delta >= 0 &&
from.classList.contains('isCosmeticFilter') &&
li.classList.contains('hasCosmeticFilter') === false;
for ( ; li.localName === 'li'; li = li.parentElement.parentElement ) {
span = li.children[2];
if ( delta !== 0 ) {
cnt = parseInt(span.getAttribute('data-cnt'), 10) + delta;
span.textContent = cnt !== 0 ? cnt.toLocaleString() : '';
span.setAttribute('data-cnt', cnt);
}
if ( patchCosmeticHide ) {
li.classList.add('hasCosmeticFilter');
}
}
};
var renderDOMIncremental = function(response) {
// Process each journal entry:
// 1 = node added
// -1 = node removed
var journal = response.journal;
var nodes = response.nodes;
var entry, previous, li, ul;
for ( var i = 0, n = journal.length; i < n; i++ ) {
entry = journal[i];
// Remove node
if ( entry.what === -1 ) {
li = document.getElementById(entry.nid);
if ( li === null ) {
continue;
}
patchIncremental(li, -1);
li.parentNode.removeChild(li);
continue;
}
// Modify node
if ( entry.what === 0 ) {
// TODO: update selector/filter
continue;
}
// Add node as sibling
if ( entry.what === 1 && entry.l ) {
previous = document.getElementById(entry.l);
// This should not happen
if ( previous === null ) {
// throw new Error('No left sibling!?');
continue;
}
ul = previous.parentElement;
li = nodeFromDomEntry(nodes[entry.nid]);
ul.insertBefore(li, previous.nextElementSibling);
patchIncremental(li, 1);
continue;
}
// Add node as child
if ( entry.what === 1 && entry.u ) {
li = document.getElementById(entry.u);
// This should not happen
if ( li === null ) {
// throw new Error('No parent!?');
continue;
}
ul = li.querySelector('ul');
if ( ul === null ) {
ul = document.createElement('ul');
li.appendChild(ul);
li.classList.add('branch');
}
li = nodeFromDomEntry(nodes[entry.nid]);
ul.appendChild(li);
patchIncremental(li, 1);
continue;
}
}
};
var selectorFromNode = function(node, nth) { var selectorFromNode = function(node, nth) {
var selector = ''; var selector = '';
var code; var code;
@ -377,13 +457,23 @@ var tabIdFromClassName = function(className) {
return; return;
} }
if ( response.layout === 'NOCHANGE' ) { switch ( response.status ) {
fetchDOMAsync(); case 'full':
return; renderDOMFull(response);
}
renderDOM(response.layout);
fingerprint = response.fingerprint; fingerprint = response.fingerprint;
break;
case 'incremental':
renderDOMIncremental(response);
break;
case 'nochange':
case 'busy':
break;
default:
break;
}
fetchDOMAsync(); fetchDOMAsync();
}; };

View File

@ -139,6 +139,7 @@ var cssEscape = (function(root) {
var localMessager = vAPI.messaging.channel('dom-inspector.js'); var localMessager = vAPI.messaging.channel('dom-inspector.js');
// Highlighter-related
var svgOcean = null; var svgOcean = null;
var svgIslands = null; var svgIslands = null;
var svgRoot = null; var svgRoot = null;
@ -149,6 +150,14 @@ var toggledNodes = new Map();
/******************************************************************************/ /******************************************************************************/
// Some kind of fingerprint for the DOM, without incurring too much overhead.
var domFingerprint = function() {
return vAPI.sessionId;
};
/******************************************************************************/
var domLayout = (function() { var domLayout = (function() {
var skipTagNames = { var skipTagNames = {
'br': true, 'br': true,
@ -165,8 +174,20 @@ var domLayout = (function() {
'object': 'data' 'object': 'data'
}; };
var idGenerator = 0;
var nodeToIdMap = new WeakMap(); // No need to iterate
// This will be used to uniquely identify nodes across process.
var newNodeId = function(node) {
var nid = 'n' + (idGenerator++).toString(36);
nodeToIdMap.set(node, nid);
return nid;
};
// Collect all nodes which are directly affected by cosmetic filters: these // Collect all nodes which are directly affected by cosmetic filters: these
// will be reported in the layout data. // will be reported in the layout data.
// TODO: take into account cosmetic filters added after the map is build.
var nodeToCosmeticFilterMap = (function() { var nodeToCosmeticFilterMap = (function() {
var out = new WeakMap(); var out = new WeakMap();
@ -189,25 +210,18 @@ var domLayout = (function() {
return out; return out;
})(); })();
var DomRoot = function() { var matchesSelector = (function() {
this.lvl = 0; if ( typeof Element.prototype.matches === 'function' ) {
this.sel = 'body'; return 'matches';
var url = window.location.href;
var pos = url.indexOf('#');
if ( pos !== -1 ) {
url = url.slice(0, pos);
} }
this.src = url; if ( typeof Element.prototype.mozMatchesSelector === 'function' ) {
this.top = window === window.top; return 'mozMatchesSelector';
this.cnt = 0; }
}; if ( typeof Element.prototype.webkitMatchesSelector === 'function' ) {
return 'webkitMatchesSelector';
var DomNode = function(level, selector, filter) { }
this.lvl = level; return '';
this.sel = selector; })();
this.cnt = 0;
this.filter = filter;
};
var hasManyMatches = function(node, selector) { var hasManyMatches = function(node, selector) {
var fnName = matchesSelector; var fnName = matchesSelector;
@ -228,19 +242,6 @@ var domLayout = (function() {
return false; return false;
}; };
var matchesSelector = (function() {
if ( typeof Element.prototype.matches === 'function' ) {
return 'matches';
}
if ( typeof Element.prototype.mozMatchesSelector === 'function' ) {
return 'mozMatchesSelector';
}
if ( typeof Element.prototype.webkitMatchesSelector === 'function' ) {
return 'webkitMatchesSelector';
}
return '';
})();
var selectorFromNode = function(node) { var selectorFromNode = function(node) {
var str, attr, pos, sw, i; var str, attr, pos, sw, i;
var tag = node.localName; var tag = node.localName;
@ -290,6 +291,22 @@ var domLayout = (function() {
return selector; return selector;
}; };
var DomRoot = function() {
this.nid = newNodeId(document.body);
this.lvl = 0;
this.sel = 'body';
this.cnt = 0;
this.filter = nodeToCosmeticFilterMap.get(document.body);
};
var DomNode = function(node, level) {
this.nid = newNodeId(node);
this.lvl = level;
this.sel = selectorFromNode(node);
this.cnt = 0;
this.filter = nodeToCosmeticFilterMap.get(node);
};
var domNodeFactory = function(level, node) { var domNodeFactory = function(level, node) {
var localName = node.localName; var localName = node.localName;
if ( skipTagNames.hasOwnProperty(localName) ) { if ( skipTagNames.hasOwnProperty(localName) ) {
@ -302,15 +319,13 @@ var domLayout = (function() {
if ( level === 0 && localName === 'body' ) { if ( level === 0 && localName === 'body' ) {
return new DomRoot(); return new DomRoot();
} }
var selector = selectorFromNode(node); return new DomNode(node, level);
var filter = nodeToCosmeticFilterMap.get(node);
return new DomNode(level, selector, filter);
}; };
// Collect layout data. // Collect layout data.
var getLayoutData = function() { var getLayoutData = function() {
var domLayout = []; var layout = [];
var stack = []; var stack = [];
var node = document.body; var node = document.body;
var domNode; var domNode;
@ -319,7 +334,7 @@ var domLayout = (function() {
for (;;) { for (;;) {
domNode = domNodeFactory(lvl, node); domNode = domNodeFactory(lvl, node);
if ( domNode !== null ) { if ( domNode !== null ) {
domLayout.push(domNode); layout.push(domNode);
} }
// children // children
if ( node.firstElementChild !== null ) { if ( node.firstElementChild !== null ) {
@ -339,19 +354,20 @@ var domLayout = (function() {
} }
node = node.nextElementSibling; node = node.nextElementSibling;
} }
return domLayout;
return layout;
}; };
// Descendant count for each node. // Descendant count for each node.
var patchLayoutData = function(domLayout) { var patchLayoutData = function(layout) {
var stack = [], ptr; var stack = [], ptr;
var lvl = 0; var lvl = 0;
var domNode, cnt; var domNode, cnt;
var i = domLayout.length; var i = layout.length;
while ( i-- ) { while ( i-- ) {
domNode = domLayout[i]; domNode = layout[i];
if ( domNode.lvl === lvl ) { if ( domNode.lvl === lvl ) {
stack[ptr] += 1; stack[ptr] += 1;
continue; continue;
@ -372,24 +388,211 @@ var domLayout = (function() {
ptr = lvl - 1; ptr = lvl - 1;
stack[ptr] += cnt + 1; stack[ptr] += cnt + 1;
} }
return domLayout; return layout;
}; };
return function() { // Track and report mutations to the DOM
return patchLayoutData(getLayoutData());
var journalEntries = [];
var journalNodes = Object.create(null);
var mutationObserver = null;
var mutationTimer = null;
var addedNodelists = [];
var removedNodelist = [];
var previousElementSiblingId = function(node) {
var sibling = node;
for (;;) {
sibling = sibling.previousElementSibling;
if ( sibling === null ) {
return null;
}
if ( skipTagNames.hasOwnProperty(sibling.localName) ) {
continue;
}
return nodeToIdMap.get(sibling);
}
};
var journalFromBranch = function(root, added) {
var domNode;
var node = root.firstElementChild;
while ( node !== null ) {
domNode = domNodeFactory(undefined, node);
if ( domNode !== null ) {
journalNodes[domNode.nid] = domNode;
added.push(node);
}
// down
if ( node.firstElementChild !== null ) {
node = node.firstElementChild;
continue;
}
// right
if ( node.nextElementSibling !== null ) {
node = node.nextElementSibling;
continue;
}
// up then right
for (;;) {
if ( node.parentElement === root ) {
return;
}
node = node.parentElement;
if ( node.nextElementSibling !== null ) {
node = node.nextElementSibling;
break;
}
}
}
};
var journalFromMutations = function() {
mutationTimer = null;
if ( mutationObserver === null ) {
addedNodelists = [];
removedNodelist = [];
return;
}
var i, m, nodelist, j, n, node, domNode, nid;
// This is used to temporarily hold all added nodes, before resolving
// their node id and relative position.
var added = [];
for ( i = 0, m = addedNodelists.length; i < m; i++ ) {
nodelist = addedNodelists[i];
for ( j = 0, n = nodelist.length; j < n; j++ ) {
node = nodelist[j];
if ( node.nodeType !== 1 ) {
continue;
}
// I don't think this can ever happen
if ( node.parentElement === null ) {
continue;
}
domNode = domNodeFactory(undefined, node);
if ( domNode !== null ) {
journalNodes[domNode.nid] = domNode;
added.push(node);
}
journalFromBranch(node, added);
}
}
addedNodelists = [];
for ( i = 0, m = removedNodelist.length; i < m; i++ ) {
nodelist = removedNodelist[i];
for ( j = 0, n = nodelist.length; j < n; j++ ) {
node = nodelist[j];
if ( node.nodeType !== 1 ) {
continue;
}
nid = nodeToIdMap.get(node);
if ( nid === undefined ) {
continue;
}
journalEntries.push({
what: -1,
nid: nid
});
}
}
removedNodelist = [];
for ( i = 0, n = added.length; i < n; i++ ) {
node = added[i];
journalEntries.push({
what: 1,
nid: nodeToIdMap.get(node),
u: nodeToIdMap.get(node.parentElement),
l: previousElementSiblingId(node)
});
}
};
var onMutationObserved = function(mutationRecords) {
var record;
for ( var i = 0, n = mutationRecords.length; i < n; i++ ) {
record = mutationRecords[i];
if ( record.addedNodes.length !== 0 ) {
addedNodelists.push(record.addedNodes);
}
if ( record.removedNodes.length !== 0 ) {
removedNodelist.push(record.removedNodes);
}
}
if ( mutationTimer === null ) {
mutationTimer = vAPI.setTimeout(journalFromMutations, 1000);
}
};
// API
var getLayout = function(fingerprint) {
if ( fingerprint !== domFingerprint() && mutationObserver !== null ) {
if ( mutationTimer !== null ) {
clearTimeout(mutationTimer);
mutationTimer = null;
}
mutationObserver.disconnect();
mutationObserver = null;
journalEntries = [];
journalNodes = Object.create(null);
}
var response = {
what: 'domLayout',
fingerprint: domFingerprint()
};
// No mutation observer means we need to send full layout
if ( mutationObserver === null ) {
mutationObserver = new MutationObserver(onMutationObserved);
mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
response.status = 'full';
response.layout = patchLayoutData(getLayoutData());
return response;
}
// Incremental layout
if ( journalEntries.length !== 0 ) {
response.status = 'incremental';
response.journal = journalEntries;
response.nodes = journalNodes;
journalEntries = [];
journalNodes = Object.create(null);
return response;
}
response.status = 'nochange';
return response;
};
var shutdown = function() {
if ( mutationTimer !== null ) {
clearTimeout(mutationTimer);
mutationTimer = null;
}
if ( mutationObserver !== null ) {
mutationObserver.disconnect();
mutationObserver = null;
}
journalEntries = [];
journalNodes = Object.create(null);
};
return {
get: getLayout,
shutdown: shutdown
}; };
})(); })();
/******************************************************************************/ /******************************************************************************/
// Some kind of fingerprint for the DOM, without incurring too much overhead.
var domFingerprint = function() {
return vAPI.sessionId + '{' + document.getElementsByTagName('*').length + '}';
};
/******************************************************************************/
var highlightElements = function(elems, scrollTo) { var highlightElements = function(elems, scrollTo) {
var wv = pickerRoot.contentWindow.innerWidth; var wv = pickerRoot.contentWindow.innerWidth;
var hv = pickerRoot.contentWindow.innerHeight; var hv = pickerRoot.contentWindow.innerHeight;
@ -555,6 +758,7 @@ var resetToggledNodes = function() {
var shutdown = function() { var shutdown = function() {
resetToggledNodes(); resetToggledNodes();
domLayout.shutdown();
localMessager.removeListener(onMessage); localMessager.removeListener(onMessage);
localMessager.close(); localMessager.close();
localMessager = null; localMessager = null;
@ -572,12 +776,7 @@ var onMessage = function(request) {
switch ( msg.what ) { switch ( msg.what ) {
case 'domLayout': case 'domLayout':
var fingerprint = domFingerprint(); response = domLayout.get(msg.fingerprint);
response = {
what: 'domLayout',
layout: msg.fingerprint !== fingerprint ? domLayout() : 'NOCHANGE',
fingerprint: fingerprint
};
break; break;
case 'highlight': case 'highlight':