diff --git a/aw_watcher_window/main.py b/aw_watcher_window/main.py index e271c70..bfb053e 100644 --- a/aw_watcher_window/main.py +++ b/aw_watcher_window/main.py @@ -25,6 +25,18 @@ logger.setLevel(logging.__getattribute__(log_level.upper())) +def compute_pulsetime(poll_time: float) -> float: + """Scale pulsetime with poll_time so OS scheduling jitter doesn't break heartbeat chains. + + At poll_time=1s, jitter ~0.15s is well within 1s margin (poll_time+1). + At poll_time=5s, jitter ~0.75s exceeds the 1s margin ~10% of the time, + causing missing time in the timeline. max(poll_time*1.5, poll_time+1) keeps + backward compatibility at poll_time≤2s while fixing the problem at higher + polling intervals. See: https://github.com/ActivityWatch/activitywatch/issues/1177 + """ + return max(poll_time * 1.5, poll_time + 1.0) + + def kill_process(pid): logger.info("Killing process {}".format(pid)) try: @@ -157,11 +169,11 @@ def heartbeat_loop( now = datetime.now(timezone.utc) current_window_event = Event(timestamp=now, data=current_window) - # Set pulsetime to 1 second more than the poll_time - # This since the loop takes more time than poll_time - # due to sleep(poll_time). client.heartbeat( - bucket_id, current_window_event, pulsetime=poll_time + 1.0, queued=True + bucket_id, + current_window_event, + pulsetime=compute_pulsetime(poll_time), + queued=True, ) sleep(poll_time) diff --git a/tests/test_main.py b/tests/test_main.py index 96e04b1..e8b5be0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,3 +1,6 @@ +import pytest + +from aw_watcher_window.main import compute_pulsetime from aw_watcher_window.macos_cli import build_swift_command @@ -42,3 +45,23 @@ def test_build_swift_command_passes_title_filters(): "--exclude-titles", "Slack.*huddle", ] + + +@pytest.mark.parametrize( + "poll_time,expected_pulsetime", + [ + (1.0, 2.0), # max(1.5, 2.0)=2.0 — backward compatible, no change + (2.0, 3.0), # max(3.0, 3.0)=3.0 — exact threshold + (5.0, 7.5), # max(7.5, 6.0)=7.5 — fix kicks in (was 6.0, caused ~10% loss) + (10.0, 15.0), # max(15.0, 11.0)=15.0 — fix kicks in (was 11.0, caused ~30% loss) + ], +) +def test_pulsetime_scales_with_poll_time(poll_time: float, expected_pulsetime: float): + """pulsetime must scale with poll_time so OS scheduling jitter doesn't break heartbeat chains. + + At poll_time=5s the old formula (poll_time+1=6s) caused ~10% of heartbeat + gaps to exceed pulsetime, resulting in missing time. The fix: max(poll_time*1.5, + poll_time+1) keeps backward compat at low poll_time while scaling the jitter + tolerance at higher values. See: ActivityWatch/activitywatch#1177 + """ + assert compute_pulsetime(poll_time) == expected_pulsetime