fix(heartbeat): scale pulsetime with poll_time to prevent missing time#131
fix(heartbeat): scale pulsetime with poll_time to prevent missing time#131TimeToBuildBob wants to merge 2 commits into
Conversation
At high poll_time values (>2s), OS scheduling jitter regularly exceeds the fixed 1s pulsetime margin (poll_time+1), breaking heartbeat chains and causing missing time in the activity timeline. Simulation (200 trials, 3h session) verified against measured user data: - poll_time=5s: 11.6% time lost with old formula → 0.09% with fix - poll_time=10s: 30.0% time lost with old formula → 0.12% with fix Fix: max(poll_time * 1.5, poll_time + 1) ensures jitter tolerance scales proportionally while staying backward-compatible at poll_time ≤ 2s. Closes ActivityWatch/activitywatch#1177
Greptile SummaryThis PR fixes missing time in the activity timeline when
Confidence Score: 5/5Safe to merge — the change is a small, well-bounded formula update with no regressions at the default poll interval. The formula max(poll_time * 1.5, poll_time + 1.0) is mathematically straightforward, its crossover point (poll_time = 2 s) is verified by the parametrized tests, and behavior at the default 1 s poll interval is identical to before. The helper is extracted rather than inlined, so both production code and tests operate on the same implementation. No risky side-effects, migrations, or interface changes are involved. No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant HB as heartbeat_loop
participant CP as compute_pulsetime(poll_time)
participant C as aw-client
HB->>CP: compute_pulsetime(poll_time)
CP-->>HB: "max(poll_time*1.5, poll_time+1.0)"
Note over CP: poll_time ≤ 2s → poll_time+1.0<br/>poll_time > 2s → poll_time*1.5
HB->>C: "client.heartbeat(bucket_id, event, pulsetime=…, queued=True)"
C-->>HB: ok
HB->>HB: sleep(poll_time)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant HB as heartbeat_loop
participant CP as compute_pulsetime(poll_time)
participant C as aw-client
HB->>CP: compute_pulsetime(poll_time)
CP-->>HB: "max(poll_time*1.5, poll_time+1.0)"
Note over CP: poll_time ≤ 2s → poll_time+1.0<br/>poll_time > 2s → poll_time*1.5
HB->>C: "client.heartbeat(bucket_id, event, pulsetime=…, queued=True)"
C-->>HB: ok
HB->>HB: sleep(poll_time)
Reviews (2): Last reviewed commit: "refactor(heartbeat): extract compute_pul..." | Re-trigger Greptile |
…oduction code Greptile review noted the pulsetime parametrized test duplicated the formula inline (max(poll_time * 1.5, poll_time + 1.0)) instead of calling the production code path. If someone later edits the formula in main.py, the inline test would still pass — a silent regression path. Fix: extract compute_pulsetime() as a module-level function, use it at the call site, and import it in the test. The test now catches drift in main.py. No behavior change; formula is identical.
|
Fixed in a1bd4dc: extracted as a module-level function and updated the test to import and call it directly. The test now exercises the production code path, so a formula change in |
|
@greptileai review |
|
All gates clear and ready to merge:
Waiting for maintainer to merge. |
Problem
At
poll_time > 2s, OS scheduling jitter regularly exceeds the fixed 1-secondpulsetime margin (
poll_time + 1), breaking heartbeat chains and causingmissing time in the activity timeline.
From a simulation (200 trials, 3-hour session, σ = 15% × poll_time) validated
against @TimoRenk-nebumind's measured data in ActivityWatch/activitywatch#1177:
The fix explains why users with default
poll_time=1snever see the problemwhile users who increase polling to 5–10s lose significant session time.
Fix
max()(notmin()) ensures:poll_time ≤ 2s, the formula returnspoll_time + 1(same as before)
poll_time = 5s, pulsetime becomes 7.5s insteadof 6.0s, absorbing the ~0.75s jitter typical at that interval
Note: @mrienstra's original
min()proposal in ActivityWatch/activitywatch#1177actually made things worse at high poll_time —
max()is the correct operator.Testing
Added parametrized regression test covering the four key boundary cases.
Closes ActivityWatch/activitywatch#1177