mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 12:52:26 +00:00
perf(subscriptions): cut redundant cold-login list fetches
Two redundancies were inflating the login REQ burst (kind 30000=19 storm in the real-env console — the cause of the slow hashtags, KNOWN_BUGS #0), isolated with the channel-sim harness's REQ-signature + timing capture: 1. Duplicate trigger: ProfileModalController.handleAuthLogin called subscriptions.loadSubscriptions() AND userBlocks.loadBlocks() DIRECTLY at login, on top of the auth coordinator's gated list loads — a second, ungated relay round-trip for each. Route them through ensureLoaded() instead so they coalesce with the coordinator's in-flight load. Harness: subscriptions updateFromRelays now fires ONCE at login instead of 3× (≈3× fewer kind-30000 fetches in prod, where each is multiplied by the relay set). 2. Fetch/decrypt coupling: a decrypt retry re-ran the full relay fetch even though only signer availability had changed. Cache the fetched ciphertext (_pendingDecryptEvent) and have scheduleDecryptRetry re-decrypt it via a cachedDecryptEvent fast-path in updateFromRelays — no redundant relay query per retry. Validated: MODE=overload (channel drops) still decrypts blocks + subscriptions + hashtags (DMs OK); full unit suite green; subscription loop + aggregation tests still pass. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
10ea4f6a22
commit
0db4f0a603
3 files changed
+103
-63
No files matched your search
+89
-59
@@ -398,6 +398,9 @@ class SubscriptionsManager {
|
||||
this.subsEventCreatedAt = null;
|
||||
// Last live-subscription event id we triggered a refresh for (loop guard).
|
||||
this._lastHandledListEventId = null;
|
||||
// Most recently fetched list ciphertext, reused by decrypt retries so they
|
||||
// don't re-query relays.
|
||||
this._pendingDecryptEvent = null;
|
||||
this.currentUserPubkey = null;
|
||||
this.uiReady = false;
|
||||
this.dataReady = false;
|
||||
@@ -517,7 +520,13 @@ class SubscriptionsManager {
|
||||
if (this.currentUserPubkey && this.currentUserPubkey !== normalized) {
|
||||
return;
|
||||
}
|
||||
this.updateFromRelays(normalized, options).catch((retryError) => {
|
||||
// Reuse the already-fetched ciphertext so the retry only re-runs the
|
||||
// DECRYPT (no redundant relay fetch). Fall back to a full fetch only if we
|
||||
// somehow have no pending event cached.
|
||||
const retryOptions = this._pendingDecryptEvent
|
||||
? { ...options, cachedDecryptEvent: this._pendingDecryptEvent }
|
||||
: options;
|
||||
this.updateFromRelays(normalized, retryOptions).catch((retryError) => {
|
||||
userLogger.warn("[SubscriptionsManager] Decryption retry failed:", retryError);
|
||||
});
|
||||
}, retryDelayMs);
|
||||
@@ -573,79 +582,95 @@ class SubscriptionsManager {
|
||||
);
|
||||
}
|
||||
}
|
||||
this.ensureSubscriptionListSubscription(normalizedUserPubkey, relayUrls);
|
||||
// DECRYPT-RETRY FAST PATH: a retry re-uses the ciphertext we already
|
||||
// fetched rather than re-querying relays. The relay data hasn't changed
|
||||
// between attempts — only signer availability has — so re-fetching on
|
||||
// every retry just floods the cold-login burst with redundant kind-30000
|
||||
// REQs (KNOWN_BUGS #0). Only the initial load (and a genuinely new live
|
||||
// event) hits the network.
|
||||
let hadCachedSnapshot = false;
|
||||
let mergedEvents = [];
|
||||
const retryEvent =
|
||||
options?.cachedDecryptEvent && options.cachedDecryptEvent.id
|
||||
? options.cachedDecryptEvent
|
||||
: null;
|
||||
|
||||
// Use incremental fetch helper
|
||||
const cachedSnapshot = parseCachedSubscriptionSnapshot(
|
||||
profileCache.getProfileData(normalizedUserPubkey, "subscriptions"),
|
||||
);
|
||||
const hadCachedSnapshot = cachedSnapshot.hasSnapshot;
|
||||
const shouldForceFullFetch = !cachedSnapshot.hasSnapshot;
|
||||
const incrementalSince = shouldForceFullFetch
|
||||
? 0
|
||||
: computeIncrementalSinceWithOverlap(cachedSnapshot.createdAt, 1);
|
||||
if (retryEvent) {
|
||||
mergedEvents = [retryEvent];
|
||||
} else {
|
||||
this.ensureSubscriptionListSubscription(normalizedUserPubkey, relayUrls);
|
||||
|
||||
// Fetch user and session actor subscription lists in parallel to reduce
|
||||
// total relay wait time during login.
|
||||
const fetchPromises = [
|
||||
nostrClient.fetchListIncrementally({
|
||||
kind: SUBSCRIPTION_SET_KIND,
|
||||
pubkey: normalizedUserPubkey,
|
||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
relayUrls,
|
||||
since: incrementalSince,
|
||||
timeoutMs: 12000,
|
||||
}),
|
||||
];
|
||||
|
||||
const normalizedSessionActorPubkey = normalizeNostrPubkey(
|
||||
nostrClient?.sessionActor?.pubkey,
|
||||
);
|
||||
const shouldFetchSessionActor =
|
||||
normalizedSessionActorPubkey &&
|
||||
normalizedSessionActorPubkey !== normalizedUserPubkey;
|
||||
if (shouldFetchSessionActor) {
|
||||
const sessionCachedSnapshot = parseCachedSubscriptionSnapshot(
|
||||
profileCache.getProfileData(
|
||||
normalizedSessionActorPubkey,
|
||||
"subscriptions",
|
||||
),
|
||||
// Use incremental fetch helper
|
||||
const cachedSnapshot = parseCachedSubscriptionSnapshot(
|
||||
profileCache.getProfileData(normalizedUserPubkey, "subscriptions"),
|
||||
);
|
||||
const shouldForceSessionFetch = !sessionCachedSnapshot.hasSnapshot;
|
||||
const sessionSince = shouldForceSessionFetch
|
||||
hadCachedSnapshot = cachedSnapshot.hasSnapshot;
|
||||
const shouldForceFullFetch = !cachedSnapshot.hasSnapshot;
|
||||
const incrementalSince = shouldForceFullFetch
|
||||
? 0
|
||||
: computeIncrementalSinceWithOverlap(sessionCachedSnapshot.createdAt, 1);
|
||||
fetchPromises.push(
|
||||
: computeIncrementalSinceWithOverlap(cachedSnapshot.createdAt, 1);
|
||||
|
||||
// Fetch user and session actor subscription lists in parallel to reduce
|
||||
// total relay wait time during login.
|
||||
const fetchPromises = [
|
||||
nostrClient.fetchListIncrementally({
|
||||
kind: SUBSCRIPTION_SET_KIND,
|
||||
pubkey: normalizedSessionActorPubkey,
|
||||
pubkey: normalizedUserPubkey,
|
||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
relayUrls,
|
||||
since: sessionSince,
|
||||
since: incrementalSince,
|
||||
timeoutMs: 12000,
|
||||
}),
|
||||
];
|
||||
|
||||
const normalizedSessionActorPubkey = normalizeNostrPubkey(
|
||||
nostrClient?.sessionActor?.pubkey,
|
||||
);
|
||||
}
|
||||
const shouldFetchSessionActor =
|
||||
normalizedSessionActorPubkey &&
|
||||
normalizedSessionActorPubkey !== normalizedUserPubkey;
|
||||
if (shouldFetchSessionActor) {
|
||||
const sessionCachedSnapshot = parseCachedSubscriptionSnapshot(
|
||||
profileCache.getProfileData(
|
||||
normalizedSessionActorPubkey,
|
||||
"subscriptions",
|
||||
),
|
||||
);
|
||||
const shouldForceSessionFetch = !sessionCachedSnapshot.hasSnapshot;
|
||||
const sessionSince = shouldForceSessionFetch
|
||||
? 0
|
||||
: computeIncrementalSinceWithOverlap(sessionCachedSnapshot.createdAt, 1);
|
||||
fetchPromises.push(
|
||||
nostrClient.fetchListIncrementally({
|
||||
kind: SUBSCRIPTION_SET_KIND,
|
||||
pubkey: normalizedSessionActorPubkey,
|
||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
relayUrls,
|
||||
since: sessionSince,
|
||||
timeoutMs: 12000,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const fetchResults = await Promise.all(fetchPromises);
|
||||
let events = fetchResults[0] || [];
|
||||
if (shouldFetchSessionActor && fetchResults[1]?.length) {
|
||||
events = events.concat(fetchResults[1]);
|
||||
}
|
||||
const fetchResults = await Promise.all(fetchPromises);
|
||||
let events = fetchResults[0] || [];
|
||||
if (shouldFetchSessionActor && fetchResults[1]?.length) {
|
||||
events = events.concat(fetchResults[1]);
|
||||
}
|
||||
|
||||
const mergedEvents = [];
|
||||
if (events.length) {
|
||||
const deduped = new Map();
|
||||
for (const event of events) {
|
||||
if (!event || typeof event !== "object" || !event.id) {
|
||||
continue;
|
||||
if (events.length) {
|
||||
const deduped = new Map();
|
||||
for (const event of events) {
|
||||
if (!event || typeof event !== "object" || !event.id) {
|
||||
continue;
|
||||
}
|
||||
const existing = deduped.get(event.id);
|
||||
if (!existing || (event.created_at ?? 0) > (existing.created_at ?? 0)) {
|
||||
deduped.set(event.id, event);
|
||||
}
|
||||
}
|
||||
const existing = deduped.get(event.id);
|
||||
if (!existing || (event.created_at ?? 0) > (existing.created_at ?? 0)) {
|
||||
deduped.set(event.id, event);
|
||||
}
|
||||
mergedEvents.push(...deduped.values());
|
||||
}
|
||||
mergedEvents.push(...deduped.values());
|
||||
}
|
||||
|
||||
if (!mergedEvents.length) {
|
||||
@@ -692,6 +717,10 @@ class SubscriptionsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache the ciphertext so a decrypt retry can re-attempt WITHOUT another
|
||||
// relay round-trip (see the retry fast-path above).
|
||||
this._pendingDecryptEvent = newest;
|
||||
|
||||
// Any event returned here is effectively "new information" (or re-fetched full state).
|
||||
// fetchListIncrementally already handles the "newer than last sync" logic per relay,
|
||||
// and we persist event metadata for additional cache context.
|
||||
@@ -870,6 +899,7 @@ class SubscriptionsManager {
|
||||
this.subsEventId = null;
|
||||
this.subsEventCreatedAt = null;
|
||||
this._lastHandledListEventId = null;
|
||||
this._pendingDecryptEvent = null;
|
||||
this.currentUserPubkey = null;
|
||||
this.uiReady = false;
|
||||
this.dataReady = false;
|
||||
|
||||
@@ -5157,12 +5157,22 @@ export class ProfileModalController {
|
||||
|
||||
this.renderSavedProfiles();
|
||||
|
||||
// Trigger aggressive parallel fetches
|
||||
// Warm blocks + subscriptions, but go through ensureLoaded() (not the raw
|
||||
// loadBlocks/loadSubscriptions) so these COALESCE with the auth coordinator's
|
||||
// login-time list loads instead of issuing a second, ungated fetch. The
|
||||
// direct calls here each kicked off an independent relay round-trip,
|
||||
// doubling the cold-login kind-30000/10000 REQ burst (KNOWN_BUGS #0).
|
||||
if (activePubkey) {
|
||||
if (this.services.userBlocks) {
|
||||
if (this.services.userBlocks?.ensureLoaded) {
|
||||
this.services.userBlocks.ensureLoaded(activePubkey).catch(noop);
|
||||
} else if (this.services.userBlocks?.loadBlocks) {
|
||||
this.services.userBlocks.loadBlocks(activePubkey).catch(noop);
|
||||
}
|
||||
if (this.subscriptionsService) {
|
||||
if (this.subscriptionsService?.ensureLoaded) {
|
||||
this.subscriptionsService
|
||||
.ensureLoaded(activePubkey, { allowPermissionPrompt: false })
|
||||
.catch(noop);
|
||||
} else if (this.subscriptionsService?.loadSubscriptions) {
|
||||
this.subscriptionsService
|
||||
.loadSubscriptions(activePubkey, { allowPermissionPrompt: false })
|
||||
.catch(noop);
|
||||
|
||||
@@ -226,7 +226,7 @@ async function main() {
|
||||
if (text.includes("nip07-channel-unresponsive")) signals.channelUnresponsive += 1;
|
||||
if (text.includes("DM decryption helpers are unavailable")) signals.dmHelpersUnavailable += 1;
|
||||
if (DEBUG && /UserBlockList|Hashtag|Subscriptions|Decryption|signer|channel|direct message/i.test(text)) {
|
||||
console.log("PAGE>", text.slice(0, 200));
|
||||
console.log("PAGE>", text.slice(0, 500));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user