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

73 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).