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: Check generic exception set size before looking up Created May 7, 2018, 4:13 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
(...skipping 24 matching lines...) Expand all
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
96 * @returns {string[]} 178 * @returns {string[]}
97 */ 179 */
98 function getConditionalGenericSelectors() 180 function getConditionalGenericSelectors()
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.size == 0 || 193 if (genericExceptions.size == 0 || !genericExceptions.has(selector))
Manish Jethani 2018/05/07 16:15:08 Right now some selectors are disabled on all domai
109 !genericExceptionSelectors.has(selector)) 194 conditionalGenericSelectors.push(selector);
110 { 195 }
111 selectors.push(selector); 196
112 } 197 return conditionalGenericSelectors;
113 } 198 }
114 199
115 return selectors; 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;
116 } 210 }
117 211
118 /** 212 /**
119 * Container for element hiding filters 213 * Container for element hiding filters
120 * @class 214 * @class
121 */ 215 */
122 let ElemHide = exports.ElemHide = { 216 exports.ElemHide = {
123 /** 217 /**
124 * Removes all known filters 218 * Removes all known filters
125 */ 219 */
126 clear() 220 clear()
127 { 221 {
128 for (let collection of [filtersByDomain, filterBySelector, 222 for (let collection of [filtersByDomain, filterBySelector,
129 knownFilters, exceptions, 223 knownFilters, exceptions,
130 genericExceptionSelectors]) 224 genericExceptions])
131 { 225 {
132 collection.clear(); 226 collection.clear();
133 } 227 }
134 unconditionalSelectors = null; 228 unconditionalSelectors = null;
229 conditionalGenericSelectors = null;
135 FilterNotifier.emit("elemhideupdate"); 230 FilterNotifier.emit("elemhideupdate");
136 },
137
138 _addToFiltersByDomain(filter)
139 {
140 let domains = filter.domains || defaultDomains;
141 if (filter instanceof ElemHideException)
142 {
143 for (let domain of domains.keys())
144 {
145 // Add an entry for each domain, but without any filters. This makes
146 // the domain "known" and helps us avoid the optimized path (which
147 // would give incorrect results).
148 if (domain != "" && !filtersByDomain.has(domain))
149 filtersByDomain.set(domain, null);
150 }
151 }
152 else
153 {
154 for (let [domain, isIncluded] of domains)
155 {
156 // There's no need to note that a filter is generically disabled.
157 if (!isIncluded && domain == "")
158 continue;
159
160 let filters = filtersByDomain.get(domain);
161 if (!filters)
162 filtersByDomain.set(domain, filters = new Map());
163 filters.set(filter, isIncluded);
164 }
165 }
166 }, 231 },
167 232
168 /** 233 /**
169 * Add a new element hiding filter 234 * Add a new element hiding filter
170 * @param {ElemHideBase} filter 235 * @param {ElemHideBase} filter
171 */ 236 */
172 add(filter) 237 add(filter)
173 { 238 {
174 if (knownFilters.has(filter)) 239 if (knownFilters.has(filter))
175 return; 240 return;
176 241
242 conditionalGenericSelectors = null;
243
177 if (filter instanceof ElemHideException) 244 if (filter instanceof ElemHideException)
178 { 245 {
179 let {selector, domains} = filter; 246 let {selector, domains} = filter;
180 247
181 let list = exceptions.get(selector); 248 let list = exceptions.get(selector);
182 if (list) 249 if (list)
183 list.push(filter); 250 list.push(filter);
184 else 251 else
185 exceptions.set(selector, [filter]); 252 exceptions.set(selector, [filter]);
186 253
187 if (domains) 254 if (domains)
188 this._addToFiltersByDomain(filter); 255 addToFiltersByDomain(filter);
189 256
190 if (filter.isGeneric()) 257 if (filter.isGeneric())
191 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 }
192 265
193 // If this is the first exception for a previously unconditionally 266 // If this is the first exception for a previously unconditionally
194 // 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
195 // lookups. 268 // lookups.
196 let unconditionalFilterForSelector = filterBySelector.get(selector); 269 let unconditionalFilterForSelector = filterBySelector.get(selector);
197 if (unconditionalFilterForSelector) 270 if (unconditionalFilterForSelector)
198 { 271 {
199 this._addToFiltersByDomain(unconditionalFilterForSelector); 272 addToFiltersByDomain(unconditionalFilterForSelector);
200 filterBySelector.delete(selector); 273 filterBySelector.delete(selector);
201 unconditionalSelectors = null; 274 unconditionalSelectors = null;
202 } 275 }
203 } 276 }
204 else if (!(filter.domains || exceptions.has(filter.selector))) 277 else if (!(filter.domains || exceptions.has(filter.selector)))
205 { 278 {
206 // The new filter's selector is unconditionally applied to all domains 279 // The new filter's selector is unconditionally applied to all domains
207 filterBySelector.set(filter.selector, filter); 280 filterBySelector.set(filter.selector, filter);
208 unconditionalSelectors = null; 281 unconditionalSelectors = null;
209 } 282 }
210 else 283 else
211 { 284 {
212 // The new filter's selector only applies to some domains 285 // The new filter's selector only applies to some domains
213 this._addToFiltersByDomain(filter); 286 addToFiltersByDomain(filter);
214 } 287 }
215 288
216 knownFilters.add(filter); 289 knownFilters.add(filter);
217 FilterNotifier.emit("elemhideupdate"); 290 FilterNotifier.emit("elemhideupdate");
218 }, 291 },
219 292
220 /** 293 /**
221 * Removes an element hiding filter 294 * Removes an element hiding filter
222 * @param {ElemHideBase} filter 295 * @param {ElemHideBase} filter
223 */ 296 */
224 remove(filter) 297 remove(filter)
225 { 298 {
226 if (!knownFilters.has(filter)) 299 if (!knownFilters.has(filter))
227 return; 300 return;
228 301
302 conditionalGenericSelectors = null;
303
229 // Whitelisting filters 304 // Whitelisting filters
230 if (filter instanceof ElemHideException) 305 if (filter instanceof ElemHideException)
231 { 306 {
232 let list = exceptions.get(filter.selector); 307 let list = exceptions.get(filter.selector);
233 let index = list.indexOf(filter); 308 let index = list.indexOf(filter);
234 if (index >= 0) 309 if (index >= 0)
235 list.splice(index, 1); 310 list.splice(index, 1);
236 311
237 if (filter.isGeneric()) 312 if (filter.isGeneric())
238 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 }
239 } 324 }
240 // Unconditially applied element hiding filters 325 // Unconditially applied element hiding filters
241 else if (filterBySelector.get(filter.selector) == filter) 326 else if (filterBySelector.get(filter.selector) == filter)
242 { 327 {
243 filterBySelector.delete(filter.selector); 328 filterBySelector.delete(filter.selector);
244 unconditionalSelectors = null; 329 unconditionalSelectors = null;
245 } 330 }
246 // Conditionally applied element hiding filters 331 // Conditionally applied element hiding filters
247 else 332 else
248 { 333 {
249 let domains = filter.domains || defaultDomains; 334 let domains = filter.domains || defaultDomains;
250 for (let domain of domains.keys()) 335 for (let domain of domains.keys())
251 { 336 {
252 let filters = filtersByDomain.get(domain); 337 let filters = filtersByDomain.get(domain);
253 if (filters) 338 if (filters)
254 filters.delete(filter); 339 filters.delete(filter);
255 } 340 }
256 } 341 }
257 342
258 knownFilters.delete(filter); 343 knownFilters.delete(filter);
259 FilterNotifier.emit("elemhideupdate"); 344 FilterNotifier.emit("elemhideupdate");
260 }, 345 },
261 346
262 /** 347 /**
263 * 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
264 * domain. 349 * domain.
265 * @param {Filter} filter 350 * @param {Filter} filter
266 * @param {string} docDomain 351 * @param {?string} docDomain
267 * @return {ElemHideException} 352 * @return {?ElemHideException}
268 */ 353 */
269 getException(filter, docDomain) 354 getException(filter, docDomain)
270 { 355 {
271 let list = exceptions.get(filter.selector); 356 let list = exceptions.get(filter.selector);
272 if (!list) 357 if (!list)
273 return null; 358 return null;
274 359
275 for (let i = list.length - 1; i >= 0; i--) 360 for (let i = list.length - 1; i >= 0; i--)
276 { 361 {
277 if (list[i].isActiveOnDomain(docDomain)) 362 if (list[i].isActiveOnDomain(docDomain))
278 return list[i]; 363 return list[i];
279 } 364 }
280 365
281 return null; 366 return null;
282 }, 367 },
283
284 /**
285 * Returns a list of selectors that apply on each website unconditionally.
286 * @returns {string[]}
287 */
288 getUnconditionalSelectors()
289 {
290 if (!unconditionalSelectors)
291 unconditionalSelectors = [...filterBySelector.keys()];
292 return unconditionalSelectors.slice();
293 },
294
295 /**
296 * Constant used by getSelectorsForDomain to return all selectors applying to
297 * a particular hostname.
298 */
299 ALL_MATCHING: 0,
300
301 /**
302 * Constant used by getSelectorsForDomain to exclude selectors which apply to
303 * all websites without exception.
304 */
305 NO_UNCONDITIONAL: 1,
306
307 /**
308 * Constant used by getSelectorsForDomain to return only selectors for filters
309 * which specifically match the given host name.
310 */
311 SPECIFIC_ONLY: 2,
312 368
313 /** 369 /**
314 * Determines from the current filter list which selectors should be applied 370 * Determines from the current filter list which selectors should be applied
315 * on a particular host name. 371 * on a particular host name.
316 * @param {string} domain 372 * @param {string} domain
317 * @param {number} [criteria] 373 * @param {boolean} [specificOnly] true if generic filters should not apply.
318 * One of the following: ElemHide.ALL_MATCHING, ElemHide.NO_UNCONDITIONAL or 374 * @returns {string[]} List of selectors.
319 * ElemHide.SPECIFIC_ONLY.
320 * @returns {string[]}
321 * List of selectors.
322 */ 375 */
323 getSelectorsForDomain(domain, criteria) 376 getSelectorsForDomain(domain, specificOnly = false)
324 { 377 {
325 let selectors = []; 378 let specificFilters = getSpecificFiltersForDomain(domain);
326 379
327 if (typeof criteria == "undefined") 380 // If there are no specific filters (nor any specific exceptions), we can
328 criteria = ElemHide.ALL_MATCHING; 381 // just return the selectors from all the generic filters modulo any
329 if (criteria < ElemHide.NO_UNCONDITIONAL) 382 // generic exceptions.
330 selectors = this.getUnconditionalSelectors(); 383 if (specificFilters.length == 0)
331 384 {
332 let specificOnly = (criteria >= ElemHide.SPECIFIC_ONLY); 385 return specificOnly ? [] :
333 let currentDomain = domain ? domain.toUpperCase() : ""; 386 getUnconditionalSelectors()
334 387 .concat(getConditionalGenericSelectors());
335 if (isDomainKnown(currentDomain)) 388 }
kzar 2018/05/08 13:46:47 I don't quite understand how your change speeds th
Manish Jethani 2018/05/08 14:20:43 Let me come back with an answer after investigatin
Manish Jethani 2018/05/08 15:49:00 Alright, I checked. Fifty percent of the speedup
kzar 2018/05/08 17:13:43 Interesting, I wonder why that is. Is something in
Manish Jethani 2018/05/08 17:37:13 Yes, I found out: https://codereview.adblockplus.
Manish Jethani 2018/05/08 17:49:30 getException should really be called hasException
336 { 389
337 let excluded = new Set(); 390 let excluded = new Set();
338 391 let selectors = matchSelectors(domain, specificFilters, excluded);
339 // This code is a performance hot-spot, which is why we've made certain 392
340 // micro-optimisations. Please be careful before making changes. 393 if (specificOnly)
341 while (true) 394 return selectors;
342 { 395
343 if (specificOnly && currentDomain == "") 396 return getUnconditionalSelectors()
344 break; 397 .concat(selectors,
345 398 matchSelectors(domain, [filtersByDomain.get("")],
346 let filters = filtersByDomain.get(currentDomain); 399 excluded));
347 if (filters)
348 {
349 for (let [filter, isIncluded] of filters)
350 {
351 if (!isIncluded)
352 {
353 excluded.add(filter);
354 }
355 else if ((excluded.size == 0 || !excluded.has(filter)) &&
356 !this.getException(filter, domain))
357 {
358 selectors.push(filter.selector);
359 }
360 }
361 }
362
363 if (currentDomain == "")
364 break;
365
366 let nextDot = currentDomain.indexOf(".");
367 currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1);
368 }
369 }
370 else if (!specificOnly)
371 {
372 selectors = selectors.concat(getConditionalGenericSelectors());
kzar 2018/05/08 13:46:47 Won't selectors always be [] here? I wonder why we
Manish Jethani 2018/05/08 14:20:43 No, the selectors array will contain the unconditi
kzar 2018/05/08 17:13:44 Ah yea, gotya.
373 }
374
375 return selectors;
376 } 400 }
377 }; 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