Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Unified Diff: lib/content/elemHideEmulation.js

Issue 29494577: Issue 5438 - Observer DOM changes to reapply filters. (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Patch Set: Review changes Created Aug. 25, 2017, 1:47 p.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | test/browser/elemHideEmulation.js » ('j') | test/browser/elemHideEmulation.js » ('J')
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: lib/content/elemHideEmulation.js
===================================================================
--- a/lib/content/elemHideEmulation.js
+++ b/lib/content/elemHideEmulation.js
@@ -14,17 +14,18 @@
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const {filterToRegExp, splitSelector} = require("common");
-const MIN_INVOCATION_INTERVAL = 3000;
+let MIN_INVOCATION_INTERVAL = 3000;
+const MAX_SYNCHRONOUS_PROCESSING_TIME = 50;
const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
/** Return position of node from parent.
* @param {Node} node the node to find the position of.
* @return {number} One-based index like for :nth-child(), or 0 on error.
*/
function positionInParent(node)
{
@@ -32,16 +33,18 @@
for (let i = 0; i < children.length; i++)
if (children[i] == node)
return i + 1;
return 0;
}
function makeSelector(node, selector)
{
+ if (node == null)
+ return null;
if (!node.parentElement)
{
let newSelector = ":root";
if (selector)
newSelector += " > " + selector;
return newSelector;
}
let idx = positionInParent(node);
@@ -123,17 +126,26 @@
{
if (index >= chain.length)
{
yield prefix;
return;
}
for (let [selector, element] of
chain[index].getSelectors(prefix, subtree, styles))
- yield* evaluate(chain, index + 1, selector, element, styles);
+ {
+ if (selector == null)
+ yield null;
+ else
+ yield* evaluate(chain, index + 1, selector, element, styles);
+ }
+ // Just in case the getSelectors() generator above had to run some heavy
+ // document.querySelectorAll() call which didn't produce any results, make
+ // sure there is at least one point where execution can pause.
+ yield null;
}
function PlainSelector(selector)
{
this._selector = selector;
}
PlainSelector.prototype = {
@@ -183,28 +195,34 @@
let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
prefix + "*" : prefix;
let elements = subtree.querySelectorAll(actualPrefix);
for (let element of elements)
{
let iter = evaluate(this._innerSelectors, 0, "", element, styles);
for (let selector of iter)
{
+ if (selector == null)
+ {
+ yield null;
+ continue;
+ }
if (relativeSelectorRegexp.test(selector))
selector = ":scope" + selector;
try
{
if (element.querySelector(selector))
yield element;
}
catch (e)
{
// :scope isn't supported on Edge, ignore error caused by it.
}
}
+ yield null;
}
}
};
function ContainsSelector(textContent)
{
this._text = textContent;
}
@@ -218,19 +236,24 @@
yield [makeSelector(element, ""), subtree];
},
*getElements(prefix, subtree, stylesheet)
{
let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
prefix + "*" : prefix;
let elements = subtree.querySelectorAll(actualPrefix);
+
for (let element of elements)
+ {
if (element.textContent.includes(this._text))
yield element;
+ else
+ yield null;
+ }
}
};
function PropsSelector(propertyExpression)
{
let regexpString;
if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
propertyExpression[propertyExpression.length - 1] == "/")
@@ -268,23 +291,30 @@
*getSelectors(prefix, subtree, styles)
{
for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
yield [selector, subtree];
}
};
+function isSelectorHidingOnlyPattern(pattern)
+{
+ return pattern.selectors.some(s => s.preferHideWithSelector) &&
+ !pattern.selectors.some(s => s.requiresHiding);
+}
+
function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
hideElemsFunc)
{
this.window = window;
this.getFiltersFunc = getFiltersFunc;
this.addSelectorsFunc = addSelectorsFunc;
this.hideElemsFunc = hideElemsFunc;
+ this.observer = new window.MutationObserver(this.observe.bind(this));
}
ElemHideEmulation.prototype = {
isSameOrigin(stylesheet)
{
try
{
return new URL(stylesheet.href).origin == this.window.location.origin;
@@ -357,30 +387,28 @@
new SyntaxError("Failed to parse Adblock Plus " +
`selector ${selector}, can't ` +
"have a lonely :-abp-contains()."));
return null;
}
return selectors;
},
- _lastInvocation: 0,
-
/**
* Processes the current document and applies all rules to it.
* @param {CSSStyleSheet[]} [stylesheets]
* The list of new stylesheets that have been added to the document and
* made reprocessing necessary. This parameter shouldn't be passed in for
* the initial processing, all of document's stylesheets will be considered
* then and all rules, including the ones not dependent on styles.
+ * @param {function} [done]
+ * Callback to call when done.
*/
- addSelectors(stylesheets)
+ _addSelectors(stylesheets, done)
{
- this._lastInvocation = Date.now();
-
let selectors = [];
let selectorFilters = [];
let elements = [];
let elementFilters = [];
let cssStyles = [];
@@ -407,72 +435,156 @@
if (rule.type != rule.STYLE_RULE)
continue;
cssStyles.push(stringifyStyle(rule));
}
}
let {document} = this.window;
- for (let pattern of this.patterns)
+
+ let patterns = this.patterns.slice();
+ let pattern = null;
+ let generator = null;
+
+ let processPatterns = () =>
{
- if (stylesheetOnlyChange &&
- !pattern.selectors.some(selector => selector.dependsOnStyles))
- {
- continue;
- }
+ let cycleStart = this.window.performance.now();
- for (let selector of evaluate(pattern.selectors,
- 0, "", document, cssStyles))
+ if (!pattern)
{
- if (pattern.selectors.some(s => s.preferHideWithSelector) &&
- !pattern.selectors.some(s => s.requiresHiding))
+ if (!patterns.length)
{
- selectors.push(selector);
- selectorFilters.push(pattern.text);
+ this.addSelectorsFunc(selectors, selectorFilters);
+ this.hideElemsFunc(elements, elementFilters);
+ if (typeof done == "function")
+ done();
+ return;
+ }
+
+ pattern = patterns.shift();
+
+ if (stylesheetOnlyChange &&
+ !pattern.selectors.some(selector => selector.dependsOnStyles))
+ {
+ pattern = null;
+ return processPatterns();
}
- else
+ generator = evaluate(pattern.selectors, 0, "", document, cssStyles);
+ }
+ for (let selector of generator)
+ {
+ if (selector != null)
{
- for (let element of document.querySelectorAll(selector))
+ if (isSelectorHidingOnlyPattern(pattern))
{
- elements.push(element);
- elementFilters.push(pattern.text);
+ selectors.push(selector);
+ selectorFilters.push(pattern.text);
+ }
+ else
+ {
+ for (let element of document.querySelectorAll(selector))
+ {
+ elements.push(element);
+ elementFilters.push(pattern.text);
+ }
}
}
+ if (this.window.performance.now() -
+ cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME)
+ {
+ this.window.setTimeout(processPatterns, 0);
+ return;
+ }
}
- }
+ pattern = null;
+ return processPatterns();
+ };
- this.addSelectorsFunc(selectors, selectorFilters);
- this.hideElemsFunc(elements, elementFilters);
+ processPatterns();
+ },
+
+ // this pair ot setter/getter is only used in the tests to
Wladimir Palant 2017/08/25 20:57:41 Nit: "This property is only used" (simpler and cap
hub 2017/08/26 01:45:06 Done.
+ // shorten the invocation interval
+ get MIN_INVOCATION_INTERVAL()
+ {
+ return MIN_INVOCATION_INTERVAL;
+ },
+
+ set MIN_INVOCATION_INTERVAL(interval)
+ {
+ MIN_INVOCATION_INTERVAL = interval;
},
- _stylesheetQueue: null,
+ _filteringInProgress: false,
+ _lastInvocation: -MIN_INVOCATION_INTERVAL,
+ _scheduledProcessing: null,
+
+ /**
+ * Re-run filtering either immediately or queued.
+ * @param {CSSStyleSheet[]} [stylesheets]
+ * new stylesheets to be processed. This parameter should be omitted
+ * for DOM modification (full reprocessing required).
+ */
+ queueFiltering(stylesheets)
+ {
+ let completion = () =>
+ {
+ this._lastInvocation = this.window.performance.now();
+ this._filteringInProgress = false;
+ if (this._scheduledProcessing)
+ {
+ let newStylesheets = this._scheduledProcessing.stylesheets;
+ this._scheduledProcessing = null;
+ this.queueFiltering(newStylesheets);
+ }
+ };
+
+ if (this._scheduledProcessing)
+ {
+ if (!stylesheets)
+ this._scheduledProcessing.stylesheets = null;
+ else if (this._scheduledProcessing.stylesheets)
+ this._scheduledProcessing.stylesheets.push(...stylesheets);
+ }
+ else if (this._filteringInProgress)
+ {
+ this._scheduledProcessing = {stylesheets};
+ }
+ else if (this.window.performance.now() -
+ this._lastInvocation < MIN_INVOCATION_INTERVAL)
+ {
+ this._scheduledProcessing = {stylesheets};
+ this.window.setTimeout(() =>
+ {
+ let newStylesheets = this._scheduledProcessing.stylesheets;
+ this._filteringInProgress = true;
+ this._scheduledProcessing = null;
+ this._addSelectors(newStylesheets, completion);
+ },
+ MIN_INVOCATION_INTERVAL -
+ (this.window.performance.now() - this._lastInvocation));
+ }
+ else
+ {
+ this._filteringInProgress = true;
+ this._addSelectors(stylesheets, completion);
+ }
+ },
onLoad(event)
{
let stylesheet = event.target.sheet;
if (stylesheet)
- {
- if (!this._stylesheetQueue &&
- Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
- {
- this._stylesheetQueue = [];
- this.window.setTimeout(() =>
- {
- let stylesheets = this._stylesheetQueue;
- this._stylesheetQueue = null;
- this.addSelectors(stylesheets);
- }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation));
- }
+ this.queueFiltering([stylesheet]);
+ },
- if (this._stylesheetQueue)
- this._stylesheetQueue.push(stylesheet);
- else
- this.addSelectors([stylesheet]);
- }
+ observe(mutations)
+ {
+ this.queueFiltering();
},
apply()
{
this.getFiltersFunc(patterns =>
{
this.patterns = [];
for (let pattern of patterns)
@@ -480,16 +592,25 @@
let selectors = this.parseSelector(pattern.selector);
if (selectors != null && selectors.length > 0)
this.patterns.push({selectors, text: pattern.text});
}
if (this.patterns.length > 0)
{
let {document} = this.window;
- this.addSelectors();
+ this.queueFiltering();
+ this.observer.observe(
+ document,
+ {
+ childList: true,
+ attributes: true,
+ characterData: true,
+ subtree: true
+ }
+ );
document.addEventListener("load", this.onLoad.bind(this), true);
}
});
}
};
exports.ElemHideEmulation = ElemHideEmulation;
« no previous file with comments | « no previous file | test/browser/elemHideEmulation.js » ('j') | test/browser/elemHideEmulation.js » ('J')

Powered by Google App Engine
This is Rietveld