| Index: lib/content/elemHideEmulation.js | 
| =================================================================== | 
| --- a/lib/content/elemHideEmulation.js | 
| +++ b/lib/content/elemHideEmulation.js | 
| @@ -15,16 +15,17 @@ | 
| * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. | 
| */ | 
|  | 
| "use strict"; | 
|  | 
| const {filterToRegExp, splitSelector} = require("common"); | 
|  | 
| const 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,144 @@ | 
| 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(); | 
| }, | 
|  | 
| -  _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 +580,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; | 
|  |