 Issue 29712655:
  Issue 6437 - Skip styles-only patterns on DOM mutations  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore/
    
  
    Issue 29712655:
  Issue 6437 - Skip styles-only patterns on DOM mutations  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore/| Index: lib/content/elemHideEmulation.js | 
| =================================================================== | 
| --- a/lib/content/elemHideEmulation.js | 
| +++ b/lib/content/elemHideEmulation.js | 
| @@ -18,16 +18,24 @@ | 
| "use strict"; | 
| const {textToRegExp, filterToRegExp, splitSelector} = require("../common"); | 
| let MIN_INVOCATION_INTERVAL = 3000; | 
| const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; | 
| const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; | 
| +function getCachedPropertyValue(object, name, defaultValueFunc = () => {}) | 
| +{ | 
| + let value = object[name]; | 
| + if (value === undefined) | 
| 
hub
2018/03/14 13:04:03
there is the argument of value === undefined vs ty
 
Manish Jethani
2018/03/14 14:54:53
Oh yeah, for the sake of consistency though I've m
 | 
| + Object.defineProperty(object, name, {value: value = defaultValueFunc()}); | 
| + return value; | 
| +} | 
| + | 
| /** 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) | 
| { | 
| let {children} = node.parentNode; | 
| for (let i = 0; i < children.length; i++) | 
| @@ -239,16 +247,17 @@ | 
| function HasSelector(selectors) | 
| { | 
| this._innerSelectors = selectors; | 
| } | 
| HasSelector.prototype = { | 
| requiresHiding: true, | 
| + dependsOnDOM: true, | 
| get dependsOnStyles() | 
| { | 
| return this._innerSelectors.some(selector => selector.dependsOnStyles); | 
| }, | 
| get dependsOnCharacterData() | 
| { | 
| @@ -301,16 +310,17 @@ | 
| function ContainsSelector(textContent) | 
| { | 
| this._regexp = makeRegExpParameter(textContent); | 
| } | 
| ContainsSelector.prototype = { | 
| requiresHiding: true, | 
| + dependsOnDOM: true, | 
| dependsOnCharacterData: true, | 
| *getSelectors(prefix, subtree, styles) | 
| { | 
| for (let element of this.getElements(prefix, subtree, styles)) | 
| yield [makeSelector(element, ""), subtree]; | 
| }, | 
| @@ -372,47 +382,104 @@ | 
| *getSelectors(prefix, subtree, styles) | 
| { | 
| for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) | 
| yield [selector, subtree]; | 
| } | 
| }; | 
| -function isSelectorHidingOnlyPattern(pattern) | 
| +function Pattern(selectors, text) | 
| { | 
| - return pattern.selectors.some(s => s.preferHideWithSelector) && | 
| - !pattern.selectors.some(s => s.requiresHiding); | 
| + this.selectors = selectors; | 
| + this.text = text; | 
| +} | 
| + | 
| +Pattern.prototype = { | 
| + isSelectorHidingOnlyPattern() | 
| + { | 
| + return getCachedPropertyValue( | 
| + this, "_selectorHidingOnlyPattern", | 
| + () => this.selectors.some(selector => selector.preferHideWithSelector) && | 
| + !this.selectors.some(selector => selector.requiresHiding) | 
| + ); | 
| + }, | 
| + | 
| + get dependsOnStyles() | 
| + { | 
| + return getCachedPropertyValue( | 
| + this, "_dependsOnStyles", | 
| + () => this.selectors.some(selector => selector.dependsOnStyles) | 
| + ); | 
| + }, | 
| + | 
| + get dependsOnDOM() | 
| + { | 
| + return getCachedPropertyValue( | 
| + this, "_dependsOnDOM", | 
| + () => this.selectors.some(selector => selector.dependsOnDOM) | 
| + ); | 
| + }, | 
| + | 
| + get dependsOnStylesAndDOM() | 
| + { | 
| + return getCachedPropertyValue( | 
| + this, "_dependsOnStylesAndDOM", | 
| + () => this.selectors.some(selector => selector.dependsOnStyles && | 
| + selector.dependsOnDOM) | 
| + ); | 
| + }, | 
| + | 
| + get maybeDependsOnAttributes() | 
| + { | 
| + // Observe changes to attributes if either there's a plain selector that | 
| + // looks like an ID selector, class selector, or attribute selector in one | 
| + // of the patterns (e.g. "a[href='https://example.com/']") | 
| + // or there's a properties selector nested inside a has selector | 
| + // (e.g. "div:-abp-has(:-abp-properties(color: blue))") | 
| + return getCachedPropertyValue( | 
| + this, "_maybeDependsOnAttributes", | 
| + () => this.selectors.some( | 
| + selector => selector.maybeDependsOnAttributes || | 
| + (selector instanceof HasSelector && | 
| + selector.dependsOnStyles) | 
| + ) | 
| + ); | 
| + }, | 
| + | 
| + get dependsOnCharacterData() | 
| + { | 
| + // Observe changes to character data only if there's a contains selector in | 
| + // one of the patterns. | 
| + return getCachedPropertyValue( | 
| + this, "_dependsOnCharacterData", | 
| + () => this.selectors.some(selector => selector.dependsOnCharacterData) | 
| + ); | 
| + } | 
| +}; | 
| + | 
| +function filterPatterns(patterns, {stylesheets, mutations}) | 
| +{ | 
| + if (!stylesheets && !mutations) | 
| + return patterns.slice(); | 
| + | 
| + return patterns.filter( | 
| + pattern => (stylesheets && pattern.dependsOnStyles) || | 
| + (mutations && pattern.dependsOnDOM) | 
| + ); | 
| } | 
| function shouldObserveAttributes(patterns) | 
| { | 
| - // Observe changes to attributes if either there's a plain selector that | 
| - // looks like an ID selector, class selector, or attribute selector in one of | 
| - // the patterns (e.g. "a[href='https://example.com/']") | 
| - // or there's a properties selector nested inside a has selector | 
| - // (e.g. "div:-abp-has(:-abp-properties(color: blue))") | 
| - return patterns.some( | 
| - pattern => pattern.selectors.some( | 
| - selector => selector.maybeDependsOnAttributes || | 
| - (selector instanceof HasSelector && | 
| - selector.dependsOnStyles) | 
| - ) | 
| - ); | 
| + return patterns.some(pattern => pattern.maybeDependsOnAttributes); | 
| } | 
| function shouldObserveCharacterData(patterns) | 
| { | 
| - // Observe changes to character data only if there's a contains selector in | 
| - // one of the patterns. | 
| - return patterns.some( | 
| - pattern => pattern.selectors.some( | 
| - selector => selector.dependsOnCharacterData | 
| - ) | 
| - ); | 
| + return patterns.some(pattern => pattern.dependsOnCharacterData); | 
| } | 
| function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) | 
| { | 
| this.document = document; | 
| this.addSelectorsFunc = addSelectorsFunc; | 
| this.hideElemsFunc = hideElemsFunc; | 
| this.observer = new MutationObserver(this.observe.bind(this)); | 
| @@ -497,34 +564,51 @@ | 
| /** | 
| * 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 {MutationRecord[]} [mutations] | 
| + * The list of DOM mutations that have been applied to the document and | 
| + * made reprocessing necessary. This parameter shouldn't be passed in for | 
| + * the initial processing, the entire document will be considered | 
| + * then and all rules, including the ones not dependent on the DOM. | 
| * @param {function} [done] | 
| * Callback to call when done. | 
| */ | 
| - _addSelectors(stylesheets, done) | 
| + _addSelectors(stylesheets, mutations, done) | 
| { | 
| + let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); | 
| + | 
| let selectors = []; | 
| let selectorFilters = []; | 
| let elements = []; | 
| let elementFilters = []; | 
| let cssStyles = []; | 
| - let stylesheetOnlyChange = !!stylesheets; | 
| - if (!stylesheets) | 
| + // If neither any style sheets nor any DOM mutations have been specified, | 
| + // do full processing. | 
| + if (!stylesheets && !mutations) | 
| stylesheets = this.document.styleSheets; | 
| - for (let stylesheet of stylesheets) | 
| + // If there are any DOM mutations and any of the patterns depends on both | 
| + // style sheets and the DOM (e.g. -abp-has(-abp-properties)), find all the | 
| + // rules in every style sheet in the document, because we need to run | 
| + // querySelectorAll afterwards. On the other hand, if we only have patterns | 
| + // that depend on either styles or DOM both not both | 
| + // (e.g. -abp-properties or -abp-contains), we can skip this part. | 
| + if (mutations && patterns.some(pattern => pattern.dependsOnStylesAndDOM)) | 
| + stylesheets = this.document.styleSheets; | 
| + | 
| + for (let stylesheet of stylesheets || []) | 
| { | 
| // Explicitly ignore third-party stylesheets to ensure consistent behavior | 
| // between Firefox and Chrome. | 
| if (!this.isSameOrigin(stylesheet)) | 
| continue; | 
| let rules = stylesheet.cssRules; | 
| if (!rules) | 
| @@ -534,51 +618,46 @@ | 
| { | 
| if (rule.type != rule.STYLE_RULE) | 
| continue; | 
| cssStyles.push(stringifyStyle(rule)); | 
| } | 
| } | 
| - let patterns = this.patterns.slice(); | 
| let pattern = null; | 
| let generator = null; | 
| let processPatterns = () => | 
| { | 
| let cycleStart = performance.now(); | 
| if (!pattern) | 
| { | 
| if (!patterns.length) | 
| { | 
| - this.addSelectorsFunc(selectors, selectorFilters); | 
| - this.hideElemsFunc(elements, elementFilters); | 
| + if (selectors.length > 0) | 
| 
Manish Jethani
2018/03/08 21:08:15
We need to do this because of an existing bug, whi
 
Manish Jethani
2018/03/08 21:28:34
Sorry, I just checked, there is no bug. In any cas
 | 
| + this.addSelectorsFunc(selectors, selectorFilters); | 
| + if (elements.length > 0) | 
| + 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(); | 
| - } | 
| generator = evaluate(pattern.selectors, 0, "", | 
| this.document, cssStyles); | 
| } | 
| for (let selector of generator) | 
| { | 
| if (selector != null) | 
| { | 
| - if (isSelectorHidingOnlyPattern(pattern)) | 
| + if (pattern.isSelectorHidingOnlyPattern()) | 
| { | 
| selectors.push(selector); | 
| selectorFilters.push(pattern.text); | 
| } | 
| else | 
| { | 
| for (let element of this.document.querySelectorAll(selector)) | 
| { | 
| @@ -615,95 +694,113 @@ | 
| _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). | 
| + * for full reprocessing. | 
| + * @param {MutationRecord[]} [mutations] | 
| + * new DOM mutations to be processed. This parameter should be omitted | 
| + * for full reprocessing. | 
| */ | 
| - queueFiltering(stylesheets) | 
| + queueFiltering(stylesheets, mutations) | 
| { | 
| let completion = () => | 
| { | 
| this._lastInvocation = performance.now(); | 
| this._filteringInProgress = false; | 
| if (this._scheduledProcessing) | 
| { | 
| - let newStylesheets = this._scheduledProcessing.stylesheets; | 
| + let params = Object.assign({}, this._scheduledProcessing); | 
| this._scheduledProcessing = null; | 
| - this.queueFiltering(newStylesheets); | 
| + this.queueFiltering(params.stylesheets, params.mutations); | 
| } | 
| }; | 
| if (this._scheduledProcessing) | 
| { | 
| - if (!stylesheets) | 
| - this._scheduledProcessing.stylesheets = null; | 
| - else if (this._scheduledProcessing.stylesheets) | 
| - this._scheduledProcessing.stylesheets.push(...stylesheets); | 
| + if (!stylesheets && !mutations) | 
| + { | 
| + this._scheduledProcessing = {}; | 
| + } | 
| + else | 
| + { | 
| + if (stylesheets) | 
| + { | 
| + if (!this._scheduledProcessing.stylesheets) | 
| + this._scheduledProcessing.stylesheets = []; | 
| + this._scheduledProcessing.stylesheets.push(...stylesheets); | 
| + } | 
| + if (mutations) | 
| + { | 
| + if (!this._scheduledProcessing.mutations) | 
| + this._scheduledProcessing.mutations = []; | 
| + this._scheduledProcessing.mutations.push(...mutations); | 
| + } | 
| + } | 
| } | 
| else if (this._filteringInProgress) | 
| { | 
| - this._scheduledProcessing = {stylesheets}; | 
| + this._scheduledProcessing = {stylesheets, mutations}; | 
| } | 
| else if (performance.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) | 
| { | 
| - this._scheduledProcessing = {stylesheets}; | 
| + this._scheduledProcessing = {stylesheets, mutations}; | 
| setTimeout(() => | 
| { | 
| - let newStylesheets = this._scheduledProcessing.stylesheets; | 
| + let params = Object.assign({}, this._scheduledProcessing); | 
| this._filteringInProgress = true; | 
| this._scheduledProcessing = null; | 
| - this._addSelectors(newStylesheets, completion); | 
| + this._addSelectors(params.stylesheets, params.mutations, completion); | 
| }, | 
| MIN_INVOCATION_INTERVAL - (performance.now() - this._lastInvocation)); | 
| } | 
| else if (this.document.readyState == "loading") | 
| { | 
| - this._scheduledProcessing = {stylesheets}; | 
| + this._scheduledProcessing = {stylesheets, mutations}; | 
| let handler = () => | 
| { | 
| document.removeEventListener("DOMContentLoaded", handler); | 
| - let newStylesheets = this._scheduledProcessing.stylesheets; | 
| + let params = Object.assign({}, this._scheduledProcessing); | 
| this._filteringInProgress = true; | 
| this._scheduledProcessing = null; | 
| - this._addSelectors(newStylesheets, completion); | 
| + this._addSelectors(params.stylesheets, params.mutations, completion); | 
| }; | 
| document.addEventListener("DOMContentLoaded", handler); | 
| } | 
| else | 
| { | 
| this._filteringInProgress = true; | 
| - this._addSelectors(stylesheets, completion); | 
| + this._addSelectors(stylesheets, mutations, completion); | 
| } | 
| }, | 
| onLoad(event) | 
| { | 
| let stylesheet = event.target.sheet; | 
| if (stylesheet) | 
| this.queueFiltering([stylesheet]); | 
| }, | 
| observe(mutations) | 
| { | 
| - this.queueFiltering(); | 
| + this.queueFiltering(null, mutations); | 
| }, | 
| apply(patterns) | 
| { | 
| this.patterns = []; | 
| for (let pattern of patterns) | 
| { | 
| let selectors = this.parseSelector(pattern.selector); | 
| if (selectors != null && selectors.length > 0) | 
| - this.patterns.push({selectors, text: pattern.text}); | 
| + this.patterns.push(new Pattern(selectors, pattern.text)); | 
| } | 
| if (this.patterns.length > 0) | 
| { | 
| this.queueFiltering(); | 
| this.observer.observe( | 
| this.document, | 
| { |