Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions aw_watcher_window/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
23 changes: 23 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import pytest

from aw_watcher_window.main import compute_pulsetime
from aw_watcher_window.macos_cli import build_swift_command


Expand Down Expand Up @@ -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
Loading