perf(aegis): history was 473 round trips in series, and persisted all of them
Second half of the Theseus-lag investigation. 0.27.1 removed the 7.4 MB store; this removes the two things that produced it and the latency that came with it. Measured on this profile's own cache: 6,981 transactions, 7.38 MB, and one consolidation with 400 inputs (median 2). - loadHistory() awaited getTx() per displayed transaction and then again per INPUT, strictly one at a time. The 400-input row alone cost 400 serial round trips. Both waves are now prefetched with bounded parallelism (getTxMany, 12 at a time). Against a simulated 10 ms link the same 473 fetches take 771 ms instead of ~4,730 ms; on a real 30-50 ms link the serial version was 15-25 seconds per refresh, per wallet. - Parent transactions were persisted forever. They exist only to compute a delta for the 25 rows on screen, and keeping every one ever seen is what grew the file. Only the displayed window is written now — 25 entries, 16.4 KB in the harness — while parents stay in a process-lifetime map bounded at 20,000, seeded from the window so a restart does not refetch what is already visible. A second refresh issues zero fetches. TX_CACHE_VERSION 4 discards v3 caches. The v3 cap of 400 was also exactly the wrong number for this data: a 400-input transaction needs 401 entries, so it would have evicted and refetched on every single refresh.
This commit is contained in:
parent
469ff482ea
commit
7983e24f2e
2 changed files with 66 additions and 16 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "aegis",
|
"id": "aegis",
|
||||||
"name": "Aegis Wallet",
|
"name": "Aegis Wallet",
|
||||||
"version": "0.27.1",
|
"version": "0.28.0",
|
||||||
"category": "plugin",
|
"category": "plugin",
|
||||||
"description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash and window.wizardconnect on any site; window.tronWeb / window.tronLink / window.ethereum / window.solana too. Every call needs your approval.",
|
"description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash and window.wizardconnect on any site; window.tronWeb / window.tronLink / window.ethereum / window.solana too. Every call needs your approval.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,15 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora
|
||||||
// drives the entire browser. A 7.7 MB cache turned every storage access
|
// drives the entire browser. A 7.7 MB cache turned every storage access
|
||||||
// into a multi-hundred-millisecond freeze of all of Theseus. Bumping the
|
// into a multi-hundred-millisecond freeze of all of Theseus. Bumping the
|
||||||
// version also discards existing oversized caches on first load.
|
// version also discards existing oversized caches on first load.
|
||||||
const TX_CACHE_VERSION = 3;
|
// v4: v2 was unbounded and v3 capped the PERSISTED set at 400, which is
|
||||||
const TX_CACHE_MAX = 400;
|
// exactly the wrong number here — one consolidation in this wallet has 400
|
||||||
|
// inputs, so a single history row needed 401 entries and the cap thrashed.
|
||||||
|
// The persisted set is now just the displayed window; parents live in RAM.
|
||||||
|
const TX_CACHE_VERSION = 4;
|
||||||
|
// Guard on RAM, not on correctness. Parents are re-fetchable; this only
|
||||||
|
// stops a long session from growing without limit.
|
||||||
|
const TX_MEM_MAX = 20000;
|
||||||
|
const TX_FETCH_CONCURRENCY = 12;
|
||||||
let txCache = storage.get("txCache", {}) || {};
|
let txCache = storage.get("txCache", {}) || {};
|
||||||
if (storage.get("txCacheVersion", 1) !== TX_CACHE_VERSION) {
|
if (storage.get("txCacheVersion", 1) !== TX_CACHE_VERSION) {
|
||||||
txCache = {};
|
txCache = {};
|
||||||
|
|
@ -55,15 +62,29 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora
|
||||||
// the cache on every refresh, so an idle wallet rewrote the whole file on
|
// the cache on every refresh, so an idle wallet rewrote the whole file on
|
||||||
// every poll for nothing.
|
// every poll for nothing.
|
||||||
let txDirty = false;
|
let txDirty = false;
|
||||||
function pruneTxCache() {
|
// Process-lifetime, never persisted. Seeded from the persisted window so a
|
||||||
const ids = Object.keys(txCache);
|
// restart does not refetch what is on screen.
|
||||||
if (ids.length <= TX_CACHE_MAX) return;
|
const memTx = new Map();
|
||||||
// Newest first by block time. Unconfirmed entries have time 0 but are by
|
for (const [id, t] of Object.entries(txCache)) memTx.set(id, t);
|
||||||
// definition current, so they sort as newest rather than being evicted
|
function rememberTx(id, slim) {
|
||||||
// first. Anything dropped is re-fetchable on demand.
|
if (memTx.size >= TX_MEM_MAX) {
|
||||||
ids.sort((a, b) => ((txCache[b].time || Infinity) - (txCache[a].time || Infinity)));
|
// Drop the oldest insertion; Map preserves insertion order.
|
||||||
for (const id of ids.slice(TX_CACHE_MAX)) delete txCache[id];
|
const oldest = memTx.keys().next();
|
||||||
txDirty = true;
|
if (!oldest.done) memTx.delete(oldest.value);
|
||||||
|
}
|
||||||
|
memTx.set(id, slim);
|
||||||
|
}
|
||||||
|
// Fetch many transactions with bounded parallelism. Already-known ids cost
|
||||||
|
// nothing, so passing the same list twice is cheap.
|
||||||
|
async function getTxMany(ids) {
|
||||||
|
const want = [...new Set(ids)].filter((id) => {
|
||||||
|
const c = memTx.get(id);
|
||||||
|
return !(c && c.confirmations > 0);
|
||||||
|
});
|
||||||
|
for (let i = 0; i < want.length; i += TX_FETCH_CONCURRENCY) {
|
||||||
|
const batch = want.slice(i, i + TX_FETCH_CONCURRENCY);
|
||||||
|
await Promise.all(batch.map((id) => getTx(id).catch(() => null)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let refreshTimer = null;
|
let refreshTimer = null;
|
||||||
let subscribedHeaders = false;
|
let subscribedHeaders = false;
|
||||||
|
|
@ -231,7 +252,7 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTx(txid) {
|
async function getTx(txid) {
|
||||||
const c = txCache[txid];
|
const c = memTx.get(txid) || txCache[txid];
|
||||||
if (c && c.confirmations > 0) return c;
|
if (c && c.confirmations > 0) return c;
|
||||||
const raw = await client.call("blockchain.transaction.get", [txid, true]);
|
const raw = await client.call("blockchain.transaction.get", [txid, true]);
|
||||||
const slim = {
|
const slim = {
|
||||||
|
|
@ -252,7 +273,7 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora
|
||||||
})),
|
})),
|
||||||
size: raw.size || 0,
|
size: raw.size || 0,
|
||||||
};
|
};
|
||||||
txCache[txid] = slim;
|
rememberTx(txid, slim);
|
||||||
txDirty = true;
|
txDirty = true;
|
||||||
return slim;
|
return slim;
|
||||||
}
|
}
|
||||||
|
|
@ -270,6 +291,20 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora
|
||||||
return hb - ha;
|
return hb - ha;
|
||||||
}).slice(0, HISTORY_LIMIT);
|
}).slice(0, HISTORY_LIMIT);
|
||||||
const ours = new Set([...state.watched.values()].map((e) => e.scriptHex));
|
const ours = new Set([...state.watched.values()].map((e) => e.scriptHex));
|
||||||
|
// Fetch in two parallel waves instead of one long sequential chain. This
|
||||||
|
// loop used to await getTx() per displayed transaction and then again per
|
||||||
|
// INPUT of each, one at a time. A consolidation here has 400 inputs, so a
|
||||||
|
// single history row cost 400 round trips in series — tens of seconds,
|
||||||
|
// during which every response still had to be parsed on the main thread.
|
||||||
|
// Median input count is 2, so the common case gets faster too.
|
||||||
|
await getTxMany(ordered.map((h) => h.txid));
|
||||||
|
const parentIds = [];
|
||||||
|
for (const h of ordered) {
|
||||||
|
const t = await getTx(h.txid);
|
||||||
|
for (const i of t.vin) if (i.txid) parentIds.push(i.txid);
|
||||||
|
}
|
||||||
|
await getTxMany(parentIds);
|
||||||
|
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const h of ordered) {
|
for (const h of ordered) {
|
||||||
const t = await getTx(h.txid);
|
const t = await getTx(h.txid);
|
||||||
|
|
@ -277,7 +312,7 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora
|
||||||
for (const o of t.vout) { outputsTotal += o.value; if (ours.has(o.scriptHex)) received += o.value; }
|
for (const o of t.vout) { outputsTotal += o.value; if (ours.has(o.scriptHex)) received += o.value; }
|
||||||
for (const i of t.vin) {
|
for (const i of t.vin) {
|
||||||
if (!i.txid) continue; // coinbase
|
if (!i.txid) continue; // coinbase
|
||||||
const p = await getTx(i.txid);
|
const p = await getTx(i.txid); // warm after the wave above
|
||||||
const po = p.vout[i.vout];
|
const po = p.vout[i.vout];
|
||||||
if (!po) continue;
|
if (!po) continue;
|
||||||
inputsTotal += po.value;
|
inputsTotal += po.value;
|
||||||
|
|
@ -295,7 +330,22 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
state.history = out;
|
state.history = out;
|
||||||
if (txDirty) { pruneTxCache(); storage.set("txCache", txCache); txDirty = false; }
|
// Persist ONLY the displayed window. Parent transactions are needed to
|
||||||
|
// compute a delta and nothing else, and persisting every one ever seen is
|
||||||
|
// what grew this store to 7.4 MB across 6,981 entries — which the host
|
||||||
|
// then re-parsed on every storage read, on the main thread. They stay in
|
||||||
|
// the in-memory map instead, so a session never refetches them and the
|
||||||
|
// file stays a few tens of KB.
|
||||||
|
const keep = {};
|
||||||
|
for (const h of ordered) {
|
||||||
|
const t = memTx.get(h.txid);
|
||||||
|
if (t) keep[h.txid] = t;
|
||||||
|
}
|
||||||
|
const before = JSON.stringify(txCache);
|
||||||
|
const after = JSON.stringify(keep);
|
||||||
|
txCache = keep;
|
||||||
|
if (after !== before) storage.set("txCache", keep);
|
||||||
|
txDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scriptToAddress(scriptHex) {
|
function scriptToAddress(scriptHex) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue