Skip to content

Conversation

@evertonstz
Copy link

@evertonstz evertonstz commented Oct 14, 2025

Backend work to implement this request #1273

Used my previous PR as base #4102, I can try to separate them if needed

Playtime Tracking – Backend PR

Summary

This PR introduces robust, low-overhead playtime tracking to Bottles’ backend. It records per-program sessions, maintains a fast aggregate table for the UI, and is resilient to crashes and concurrent sessions. The solution is event-driven (signals), easy to toggle via GSettings, and thoroughly tested.

Motivation

  • Provide reliable per-program playtime, last played timestamps, and session history for present and future UI features
  • Ensure minimal write amplification and resilience to abrupt exits
  • Keep the backend decoupled from execution flow through signals instead of tight coupling

Design Overview

  • Core tracker: ProcessSessionTracker (bottles/backend/managers/playtime.py)

    • Single SQLite database: $XDG_DATA_HOME/bottles/process_metrics.sqlite
    • Tables: sessions (history) and playtime_totals (materialized aggregate)
    • Heartbeats update last_seen periodically
    • Recovery on startup marks unfinished sessions as forced using last_seen
    • Totals are kept in sync on session finalization and recovery
  • Event-driven lifecycle:

    • Signals added in bottles/backend/state.py:
      • Signals.ProgramStarted
      • Signals.ProgramFinished
    • Payloads typed in bottles/backend/models/process.py:
      • ProcessStartedPayload
      • ProcessFinishedPayload (status: Literal["success", "unknown"])
    • WineExecutor emits these signals instead of calling the tracker directly
    • Manager subscribes to signals and calls the tracker
  • Public backend API consistency

    • New Manager.playtime_start(...) -> Result[int] and Manager.playtime_finish(...) -> Result[None]
    • Signals and public APIs consistently use Result[T]
  • Concurrency & durability

    • One tracker instance per process (owned by Manager)
    • Single tracker instance is able to reliably monitor multiple apps initiated via bottles GUI (no matter if in the same or different bottles), tho heartbeat is synced between all apps
    • Single SQLite connection with WAL and busy timeout
    • A global _lock guards all DB operations and _tracked updates
    • Atomic finalization: session UPDATE and totals UPSERT happen in one transaction
    • Graceful shutdown runs a final WAL checkpoint to avoid leftover -wal
    • atexit handler ensures shutdown on normal process exit

Reliability & WAL behavior

  • WAL enabled for performance and concurrency
  • Final shutdown triggers PRAGMA wal_checkpoint(TRUNCATE) to avoid lingering -wal
  • If the process is force-killed, -wal can remain; SQLite replays it on next open. The tracker can also checkpoint at startup if needed

Risks & Mitigations

  • Force-kill can leave -wal on disk: mitigated by a final checkpoint on shutdown and automatic WAL replay by SQLite on next open, guarantees the db wont be written until next reconciliation
  • Concurrency: _lock serializes connection usage; atomic finalization prevents torn updates

Data Model

Schema is created on-demand by the tracker (_ensure_schema()):

  • sessions (history)

    • Columns: id, bottle_id, bottle_name, bottle_path, program_id, program_name, program_path,
      started_at (epoch), ended_at (epoch), last_seen (epoch), duration_seconds, status (running|success|crash|forced|unknown)
    • Indexes: (bottle_id, program_id), (status)
    • Unique: (bottle_id, program_id, started_at)
  • playtime_totals (materialized aggregate)

    • Columns: bottle_id, bottle_name, program_id, program_name, program_path, total_seconds, sessions_count, last_played
    • PK: (bottle_id, program_id)
    • Index: last_played DESC

Notes:

  • program_id = sha1(f"{bottle_id}:{program_path}") to handle rename scenarios
  • Duplicate collapse: starting the same (bottle_id, program_id) again while running returns the existing session instead of inserting a new one

Settings (GSettings)

  • playtime-enabled (default: true): master toggle
  • playtime-heartbeat-interval (seconds, default: 60)

Backend reads these in Manager at startup. UI toggle is planned for Phase 3.

Tests

Unit and integration tests added/updated:

  • Tracker behavior (unit): schema creation, start/heartbeat/exit, recovery, aggregates, disable toggle, duplicate collapse, uniqueness retry
  • Integration – playtime (direct tracker): success, failure, concurrent, recovery, uniqueness retry, schema meta
  • Integration – signals: emit ProgramStarted/Finished and assert sessions/totals
  • Integration – WineExecutor: stub launch; verify signals drive sessions/totals via Manager

Dev dependencies (tests)

  • freezegun: freeze time to make session durations deterministic without sleep loops.
  • pytest-mock: convenient mocker fixture for stubbing without verbose manual monkeypatching.

PSA

This description was originally written in portuguese and translated by AI

Manual testing

I've been testing this for a couple of days and monitoring the database while simulating scenarios like force closes (inter bottle and multiple apps in same bottle), resets and day-to-day gaming, for now the tracking is spot-on and the heartbeat system is working flawlessly

I mainly work with distributed systems and have minimal experience developing desktop apps, so feel free to change anything in the code!

…ineProgram

Enhance the WineCommand and WineProgram classes to accept pre-run and post-run script arguments. Update the UI to include fields for these arguments in the launch options dialog.
…ssFinishedPayload for improved clarity and structure.
…and WinePath, improving test clarity and maintainability.
… enabling tracking and setting heartbeat interval.
@mirkobrombin
Copy link
Member

mirkobrombin commented Nov 2, 2025

Hi, sorry for the delay, would be better if they were 2 separated MRs. Also because the one dedicated to playtime tracking, could be a general one, including both frontend and backend. Do you like the idea?

Code looks good but I did not test it yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants