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

Delta Between Two Patch Sets: lib/elemHide.js

Issue 29773570: Issue 6652 - Implement fast selector lookups for unknown domains (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Left Patch Set: Created May 7, 2018, 3:38 p.m.
Right Patch Set: Avoid unnecessary Array.concat Created May 23, 2018, 12:22 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « no previous file | test/elemHide.js » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 /* 1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>, 2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-present eyeo GmbH 3 * Copyright (C) 2006-present eyeo GmbH
4 * 4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify 5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as 6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation. 7 * published by the Free Software Foundation.
8 * 8 *
9 * Adblock Plus is distributed in the hope that it will be useful, 9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details. 12 * GNU General Public License for more details.
13 * 13 *
14 * You should have received a copy of the GNU General Public License 14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */ 16 */
17 17
18 "use strict"; 18 "use strict";
19 19
20 /** 20 /**
21 * @fileOverview Element hiding implementation. 21 * @fileOverview Element hiding implementation.
22 */ 22 */
23 23
24 const {ElemHideException} = require("./filterClasses"); 24 const {ElemHideException} = require("./filterClasses");
25 const {FilterNotifier} = require("./filterNotifier"); 25 const {FilterNotifier} = require("./filterNotifier");
26 26
27 /** 27 /**
28 * Lookup table, active flag, by filter by domain. 28 * Lookup table, active flag, by filter by domain.
29 * (Only contains filters that aren't unconditionally matched for all domains.) 29 * (Only contains filters that aren't unconditionally matched for all domains.)
30 * @type {Map.<string,?Map.<Filter,boolean>>} 30 * @type {Map.<string,?Map.<Filter,boolean>>}
Manish Jethani 2018/05/07 16:03:02 The value here can now be null.
31 */ 31 */
32 let filtersByDomain = new Map(); 32 let filtersByDomain = new Map();
33 33
34 /** 34 /**
35 * Lookup table, filter by selector. (Only used for selectors that are 35 * Lookup table, filter by selector. (Only used for selectors that are
36 * unconditionally matched for all domains.) 36 * unconditionally matched for all domains.)
37 * @type {Map.<string,Filter>} 37 * @type {Map.<string,Filter>}
38 */ 38 */
39 let filterBySelector = new Map(); 39 let filterBySelector = new Map();
40 40
41 /** 41 /**
42 * This array caches the keys of filterBySelector table (selectors 42 * This array caches the keys of filterBySelector table (selectors
43 * which unconditionally apply on all domains). It will be null if the 43 * which unconditionally apply on all domains). It will be null if the
44 * cache needs to be rebuilt. 44 * cache needs to be rebuilt.
45 * @type {?string[]}
45 */ 46 */
46 let unconditionalSelectors = null; 47 let unconditionalSelectors = null;
47 48
48 /** 49 /**
49 * Map to be used instead when a filter has a blank domains property. 50 * Map to be used instead when a filter has a blank domains property.
51 * @type {Map.<string,boolean>}
52 * @const
50 */ 53 */
51 let defaultDomains = new Map([["", true]]); 54 let defaultDomains = new Map([["", true]]);
52 55
53 /** 56 /**
54 * Set containing known element hiding and exception filters 57 * Set containing known element hiding and exception filters
55 * @type {Set.<ElemHideBase>} 58 * @type {Set.<ElemHideBase>}
56 */ 59 */
57 let knownFilters = new Set(); 60 let knownFilters = new Set();
58 61
59 /** 62 /**
60 * Lookup table, lists of element hiding exceptions by selector 63 * Lookup table, lists of element hiding exceptions by selector
61 * @type {Map.<string,Filter>} 64 * @type {Map.<string,Filter[]>}
62 */ 65 */
63 let exceptions = new Map(); 66 let exceptions = new Map();
64 67
65 /* 68 /**
66 * Set containing selectors with generic exceptions 69 * Lookup table, lists of generic element hiding exceptions by selector
67 * @type {Set.<string>} 70 * @type {Map.<string,Filter[]>}
68 */ 71 */
69 let genericExceptionSelectors = new Set(); 72 let genericExceptions = new Map();
70 73
71 /* 74 /**
72 * Checks if a domain is known 75 * List of selectors that apply on any unknown domain
73 * @param {string} domain 76 * @type {?string[]}
74 * @returns {boolean} 77 */
75 */ 78 let conditionalGenericSelectors = null;
76 function isDomainKnown(domain) 79
80 /**
81 * Adds a filter to the lookup table of filters by domain.
82 * @param {Filter} filter
83 */
84 function addToFiltersByDomain(filter)
77 { 85 {
86 let domains = filter.domains || defaultDomains;
87 if (filter instanceof ElemHideException)
88 {
89 for (let domain of domains.keys())
90 {
91 // Add an entry for each domain, but without any filters. This makes
92 // the domain "known" and helps us avoid certain optimizations that
93 // would otherwise yield incorrect results.
94 if (domain != "" && !filtersByDomain.has(domain))
95 filtersByDomain.set(domain, null);
96 }
97 }
98 else
99 {
100 for (let [domain, isIncluded] of domains)
101 {
102 // There's no need to note that a filter is generically disabled.
103 if (!isIncluded && domain == "")
104 continue;
105
106 let filters = filtersByDomain.get(domain);
107 if (!filters)
108 filtersByDomain.set(domain, filters = new Map());
109 filters.set(filter, isIncluded);
110 }
111 }
112 }
113
114 /**
115 * Returns a list of domain-specific filters matching a domain
116 * @param {string} [domain]
117 * @returns {Array.<?Map.<Filter,boolean>>}
118 */
119 function getSpecificFiltersForDomain(domain)
120 {
121 let filtersList = [];
122
123 if (domain)
124 domain = domain.toUpperCase();
125
78 while (domain) 126 while (domain)
79 { 127 {
80 // A domain is "known" if we have seen any filters that would apply to it. 128 let filters = filtersByDomain.get(domain);
81 // For example, given the filters "##foo" and "example.com#@#foo", 129 if (typeof filters != "undefined")
82 // example.com is a known domain, as is mail.example.com and any other 130 filtersList.push(filters);
83 // subdomains of example.com.
84 if (filtersByDomain.has(domain))
85 return true;
86 131
87 let nextDot = domain.indexOf("."); 132 let nextDot = domain.indexOf(".");
88 domain = nextDot == -1 ? null : domain.substring(nextDot + 1); 133 domain = nextDot == -1 ? null : domain.substring(nextDot + 1);
89 } 134 }
90 135
91 return false; 136 return filtersList;
92 } 137 }
93 138
94 /* 139 /**
140 * Returns a list of selectors that apply on a domain from a given list of
141 * filters
142 * @param {string} [domain]
143 * @param {Array.<?Map.<Filter,boolean>>} filtersList
144 * @param {Set.<Filter>} excludeSet
145 * @returns {string[]}
146 */
147 function matchSelectors(domain, filtersList, excludeSet)
148 {
149 let matches = [];
150
151 // This code is a performance hot-spot, which is why we've made certain
152 // micro-optimisations. Please be careful before making changes.
153 for (let i = 0; i < filtersList.length; i++)
154 {
155 let filters = filtersList[i];
156 if (filters)
157 {
158 for (let [filter, isIncluded] of filters)
159 {
160 if (!isIncluded)
161 {
162 excludeSet.add(filter);
163 }
164 else if ((excludeSet.size == 0 || !excludeSet.has(filter)) &&
165 !exports.ElemHide.getException(filter, domain))
166 {
167 matches.push(filter.selector);
168 }
169 }
170 }
171 }
172
173 return matches;
174 }
175
176 /**
95 * Returns a list of selectors that apply on any unknown domain 177 * Returns a list of selectors that apply on any unknown domain
Manish Jethani 2018/05/07 16:03:02 There are generic selectors that are "conditional"
96 * @returns {string[]} 178 * @returns {string[]}
97 */ 179 */
98 function getConditionalGenericSelectors() 180 function getConditionalGenericSelectors()
Manish Jethani 2018/05/07 16:10:33 To give you some numbers, there are 18,300 uncondi
99 { 181 {
100 let selectors = []; 182 if (conditionalGenericSelectors)
183 return conditionalGenericSelectors;
184
185 conditionalGenericSelectors = [];
101 186
102 let filters = filtersByDomain.get(""); 187 let filters = filtersByDomain.get("");
103 if (!filters) 188 if (!filters)
104 return selectors; 189 return conditionalGenericSelectors;
105 190
106 for (let {selector} of filters.keys()) 191 for (let {selector} of filters.keys())
107 { 192 {
108 if (!genericExceptionSelectors.has(selector)) 193 if (genericExceptions.size == 0 || !genericExceptions.has(selector))
Manish Jethani 2018/05/07 16:03:02 Here we have to check for generic exceptions like
109 selectors.push(selector); 194 conditionalGenericSelectors.push(selector);
110 } 195 }
111 196
112 return selectors; 197 return conditionalGenericSelectors;
198 }
199
200 /**
201 * Returns a list of selectors that apply on each website unconditionally.
202 * @returns {string[]}
203 */
204 function getUnconditionalSelectors()
205 {
206 if (!unconditionalSelectors)
207 unconditionalSelectors = [...filterBySelector.keys()];
208
209 return unconditionalSelectors;
113 } 210 }
114 211
115 /** 212 /**
116 * Container for element hiding filters 213 * Container for element hiding filters
117 * @class 214 * @class
118 */ 215 */
119 let ElemHide = exports.ElemHide = { 216 exports.ElemHide = {
120 /** 217 /**
121 * Removes all known filters 218 * Removes all known filters
122 */ 219 */
123 clear() 220 clear()
124 { 221 {
125 for (let collection of [filtersByDomain, filterBySelector, 222 for (let collection of [filtersByDomain, filterBySelector,
126 knownFilters, exceptions, 223 knownFilters, exceptions,
127 genericExceptionSelectors]) 224 genericExceptions])
128 { 225 {
129 collection.clear(); 226 collection.clear();
130 } 227 }
131 unconditionalSelectors = null; 228 unconditionalSelectors = null;
229 conditionalGenericSelectors = null;
132 FilterNotifier.emit("elemhideupdate"); 230 FilterNotifier.emit("elemhideupdate");
133 },
134
135 _addToFiltersByDomain(filter)
136 {
137 let domains = filter.domains || defaultDomains;
138 if (filter instanceof ElemHideException)
139 {
140 for (let domain of domains.keys())
141 {
142 // Add an entry for each domain, but without any filters. This makes
143 // the domain "known" and helps us avoid the optimized path (which
144 // would give incorrect results).
145 if (domain != "" && !filtersByDomain.has(domain))
Manish Jethani 2018/05/07 16:03:02 We could have had a separate knownExceptionDomains
146 filtersByDomain.set(domain, null);
147 }
148 }
149 else
150 {
151 for (let [domain, isIncluded] of domains)
152 {
153 // There's no need to note that a filter is generically disabled.
154 if (!isIncluded && domain == "")
155 continue;
156
157 let filters = filtersByDomain.get(domain);
158 if (!filters)
159 filtersByDomain.set(domain, filters = new Map());
160 filters.set(filter, isIncluded);
161 }
162 }
163 }, 231 },
164 232
165 /** 233 /**
166 * Add a new element hiding filter 234 * Add a new element hiding filter
167 * @param {ElemHideBase} filter 235 * @param {ElemHideBase} filter
168 */ 236 */
169 add(filter) 237 add(filter)
170 { 238 {
171 if (knownFilters.has(filter)) 239 if (knownFilters.has(filter))
172 return; 240 return;
173 241
242 conditionalGenericSelectors = null;
243
174 if (filter instanceof ElemHideException) 244 if (filter instanceof ElemHideException)
175 { 245 {
176 let {selector, domains} = filter; 246 let {selector, domains} = filter;
177 247
178 let list = exceptions.get(selector); 248 let list = exceptions.get(selector);
179 if (list) 249 if (list)
180 list.push(filter); 250 list.push(filter);
181 else 251 else
182 exceptions.set(selector, [filter]); 252 exceptions.set(selector, [filter]);
183 253
184 if (domains) 254 if (domains)
Manish Jethani 2018/05/07 16:03:02 For exceptions too we should remember the domains,
185 this._addToFiltersByDomain(filter); 255 addToFiltersByDomain(filter);
186 256
187 if (filter.isGeneric()) 257 if (filter.isGeneric())
188 genericExceptionSelectors.add(filter.selector); 258 {
259 list = genericExceptions.get(selector);
260 if (list)
261 list.push(filter);
262 else
263 genericExceptions.set(selector, [filter]);
264 }
189 265
190 // If this is the first exception for a previously unconditionally 266 // If this is the first exception for a previously unconditionally
191 // applied element hiding selector we need to take care to update the 267 // applied element hiding selector we need to take care to update the
192 // lookups. 268 // lookups.
193 let unconditionalFilterForSelector = filterBySelector.get(selector); 269 let unconditionalFilterForSelector = filterBySelector.get(selector);
194 if (unconditionalFilterForSelector) 270 if (unconditionalFilterForSelector)
195 { 271 {
196 this._addToFiltersByDomain(unconditionalFilterForSelector); 272 addToFiltersByDomain(unconditionalFilterForSelector);
197 filterBySelector.delete(selector); 273 filterBySelector.delete(selector);
198 unconditionalSelectors = null; 274 unconditionalSelectors = null;
199 } 275 }
200 } 276 }
201 else if (!(filter.domains || exceptions.has(filter.selector))) 277 else if (!(filter.domains || exceptions.has(filter.selector)))
202 { 278 {
203 // The new filter's selector is unconditionally applied to all domains 279 // The new filter's selector is unconditionally applied to all domains
204 filterBySelector.set(filter.selector, filter); 280 filterBySelector.set(filter.selector, filter);
205 unconditionalSelectors = null; 281 unconditionalSelectors = null;
206 } 282 }
207 else 283 else
208 { 284 {
209 // The new filter's selector only applies to some domains 285 // The new filter's selector only applies to some domains
210 this._addToFiltersByDomain(filter); 286 addToFiltersByDomain(filter);
211 } 287 }
212 288
213 knownFilters.add(filter); 289 knownFilters.add(filter);
214 FilterNotifier.emit("elemhideupdate"); 290 FilterNotifier.emit("elemhideupdate");
215 }, 291 },
216 292
217 /** 293 /**
218 * Removes an element hiding filter 294 * Removes an element hiding filter
219 * @param {ElemHideBase} filter 295 * @param {ElemHideBase} filter
220 */ 296 */
221 remove(filter) 297 remove(filter)
222 { 298 {
223 if (!knownFilters.has(filter)) 299 if (!knownFilters.has(filter))
224 return; 300 return;
225 301
302 conditionalGenericSelectors = null;
303
226 // Whitelisting filters 304 // Whitelisting filters
227 if (filter instanceof ElemHideException) 305 if (filter instanceof ElemHideException)
228 { 306 {
229 let list = exceptions.get(filter.selector); 307 let list = exceptions.get(filter.selector);
230 let index = list.indexOf(filter); 308 let index = list.indexOf(filter);
231 if (index >= 0) 309 if (index >= 0)
232 list.splice(index, 1); 310 list.splice(index, 1);
233 311
Manish Jethani 2018/05/07 16:03:02 Note that we don't bother "unknowing" a domain onc
234 if (filter.isGeneric()) 312 if (filter.isGeneric())
235 genericExceptionSelectors.delete(filter.selector); 313 {
314 list = genericExceptions.get(filter.selector);
315 index = list.indexOf(filter);
316 if (index >= 0)
317 list.splice(index, 1);
318
319 // It's important to delete the entry here so the selector no longer
320 // appears to have any generic exceptions.
321 if (list.length == 0)
322 genericExceptions.delete(filter.selector);
323 }
236 } 324 }
237 // Unconditially applied element hiding filters 325 // Unconditially applied element hiding filters
238 else if (filterBySelector.get(filter.selector) == filter) 326 else if (filterBySelector.get(filter.selector) == filter)
239 { 327 {
240 filterBySelector.delete(filter.selector); 328 filterBySelector.delete(filter.selector);
241 unconditionalSelectors = null; 329 unconditionalSelectors = null;
242 } 330 }
243 // Conditionally applied element hiding filters 331 // Conditionally applied element hiding filters
244 else 332 else
245 { 333 {
246 let domains = filter.domains || defaultDomains; 334 let domains = filter.domains || defaultDomains;
247 for (let domain of domains.keys()) 335 for (let domain of domains.keys())
248 { 336 {
249 let filters = filtersByDomain.get(domain); 337 let filters = filtersByDomain.get(domain);
250 if (filters) 338 if (filters)
251 filters.delete(filter); 339 filters.delete(filter);
252 } 340 }
253 } 341 }
254 342
255 knownFilters.delete(filter); 343 knownFilters.delete(filter);
256 FilterNotifier.emit("elemhideupdate"); 344 FilterNotifier.emit("elemhideupdate");
257 }, 345 },
258 346
259 /** 347 /**
260 * Checks whether an exception rule is registered for a filter on a particular 348 * Checks whether an exception rule is registered for a filter on a particular
261 * domain. 349 * domain.
262 * @param {Filter} filter 350 * @param {Filter} filter
263 * @param {string} docDomain 351 * @param {?string} docDomain
264 * @return {ElemHideException} 352 * @return {?ElemHideException}
265 */ 353 */
266 getException(filter, docDomain) 354 getException(filter, docDomain)
267 { 355 {
268 let list = exceptions.get(filter.selector); 356 let list = exceptions.get(filter.selector);
269 if (!list) 357 if (!list)
270 return null; 358 return null;
271 359
272 for (let i = list.length - 1; i >= 0; i--) 360 for (let i = list.length - 1; i >= 0; i--)
273 { 361 {
274 if (list[i].isActiveOnDomain(docDomain)) 362 if (list[i].isActiveOnDomain(docDomain))
275 return list[i]; 363 return list[i];
276 } 364 }
277 365
278 return null; 366 return null;
279 }, 367 },
280
281 /**
282 * Returns a list of selectors that apply on each website unconditionally.
283 * @returns {string[]}
284 */
285 getUnconditionalSelectors()
286 {
287 if (!unconditionalSelectors)
288 unconditionalSelectors = [...filterBySelector.keys()];
289 return unconditionalSelectors.slice();
290 },
291
292 /**
293 * Constant used by getSelectorsForDomain to return all selectors applying to
294 * a particular hostname.
295 */
296 ALL_MATCHING: 0,
297
298 /**
299 * Constant used by getSelectorsForDomain to exclude selectors which apply to
300 * all websites without exception.
301 */
302 NO_UNCONDITIONAL: 1,
303
304 /**
305 * Constant used by getSelectorsForDomain to return only selectors for filters
306 * which specifically match the given host name.
307 */
308 SPECIFIC_ONLY: 2,
309 368
310 /** 369 /**
311 * Determines from the current filter list which selectors should be applied 370 * Determines from the current filter list which selectors should be applied
312 * on a particular host name. 371 * on a particular host name.
313 * @param {string} domain 372 * @param {string} domain
314 * @param {number} [criteria] 373 * @param {boolean} [specificOnly] true if generic filters should not apply.
315 * One of the following: ElemHide.ALL_MATCHING, ElemHide.NO_UNCONDITIONAL or 374 * @returns {string[]} List of selectors.
316 * ElemHide.SPECIFIC_ONLY.
317 * @returns {string[]}
318 * List of selectors.
319 */ 375 */
320 getSelectorsForDomain(domain, criteria) 376 getSelectorsForDomain(domain, specificOnly = false)
321 { 377 {
322 let selectors = []; 378 let specificFilters = getSpecificFiltersForDomain(domain);
323 379
324 if (typeof criteria == "undefined") 380 // If there are no specific filters (nor any specific exceptions), we can
325 criteria = ElemHide.ALL_MATCHING; 381 // just return the selectors from all the generic filters modulo any
326 if (criteria < ElemHide.NO_UNCONDITIONAL) 382 // generic exceptions.
327 selectors = this.getUnconditionalSelectors(); 383 if (specificFilters.length == 0)
328 384 {
329 let specificOnly = (criteria >= ElemHide.SPECIFIC_ONLY); 385 return specificOnly ? [] :
330 let currentDomain = domain ? domain.toUpperCase() : ""; 386 getUnconditionalSelectors()
331 387 .concat(getConditionalGenericSelectors());
332 if (isDomainKnown(currentDomain)) 388 }
Manish Jethani 2018/05/07 16:03:02 The call to isDomainKnown is the only additional c
333 { 389
334 let excluded = new Set(); 390 let excluded = new Set();
335 391 let selectors = matchSelectors(domain, specificFilters, excluded);
336 // This code is a performance hot-spot, which is why we've made certain 392
337 // micro-optimisations. Please be careful before making changes. 393 if (specificOnly)
338 while (true) 394 return selectors;
339 { 395
340 if (specificOnly && currentDomain == "") 396 return getUnconditionalSelectors()
341 break; 397 .concat(selectors,
342 398 matchSelectors(domain, [filtersByDomain.get("")],
343 let filters = filtersByDomain.get(currentDomain); 399 excluded));
344 if (filters)
345 {
346 for (let [filter, isIncluded] of filters)
347 {
348 if (!isIncluded)
349 {
350 excluded.add(filter);
351 }
352 else if ((excluded.size == 0 || !excluded.has(filter)) &&
353 !this.getException(filter, domain))
354 {
355 selectors.push(filter.selector);
356 }
357 }
358 }
359
360 if (currentDomain == "")
361 break;
362
363 let nextDot = currentDomain.indexOf(".");
364 currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1);
365 }
366 }
367 else if (!specificOnly)
368 {
369 selectors = selectors.concat(getConditionalGenericSelectors());
Manish Jethani 2018/05/07 16:03:02 We might want to cache the value returned by getCo
370 }
371
372 return selectors;
373 } 400 }
374 }; 401 };
LEFTRIGHT
« no previous file | test/elemHide.js » ('j') | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Toggle Comments ('s')

Powered by Google App Engine
This is Rietveld