diff --git a/INITIAL_BASELINE.md b/INITIAL_BASELINE.md index d6e61b1c..3cc012e3 100644 --- a/INITIAL_BASELINE.md +++ b/INITIAL_BASELINE.md @@ -3,7 +3,11 @@ ## Performance Bottlenecks - **Unbounded Concurrency (P0):** `js/nostr/commentEvents.js` uses `Promise.all` for relay requests in `publishComment` and `listVideoComments`. This can saturate network connections if a user has many relays (20+), causing UI freeze or timeouts. - **Remediation:** Apply `pMap` with `RELAY_BACKGROUND_CONCURRENCY` (3). +- **VideoEventBuffer (P1):** Flushes every 75ms on event ingest regardless of visibility. + - **Remediation:** Implemented visibility gating (2026-02-13). +- **WatchHistory (P2):** Republish loop has backoff but no visibility check. ## Documentation Gaps - **CORS Configuration:** `content/docs/guides/upload-content.md` suggests `http://localhost:5500` for CORS, but the default dev port is `3000`. - **Remediation:** Update port to `3000`. +- **Upload Limit:** 2GB (documented and code-verified). diff --git a/context/CONTEXT_2026-02-13_20-35-00.md b/context/CONTEXT_2026-02-13_20-35-00.md new file mode 100644 index 00000000..e1d21d35 --- /dev/null +++ b/context/CONTEXT_2026-02-13_20-35-00.md @@ -0,0 +1,18 @@ +# Context + +## Goal +Improve application responsiveness by optimizing background tasks and ensuring documentation accuracy. + +## Scope +- Analyze codebase for performance bottlenecks (timers, loops, concurrency). +- Fix: Visibility-gate `VideoEventBuffer` to prevent UI thrashing when hidden. +- Audit: Verify upload documentation matches implementation. + +## Assumptions +- `document.hidden` is reliable. +- `VideoEventBuffer` is the primary feed ingestion path. + +## Definition of Done +- `VideoEventBuffer` pauses UI updates when hidden. +- `daily-perf-report` generated. +- Lint passes. diff --git a/daily-perf-report-2026-02-13.md b/daily-perf-report-2026-02-13.md new file mode 100644 index 00000000..0f682d3b --- /dev/null +++ b/daily-perf-report-2026-02-13.md @@ -0,0 +1,24 @@ +# Daily Perf Report - 2026-02-13 + +## Summary +Implemented visibility gating for `VideoEventBuffer` to prevent UI thrashing when the tab is hidden. Audited upload documentation against implementation. + +## P0/P1 Findings +- **P1 - VideoEventBuffer Thrashing**: The `VideoEventBuffer` was flushing events and triggering UI callbacks every 75ms regardless of visibility. This wasted CPU on layout/paint recalculations for hidden tabs. + - **Fix**: Modified `flush()` to buffer updates when `document.hidden` is true, and flush them in a batch upon `visibilitychange`. +- **P2 - Upload Limits**: Documentation states a 2GB limit due to client-side hashing. Verification confirmed `js/utils/torrentHash.js` reads the file for hashing, which is memory-bound in browsers. Docs are accurate. + +## Metrics +- **Login Time**: (Not measured this run) +- **Queue Sizes**: (Not measured this run) + +## PRs / Changes +- **VideoEventBuffer**: Added visibility gating. + +## Blockers & Decisions +- **Decision**: Prioritized `VideoEventBuffer` fix over `WatchHistory` backoff as it affects the main feed loop directly. +- **Decision**: Accepted the 2GB upload limit documentation as accurate for the current architecture. + +## Next Steps +- Investigate `WatchHistory` republish loop for similar visibility gating. +- Consider using streaming hashing (e.g., `crypto.subtle`) to lift the 2GB limit in the future. diff --git a/decisions/DECISIONS_2026-02-13_20-35-00.md b/decisions/DECISIONS_2026-02-13_20-35-00.md new file mode 100644 index 00000000..940ba447 --- /dev/null +++ b/decisions/DECISIONS_2026-02-13_20-35-00.md @@ -0,0 +1,6 @@ +# Decisions + +## Visibility Gating for VideoEventBuffer +- **Problem**: `VideoEventBuffer` flushes every 75ms if events are arriving, triggering UI updates via `onVideo` callback even if the tab is hidden. +- **Solution**: Check `document.hidden` in `flush()`. If hidden, process events into internal state (`activeMap`) but skip `onVideo` and buffer the updated video objects. On `visibilitychange` (visible), flush the buffered video objects to UI. +- **Tradeoff**: Memory usage might increase slightly to hold pending video references, but CPU usage on main thread (layout/paint) will decrease significantly. diff --git a/docs/agents/task-logs/daily/2026-02-13_20-35-00_perf-agent_started.md b/docs/agents/task-logs/daily/2026-02-13_20-35-00_perf-agent_started.md new file mode 100644 index 00000000..61e657cf --- /dev/null +++ b/docs/agents/task-logs/daily/2026-02-13_20-35-00_perf-agent_started.md @@ -0,0 +1 @@ +Started perf-agent run for 2026-02-13. diff --git a/docs/agents/task-logs/daily/2026-02-13_20-45-00_perf-agent_completed.md b/docs/agents/task-logs/daily/2026-02-13_20-45-00_perf-agent_completed.md new file mode 100644 index 00000000..be4f28c9 --- /dev/null +++ b/docs/agents/task-logs/daily/2026-02-13_20-45-00_perf-agent_completed.md @@ -0,0 +1,6 @@ +# Completed: perf-agent + +**Timestamp:** 2026-02-13 20:45:00 +**Agent:** perf-agent +**Summary:** +Implemented visibility gating for `VideoEventBuffer`. Now pauses UI updates (flushes) when the tab is hidden, accumulating changes to be applied in a batch when visibility is restored. This reduces background CPU usage and UI thrashing. Verified upload documentation against code (2GB limit is accurate). Generated daily perf report. diff --git a/js/nostr/videoEventBuffer.js b/js/nostr/videoEventBuffer.js index f118255d..df6c87a5 100644 --- a/js/nostr/videoEventBuffer.js +++ b/js/nostr/videoEventBuffer.js @@ -21,6 +21,13 @@ export class VideoEventBuffer { this.invalidEvents = []; this.flushTimerId = null; this.FLUSH_DEBOUNCE_MS = 75; + + // Buffer updates while hidden to avoid UI thrashing + this.pendingVideos = []; + this.handleVisibilityChange = this.handleVisibilityChange.bind(this); + if (typeof document !== "undefined") { + document.addEventListener("visibilitychange", this.handleVisibilityChange); + } } /** @@ -140,8 +147,12 @@ export class VideoEventBuffer { } if (updatedVideos.length > 0) { - // Trigger the callback once per batch to avoid UI thrashing - this.onVideo(updatedVideos); + if (typeof document !== "undefined" && document.hidden) { + this.pendingVideos.push(...updatedVideos); + } else { + // Trigger the callback once per batch to avoid UI thrashing + this.onVideo(updatedVideos); + } // Fetch NIP-71 metadata (categorization tags) in the background for the whole batch this.client.populateNip71MetadataForVideos(updatedVideos) @@ -162,6 +173,16 @@ export class VideoEventBuffer { this.client.saveLocalData("subscribeVideos:flush"); } + handleVisibilityChange() { + if (typeof document !== "undefined" && !document.hidden) { + if (this.pendingVideos.length > 0) { + const batch = this.pendingVideos; + this.pendingVideos = []; + this.onVideo(batch); + } + } + } + /** * Called when the subscription reaches End-of-Stored-Events (EOSE). * Triggers an immediate flush to ensure the UI is fully up-to-date. @@ -188,6 +209,9 @@ export class VideoEventBuffer { clearTimeout(this.flushTimerId); this.flushTimerId = null; } + if (typeof document !== "undefined") { + document.removeEventListener("visibilitychange", this.handleVisibilityChange); + } // Ensure any straggling events are flushed before tearing down. this.flush(); } diff --git a/perf/hits-2026-02-13.json b/perf/hits-2026-02-13.json new file mode 100644 index 00000000..3b75b99e --- /dev/null +++ b/perf/hits-2026-02-13.json @@ -0,0 +1 @@ +{"hits": ["VideoEventBuffer flush loop", "WatchHistory republish loop", "ViewCounter debounce"]} diff --git a/test_logs/TEST_LOG_2026-02-13_20-35-00.md b/test_logs/TEST_LOG_2026-02-13_20-35-00.md new file mode 100644 index 00000000..1dc9b55f --- /dev/null +++ b/test_logs/TEST_LOG_2026-02-13_20-35-00.md @@ -0,0 +1,8 @@ +# Test Log + +## Environment +- Agent: perf-agent +- Date: 2026-02-13 + +## Verification +- Lint: Passed diff --git a/tests/nostr/videoEventBuffer.test.mjs b/tests/nostr/videoEventBuffer.test.mjs index 025dc550..0cbe0000 100644 --- a/tests/nostr/videoEventBuffer.test.mjs +++ b/tests/nostr/videoEventBuffer.test.mjs @@ -151,4 +151,43 @@ test('VideoEventBuffer', async (t) => { assert.equal(buffer.buffer.length, 0); // Should have flushed assert.equal(client.allEvents.size, 1); }); + + await t.test('Visibility Gating', () => { + // Mock document + const originalDocument = global.document; + let visibilityHandler = null; + global.document = { + hidden: true, + addEventListener: (type, handler) => { + if (type === 'visibilitychange') visibilityHandler = handler; + }, + removeEventListener: () => {} + }; + + const client = new MockClient(); + let onVideoCalls = []; + const onVideo = (videos) => onVideoCalls.push(videos); + const buffer = new VideoEventBuffer(client, onVideo); + + // Push event while hidden + buffer.push(validEvent); + buffer.scheduleFlush(true); + + // Should NOT have called onVideo yet + assert.equal(onVideoCalls.length, 0); + // Should have buffered pending videos + assert.equal(buffer.pendingVideos.length, 1); + + // Simulate visibility change + global.document.hidden = false; + assert.ok(visibilityHandler, "Listener attached"); + visibilityHandler(); + + // Should flush now + assert.equal(onVideoCalls.length, 1); + assert.equal(buffer.pendingVideos.length, 0); + + // Cleanup mock + global.document = originalDocument; + }); }); diff --git a/todo/TODO_2026-02-13_20-35-00.md b/todo/TODO_2026-02-13_20-35-00.md new file mode 100644 index 00000000..eab73f54 --- /dev/null +++ b/todo/TODO_2026-02-13_20-35-00.md @@ -0,0 +1,6 @@ +- [ ] Create context/logs +- [ ] Create INITIAL_BASELINE.md +- [ ] Save search hits +- [ ] Implement visibility gating in `VideoEventBuffer.js` +- [ ] Generate report +- [ ] Lint