perf(explore): pause background updates when hidden

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-15 07:14:29 +00:00
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`
+21
View File
@@ -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");
+20
View File
@@ -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.