mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
perf(explore): pause background updates when hidden
Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
5b188bca31
commit
6181041b42
9 files changed
+1523
No files matched your search
@@ -0,0 +1,18 @@
|
||||
# Context: Explore Data Service Performance Optimization
|
||||
|
||||
**Goal**: Reduce background CPU and battery usage by pausing `ExploreDataService` updates when the tab is hidden.
|
||||
|
||||
**Scope**:
|
||||
- `js/services/exploreDataService.js`
|
||||
|
||||
**Problem**:
|
||||
- The service runs two `setInterval` timers (1 minute and 5 minutes) to refresh watch history counts and tag IDF.
|
||||
- These run even when the tab is hidden or minimized, consuming resources unnecessarily.
|
||||
|
||||
**Proposed Solution**:
|
||||
- Modify the interval callbacks to check `document.hidden`. If true, skip the update.
|
||||
- Add a `visibilitychange` listener to trigger an immediate update when the tab becomes visible (if the data is stale).
|
||||
|
||||
**Assumptions**:
|
||||
- `document.hidden` is supported in target browsers (modern browsers support it).
|
||||
- Users do not need real-time updates for "explore" data when the tab is not visible.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Decisions: Explore Data Service Performance Optimization
|
||||
|
||||
## Decision 1: Gate intervals with `document.hidden`
|
||||
**Context**: `setInterval` runs unconditionally.
|
||||
**Choice**: Add `if (document.hidden) return;` at the start of the interval callback.
|
||||
**Rationale**: Simple, effective way to stop processing. `clearInterval` / `setInterval` on visibility change is more complex to manage state.
|
||||
|
||||
## Decision 2: Add `visibilitychange` listener
|
||||
**Context**: If we skip updates, data becomes stale.
|
||||
**Choice**: Listen for `visibilitychange`. If visible, call `refreshWatchHistoryTagCounts()` and `refreshTagIdf()`.
|
||||
**Rationale**: Ensures that when the user returns, they see fresh data immediately (or very soon). The existing `refresh...` methods already handle debounce/staleness checks, so calling them is safe.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Agent Task Log: perf-agent
|
||||
|
||||
- **Date**: 2026-02-15
|
||||
- **Status**: Completed
|
||||
- **Agent**: perf-agent
|
||||
|
||||
## Summary
|
||||
Optimized `ExploreDataService` to reduce background resource usage.
|
||||
|
||||
### Changes
|
||||
- **ExploreDataService**:
|
||||
- Gated `setInterval` for watch history (1m) and IDF (5m) updates to skip execution when `document.hidden` is true.
|
||||
- Added `visibilitychange` listener to trigger immediate refresh when tab becomes visible.
|
||||
- **Tests**:
|
||||
- Added `tests/unit/services/exploreDataService.test.mjs` to verify visibility logic.
|
||||
|
||||
### Artifacts
|
||||
- `perf/daily-perf-report-2026-02-15.md`
|
||||
- `tests/unit/services/exploreDataService.test.mjs`
|
||||
- `perf/hits-2026-02-15.json`
|
||||
- `context/CONTEXT_2026-02-15_perf_explore_data.md`
|
||||
- `decisions/DECISIONS_2026-02-15_perf_explore_data.md`
|
||||
@@ -156,16 +156,29 @@ export default class ExploreDataService {
|
||||
this.clearIntervals();
|
||||
if (Number.isFinite(this.historyRefreshIntervalMs) && this.historyRefreshIntervalMs > 0) {
|
||||
this.watchHistoryInterval = setInterval(() => {
|
||||
if (typeof document !== "undefined" && document.hidden) {
|
||||
return;
|
||||
}
|
||||
this.refreshWatchHistoryTagCounts({ reason: "interval" });
|
||||
}, this.historyRefreshIntervalMs);
|
||||
}
|
||||
if (Number.isFinite(this.idfRefreshIntervalMs) && this.idfRefreshIntervalMs > 0) {
|
||||
this.tagIdfInterval = setInterval(() => {
|
||||
if (typeof document !== "undefined" && document.hidden) {
|
||||
return;
|
||||
}
|
||||
this.refreshTagIdf({ reason: "interval" });
|
||||
}, this.idfRefreshIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
handleVisibilityChange() {
|
||||
if (typeof document !== "undefined" && !document.hidden) {
|
||||
this.refreshWatchHistoryTagCounts({ reason: "visibility" });
|
||||
this.refreshTagIdf({ reason: "visibility" });
|
||||
}
|
||||
}
|
||||
|
||||
clearIntervals() {
|
||||
if (this.watchHistoryInterval) {
|
||||
clearInterval(this.watchHistoryInterval);
|
||||
@@ -185,6 +198,14 @@ export default class ExploreDataService {
|
||||
});
|
||||
this.unsubscribeHandlers = [];
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
const handler = () => this.handleVisibilityChange();
|
||||
document.addEventListener("visibilitychange", handler);
|
||||
this.unsubscribeHandlers.push(() => {
|
||||
document.removeEventListener("visibilitychange", handler);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.watchHistoryService && typeof this.watchHistoryService.subscribe === "function") {
|
||||
const unsubscribe = this.watchHistoryService.subscribe("fingerprint", () => {
|
||||
this.queueWatchHistoryRefresh("watch-history");
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Daily Performance Report: 2026-02-15
|
||||
|
||||
## Summary
|
||||
Focus: Reduce background processing in `ExploreDataService`.
|
||||
Findings: `setInterval` used for periodic updates runs unconditionally.
|
||||
|
||||
## Findings (P1)
|
||||
- **File**: `js/services/exploreDataService.js`
|
||||
- **Issue**: `setInterval` for watch history (1m) and IDF (5m) runs when tab is hidden.
|
||||
- **Impact**: Unnecessary CPU/battery usage.
|
||||
- **Fix**: Gated intervals with `document.hidden` and added `visibilitychange` listener.
|
||||
|
||||
## Actions
|
||||
- Created reproduction test case `tests/unit/services/exploreDataService.test.mjs`.
|
||||
- Implemented visibility gating in `js/services/exploreDataService.js`.
|
||||
- Verified fix with tests.
|
||||
|
||||
## Metrics
|
||||
- **Before**: Unconditional execution every 1m/5m regardless of visibility.
|
||||
- **After**: Zero execution when hidden. Immediate refresh on visibility change (if stale).
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
# Test Log: Explore Data Service Performance Optimization
|
||||
|
||||
## Initial State
|
||||
- No tests for `exploreDataService.js`.
|
||||
- Creating `tests/unit/services/exploreDataService.test.mjs`.
|
||||
|
||||
## Test Execution
|
||||
|
||||
**Command**: `node --test tests/unit/services/exploreDataService.test.mjs`
|
||||
|
||||
**Output**:
|
||||
```
|
||||
TAP version 13
|
||||
# Subtest: ExploreDataService
|
||||
# Subtest: intervals trigger refresh when visible
|
||||
ok 1 - intervals trigger refresh when visible
|
||||
# Subtest: intervals skip refresh when hidden
|
||||
ok 2 - intervals skip refresh when hidden
|
||||
# Subtest: visibility change triggers refresh
|
||||
ok 3 - visibility change triggers refresh
|
||||
# Subtest: visibility change DOES NOT trigger refresh if hidden
|
||||
ok 4 - visibility change DOES NOT trigger refresh if hidden
|
||||
1..4
|
||||
ok 1 - ExploreDataService
|
||||
# tests 4
|
||||
# suites 1
|
||||
# pass 4
|
||||
# fail 0
|
||||
```
|
||||
|
||||
**Result**: All tests passed. The fix effectively gates intervals with `document.hidden` and triggers refresh on visibility change.
|
||||
@@ -0,0 +1,91 @@
|
||||
import { test, describe, beforeEach, afterEach, mock } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import ExploreDataService from '../../../js/services/exploreDataService.js';
|
||||
|
||||
describe('ExploreDataService', () => {
|
||||
let service;
|
||||
let originalDocument;
|
||||
let visibilityHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock global document
|
||||
originalDocument = global.document;
|
||||
visibilityHandler = null;
|
||||
global.document = {
|
||||
hidden: false,
|
||||
addEventListener: (event, handler) => {
|
||||
if (event === 'visibilitychange') {
|
||||
visibilityHandler = handler;
|
||||
}
|
||||
},
|
||||
removeEventListener: () => {}
|
||||
};
|
||||
|
||||
service = new ExploreDataService({
|
||||
historyRefreshIntervalMs: 100,
|
||||
idfRefreshIntervalMs: 100,
|
||||
logger: { warn: () => {} } // silent logger
|
||||
});
|
||||
|
||||
// Mock the refresh methods to track calls
|
||||
service.refreshWatchHistoryTagCounts = mock.fn();
|
||||
service.refreshTagIdf = mock.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (service) {
|
||||
service.destroy();
|
||||
}
|
||||
global.document = originalDocument;
|
||||
mock.restoreAll();
|
||||
});
|
||||
|
||||
test('intervals trigger refresh when visible', (t) => {
|
||||
t.mock.timers.enable({ apis: ['setInterval'] });
|
||||
service.startIntervals();
|
||||
|
||||
// Advance time by 150ms (interval is 100ms)
|
||||
t.mock.timers.tick(150);
|
||||
|
||||
assert.strictEqual(service.refreshWatchHistoryTagCounts.mock.callCount(), 1);
|
||||
assert.strictEqual(service.refreshTagIdf.mock.callCount(), 1);
|
||||
});
|
||||
|
||||
test('intervals skip refresh when hidden', (t) => {
|
||||
global.document.hidden = true;
|
||||
t.mock.timers.enable({ apis: ['setInterval'] });
|
||||
service.startIntervals();
|
||||
|
||||
// Advance time by 150ms
|
||||
t.mock.timers.tick(150);
|
||||
|
||||
assert.strictEqual(service.refreshWatchHistoryTagCounts.mock.callCount(), 0, "Should skip history refresh when hidden");
|
||||
assert.strictEqual(service.refreshTagIdf.mock.callCount(), 0, "Should skip IDF refresh when hidden");
|
||||
});
|
||||
|
||||
test('visibility change triggers refresh', () => {
|
||||
service.subscribeToUpdates();
|
||||
|
||||
assert.ok(visibilityHandler, "Visibility change listener should be registered");
|
||||
|
||||
// Simulate hidden -> visible
|
||||
global.document.hidden = false;
|
||||
visibilityHandler();
|
||||
|
||||
assert.strictEqual(service.refreshWatchHistoryTagCounts.mock.callCount(), 1, "Should refresh history on visibility");
|
||||
assert.strictEqual(service.refreshTagIdf.mock.callCount(), 1, "Should refresh IDF on visibility");
|
||||
});
|
||||
|
||||
test('visibility change DOES NOT trigger refresh if hidden', () => {
|
||||
service.subscribeToUpdates();
|
||||
|
||||
assert.ok(visibilityHandler, "Visibility change listener should be registered");
|
||||
|
||||
// Simulate visible -> hidden (handler called but document.hidden is true)
|
||||
global.document.hidden = true;
|
||||
visibilityHandler();
|
||||
|
||||
assert.strictEqual(service.refreshWatchHistoryTagCounts.mock.callCount(), 0, "Should NOT refresh when becoming hidden");
|
||||
assert.strictEqual(service.refreshTagIdf.mock.callCount(), 0, "Should NOT refresh when becoming hidden");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
# TODO: Explore Data Service Performance Optimization
|
||||
|
||||
- [ ] Create unit test `tests/unit/services/exploreDataService.test.mjs` reproducing the issue (intervals running when hidden).
|
||||
- [ ] Verify test fails (or confirms behavior).
|
||||
- [ ] Modify `js/services/exploreDataService.js` to add `document.hidden` check.
|
||||
- [ ] Modify `js/services/exploreDataService.js` to add `visibilitychange` listener.
|
||||
- [ ] Verify test passes.
|
||||
- [ ] Run full lint check.
|
||||
- [ ] update daily report.
|
||||
Reference in new issue
Block a user