InfoLens / extension /test /analyzeCache.test.mjs
dqy08's picture
超长文性能优化;缓存改进;UI改进
2e580ae
Raw
History Blame Contribute Delete
17.9 kB
/**
* 请求缓存:命中不打网、失败不写。运行:node --test extension/test/analyzeCache.test.mjs
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { runInThisContext } from 'node:vm';
import { webcrypto } from 'node:crypto';
if (!globalThis.crypto) {
Object.defineProperty(globalThis, 'crypto', { value: webcrypto, configurable: true });
}
const dir = dirname(fileURLToPath(import.meta.url));
const src = readFileSync(join(dir, '../semantic/analyzeCache.js'), 'utf8');
runInThisContext(src, { filename: 'analyzeCache.js' });
const cache = globalThis.IL_analyzeCache;
test.beforeEach(() => {
cache.clear();
delete globalThis.chrome;
});
function mockLocal(initial = {}) {
const data = { ...initial };
globalThis.chrome = {
storage: {
local: {
async get(key) {
if (key == null) return { ...data };
if (Array.isArray(key)) {
const out = {};
for (const k of key) {
if (k in data) out[k] = data[k];
}
return out;
}
return { [key]: data[key] };
},
async set(obj) {
Object.assign(data, obj);
},
async remove(key) {
for (const k of Array.isArray(key) ? key : [key]) delete data[k];
},
},
},
};
return data;
}
function rowsOf(texts, degrees) {
return async (query, sent, onRow) => {
sent.forEach((t, i) => onRow(i + 1, degrees[texts.indexOf(t)]));
};
}
test('全未命中:一次 send 全部 texts,再读走缓存', async () => {
const texts = ['a', 'b', 'c'];
const sent = [];
const send = async (query, ts, onRow) => {
sent.push(ts.slice());
ts.forEach((_, i) => onRow(i + 1, [0.1, 0.2, 0.3][i]));
};
const rows = [];
await cache.relevance('q', texts, (n, d) => rows.push([n, d]), undefined, send);
assert.deepEqual(sent, [['a', 'b', 'c']]);
assert.deepEqual(rows, [
[1, 0.1],
[2, 0.2],
[3, 0.3],
]);
sent.length = 0;
rows.length = 0;
await cache.relevance('q', texts, (n, d) => rows.push([n, d]), undefined, send);
assert.deepEqual(sent, []);
assert.deepEqual(rows, [
[1, 0.1],
[2, 0.2],
[3, 0.3],
]);
});
test('全命中:不调用 send', async () => {
const texts = ['x', 'y'];
await cache.relevance('q', texts, () => {}, undefined, rowsOf(texts, [0.4, 0.9]));
let called = 0;
const rows = [];
await cache.relevance(
'q',
texts,
(n, d) => rows.push([n, d]),
undefined,
async () => {
called += 1;
}
);
assert.equal(called, 0);
assert.deepEqual(rows, [
[1, 0.4],
[2, 0.9],
]);
});
test('部分在缓存:回放前缀,只 send 后缀', async () => {
await cache.relevance('q', ['a'], () => {}, undefined, async (_q, _ts, onRow) => {
onRow(1, 0.5);
});
const sent = [];
const rows = [];
await cache.relevance(
'q',
['a', 'b', 'c'],
(n, d) => rows.push([n, d]),
undefined,
async (_q, ts, onRow) => {
sent.push(ts.slice());
ts.forEach((_, i) => onRow(i + 1, 0.2));
}
);
assert.deepEqual(sent, [['b', 'c']]);
assert.deepEqual(rows, [
[1, 0.5],
[2, 0.2],
[3, 0.2],
]);
});
test('relevance row 已到则写入;send 随后失败不影响已到的 degree', async () => {
const send = async (_q, ts, onRow) => {
onRow(1, 0.7);
throw new Error('network');
};
await assert.rejects(() => cache.relevance('q', ['a', 'b'], () => {}, undefined, send));
const sent = [];
const rows = [];
await cache.relevance(
'q',
['a', 'b'],
(n, d) => rows.push([n, d]),
undefined,
async (_q, ts, onRow) => {
sent.push(ts.slice());
ts.forEach((_, i) => onRow(i + 1, 0.3));
}
);
assert.deepEqual(sent, [['b']]);
assert.deepEqual(rows, [
[1, 0.7],
[2, 0.3],
]);
});
test('cachedWindowLength:满窗都在则不截', () => {
const all = [0, 0.2, 0.05];
assert.equal(cache.cachedWindowLength(all, 0.1), 3);
});
test('cachedWindowLength:前缀有匹配、后面有洞 → 收到连续缓存末', () => {
assert.equal(cache.cachedWindowLength([0, 0.2, 0.05, undefined, 0.9], 0.1), 3);
});
test('cachedWindowLength:前缀无匹配且有洞 → 从洞起再取 maxSend', () => {
assert.equal(cache.cachedWindowLength([0, 0.05, undefined, 0.9], 0.1), 4);
const degrees = [0, 0.05, undefined, ...Array(40).fill(undefined)];
assert.equal(cache.cachedWindowLength(degrees, 0.1, 32), 34);
});
test('cachedWindowLength:第一块就未缓存 → 整窗', () => {
assert.equal(cache.cachedWindowLength([undefined, 0.9], 0.1), 2);
});
test('windowPlan:前缀有匹配则只取连续缓存,不把后面的洞算进窗', async () => {
await cache.relevance('q', ['a', 'b', 'c'], () => {}, undefined, async (_q, ts, onRow) => {
ts.forEach((t, i) => onRow(i + 1, t === 'b' ? 0.2 : 0.05));
});
const plan = await cache.windowPlan('q', ['a', 'b', 'c', 'd'], 0.1);
assert.equal(plan.n, 3);
assert.deepEqual(plan.degrees, [0.05, 0.2, 0.05]);
});
test('windowPlan:前缀无匹配有洞 → 前缀加 maxSend,不把前缀算进配额', async () => {
const texts = Array.from({ length: 50 }, (_, i) => `t${i}`);
await cache.relevance('q', texts.slice(0, 10), () => {}, undefined, async (_q, ts, onRow) => {
ts.forEach((_, i) => onRow(i + 1, 0.05));
});
const plan = await cache.windowPlan('q', texts, 0.1, 32);
assert.equal(plan.n, 42);
assert.equal(plan.degrees.length, 32);
assert.equal(plan.degrees[9], 0.05);
assert.equal(plan.degrees[10], undefined);
});
test('windowPlan:遇洞即停,不扫完全部剩余', async () => {
mockLocal();
const api = globalThis.chrome.storage.local;
const orig = api.get;
let maxKeys = 0;
api.get = async (key) => {
if (Array.isArray(key)) maxKeys = Math.max(maxKeys, key.length);
return orig.call(api, key);
};
const texts = Array.from({ length: 80 }, (_, i) => `t${i}`);
const plan = await cache.windowPlan('q', texts, 0.1, 32);
assert.equal(plan.n, 32);
assert.ok(maxKeys > 0 && maxKeys <= 32);
});
function planNeedsSend(plan) {
if (plan.n > plan.degrees.length) return true;
for (let i = 0; i < plan.n; i++) {
if (!Number.isFinite(plan.degrees[i])) return true;
}
return false;
}
test('定窗是否打网:全缓存 / 有匹配前缀停在洞前 → 否;从洞起 → 是', async () => {
await cache.relevance('q', ['a', 'b', 'c'], () => {}, undefined, async (_q, ts, onRow) => {
ts.forEach((t, i) => onRow(i + 1, t === 'b' ? 0.2 : 0.05));
});
assert.equal(planNeedsSend(await cache.windowPlan('q', ['a', 'b', 'c'], 0.1)), false);
assert.equal(planNeedsSend(await cache.windowPlan('q', ['a', 'b', 'c', 'd'], 0.1)), false);
assert.equal(planNeedsSend(await cache.windowPlan('q', ['d', 'e'], 0.1, 32)), true);
});
test('一次 send 的多行只改一次占用表', async () => {
mockLocal();
const api = globalThis.chrome.storage.local;
const orig = api.set;
let orderWrites = 0;
api.set = async (obj) => {
if ('il_ac/order' in obj) orderWrites += 1;
return orig.call(api, obj);
};
await cache.relevance('q', ['a', 'b', 'c'], () => {}, undefined, async (_q, ts, onRow) => {
ts.forEach((_, i) => onRow(i + 1, 0.2));
});
assert.equal(orderWrites, 1);
});
test('keywords 成功(含空 runs)才写;再读不打网', async () => {
let calls = 0;
const sendEmpty = async () => {
calls += 1;
};
await cache.keywords('q', 't0', () => {}, undefined, sendEmpty);
await cache.keywords('q', 't0', () => {}, undefined, sendEmpty);
assert.equal(calls, 1);
const runs = [
{ offset: [0, 2], raw: 'ab', score: 0.9 },
{ offset: [4, 6], raw: 'cd', score: 0.4 },
];
await cache.keywords('q', 't1', () => {}, undefined, async (_q, _t, onRun) => {
calls += 1;
for (const r of runs) onRun(r);
});
const replayed = [];
await cache.keywords('q', 't1', (r) => replayed.push(r), undefined, async () => {
calls += 1;
});
assert.equal(calls, 2);
assert.deepEqual(replayed, [
{ offset: [0, 2], score: 0.9 },
{ offset: [4, 6], score: 0.4 },
]);
});
test('keywords abort / 失败不写', async () => {
const boom = async (_q, _t, onRun) => {
onRun({ offset: [0, 1], score: 1 });
throw new Error('aborted');
};
await assert.rejects(() => cache.keywords('q', 'tx', () => {}, undefined, boom));
let calls = 0;
const seen = [];
await cache.keywords(
'q',
'tx',
(r) => seen.push(r),
undefined,
async (_q, _t, onRun) => {
calls += 1;
onRun({ offset: [2, 3], score: 0.5 });
}
);
assert.equal(calls, 1);
assert.deepEqual(seen, [{ offset: [2, 3], score: 0.5 }]);
});
test('再次执行脚本不丢已有缓存', async () => {
await cache.relevance('q', ['keep'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.6);
});
runInThisContext(src, { filename: 'analyzeCache.js' });
assert.equal(globalThis.IL_analyzeCache, cache);
let called = 0;
await globalThis.IL_analyzeCache.relevance(
'q',
['keep'],
() => {},
undefined,
async () => {
called += 1;
}
);
assert.equal(called, 0);
});
test('storage:内存清空后从 local 命中', async () => {
mockLocal();
await cache.relevance('q', ['a'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.4);
});
cache.clear();
let called = 0;
await cache.relevance('q', ['a'], () => {}, undefined, async () => {
called += 1;
});
assert.equal(called, 0);
});
test('并发写入都进环,再读不打网', async () => {
mockLocal();
await Promise.all(
['a', 'b', 'c'].map((t) =>
cache.relevance('q', [t], () => {}, undefined, async (_q, _ts, onRow) => {
onRow(1, 0.2);
})
)
);
let called = 0;
await cache.relevance('q', ['a', 'b', 'c'], () => {}, undefined, async () => {
called += 1;
});
assert.equal(called, 0);
});
test('满员时覆盖环上最旧条', async () => {
const data = mockLocal();
const keys = [];
for (let i = 0; i < cache.MAX_ENTRIES; i++) {
const k = `il_ac/r/${String(i).padStart(64, '0')}`;
keys.push(k);
data[k] = 0.1;
}
data['il_ac/order'] = { keys, i: 0 };
await cache.relevance('q', ['new'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.9);
});
assert.equal(data[keys[0]], undefined);
assert.equal(data[keys[1]], 0.1);
assert.equal(data[`il_ac/r/${await cache.key('q', 'new')}`], 0.9);
});
test('storage:按条写入后可直接命中', async () => {
const k = await cache.key('q', 'a');
mockLocal({ [`il_ac/r/${k}`]: 0.99 });
let called = 0;
await cache.relevance('q', ['a'], () => {}, undefined, async () => {
called += 1;
});
assert.equal(called, 0);
});
test('文本哈希为 32 位十六进制', async () => {
const k = await cache.key('q', 'hello');
const th = k.slice(k.lastIndexOf('/') + 1);
assert.equal(th.length, 32);
assert.match(th, /^[0-9a-f]{32}$/);
});
test('键含 query:不同 query 不命中', async () => {
await cache.relevance('q1', ['same'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.1);
});
let called = 0;
await cache.relevance('q2', ['same'], () => {}, undefined, async (_q, _t, onRow) => {
called += 1;
onRow(1, 0.9);
});
assert.equal(called, 1);
});
test('syncRemoteModel:每次打开都 fetch', async () => {
const data = mockLocal();
let n = 0;
const fetchVer = async () => {
n += 1;
return { relevance: 1, keywords: 1 };
};
await cache.syncRemoteModel(fetchVer);
await cache.syncRemoteModel(fetchVer);
assert.equal(n, 2);
assert.equal(data['il_ac/meta'].relevanceKey, '2:1');
assert.equal(data['il_ac/meta'].keywordsKey, '2:1');
});
test('syncRemoteModel:只升相关度不清 keywords', async () => {
mockLocal();
await cache.syncRemoteModel(async () => ({ relevance: 1, keywords: 1 }));
await cache.relevance('q', ['a'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.9);
});
await cache.keywords('q', 'a', () => {}, undefined, async (_q, _t, onRun) => {
onRun({ offset: [0, 1], score: 1 });
});
cache.clear();
await cache.syncRemoteModel(async () => ({ relevance: 2, keywords: 1 }));
let rel = 0;
await cache.relevance('q', ['a'], () => {}, undefined, async () => {
rel += 1;
});
let kw = 0;
await cache.keywords('q', 'a', () => {}, undefined, async () => {
kw += 1;
});
assert.equal(rel, 1);
assert.equal(kw, 0);
});
test('syncRemoteModel:只升 keywords 不清相关度', async () => {
mockLocal();
await cache.syncRemoteModel(async () => ({ relevance: 1, keywords: 1 }));
await cache.relevance('q', ['a'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.9);
});
await cache.keywords('q', 'a', () => {}, undefined, async (_q, _t, onRun) => {
onRun({ offset: [0, 1], score: 1 });
});
cache.clear();
await cache.syncRemoteModel(async () => ({ relevance: 1, keywords: 2 }));
let rel = 0;
await cache.relevance('q', ['a'], () => {}, undefined, async () => {
rel += 1;
});
let kw = 0;
await cache.keywords('q', 'a', () => {}, undefined, async () => {
kw += 1;
});
assert.equal(rel, 0);
assert.equal(kw, 1);
});
test('只升 keywords 且环已满:不拧淘汰指针', async () => {
const data = mockLocal();
const keys = [];
for (let i = 0; i < cache.MAX_ENTRIES; i++) {
const k = `il_ac/r/${String(i).padStart(64, '0')}`;
keys.push(k);
data[k] = 0.1;
}
data['il_ac/order'] = { keys, i: 7 };
data['il_ac/meta'] = { relevanceKey: '2:1', keywordsKey: '2:1' };
await cache.syncRemoteModel(async () => ({ relevance: 1, keywords: 2 }));
assert.equal(data['il_ac/order'].i, 7);
assert.equal(data['il_ac/order'].keys.length, cache.MAX_ENTRIES);
});
test('丢一类后剩余按年龄排', async () => {
const data = mockLocal();
const keys = [];
const kNew = `il_ac/k/${'a'.repeat(64)}`;
const kOld = `il_ac/k/${'b'.repeat(64)}`;
for (let i = 0; i < cache.MAX_ENTRIES; i++) {
const k =
i === 0 ? kNew : i === cache.MAX_ENTRIES - 1 ? kOld : `il_ac/r/${String(i).padStart(64, '0')}`;
keys.push(k);
data[k] = k.startsWith('il_ac/k/') ? [] : 0.1;
}
data['il_ac/order'] = { keys, i: 1 };
data['il_ac/meta'] = { relevanceKey: '2:1', keywordsKey: '2:1' };
await cache.syncRemoteModel(async () => ({ relevance: 2, keywords: 1 }));
assert.deepEqual(data['il_ac/order'].keys, [kOld, kNew]);
assert.equal(data['il_ac/order'].i, 0);
});
test('打开时清掉旧整包 key', async () => {
const data = mockLocal({ il_analyze_cache: { degrees: {}, runs: {} } });
await cache.syncRemoteModel(async () => ({ relevance: 1, keywords: 1 }));
assert.equal(data.il_analyze_cache, undefined);
});
test('usage:条数为数据 key,不含 meta/order', async () => {
mockLocal();
await cache.relevance('q', ['a', 'b'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.1);
onRow(2, 0.2);
});
await cache.keywords('q', 'a', () => {}, undefined, async (_q, _t, onRun) => {
onRun({ offset: [0, 1], score: 1 });
});
const u = await cache.usage();
assert.equal(u.entries, 3);
assert.ok(u.bytes > 0);
});
test('dropQuery:只丢该 query 的 r/k,其它 query 留下', async () => {
mockLocal();
await cache.relevance('q1', ['a', 'b'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.1);
onRow(2, 0.2);
});
await cache.relevance('q2', ['a'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.3);
});
await cache.keywords('q1', 'a', () => {}, undefined, async (_q, _t, onRun) => {
onRun({ offset: [0, 1], score: 1 });
});
await cache.dropQuery('q1');
let q1 = 0;
await cache.relevance('q1', ['a'], () => {}, undefined, async () => {
q1 += 1;
});
assert.equal(q1, 1);
let q2 = 0;
await cache.relevance('q2', ['a'], () => {}, undefined, async () => {
q2 += 1;
});
assert.equal(q2, 0);
let kw = 0;
await cache.keywords('q1', 'a', () => {}, undefined, async () => {
kw += 1;
});
assert.equal(kw, 1);
});
test('dropQuery:query 含斜杠不误伤前缀', async () => {
mockLocal();
await cache.relevance('a', ['t'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.1);
});
await cache.relevance('a/b', ['t'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.2);
});
await cache.dropQuery('a');
let a = 0;
await cache.relevance('a', ['t'], () => {}, undefined, async () => {
a += 1;
});
assert.equal(a, 1);
let ab = 0;
await cache.relevance('a/b', ['t'], () => {}, undefined, async () => {
ab += 1;
});
assert.equal(ab, 0);
});
test('dropAll:清掉缓存条,不碰其它 key', async () => {
const data = mockLocal({ il_semantic_find_history: ['keep'] });
await cache.relevance('q', ['a'], () => {}, undefined, async (_q, _t, onRow) => {
onRow(1, 0.4);
});
await cache.dropAll();
const u = await cache.usage();
assert.equal(u.entries, 0);
assert.equal(u.bytes, 0);
assert.deepEqual(data.il_semantic_find_history, ['keep']);
assert.equal(Object.keys(data).some((k) => k.startsWith('il_ac/')), false);
let called = 0;
await cache.relevance('q', ['a'], () => {}, undefined, async (_q, _t, onRow) => {
called += 1;
onRow(1, 0.4);
});
assert.equal(called, 1);
});
test('syncRemoteModel:fetch 失败不改 key,下次仍问', async () => {
mockLocal();
await assert.rejects(() => cache.syncRemoteModel(async () => {
throw new Error('net');
}));
let n = 0;
await cache.syncRemoteModel(async () => {
n += 1;
return { relevance: 1, keywords: 1 };
});
assert.equal(n, 1);
});