Просмотр исходного кода

speedup: script injection (#629)

* speedup: tester and cache

* speedup: increase cache lifetime

* speedup: precalculate getScriptsByURL

* cosmetics: inline a constant

* kludge: polyfill performance.now() for tests

* fix: reschedule if setTimeout was set [far] into the future
tophf 6 лет назад
Родитель
Сommit
79ed2ca7a5
5 измененных файлов с 86 добавлено и 29 удалено
  1. 13 1
      src/background/index.js
  2. 1 1
      src/background/utils/cache.js
  3. 16 8
      src/background/utils/tester.js
  4. 54 19
      src/common/cache.js
  5. 2 0
      test/mock/polyfill.js

+ 13 - 1
src/background/index.js

@@ -102,7 +102,10 @@ const commands = {
       version: VM_VER,
     };
     if (!data.isApplied) return data;
-    return getScriptsByURL(url)
+    const key = `getScriptsByURL:${url}`;
+    const cachedData = cache.get(key);
+    if (cachedData) cache.del(key);
+    return Promise.resolve(cachedData || getScriptsByURL(url))
     .then((res) => {
       addValueOpener(srcTab.id, Object.keys(res.values));
       return Object.assign(data, res);
@@ -233,6 +236,15 @@ initialize()
     // undefined will be ignored
     return res || null;
   });
+  browser.webRequest.onHeadersReceived.addListener(async ({ url }) => {
+    const key = `getScriptsByURL:${url}`;
+    if (!cache.has(key)) {
+      cache.put(key, await getScriptsByURL(url), 250);
+    }
+  }, {
+    urls: ['*://*/*'],
+    types: ['main_frame', 'sub_frame'],
+  });
   setTimeout(autoUpdate, 2e4);
   sync.initialize();
   resetBlacklist();

+ 1 - 1
src/background/utils/cache.js

@@ -1,5 +1,5 @@
 import initCache from '#/common/cache';
 
 export default initCache({
-  lifetime: 10 * 1000,
+  lifetime: 5 * 60 * 1000,
 });

+ 16 - 8
src/background/utils/tester.js

@@ -30,15 +30,14 @@ let blCacheSize = 0;
  * Test glob rules like `@include` and `@exclude`.
  */
 export function testGlob(url, rules) {
-  const lifetime = 60 * 1000;
   return rules.some((rule) => {
     const key = `re:${rule}`;
     let re = cache.get(key);
     if (re) {
-      cache.hit(key, lifetime);
+      cache.hit(key);
     } else {
       re = autoReg(rule);
-      cache.put(key, re, lifetime);
+      cache.put(key, re);
     }
     return re.test(url);
   });
@@ -48,15 +47,14 @@ export function testGlob(url, rules) {
  * Test match rules like `@match` and `@exclude_match`.
  */
 export function testMatch(url, rules) {
-  const lifetime = 60 * 1000;
   return rules.some((rule) => {
     const key = `match:${rule}`;
     let matcher = cache.get(key);
     if (matcher) {
-      cache.hit(key, lifetime);
+      cache.hit(key);
     } else {
       matcher = matchTester(rule);
-      cache.put(key, matcher, lifetime);
+      cache.put(key, matcher);
     }
     return matcher.test(url);
   });
@@ -81,6 +79,16 @@ export function testScript(url, script) {
   return ok;
 }
 
+function testRegExp(re, text) {
+  const key = `re-test:${re.source}:${text}`;
+  let res = cache.get(key);
+  if (!res) {
+    res = re.test(text) ? 1 : -1;
+    cache.put(key, res);
+  }
+  return res === 1;
+}
+
 function mergeLists(...args) {
   return args.reduce((res, item) => (item ? res.concat(item) : res), []);
 }
@@ -109,7 +117,7 @@ function autoReg(str) {
     };
   }
   const re = new RegExp(`^${reStr}$`); // String with wildcards
-  return { test: tstr => re.test(tstr) };
+  return { test: tstr => testRegExp(re, tstr) };
 }
 
 function matchScheme(rule, data) {
@@ -168,7 +176,7 @@ function pathMatcher(rule) {
     else strRe = `^${strRe}(?:#|$)`;
   }
   const reRule = new RegExp(strRe);
-  return data => reRule.test(data);
+  return data => testRegExp(reRule, data);
 }
 function matchTester(rule) {
   let test;

+ 54 - 19
src/common/cache.js

@@ -1,10 +1,13 @@
-const defaults = {
-  lifetime: 3000,
-};
-
-export default function initCache(options) {
-  const cache = {};
-  const { lifetime: defaultLifetime } = options || defaults;
+export default function initCache({
+  lifetime: defaultLifetime = 3000,
+} = {}) {
+  let cache = {};
+  // setTimeout call is very expensive when done frequently,
+  // 1000 calls performed for 50 scripts consume 50ms on each tab load,
+  // so we'll schedule trim() just once per event loop cycle,
+  // and then trim() will trim the cache and reschedule itself to the earliest expiry time.
+  let timer;
+  let minLifetime = -1;
   return {
     get, put, del, has, hit, destroy,
   };
@@ -12,29 +15,61 @@ export default function initCache(options) {
     const item = cache[key];
     return item ? item.value : def;
   }
-  function put(key, value, lifetime) {
-    del(key);
+  function put(key, value, lifetime = defaultLifetime) {
     if (value) {
       cache[key] = {
         value,
-        timer: setTimeout(del, lifetime || defaultLifetime, key),
+        expiry: lifetime + performance.now(),
       };
+      reschedule(lifetime);
+    } else {
+      delete cache[key];
     }
   }
   function del(key) {
-    const item = cache[key];
-    if (item) {
-      if (item.timer) clearTimeout(item.timer);
-      delete cache[key];
-    }
+    delete cache[key];
   }
   function has(key) {
-    return !!cache[key];
+    return Object.hasOwnProperty.call(cache, key);
   }
-  function hit(key, lifetime) {
-    put(key, get(key), lifetime);
+  function hit(key, lifetime = defaultLifetime) {
+    const entry = cache[key];
+    if (entry) {
+      entry.expiry = performance.now() + lifetime;
+      reschedule(lifetime);
+    }
   }
   function destroy() {
-    Object.keys(cache).forEach(del);
+    clearTimeout(timer);
+    timer = 0;
+    cache = {};
+  }
+  function reschedule(lifetime) {
+    if (timer) {
+      if (lifetime >= minLifetime) return;
+      clearTimeout(timer);
+    }
+    minLifetime = lifetime;
+    timer = setTimeout(trim, lifetime);
+  }
+  function trim() {
+    // next timer won't be able to run earlier than 10ms
+    // so we'll sweep the upcoming expired entries in this run
+    const now = performance.now() + 10;
+    let closestExpiry = Number.MAX_SAFE_INTEGER;
+    for (const key in cache) {
+      if (Object.hasOwnProperty.call(cache, key)) {
+        const { expiry } = cache[key];
+        if (expiry < now) {
+          delete cache[key];
+        } else if (expiry < closestExpiry) {
+          closestExpiry = expiry;
+        }
+      }
+    }
+    minLifetime = closestExpiry - now;
+    timer = closestExpiry < Number.MAX_SAFE_INTEGER
+      ? setTimeout(trim, minLifetime)
+      : 0;
   }
 }

+ 2 - 0
test/mock/polyfill.js

@@ -16,3 +16,5 @@ global.browser = {
     },
   },
 };
+
+global.performance = { now: () => Date.now() };