mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
perf-agent: visibility gate VideoEventBuffer
Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
f8cd7369b5
commit
bb39226ca5
11 files changed
+139
-2
No files matched your search
@@ -3,7 +3,11 @@
|
|||||||
## Performance Bottlenecks
|
## 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.
|
- **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).
|
- **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
|
## Documentation Gaps
|
||||||
- **CORS Configuration:** `content/docs/guides/upload-content.md` suggests `http://localhost:5500` for CORS, but the default dev port is `3000`.
|
- **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`.
|
- **Remediation:** Update port to `3000`.
|
||||||
|
- **Upload Limit:** 2GB (documented and code-verified).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Started perf-agent run for 2026-02-13.
|
||||||
@@ -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.
|
||||||
@@ -21,6 +21,13 @@ export class VideoEventBuffer {
|
|||||||
this.invalidEvents = [];
|
this.invalidEvents = [];
|
||||||
this.flushTimerId = null;
|
this.flushTimerId = null;
|
||||||
this.FLUSH_DEBOUNCE_MS = 75;
|
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) {
|
if (updatedVideos.length > 0) {
|
||||||
// Trigger the callback once per batch to avoid UI thrashing
|
if (typeof document !== "undefined" && document.hidden) {
|
||||||
this.onVideo(updatedVideos);
|
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
|
// Fetch NIP-71 metadata (categorization tags) in the background for the whole batch
|
||||||
this.client.populateNip71MetadataForVideos(updatedVideos)
|
this.client.populateNip71MetadataForVideos(updatedVideos)
|
||||||
@@ -162,6 +173,16 @@ export class VideoEventBuffer {
|
|||||||
this.client.saveLocalData("subscribeVideos:flush");
|
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).
|
* Called when the subscription reaches End-of-Stored-Events (EOSE).
|
||||||
* Triggers an immediate flush to ensure the UI is fully up-to-date.
|
* Triggers an immediate flush to ensure the UI is fully up-to-date.
|
||||||
@@ -188,6 +209,9 @@ export class VideoEventBuffer {
|
|||||||
clearTimeout(this.flushTimerId);
|
clearTimeout(this.flushTimerId);
|
||||||
this.flushTimerId = null;
|
this.flushTimerId = null;
|
||||||
}
|
}
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
||||||
|
}
|
||||||
// Ensure any straggling events are flushed before tearing down.
|
// Ensure any straggling events are flushed before tearing down.
|
||||||
this.flush();
|
this.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"hits": ["VideoEventBuffer flush loop", "WatchHistory republish loop", "ViewCounter debounce"]}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Test Log
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- Agent: perf-agent
|
||||||
|
- Date: 2026-02-13
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Lint: Passed
|
||||||
@@ -151,4 +151,43 @@ test('VideoEventBuffer', async (t) => {
|
|||||||
assert.equal(buffer.buffer.length, 0); // Should have flushed
|
assert.equal(buffer.buffer.length, 0); // Should have flushed
|
||||||
assert.equal(client.allEvents.size, 1);
|
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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- [ ] Create context/logs
|
||||||
|
- [ ] Create INITIAL_BASELINE.md
|
||||||
|
- [ ] Save search hits
|
||||||
|
- [ ] Implement visibility gating in `VideoEventBuffer.js`
|
||||||
|
- [ ] Generate report
|
||||||
|
- [ ] Lint
|
||||||
Reference in new issue
Block a user