Browse Source

fix: show sizes instantly

tophf 3 years ago
parent
commit
ba9ce43909

+ 2 - 3
src/background/index.js

@@ -27,9 +27,8 @@ hookOptions((changes) => {
 });
 
 Object.assign(commands, {
-  /** @return {Promise<{ scripts: VMScript[], cache: Object, sync: Object }>} */
-  async GetData(ids) {
-    const data = await getData(ids);
+  async GetData(opts) {
+    const data = await getData(opts);
     data.sync = sync.getStates();
     return data;
   },

+ 93 - 104
src/background/utils/db.js

@@ -11,13 +11,20 @@ import { preInitialize } from './init';
 import { commands } from './message';
 import patchDB from './patch-db';
 import { setOption } from './options';
-import storage from './storage';
+import storage, {
+  S_CACHE, S_CODE, S_REQUIRE, S_VALUE,
+  S_CACHE_PRE, S_CODE_PRE, S_MOD_PRE, S_REQUIRE_PRE, S_SCRIPT_PRE, S_VALUE_PRE,
+} from './storage';
 
 export const store = {
   /** @type {VMScript[]} */
   scripts: [],
   /** @type {Object<string,VMScript[]>} */
   scriptMap: {},
+  /** @type {{ [url:string]: number }} */
+  sizes: {},
+  /** Same order as in SIZE_TITLES and getSizes */
+  sizesPrefixRe: RegExp(`^(${S_CODE_PRE}|${S_SCRIPT_PRE}|${S_VALUE_PRE}|${S_REQUIRE_PRE}|${S_CACHE_PRE}${S_MOD_PRE})`),
   storeInfo: {
     id: 0,
     position: 0,
@@ -99,18 +106,9 @@ preInitialize.push(async () => {
   const data = await storage.base.getMulti();
   const { scripts, storeInfo, scriptMap } = store;
   const uriMap = {};
-  const mods = [];
-  const toRemove = [];
-  const resUrls = new Set();
-  /** @this {StringMap} */
-  const rememberUrl = function _(url) {
-    if (url && !isDataUri(url)) {
-      resUrls.add(this[url] || url);
-    }
-  };
   data::forEachEntry(([key, script]) => {
-    let id = +storage.script.toId(key);
-    if (id) {
+    const id = +storage.script.toId(key);
+    if (id && script) {
       if (scriptMap[id] && scriptMap[id] !== script) {
         // ID conflicts!
         // Should not happen, discard duplicates.
@@ -137,29 +135,13 @@ preInitialize.push(async () => {
       scripts.push(script);
       // listing all known resource urls in order to remove unused mod keys
       const {
-        custom,
         meta = script.meta = {},
       } = script;
-      const {
-        pathMap = custom.pathMap = {},
-      } = custom;
-      const {
-        require = meta.require = [],
-        resources = meta.resources = {},
-      } = meta;
+      if (!meta.require) meta.require = [];
+      if (!meta.resources) meta.resources = {};
       meta.grant = [...new Set(meta.grant || [])]; // deduplicate
-      require.forEach(rememberUrl, pathMap);
-      resources::forEachValue(rememberUrl, pathMap);
-      pathMap::rememberUrl(meta.icon);
-      getScriptUpdateUrl(script, true)?.forEach(rememberUrl, pathMap);
-    } else if ((id = storage.mod.toId(key))) {
-      mods.push(id);
-    } else if (/^\w+:data:/.test(key)) { // VM before 2.13.2 stored them unnecessarily
-      toRemove.push(key);
     }
   });
-  storage.base.remove(toRemove);
-  storage.mod.remove(mods.filter(url => !resUrls.has(url)));
   // Switch defaultInjectInto from `page` to `auto` when upgrading VM2.12.7 or older
   if (version !== lastVersion
   && IS_FIREFOX
@@ -244,10 +226,6 @@ export const ENV_SCRIPTS = 'scripts';
 export const ENV_VALUE_IDS = 'valueIds';
 const GMVALUES_RE = /^GM[_.](listValues|([gs]et|delete)Value)$/;
 const RUN_AT_RE = /^document-(start|body|end|idle)$/;
-const S_REQUIRE = storage.require.name;
-const S_CACHE = storage.cache.name;
-const S_VALUE = storage.value.name;
-const S_CODE = storage.code.name;
 const STORAGE_ROUTES = {
   [S_CACHE]: ENV_CACHE_KEYS,
   [S_CODE]: 'ids',
@@ -380,11 +358,12 @@ async function readEnvironmentData(env, isRetry) {
  * @desc Get data for dashboard.
  * @return {Promise<{ scripts: VMScript[], cache: Object }>}
  */
-export async function getData(ids) {
+export async function getData({ ids, sizes }) {
   const scripts = ids ? ids.map(getScriptById) : store.scripts;
   return {
     scripts,
     cache: await getIconCache(scripts),
+    sizes: sizes && getSizes(ids),
   };
 }
 
@@ -409,26 +388,36 @@ async function getIconCache(scripts) {
   return res;
 }
 
-export async function getSizes(ids) {
+/**
+ * @param {number[]} [ids]
+ * @return {number[][]}
+ */
+export function getSizes(ids) {
   const scripts = ids ? ids.map(getScriptById) : store.scripts;
-  const {
-    [S_CACHE]: cache, [S_CODE]: code, [S_VALUE]: value, [S_REQUIRE]: require,
-  } = await getScriptEnv(scripts, true).promise;
   return scripts.map(({
     meta,
     custom: { pathMap = {} },
     props: { id },
-  }, index) => [
-    code[id]?.length,
-    deepSize(scripts[index]),
-    deepSize(value[id]),
-    meta.require.reduce((len, v) => len
-      + (require[pathMap[v] || v]?.length || 0), 0),
-    Object.entries(meta.resources).reduce((len, e) => len
-      + e[0].length + 4 + (cache[pathMap[e[1]] || e[1]]?.length || 0), 0),
+  }, i) => [
+    // Same order as SIZE_TITLES and sizesPrefixRe
+    store.sizes[S_CODE_PRE + id] || 0,
+    deepSize(scripts[i]),
+    store.sizes[S_VALUE_PRE + id] || 0,
+    meta.require.reduce(getSizeForRequires, { len: 0, pathMap }).len,
+    Object.values(meta.resources).reduce(getSizeForResources, { len: 0, pathMap }).len,
   ]);
 }
 
+function getSizeForRequires(accum, url) {
+  accum.len += (store.sizes[S_REQUIRE_PRE + (accum.pathMap[url] || url)] || 0) + url.length;
+  return accum;
+}
+
+function getSizeForResources(accum, url) {
+  accum.len += (store.sizes[S_CACHE_PRE + (accum.pathMap[url] || url)] || 0) + url.length;
+  return accum;
+}
+
 /** @return {?Promise<void>} only if something was removed, otherwise undefined */
 export function checkRemove({ force } = {}) {
   const now = Date.now();
@@ -643,82 +632,82 @@ let _vacuuming;
  */
 export async function vacuum(data) {
   if (_vacuuming) return _vacuuming;
-  let numFixes = 0;
   let resolveSelf;
   _vacuuming = new Promise(r => { resolveSelf = r; });
+  const sizes = {};
   const result = {};
   const toFetch = [];
-  const keysToRemove = [
-    'editorThemeNames', // TODO: remove in 2022
-  ];
-  const valueKeys = {};
-  const cacheKeys = {};
-  const requireKeys = {};
-  const codeKeys = {};
-  const mappings = [
-    [storage[S_VALUE], valueKeys],
-    [storage[S_CACHE], cacheKeys],
-    [storage[S_REQUIRE], requireKeys],
-    [storage[S_CODE], codeKeys],
-  ];
+  const keysToRemove = [];
+  /** -1=untouched, 1=touched, 2(+scriptId)=missing */
+  const status = {};
+  const prefixRe = RegExp(`^(${S_VALUE_PRE}|${S_CACHE_PRE}|${S_REQUIRE_PRE}|${S_CODE_PRE}${S_MOD_PRE})`);
+  const downloadUrls = {};
+  const touch = (prefix, id, scriptId, pathMap) => {
+    if (!id || pathMap && isDataUri(id)) {
+      return 0;
+    }
+    const key = prefix + (pathMap?.[id] || id);
+    const val = status[key];
+    if (val < 0) {
+      status[key] = 1;
+      if (id !== scriptId) {
+        status[S_MOD_PRE + id] = 1;
+      }
+      if (prefix === S_CACHE_PRE || prefix === S_REQUIRE_PRE || prefix === S_VALUE_PRE) {
+        sizes[key] = deepSize(data[key]) + (prefix === S_VALUE_PRE ? 0 : key.length);
+      }
+    } else if (!val) {
+      status[key] = 2 + scriptId;
+    }
+  };
   if (!data) data = await storage.base.getMulti();
   data::forEachKey((key) => {
-    mappings.some(([substore, map]) => {
-      const id = substore.toId(key);
-      // -1 for untouched, 1 for touched, 2 for missing
-      if (id) map[id] = -1;
-      return id;
-    });
-  });
-  const touch = (obj, key, scriptId) => {
-    if (obj[key] < 0) {
-      obj[key] = 1;
-    } else if (!obj[key]) {
-      obj[key] = 2 + scriptId;
+    if (prefixRe.test(key)) {
+      status[key] = -1;
     }
-  };
+  });
+  store.sizes = sizes;
   store.scripts.forEach((script) => {
-    const { id } = script.props;
-    touch(codeKeys, id, id);
-    touch(valueKeys, id, id);
-    if (!script.custom.pathMap) buildPathMap(script);
-    const { pathMap } = script.custom;
-    script.meta.require.forEach((url) => {
-      if (url) touch(requireKeys, pathMap[url] || url, id);
-    });
-    script.meta.resources::forEachValue((url) => {
-      if (url) touch(cacheKeys, pathMap[url] || url, id);
-    });
-    const { icon } = script.meta;
-    if (isRemote(icon)) {
-      const fullUrl = pathMap[icon] || icon;
-      touch(cacheKeys, fullUrl, id);
+    const { meta, props } = script;
+    const { icon } = meta;
+    const { id } = props;
+    const pathMap = script.custom.pathMap || buildPathMap(script);
+    const updUrls = getScriptUpdateUrl(script, true);
+    if (updUrls) {
+      updUrls.forEach(url => touch(S_MOD_PRE, url, id));
+      downloadUrls[id] = updUrls[0];
     }
+    touch(S_CODE_PRE, id, id);
+    touch(S_VALUE_PRE, id, id);
+    meta.require.forEach(url => touch(S_REQUIRE_PRE, url, id, pathMap));
+    meta.resources::forEachValue(url => touch(S_CACHE_PRE, url, id, pathMap));
+    if (isRemote(icon)) touch(S_CACHE_PRE, icon, id, pathMap);
   });
-  mappings.forEach(([substore, map]) => {
-    map::forEachEntry(([key, value]) => {
-      if (value < 0) {
-        // redundant value
-        keysToRemove.push(substore.toKey(key));
-        numFixes += 1;
-      } else if (value >= 2 && substore.fetch) {
-        // missing resource
-        keysToRemove.push(storage.mod.toKey(key));
-        toFetch.push(substore.fetch(key).catch(err => `${
-          getScriptName(getScriptById(value - 2))
+  status::forEachEntry(([key, value]) => {
+    if (value < 0) {
+      // Removing redundant value
+      keysToRemove.push(key);
+    } else if (value >= 2) {
+      // Downloading the missing code or resource
+      const area = storage.forKey(key);
+      const id = area.toId(key);
+      const url = area.name === S_CODE ? downloadUrls[id] : id;
+      if (url && area.fetch) {
+        keysToRemove.push(S_MOD_PRE + url);
+        toFetch.push(area.fetch(url).catch(err => `${
+          getScriptName(getScriptById(+id || value - 2))
         }: ${
           formatHttpError(err)
         }`));
-        numFixes += 1;
       }
-    });
+    }
   });
-  if (numFixes) {
+  if (keysToRemove.length) {
     await storage.base.remove(keysToRemove); // Removing `mod` before fetching
     result.errors = (await Promise.all(toFetch)).filter(Boolean);
   }
   _vacuuming = null;
-  result.fixes = numFixes;
+  result.fixes = toFetch.length + keysToRemove.length;
   resolveSelf(result);
   return result;
 }

+ 1 - 1
src/background/utils/popup-tracker.js

@@ -32,7 +32,7 @@ async function prefetchSetPopup() {
   const tabId = (await getActiveTab()).id;
   sendTabCmd(tabId, 'PopupShown', true);
   commands.SetPopup = async (data, src) => {
-    Object.assign(data, await getData(data.ids));
+    Object.assign(data, await getData({ ids: data.ids }));
     cache.put('SetPopup', Object.assign({ [src.frameId]: [data, src] }, cache.get('SetPopup')));
   };
 }

+ 11 - 2
src/background/utils/storage-cache.js

@@ -1,9 +1,9 @@
 import { debounce, ensureArray, initHooks, isEmpty } from '@/common';
 import initCache from '@/common/cache';
 import { WATCH_STORAGE } from '@/common/consts';
-import { deepCopy, deepCopyDiff, forEachEntry } from '@/common/object';
+import { deepCopy, deepCopyDiff, deepSize, forEachEntry } from '@/common/object';
 import { store } from './db';
-import storage from './storage';
+import storage, { S_SCRIPT_PRE } from './storage';
 
 /** Throttling browser API for `storage.value`, processing requests sequentially,
  so that we can supersede an earlier chained request if it's obsolete now,
@@ -80,6 +80,7 @@ storage.api = {
           keys.push(key);
           toWrite[key] = val;
           updateScriptMap(key, val);
+          updateScriptSizeContributor(key, val);
         }
       }
     });
@@ -104,6 +105,7 @@ storage.api = {
           ok = false;
         } else {
           updateScriptMap(key);
+          updateScriptSizeContributor(key);
         }
       }
       return ok;
@@ -170,6 +172,13 @@ function updateScriptMap(key, val) {
   }
 }
 
+async function updateScriptSizeContributor(key, val) {
+  const area = store.sizesPrefixRe.exec(key);
+  if (area && area[0] !== S_SCRIPT_PRE) {
+    store.sizes[key] = deepSize(val);
+  }
+}
+
 async function flush() {
   const keys = Object.keys(valuesToFlush);
   const toRemove = keys.filter(key => !valuesToFlush[key] && delete valuesToFlush[key]);

+ 3 - 3
src/background/utils/storage-fetch.js

@@ -1,7 +1,6 @@
 import { isDataUri, makeRaw, request } from '@/common';
 import storage from './storage';
 
-/** @type { function(url, options, check): Promise<void> } or throws on error */
 storage.cache.fetch = cacheOrFetch({
   init(options) {
     return { ...options, responseType: 'blob' };
@@ -13,7 +12,6 @@ storage.cache.fetch = cacheOrFetch({
   },
 });
 
-/** @type { function(url, options): Promise<void> } or throws on error */
 storage.require.fetch = cacheOrFetch({
   transform: ({ data }, url) => (
     /^\s*</.test(data)
@@ -22,10 +20,12 @@ storage.require.fetch = cacheOrFetch({
   ),
 });
 
+storage.code.fetch = cacheOrFetch();
+
+/** @return {VMStorageFetch} */
 function cacheOrFetch(handlers = {}) {
   const requests = {};
   const { init, transform } = handlers;
-  /** @this StorageArea */
   return function cacheOrFetchHandler(...args) {
     const [url] = args;
     const promise = requests[url] || (requests[url] = this::doFetch(...args));

+ 28 - 9
src/background/utils/storage.js

@@ -4,6 +4,7 @@ import { commands } from './message';
 
 let api = browser.storage.local;
 
+/** @prop {VMStorageFetch} [fetch] */
 class StorageArea {
   constructor(prefix) {
     this.name = '';
@@ -15,9 +16,11 @@ class StorageArea {
     return this.prefix + id;
   }
 
-  /** @return {?string} */
+  /** @return {string} */
   toId(key) {
-    if (key.startsWith(this.prefix)) return key.slice(this.prefix.length);
+    return key.startsWith(this.prefix)
+      ? key.slice(this.prefix.length)
+      : '';
   }
 
   async getOne(id) {
@@ -67,23 +70,39 @@ class StorageArea {
   }
 }
 
+export const S_CACHE_PRE = 'cac:';
+export const S_CODE_PRE = 'code:';
+export const S_MOD_PRE = 'mod:';
+export const S_REQUIRE_PRE = 'req:';
+export const S_SCRIPT_PRE = 'scr:';
+export const S_VALUE_PRE = 'val:';
 const storage = {
   get api() { return api; },
   set api(val) { api = val; },
+  /** @return {?StorageArea} */// eslint-disable-next-line no-use-before-define
+  forKey: key => storageByPrefix[/^\w+:|$/.exec(key)[0]],
   base: new StorageArea(''),
-  cache: new StorageArea('cac:'),
-  code: new StorageArea('code:'),
+  cache: new StorageArea(S_CACHE_PRE),
+  code: new StorageArea(S_CODE_PRE),
   /** last-modified HTTP header value per URL */
-  mod: new StorageArea('mod:'),
-  require: new StorageArea('req:'),
-  script: new StorageArea('scr:'),
-  value: new StorageArea('val:'),
+  mod: new StorageArea(S_MOD_PRE),
+  require: new StorageArea(S_REQUIRE_PRE),
+  script: new StorageArea(S_SCRIPT_PRE),
+  value: new StorageArea(S_VALUE_PRE),
 };
-storage::mapEntry((val, name) => {
+/** @type {{ [prefix: string]: StorageArea }} */
+export const storageByPrefix = storage::mapEntry(null, (name, val) => {
   if (val instanceof StorageArea) {
     val.name = name;
+    return val.prefix;
   }
 });
+export const S_CACHE = storageByPrefix[S_CACHE_PRE].name;
+export const S_CODE = storageByPrefix[S_CODE_PRE].name;
+export const S_MOD = storageByPrefix[S_MOD_PRE].name;
+export const S_REQUIRE = storageByPrefix[S_REQUIRE_PRE].name;
+export const S_SCRIPT = storageByPrefix[S_SCRIPT_PRE].name;
+export const S_VALUE = storageByPrefix[S_VALUE_PRE].name;
 export default storage;
 
 Object.assign(commands, {

+ 25 - 35
src/options/index.js

@@ -1,6 +1,6 @@
 import Vue from 'vue';
 import '@/common/browser';
-import { formatByteLength, getLocaleString, i18n, makePause, sendCmdDirectly } from '@/common';
+import { formatByteLength, getLocaleString, i18n, sendCmdDirectly } from '@/common';
 import handlers from '@/common/handlers';
 import { loadScriptIcon } from '@/common/load-script-icon';
 import options from '@/common/options';
@@ -8,6 +8,7 @@ import '@/common/ui/style';
 import { store } from './utils';
 import App from './views/app';
 
+// Same order as getSizes and sizesPrefixRe
 const SIZE_TITLES = [
   i18n('editNavCode'),
   i18n('editNavSettings'),
@@ -34,8 +35,9 @@ function initialize() {
 
 /**
  * @param {VMScript} script
+ * @param {number[]} sizes
  */
-async function initScript(script) {
+function initScript(script, sizes) {
   const meta = script.meta || {};
   const localeName = getLocaleString(meta, 'name');
   const search = [
@@ -48,35 +50,21 @@ async function initScript(script) {
   ].filter(Boolean).join('\n');
   const name = script.custom.name || localeName;
   const lowerName = name.toLowerCase();
-  script.$cache = { search, name, lowerName, size: '', sizes: '', sizeNum: 0 };
-  loadScriptIcon(script, store.cache, store.HiDPI || -1);
-}
-
-/**
- * @param {number[]} sz
- * @param {VMScript} script
- */
-function initSize(sz, { $cache }) {
   let total = 0;
   let str = '';
-  for (let i = 0, val; i < sz.length; i += 1) {
-    val = sz[i];
+  sizes.forEach((val, i) => {
     total += val;
     if (val) str += `${SIZE_TITLES[i]}: ${formatByteLength(val)}\n`;
-  }
-  $cache.sizes = str.slice(0, -1).replace(/\x20/g, '\xA0').replace(/[^B]$/gm, '$&B');
-  $cache.sizeNum = total;
-  $cache.size = formatByteLength(total, true).replace(' ', '');
-}
-
-/**
- * @param {VMScript} script
- */
-async function initScriptAndSize(script) {
-  const res = initScript(script);
-  const [sz] = await sendCmdDirectly('GetSizes', [script.props.id]);
-  initSize(sz, script);
-  return res;
+  });
+  script.$cache = {
+    search,
+    name,
+    lowerName,
+    size: formatByteLength(total, true).replace(' ', ''),
+    sizes: str.slice(0, -1).replace(/\x20/g, '\xA0').replace(/[^B]$/gm, '$&B'),
+    sizeNum: total,
+  };
+  loadScriptIcon(script, store.cache, store.HiDPI || -1);
 }
 
 export function loadData() {
@@ -86,14 +74,15 @@ export function loadData() {
 }
 
 async function requestData(ids) {
-  const getDataP = sendCmdDirectly('GetData', ids, { retry: true });
-  const [data] = await Promise.all([getDataP, options.ready]);
-  const { scripts, ...auxData } = data;
-  const getSizesP = sendCmdDirectly('GetSizes', ids, { retry: true })
-  .then(sizes => sizes.forEach((sz, i) => initSize(sz, scripts[i])));
+  const [data] = await Promise.all([
+    sendCmdDirectly('GetData', { ids, sizes: true }, { retry: true }),
+    options.ready,
+  ]);
+  const { scripts, sizes, ...auxData } = data;
   Object.assign(store, auxData); // initScripts needs `cache` in store
-  scripts.forEach(initScript); // modifying scripts without triggering reactivity
-  await Promise.race([makePause(0), getSizesP]); // blocking render for one event loop tick
+  scripts.forEach((script, i) => { // modifying scripts without triggering reactivity
+    initScript(script, sizes[i]);
+  });
   store.scripts = scripts; // now we can render
   store.loading = false;
 }
@@ -110,11 +99,12 @@ function initMain() {
     },
     async UpdateScript({ update, where } = {}) {
       if (!update) return;
+      const [sizes] = await sendCmdDirectly('GetSizes', [where.id]);
       const { scripts } = store;
       const index = scripts.findIndex(item => item.props.id === where.id);
       const updated = Object.assign({}, scripts[index], update);
       if (updated.error && !update.error) updated.error = null;
-      await initScriptAndSize(updated);
+      initScript(updated, sizes);
       if (index < 0) {
         update.message = '';
         scripts.push(updated);

+ 1 - 1
src/popup/index.js

@@ -36,7 +36,7 @@ Object.assign(handlers, {
       // frameScripts may be appended multiple times if iframes have unique scripts
       const scope = store[isTop ? 'scripts' : 'frameScripts'];
       const metas = data.scripts?.filter(({ props: { id } }) => ids.includes(id))
-        || (Object.assign(data, await sendCmdDirectly('GetData', ids))).scripts;
+        || (Object.assign(data, await sendCmdDirectly('GetData', { ids }))).scripts;
       metas.forEach(script => {
         loadScriptIcon(script, data.cache);
         const { id } = script.props;

+ 8 - 0
src/types.d.ts

@@ -168,6 +168,8 @@ declare namespace VMScript {
     position: number;
     uri: string;
     uuid: string;
+    /** Added in memory at extension start */
+    sizes?: number[];
   }
 }
 /**
@@ -269,6 +271,12 @@ declare type VMSearchOptions = {
   reuseCursor?: boolean;
   pos?: { line: number, ch: number };
 }
+/** Throws on error */
+declare type VMStorageFetch = (
+  url: string,
+  options?: VMReq.Options,
+  check?: (...args) => void // throws on error
+) => Promise<void>
 declare interface VMUserAgent extends VMScriptGMInfoPlatform {
   /** Chrome/ium version number */
   chrome: number | typeof NaN;