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: Merge getConditionalGenericSelectors into getConditionalSelectorsForDomain Created May 9, 2018, 9:31 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 * Lookup table, lists of generic element hiding exceptions by selector 69 * Lookup table, lists of generic element hiding exceptions by selector
67 * @type {Map.<string,Filter[]>} 70 * @type {Map.<string,Filter[]>}
68 */ 71 */
69 let genericExceptions = new Map(); 72 let genericExceptions = new Map();
Manish Jethani 2018/05/09 21:35:44 This needs to be a map actually, because we want t
70 73
71 /** 74 /**
72 * Checks whether a filter has an exception 75 * List of selectors that apply on any unknown domain
76 * @type {?string[]}
77 */
78 let conditionalGenericSelectors = null;
79
80 /**
81 * Adds a filter to the lookup table of filters by domain.
73 * @param {Filter} filter 82 * @param {Filter} filter
74 * @param {string} [domain] 83 */
75 * @returns {boolean} 84 function addToFiltersByDomain(filter)
76 */
77 function hasException(filter, domain)
78 { 85 {
79 if (!domain) 86 let domains = filter.domains || defaultDomains;
80 return genericExceptions.has(filter.selector); 87 if (filter instanceof ElemHideException)
81 88 {
82 return !!ElemHide.getException(filter, domain); 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 }
83 } 112 }
84 113
85 /* 114 /**
86 * Returns a list of domain-specific filters matching a domain 115 * Returns a list of domain-specific filters matching a domain
87 * @param {string} [domain] 116 * @param {string} [domain]
88 * @returns {Array.<?Map.<Filter,boolean>>} 117 * @returns {Array.<?Map.<Filter,boolean>>}
89 */ 118 */
90 function getSpecificFiltersForDomain(domain) 119 function getSpecificFiltersForDomain(domain)
91 { 120 {
92 let filtersList = []; 121 let filtersList = [];
93 122
94 if (domain) 123 if (domain)
95 domain = domain.toUpperCase(); 124 domain = domain.toUpperCase();
96 125
97 while (domain) 126 while (domain)
98 { 127 {
99 // Note that we also push null values into the list, because
100 // ElemHide.getSelectorsForDomain still needs to know if there are any
101 // entries for the domain.
102 let filters = filtersByDomain.get(domain); 128 let filters = filtersByDomain.get(domain);
103 if (typeof filters != "undefined") 129 if (typeof filters != "undefined")
104 filtersList.push(filters); 130 filtersList.push(filters);
105 131
106 let nextDot = domain.indexOf("."); 132 let nextDot = domain.indexOf(".");
107 domain = nextDot == -1 ? null : domain.substring(nextDot + 1); 133 domain = nextDot == -1 ? null : domain.substring(nextDot + 1);
108 } 134 }
109 135
110 return filtersList; 136 return filtersList;
111 } 137 }
112 138
113 /** 139 /**
114 * Returns a list of selectors from a given list of filters that apply on a 140 * Returns a list of selectors that apply on a domain from a given list of
115 * domain 141 * filters
116 * @param {string} [domain] 142 * @param {string} [domain]
117 * @param {Array.<?Map.<Filter,boolean>>} filtersList 143 * @param {Array.<?Map.<Filter,boolean>>} filtersList
118 * @param {boolean} genericOnly 144 * @param {Set.<Filter>} excludeSet
119 * @returns {string[]} 145 * @returns {string[]}
120 */ 146 */
121 function getConditionalSelectorsForDomain(domain, filtersList, genericOnly) 147 function matchSelectors(domain, filtersList, excludeSet)
122 { 148 {
123 let selectors = []; 149 let matches = [];
124
125 let excluded = !genericOnly ? new Set() : null;
126 let domainForExceptionCheck = !genericOnly ? domain : null;
127 150
128 // This code is a performance hot-spot, which is why we've made certain 151 // This code is a performance hot-spot, which is why we've made certain
129 // micro-optimisations. Please be careful before making changes. 152 // micro-optimisations. Please be careful before making changes.
130 for (let i = 0; i < filtersList.length; i++) 153 for (let i = 0; i < filtersList.length; i++)
131 { 154 {
132 if (!filtersList[i]) 155 let filters = filtersList[i];
133 continue; 156 if (filters)
134 157 {
135 for (let [filter, isIncluded] of filtersList[i]) 158 for (let [filter, isIncluded] of filters)
136 {
137 if (!isIncluded)
138 { 159 {
139 excluded.add(filter); 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 }
140 } 169 }
141 else if ((!excluded || excluded.size == 0 || !excluded.has(filter)) && 170 }
142 !hasException(filter, domainForExceptionCheck)) 171 }
143 { 172
144 selectors.push(filter.selector); 173 return matches;
145 } 174 }
146 } 175
147 } 176 /**
148 177 * Returns a list of selectors that apply on any unknown domain
149 return selectors; 178 * @returns {string[]}
179 */
180 function getConditionalGenericSelectors()
181 {
182 if (conditionalGenericSelectors)
183 return conditionalGenericSelectors;
184
185 conditionalGenericSelectors = [];
186
187 let filters = filtersByDomain.get("");
188 if (!filters)
189 return conditionalGenericSelectors;
190
191 for (let {selector} of filters.keys())
192 {
193 if (genericExceptions.size == 0 || !genericExceptions.has(selector))
194 conditionalGenericSelectors.push(selector);
195 }
196
197 return conditionalGenericSelectors;
150 } 198 }
151 199
152 /** 200 /**
153 * Returns a list of selectors that apply on each website unconditionally. 201 * Returns a list of selectors that apply on each website unconditionally.
154 * @returns {string[]} 202 * @returns {string[]}
155 */ 203 */
156 function getUnconditionalSelectors() 204 function getUnconditionalSelectors()
157 { 205 {
158 if (!unconditionalSelectors) 206 if (!unconditionalSelectors)
159 unconditionalSelectors = [...filterBySelector.keys()]; 207 unconditionalSelectors = [...filterBySelector.keys()];
160 208
161 return unconditionalSelectors; 209 return unconditionalSelectors;
162 } 210 }
163 211
164 /** 212 /**
165 * Container for element hiding filters 213 * Container for element hiding filters
166 * @class 214 * @class
167 */ 215 */
168 let ElemHide = exports.ElemHide = { 216 exports.ElemHide = {
169 /** 217 /**
170 * Removes all known filters 218 * Removes all known filters
171 */ 219 */
172 clear() 220 clear()
173 { 221 {
174 for (let collection of [filtersByDomain, filterBySelector, 222 for (let collection of [filtersByDomain, filterBySelector,
175 knownFilters, exceptions, 223 knownFilters, exceptions,
176 genericExceptions]) 224 genericExceptions])
177 { 225 {
178 collection.clear(); 226 collection.clear();
179 } 227 }
180 unconditionalSelectors = null; 228 unconditionalSelectors = null;
229 conditionalGenericSelectors = null;
181 FilterNotifier.emit("elemhideupdate"); 230 FilterNotifier.emit("elemhideupdate");
182 },
183
184 _addToFiltersByDomain(filter)
185 {
186 let domains = filter.domains || defaultDomains;
187 if (filter instanceof ElemHideException)
188 {
189 for (let domain of domains.keys())
190 {
191 // Add an entry for each domain, but without any filters. This makes
192 // the domain "known" and helps us avoid certain optimizations that
193 // would otherwise yield incorrect results.
194 if (domain != "" && !filtersByDomain.has(domain))
195 filtersByDomain.set(domain, null);
196 }
197 }
198 else
199 {
200 for (let [domain, isIncluded] of domains)
201 {
202 // There's no need to note that a filter is generically disabled.
203 if (!isIncluded && domain == "")
204 continue;
205
206 let filters = filtersByDomain.get(domain);
207 if (!filters)
208 filtersByDomain.set(domain, filters = new Map());
209 filters.set(filter, isIncluded);
210 }
211 }
212 }, 231 },
213 232
214 /** 233 /**
215 * Add a new element hiding filter 234 * Add a new element hiding filter
216 * @param {ElemHideBase} filter 235 * @param {ElemHideBase} filter
217 */ 236 */
218 add(filter) 237 add(filter)
219 { 238 {
220 if (knownFilters.has(filter)) 239 if (knownFilters.has(filter))
221 return; 240 return;
222 241
242 conditionalGenericSelectors = null;
243
223 if (filter instanceof ElemHideException) 244 if (filter instanceof ElemHideException)
224 { 245 {
225 let {selector, domains} = filter; 246 let {selector, domains} = filter;
226 247
227 let list = exceptions.get(selector); 248 let list = exceptions.get(selector);
228 if (list) 249 if (list)
229 list.push(filter); 250 list.push(filter);
230 else 251 else
231 exceptions.set(selector, [filter]); 252 exceptions.set(selector, [filter]);
232 253
233 if (domains) 254 if (domains)
234 this._addToFiltersByDomain(filter); 255 addToFiltersByDomain(filter);
235 256
236 if (filter.isGeneric()) 257 if (filter.isGeneric())
237 { 258 {
238 list = genericExceptions.get(selector); 259 list = genericExceptions.get(selector);
239 if (list) 260 if (list)
240 list.push(filter); 261 list.push(filter);
241 else 262 else
242 genericExceptions.set(selector, [filter]); 263 genericExceptions.set(selector, [filter]);
243 } 264 }
244 265
245 // If this is the first exception for a previously unconditionally 266 // If this is the first exception for a previously unconditionally
246 // 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
247 // lookups. 268 // lookups.
248 let unconditionalFilterForSelector = filterBySelector.get(selector); 269 let unconditionalFilterForSelector = filterBySelector.get(selector);
249 if (unconditionalFilterForSelector) 270 if (unconditionalFilterForSelector)
250 { 271 {
251 this._addToFiltersByDomain(unconditionalFilterForSelector); 272 addToFiltersByDomain(unconditionalFilterForSelector);
252 filterBySelector.delete(selector); 273 filterBySelector.delete(selector);
253 unconditionalSelectors = null; 274 unconditionalSelectors = null;
254 } 275 }
255 } 276 }
256 else if (!(filter.domains || exceptions.has(filter.selector))) 277 else if (!(filter.domains || exceptions.has(filter.selector)))
257 { 278 {
258 // The new filter's selector is unconditionally applied to all domains 279 // The new filter's selector is unconditionally applied to all domains
259 filterBySelector.set(filter.selector, filter); 280 filterBySelector.set(filter.selector, filter);
260 unconditionalSelectors = null; 281 unconditionalSelectors = null;
261 } 282 }
262 else 283 else
263 { 284 {
264 // The new filter's selector only applies to some domains 285 // The new filter's selector only applies to some domains
265 this._addToFiltersByDomain(filter); 286 addToFiltersByDomain(filter);
266 } 287 }
267 288
268 knownFilters.add(filter); 289 knownFilters.add(filter);
269 FilterNotifier.emit("elemhideupdate"); 290 FilterNotifier.emit("elemhideupdate");
270 }, 291 },
271 292
272 /** 293 /**
273 * Removes an element hiding filter 294 * Removes an element hiding filter
274 * @param {ElemHideBase} filter 295 * @param {ElemHideBase} filter
275 */ 296 */
276 remove(filter) 297 remove(filter)
277 { 298 {
278 if (!knownFilters.has(filter)) 299 if (!knownFilters.has(filter))
279 return; 300 return;
301
302 conditionalGenericSelectors = null;
280 303
281 // Whitelisting filters 304 // Whitelisting filters
282 if (filter instanceof ElemHideException) 305 if (filter instanceof ElemHideException)
283 { 306 {
284 let list = exceptions.get(filter.selector); 307 let list = exceptions.get(filter.selector);
285 let index = list.indexOf(filter); 308 let index = list.indexOf(filter);
286 if (index >= 0) 309 if (index >= 0)
287 list.splice(index, 1); 310 list.splice(index, 1);
288 311
289 if (filter.isGeneric()) 312 if (filter.isGeneric())
290 { 313 {
291 list = genericExceptions.get(filter.selector); 314 list = genericExceptions.get(filter.selector);
292 index = list.indexOf(filter); 315 index = list.indexOf(filter);
293 if (index >= 0) 316 if (index >= 0)
294 list.splice(index, 1); 317 list.splice(index, 1);
295 318
319 // It's important to delete the entry here so the selector no longer
320 // appears to have any generic exceptions.
296 if (list.length == 0) 321 if (list.length == 0)
297 genericExceptions.delete(filter.selector); 322 genericExceptions.delete(filter.selector);
298 } 323 }
299 } 324 }
300 // Unconditially applied element hiding filters 325 // Unconditially applied element hiding filters
301 else if (filterBySelector.get(filter.selector) == filter) 326 else if (filterBySelector.get(filter.selector) == filter)
302 { 327 {
303 filterBySelector.delete(filter.selector); 328 filterBySelector.delete(filter.selector);
304 unconditionalSelectors = null; 329 unconditionalSelectors = null;
305 } 330 }
(...skipping 10 matching lines...) Expand all
316 } 341 }
317 342
318 knownFilters.delete(filter); 343 knownFilters.delete(filter);
319 FilterNotifier.emit("elemhideupdate"); 344 FilterNotifier.emit("elemhideupdate");
320 }, 345 },
321 346
322 /** 347 /**
323 * 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
324 * domain. 349 * domain.
325 * @param {Filter} filter 350 * @param {Filter} filter
326 * @param {string} docDomain 351 * @param {?string} docDomain
327 * @return {ElemHideException} 352 * @return {?ElemHideException}
328 */ 353 */
329 getException(filter, docDomain) 354 getException(filter, docDomain)
330 { 355 {
331 let list = exceptions.get(filter.selector); 356 let list = exceptions.get(filter.selector);
332 if (!list) 357 if (!list)
333 return null; 358 return null;
334 359
335 for (let i = list.length - 1; i >= 0; i--) 360 for (let i = list.length - 1; i >= 0; i--)
336 { 361 {
337 if (list[i].isActiveOnDomain(docDomain)) 362 if (list[i].isActiveOnDomain(docDomain))
338 return list[i]; 363 return list[i];
339 } 364 }
340 365
341 return null; 366 return null;
342 }, 367 },
343
344 /**
345 * Constant used by getSelectorsForDomain to return all selectors applying to
346 * a particular hostname.
347 */
348 ALL_MATCHING: 0,
349
350 /**
351 * Constant used by getSelectorsForDomain to exclude selectors which apply to
352 * all websites without exception.
353 */
354 NO_UNCONDITIONAL: 1,
355
356 /**
357 * Constant used by getSelectorsForDomain to return only selectors for filters
358 * which specifically match the given host name.
359 */
360 SPECIFIC_ONLY: 2,
361 368
362 /** 369 /**
363 * Determines from the current filter list which selectors should be applied 370 * Determines from the current filter list which selectors should be applied
364 * on a particular host name. 371 * on a particular host name.
365 * @param {string} domain 372 * @param {string} domain
366 * @param {number} [criteria] 373 * @param {boolean} [specificOnly] true if generic filters should not apply.
367 * One of the following: ElemHide.ALL_MATCHING, ElemHide.NO_UNCONDITIONAL or 374 * @returns {string[]} List of selectors.
368 * ElemHide.SPECIFIC_ONLY.
369 * @returns {string[]}
370 * List of selectors.
371 */ 375 */
372 getSelectorsForDomain(domain, criteria) 376 getSelectorsForDomain(domain, specificOnly = false)
373 { 377 {
374 if (typeof criteria == "undefined") 378 let specificFilters = getSpecificFiltersForDomain(domain);
375 criteria = ElemHide.ALL_MATCHING; 379
376 380 // If there are no specific filters (nor any specific exceptions), we can
377 let specificOnly = criteria >= ElemHide.SPECIFIC_ONLY; 381 // just return the selectors from all the generic filters modulo any
378 let filtersList = getSpecificFiltersForDomain(domain); 382 // generic exceptions.
379 383 if (specificFilters.length == 0)
380 let genericOnly = filtersList.length == 0; 384 {
381 385 return specificOnly ? [] :
382 if (!specificOnly) 386 getUnconditionalSelectors()
383 filtersList.push(filtersByDomain.get("")); 387 .concat(getConditionalGenericSelectors());
384 388 }
385 let selectors = getConditionalSelectorsForDomain(domain, filtersList, 389
386 genericOnly); 390 let excluded = new Set();
387 391 let selectors = matchSelectors(domain, specificFilters, excluded);
388 if (criteria < ElemHide.NO_UNCONDITIONAL) 392
389 selectors = getUnconditionalSelectors().concat(selectors); 393 if (specificOnly)
390 394 return selectors;
391 return selectors; 395
396 return getUnconditionalSelectors()
397 .concat(selectors,
398 matchSelectors(domain, [filtersByDomain.get("")],
399 excluded));
392 } 400 }
393 }; 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