| Index: lib/content/elemHideEmulation.js | 
| =================================================================== | 
| --- a/lib/content/elemHideEmulation.js | 
| +++ b/lib/content/elemHideEmulation.js | 
| @@ -19,16 +19,34 @@ | 
| const {textToRegExp, filterToRegExp, splitSelector} = require("../common"); | 
| const {indexOf} = require("../coreUtils"); | 
| let MIN_INVOCATION_INTERVAL = 3000; | 
| const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; | 
| const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; | 
| +let debugInfo = null; | 
| + | 
| +function setDebugMode() | 
| +{ | 
| + debugInfo = { | 
| + lastProcessedElements: new Set() | 
| + }; | 
| +} | 
| + | 
| +exports.setDebugMode = setDebugMode; | 
| + | 
| +function getDebugInfo() | 
| +{ | 
| + return debugInfo; | 
| +} | 
| + | 
| +exports.getDebugInfo = getDebugInfo; | 
| + | 
| function getCachedPropertyValue(object, name, defaultValueFunc = () => {}) | 
| { | 
| let value = object[name]; | 
| if (typeof value == "undefined") | 
| Object.defineProperty(object, name, {value: value = defaultValueFunc()}); | 
| return value; | 
| } | 
| @@ -194,53 +212,55 @@ | 
| return new RegExp(pattern, flags); | 
| } | 
| catch (e) | 
| { | 
| } | 
| return null; | 
| } | 
| -function* evaluate(chain, index, prefix, subtree, styles) | 
| +function* evaluate(chain, index, prefix, subtree, styles, targets) | 
| { | 
| if (index >= chain.length) | 
| { | 
| yield prefix; | 
| return; | 
| } | 
| for (let [selector, element] of | 
| - chain[index].getSelectors(prefix, subtree, styles)) | 
| + chain[index].getSelectors(prefix, subtree, styles, targets)) | 
| { | 
| if (selector == null) | 
| yield null; | 
| else | 
| - yield* evaluate(chain, index + 1, selector, element, styles); | 
| + yield* evaluate(chain, index + 1, selector, element, styles, targets); | 
| } | 
| // 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; | 
| this.maybeDependsOnAttributes = /[#.]|\[.+\]/.test(selector); | 
| this.dependsOnDOM = this.maybeDependsOnAttributes; | 
| + this.maybeContainsSiblingCombinators = /[~+]/.test(selector); | 
| } | 
| PlainSelector.prototype = { | 
| /** | 
| * Generator function returning a pair of selector | 
| * string and subtree. | 
| * @param {string} prefix the prefix for the selector. | 
| * @param {Node} subtree the subtree we work on. | 
| * @param {StringifiedStyle[]} styles the stringified style objects. | 
| + * @param {Node[]} [targets] the nodes we are interested in. | 
| */ | 
| - *getSelectors(prefix, subtree, styles) | 
| + *getSelectors(prefix, subtree, styles, targets) | 
| { | 
| yield [prefix + this._selector, subtree]; | 
| } | 
| }; | 
| const incompletePrefixRegexp = /[\s>+~]$/; | 
| function HasSelector(selectors) | 
| @@ -265,67 +285,81 @@ | 
| get maybeDependsOnAttributes() | 
| { | 
| return this._innerSelectors.some( | 
| selector => selector.maybeDependsOnAttributes | 
| ); | 
| }, | 
| - *getSelectors(prefix, subtree, styles) | 
| + *getSelectors(prefix, subtree, styles, targets) | 
| { | 
| - for (let element of this.getElements(prefix, subtree, styles)) | 
| + for (let element of this.getElements(prefix, subtree, styles, targets)) | 
| yield [makeSelector(element), element]; | 
| }, | 
| /** | 
| * Generator function returning selected elements. | 
| * @param {string} prefix the prefix for the selector. | 
| * @param {Node} subtree the subtree we work on. | 
| * @param {StringifiedStyle[]} styles the stringified style objects. | 
| + * @param {Node[]} [targets] the nodes we are interested in. | 
| */ | 
| - *getElements(prefix, subtree, styles) | 
| + *getElements(prefix, subtree, styles, targets) | 
| { | 
| let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? | 
| prefix + "*" : prefix; | 
| let elements = scopedQuerySelectorAll(subtree, actualPrefix); | 
| if (elements) | 
| { | 
| for (let element of elements) | 
| { | 
| - let iter = evaluate(this._innerSelectors, 0, "", element, styles); | 
| + // If the element is neither an ancestor nor a descendant of one of the | 
| + // targets, we can skip it. | 
| + if (targets && !targets.some(target => element.contains(target) || | 
| + target.contains(element))) | 
| + { | 
| + yield null; | 
| + continue; | 
| + } | 
| + | 
| + let iter = evaluate(this._innerSelectors, 0, "", element, styles, | 
| + targets); | 
| for (let selector of iter) | 
| { | 
| if (selector == null) | 
| yield null; | 
| else if (scopedQuerySelector(element, selector)) | 
| yield element; | 
| } | 
| yield null; | 
| + | 
| + if (debugInfo) | 
| + debugInfo.lastProcessedElements.add(element); | 
| } | 
| } | 
| } | 
| }; | 
| function ContainsSelector(textContent) | 
| { | 
| this._regexp = makeRegExpParameter(textContent); | 
| } | 
| ContainsSelector.prototype = { | 
| dependsOnDOM: true, | 
| dependsOnCharacterData: true, | 
| - *getSelectors(prefix, subtree, styles) | 
| + *getSelectors(prefix, subtree, styles, targets) | 
| { | 
| - for (let element of this.getElements(prefix, subtree, styles)) | 
| + for (let element of this.getElements(prefix, subtree, styles, targets)) | 
| yield [makeSelector(element), subtree]; | 
| }, | 
| - *getElements(prefix, subtree, styles) | 
| + *getElements(prefix, subtree, styles, targets) | 
| { | 
| let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? | 
| prefix + "*" : prefix; | 
| let elements = scopedQuerySelectorAll(subtree, actualPrefix); | 
| if (elements) | 
| { | 
| @@ -338,20 +372,30 @@ | 
| if (lastRoot && lastRoot.contains(element)) | 
| { | 
| yield null; | 
| continue; | 
| } | 
| lastRoot = element; | 
| + if (targets && !targets.some(target => element.contains(target) || | 
| + target.contains(element))) | 
| + { | 
| + yield null; | 
| + continue; | 
| + } | 
| + | 
| if (this._regexp && this._regexp.test(element.textContent)) | 
| yield element; | 
| else | 
| yield null; | 
| + | 
| + if (debugInfo) | 
| + debugInfo.lastProcessedElements.add(element); | 
| } | 
| } | 
| } | 
| }; | 
| function PropsSelector(propertyExpression) | 
| { | 
| let regexpString; | 
| @@ -383,17 +427,17 @@ | 
| } | 
| let idx = subSelector.lastIndexOf("::"); | 
| if (idx != -1) | 
| subSelector = subSelector.substr(0, idx); | 
| yield prefix + subSelector; | 
| } | 
| }, | 
| - *getSelectors(prefix, subtree, styles) | 
| + *getSelectors(prefix, subtree, styles, targets) | 
| { | 
| for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) | 
| yield [selector, subtree]; | 
| } | 
| }; | 
| function Pattern(selectors, text) | 
| { | 
| @@ -449,16 +493,25 @@ | 
| // 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) | 
| ); | 
| }, | 
| + get maybeContainsSiblingCombinators() | 
| + { | 
| + return getCachedPropertyValue( | 
| + this, "_maybeContainsSiblingCombinators", | 
| + () => this.selectors.some(selector => | 
| + selector.maybeContainsSiblingCombinators) | 
| + ); | 
| + }, | 
| + | 
| matchesMutationTypes(mutationTypes) | 
| { | 
| let mutationTypeMatchMap = getCachedPropertyValue( | 
| this, "_mutationTypeMatchMap", | 
| () => new Map([ | 
| // All types of DOM-dependent patterns are affected by mutations of | 
| // type "childList". | 
| ["childList", true], | 
| @@ -489,16 +542,41 @@ | 
| // "childList". | 
| if (types.size == 3) | 
| break; | 
| } | 
| return types; | 
| } | 
| +function extractMutationTargets(mutations) | 
| +{ | 
| + if (!mutations) | 
| + return null; | 
| + | 
| + let targets = new Set(); | 
| + | 
| + for (let mutation of mutations) | 
| + { | 
| + if (mutation.type == "childList") | 
| + { | 
| + // When new nodes are added, we're interested in the added nodes rather | 
| + // than the parent. | 
| + for (let node of mutation.addedNodes) | 
| + targets.add(node); | 
| + } | 
| + else | 
| + { | 
| + targets.add(mutation.target); | 
| + } | 
| + } | 
| + | 
| + return [...targets]; | 
| +} | 
| + | 
| function filterPatterns(patterns, {stylesheets, mutations}) | 
| { | 
| if (!stylesheets && !mutations) | 
| return patterns.slice(); | 
| let mutationTypes = mutations ? extractMutationTypes(mutations) : null; | 
| return patterns.filter( | 
| @@ -616,16 +694,19 @@ | 
| * 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, mutations, done) | 
| { | 
| + if (debugInfo) | 
| + debugInfo.lastProcessedElements.clear(); | 
| + | 
| let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); | 
| let selectors = []; | 
| let selectorFilters = []; | 
| let elements = []; | 
| let elementFilters = []; | 
| @@ -673,16 +754,18 @@ | 
| { | 
| if (rule.type != rule.STYLE_RULE) | 
| continue; | 
| cssStyles.push(stringifyStyle(rule)); | 
| } | 
| } | 
| + let targets = extractMutationTargets(mutations); | 
| + | 
| let pattern = null; | 
| let generator = null; | 
| let processPatterns = () => | 
| { | 
| let cycleStart = performance.now(); | 
| if (!pattern) | 
| @@ -695,18 +778,32 @@ | 
| this.hideElemsFunc(elements, elementFilters); | 
| if (typeof done == "function") | 
| done(); | 
| return; | 
| } | 
| pattern = patterns.shift(); | 
| + let evaluationTargets = targets; | 
| + | 
| + // If the pattern appears to contain any sibling combinators, we can't | 
| + // easily optimize based on the mutation targets. Since this is a | 
| + // special case, skip the optimization. By setting it to null here we | 
| + // make sure we process the entire DOM. | 
| + if (pattern.maybeContainsSiblingCombinators) | 
| + evaluationTargets = null; | 
| + | 
| + // Ignore mutation targets when using style sheets, because we may have | 
| + // to update all the CSS selectors. | 
| + if (!this.useInlineStyles) | 
| + evaluationTargets = null; | 
| + | 
| generator = evaluate(pattern.selectors, 0, "", | 
| - this.document, cssStyles); | 
| + this.document, cssStyles, evaluationTargets); | 
| } | 
| for (let selector of generator) | 
| { | 
| if (selector != null) | 
| { | 
| if (!this.useInlineStyles) | 
| { | 
| selectors.push(selector); | 
| @@ -836,16 +933,34 @@ | 
| { | 
| let stylesheet = event.target.sheet; | 
| if (stylesheet) | 
| this.queueFiltering([stylesheet]); | 
| }, | 
| observe(mutations) | 
| { | 
| + if (debugInfo) | 
| + { | 
| + // In debug mode, filter out any mutations likely done by us | 
| + // (i.e. style="display: none !important"). This makes it easier to | 
| + // observe how the code responds to DOM mutations. | 
| + // | 
| + // Note: This also creates the potential for "heisenbugs", but as long as | 
| + // this is the only instance it should be manageable. | 
| + mutations = mutations.filter( | 
| + ({type, attributeName, target: {style: newValue}, oldValue}) => | 
| + !(type == "attributes" && attributeName == "style" && | 
| + newValue.display == "none" && oldValue.display != "none") | 
| + ); | 
| + | 
| + if (mutations.length == 0) | 
| + return; | 
| + } | 
| + | 
| this.queueFiltering(null, mutations); | 
| }, | 
| apply(patterns) | 
| { | 
| this.patterns = []; | 
| for (let pattern of patterns) | 
| { |