Files
TimeTracker/docs/PERFORMANCE.md
T
Dries Peeters 9547937be2 docs: update README and guides, add audit and strategy docs
- Simplify README version section and point to CHANGELOG
- Update UI overview with Reports and installation reference
- Refresh CONTRIBUTING, DEVELOPMENT, API.md links/consistency
- Add ARCHITECTURE_AUDIT, DOCS_AUDIT, PRODUCT_UX_AUDIT, FRONTEND, PERFORMANCE
- Add API_CONSISTENCY_AUDIT, RESPONSE_FORMAT, CONTRIBUTOR_GUIDE, TESTING_STRATEGY
- Update GETTING_STARTED, REST_API, PROJECT_STRUCTURE, DEPLOYMENT_GUIDE
2026-03-15 09:36:37 +01:00

6.4 KiB
Raw Blame History

TimeTracker Performance Optimizations

This document summarizes performance improvements applied to the TimeTracker Flask application, configuration options, follow-up index candidates, benchmark targets, and where async or background processing would help.

Summary of Changes

Area Change Rationale
Instrumentation Optional slow-request logging and query-count profiling via PERF_LOG_SLOW_REQUESTS_MS and PERF_QUERY_PROFILE. Confirm assumptions and measure impact without production overhead by default.
Task report Eager load tasks (project, assignee); single aggregation query for hours/count per task via TimeEntryRepository.get_task_aggregates(). Same for Excel export. Eliminates N+1 (1 + N task queries + N time-entry queries).
Admin dashboard Replaced two 30-iteration loops with two GROUP BY queries (user activity by date, daily hours by date). Cuts ~60 queries to 2.
Gantt Load all tasks for selected projects in one query; group by project_id. calculate_project_progress accepts optional tasks to avoid extra query. One query instead of N per project.
Time entries report Pagination (page/per_page, default 50, max 500); summary from COUNT and SUM aggregation. Prevents unbounded load and timeouts on large date ranges.
ReportingService.get_time_summary Use count_for_date_range() and get_total_duration() instead of loading all entries for count. Avoids loading full result set to compute totals.
Admin user edit Batch load clients: Client.query.filter(Client.id.in_(assigned_client_ids)).all(). One query instead of N for subcontractor assigned clients.
Dashboard last_timer_context joinedload(TimeEntry.project), joinedload(TimeEntry.client) on the single “last entry” query. Avoids two lazy loads when rendering.
Caching Dashboard: cache get_dashboard_stats and get_time_by_project_chart per user (TTL 90s). Admin: cache chart data (TTL 10 min). invalidate_dashboard_for_user() clears stats, chart, and legacy key. Reduces repeated DB work on hot paths.
Team chat Batch load read receipts for message IDs; build dict and create only missing receipts. Removes N+1 per message.
Integrations list One query for all credentials for listed integration IDs; set of IDs for “has_credentials” check. Removes N+1 per integration.
Inventory Batch load WarehouseStock for reorder/low-stock items; group by stock_item_id. Use joinedload(WarehouseStock.warehouse) where warehouse is rendered. Removes N+1 per item.
AnalyticsService get_dashboard_top_projects and get_time_by_project_chart use DB GROUP BY + SUM instead of loading all time entries. Scales with large entry counts.

Tradeoffs

  • Dashboard cache: Stats and chart can be up to 90 seconds stale; invalidated on timer start/stop and time entry changes.
  • Admin chart cache: Up to 10 minutes stale; no invalidation on time entry/project change (TTL only).
  • Time entries report: Pagination adds page and per_page to the URL; exports still fetch full result set (consider a hard limit or background job for very large ranges).
  • Reports summary: Not cached (return value includes ORM objects); dashboard and admin chart caches are the main wins.

Configuration

Env / config Description
PERF_LOG_SLOW_REQUESTS_MS Log one line when request duration exceeds this many milliseconds (0 = disabled).
PERF_QUERY_PROFILE When true, track DB query count per request and include in slow-request logs.

Instrumentation is in app/utils/performance.py and registered in create_app().

Follow-up: DB Index Candidates

Add via migrations after validating with EXPLAIN ANALYZE on real workloads:

  • time_entries: (user_id, start_time), (project_id, start_time), (task_id, end_time), (start_time) or (start_time, end_time) for range filters and admin 30-day charts. Consider partial index WHERE end_time IS NOT NULL for completed-entry-heavy queries.
  • payments: (payment_date, status) for reporting and last-30-days stats.
  • activities: (user_id, created_at) for activity feed; (entity_type, action) if filtered by type.
  • projects: (status) for active lists; (client_id, status) for client-scoped lists.
  • tasks: (project_id, status) for Gantt and task lists per project.

Endpoints and Pages to Benchmark

  • Web: GET /, GET /dashboard, GET /reports, GET /reports/tasks, GET /reports/time-entries, GET /admin, GET /admin/dashboard, GET /gantt, GET /api/gantt/data (with project_id and date range).
  • API: GET /api/v1/time-entries, GET /api/v1/clients, GET /api/entries, GET /api/activities, and any dashboard/summary endpoints used by the UI.

Measure p50/p95 response time and, where possible, DB query count per request before and after optimizations. Use production-like data (e.g. 10k+ time entries, hundreds of projects/tasks).

API Pagination Consistency

  • List endpoints use page and per_page (default 50, max 100 in API v1 common). Response shape: resource key (e.g. time_entries) plus pagination with page, per_page, total, pages, has_next, has_prev, next_page, prev_page.
  • Align any remaining list endpoints with app/routes/api_v1_common.paginate_query() and document defaults in OpenAPI and REST_API.md.

Where Async or Background Processing Would Help

  • Heavy report exports: Time entries (and similar) CSV/Excel/PDF with large date ranges. Offload to a background job (e.g. APScheduler or Celery), write file to storage or email link; return “report queued” or a poll endpoint. Reduces timeouts and keeps request workers free.
  • Scheduled reports: Ensure generation runs in a worker/process that does not block the web app.
  • Precomputed rollups (optional): Daily/hourly rollup table for time_entries filled by a job; dashboard and admin charts could read from rollups at scale.
  • Cache warming: Optional low-priority job that periodically hits dashboard or reports summary for active users.

Tests

  • Task report: tests/test_reports_task_report.py correct hours/entries and repository aggregation.
  • Admin dashboard: tests/test_admin_dashboard_charts.py chart data present and 30-day series.
  • ReportingService: get_time_summary uses count and duration only (no full fetch).