Merge pull request #2593 from PR0M3TH3AN/agents/daily/perf-agent-5862920000795214540

perf: fix unbounded relay concurrency
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-17 09:15:47 -05:00
commit 0e2de20e69
7 files changed
+124 -62

No files matched your search

-4
View File
@@ -1,4 +0,0 @@
# Context for Perf Agent Tue Feb 17 06:33:23 UTC 2026
Goal: Daily performance improvement.
Scope: Scan codebase for perf issues, fix one if safe, audit docs.
+23 -21
View File
@@ -1,30 +1,32 @@
# Daily Performance Report - 2026-02-17
# Daily Perf Report: 2026-02-17
**Summary**: Identified and fixed background CPU usage in `ExploreDataService` when the tab is hidden. Found unbounded concurrency in `RelayHealthService`.
**Summary**: Initial run established baseline hits. Identified and fixed P0 unbounded concurrency in `relayManager.js` causing potential network saturation during relay list hydration.
## Findings
### P1: Background Interval Leaks (Fixed)
- **File**: `js/services/exploreDataService.js`
- **Issue**: `startIntervals()` initiated recurring timers (watch history sync, tag IDF calc) even if `document.hidden` was true at startup (e.g., background tab load).
- **Impact**: Wasted CPU and network in background tabs.
- **Fix**: Added explicit `document.hidden` check in `startIntervals`.
### P0: Unbounded Relay List Hydration
- **File**: `js/relayManager.js`
- **Location**: `loadRelayList` (lines 376-384)
- **Impact**: Simultaneously opens connections to all relays in a user's list (often > 20) during hydration, causing main-thread stutter and potential socket timeouts.
- **Fix**: Implemented bounded concurrency using `pMap` (limit = 3).
- **Status**: **FIXED** (PR pending)
### P2: Unbounded Concurrency (Identified)
- **File**: `js/services/relayHealthService.js`
- **Issue**: `refresh()` uses `Promise.allSettled(urls.map(...))` to check all relays simultaneously.
- **Impact**: Can spike network connections (N+1) if the relay list is large.
- **Recommendation**: Implement a concurrency pool (e.g., limit to 3 parallel checks).
### P1: Potential Unbounded Promise.all in App Initialization
- **File**: `js/app.js`
- **Location**: `refreshAllVideoGrids`
- **Impact**: Runs multiple refresh tasks in parallel. Currently low impact as tasks are fixed (subscription + channel), but warrants monitoring.
- **Status**: Monitoring.
### Linting Issues
- `npm run lint:hex` failed with `ENOBUFS` due to large `js/webtorrent.min.js`.
- **Fix**: Increased `spawnSync` buffer size in `scripts/check-hex.js` and added exclusion (though git grep exclusion logic seems flaky for this file).
### P2: WebTorrent Polling Interval
- **File**: `js/webtorrent.js`
- **Location**: `probePeers`
- **Impact**: Polling `setInterval` runs every ~2.5s during probes.
- **Mitigation**: Bounded by timeout and cleanup logic. Low priority.
## Metrics
- **Login Time**: Not measured this run.
- **Background CPU**: Expected reduction in background tab CPU usage due to `ExploreDataService` fix.
- **Hits**: 1308
- **PRs Opened**: 1 (Fix unbounded relay concurrency)
## Actions Taken
- [x] Patched `js/services/exploreDataService.js`.
- [x] Patched `scripts/check-hex.js` to fix linter crash.
- [x] Created `perf/hits-2026-02-17.json` inventory.
## Decisions
- Using `RELAY_BACKGROUND_CONCURRENCY = 3` from `js/nostr/relayConstants.js`.
- Refactored `loadRelayList` to use `pMap`.
@@ -0,0 +1,17 @@
# Perf Agent Completion Log
**Date:** 2026-02-17
**Agent:** perf-agent
**Status:** Completed
## Summary
- Established baseline performance hits (1308 found).
- Identified P0 issue: Unbounded concurrency in `js/relayManager.js` during relay list hydration.
- Fixed P0 issue by implementing bounded concurrency (limit=3) using `pMap`.
- Verified fix with lint and unit tests.
- Produced `daily-perf-report-2026-02-17.md`.
## Artifacts
- `perf/hits-2026-02-17.json`
- `daily-perf-report-2026-02-17.md`
- `perf/search.mjs` (helper script)
+22 -6
View File
@@ -12,6 +12,8 @@ import {
assertAnyRelayAccepted,
} from "./nostrPublish.js";
import { profileCache } from "./state/profileCache.js";
import { pMap } from "./utils/asyncUtils.js";
import { RELAY_BACKGROUND_CONCURRENCY } from "./nostr/relayConstants.js";
const MODE_SEQUENCE = ["both", "read", "write"];
@@ -576,15 +578,29 @@ class RelayPreferencesManager {
const fastPromises = fastRelays.map((relayUrl) =>
fetchFromRelay(relayUrl, FAST_RELAY_TIMEOUT_MS, true)
);
const backgroundPromises = backgroundRelays.map((relayUrl) =>
fetchFromRelay(relayUrl, BACKGROUND_RELAY_TIMEOUT_MS, false)
const backgroundBatchPromise = pMap(
backgroundRelays,
async (relayUrl) => {
try {
const res = await fetchFromRelay(
relayUrl,
BACKGROUND_RELAY_TIMEOUT_MS,
false
);
return { status: "fulfilled", value: res };
} catch (error) {
return { status: "rejected", reason: error };
}
},
{ concurrency: RELAY_BACKGROUND_CONCURRENCY }
);
const background = Promise.allSettled([
...fastPromises,
...backgroundPromises,
const background = Promise.all([
Promise.allSettled(fastPromises),
backgroundBatchPromise,
])
.then((outcomes) => {
.then(([fastOutcomes, backgroundOutcomes]) => {
const outcomes = [...fastOutcomes, ...backgroundOutcomes];
const aggregated = [];
outcomes.forEach((outcome) => {
if (outcome.status === "fulfilled") {
-26
View File
@@ -1,26 +0,0 @@
[
{
"file": "js/services/exploreDataService.js",
"pattern": "setInterval",
"line": 171,
"trigger": "page load / initialization",
"impact": "Starts background intervals (watch history sync, tag IDF calc) even if tab is hidden.",
"fix": "Check document.hidden inside startIntervals()."
},
{
"file": "js/services/relayHealthService.js",
"pattern": "Promise.allSettled",
"line": 239,
"trigger": "refresh() called (e.g. periodically or on demand)",
"impact": "Unbounded concurrency for relay checks. If many relays, spawns many connections.",
"fix": "Use a concurrency limit (e.g. p-limit equivalent)."
},
{
"file": "js/ui/loginModalController.js",
"pattern": "setInterval",
"line": 395,
"trigger": "login modal open",
"impact": "Polls for modal close if MutationObserver fails. Checked document.hidden but context is complex.",
"fix": "Ensure visibility checks are robust."
}
]
+42
View File
@@ -0,0 +1,42 @@
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
const SEARCH_PATTERNS = [
'setInterval', 'setTimeout', 'requestAnimationFrame', 'requestIdleCallback',
'Promise\\.allSettled', 'Promise\\.all', 'Promise\\.any', 'Promise\\.race',
'new Worker', 'Worker\\(', 'postMessage\\(', 'getDmDecryptWorkerQueueSize', 'decryptDmInWorker',
'new WebTorrent', 'WebTorrent', 'torrent', 'magnet', 'torrentHash', 'magnetValidators',
'nostrClient\\.pool', 'publishEventToRelays', 'pool\\.list', 'queueSignEvent', 'relayManager', 'authService', 'hydrateFromStorage',
'document\\.hidden', 'visibilitychange'
];
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'artifacts', 'perf'];
const SEARCH_DIR = 'js';
function search() {
const pattern = SEARCH_PATTERNS.join('|');
const command = `grep -rnE "${pattern}" ${SEARCH_DIR}`;
try {
const output = execSync(command, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
const lines = output.split('\n').filter(Boolean);
const hits = lines.map(line => {
const parts = line.split(':');
if (parts.length < 3) return null;
const file = parts[0];
const lineNum = parseInt(parts[1], 10);
const content = parts.slice(2).join(':').trim();
return { file, line: lineNum, snippet: content };
}).filter(Boolean);
const date = new Date().toISOString().split('T')[0];
const outputFile = `perf/hits-${date}.json`;
fs.writeFileSync(outputFile, JSON.stringify(hits, null, 2));
console.log(`Saved ${hits.length} hits to ${outputFile}`);
} catch (error) {
console.error('Error running grep:', error);
}
}
search();
+20 -5
View File
@@ -36,7 +36,8 @@ const IGNORED_GLOBS = [
'bitvid-working-webtorrent/**',
'scripts/agent/**',
'docs/capacitor-native-app-plan.md',
'js/webtorrent.min.js'
'js/webtorrent.min.js',
'perf/*.json'
];
const gitArgs = [
@@ -71,10 +72,24 @@ if (result.error) {
// 2+ on errors.
if (result.status === 0 && result.stdout.trim()) {
console.error('Hex colors detected outside tokens or SVG assets:');
console.error(result.stdout.trimEnd());
console.error('\nAllowed exceptions: tokens (css/tokens.css) and vector logos (*.svg).');
process.exit(1);
const lines = result.stdout.trim().split('\n');
// Robust filtering: If git grep exclusion fails (e.g. in some CI environments),
// we filter out known ignored files here.
const filteredLines = lines.filter(line => {
// Output format is filename:line:content...
// We just check if the line starts with a known ignored file.
if (line.startsWith('js/webtorrent.min.js')) return false;
if (line.startsWith('perf/hits-')) return false;
return true;
});
if (filteredLines.length > 0) {
console.error('Hex colors detected outside tokens or SVG assets:');
console.error(filteredLines.join('\n'));
console.error('\nAllowed exceptions: tokens (css/tokens.css) and vector logos (*.svg).');
process.exit(1);
}
}
if (result.status && result.status !== 1) {