ddec3a927a
Major timeline enhancements: - Right-click drag multi-selection with floating action bar (batch delete/assign) - DemandPopover for demand strip details (replaces broken "Loading" modal) - ResourceHoverCard on name hover showing skills, rates, role, chapter - Merged heatmap+vacation tooltips into unified TimelineTooltip component - Fixed overbooking blink animation (date normalization, z-index ordering) - Fixed dark mode sticky column bleed-through in project view - System roles admin page, notification task management, performance review docs Co-Authored-By: claude-flow <ruv@ruv.net>
418 lines
14 KiB
Markdown
418 lines
14 KiB
Markdown
# Performance Optimization Review
|
|
|
|
Date: 2026-03-18
|
|
|
|
Scope: analysis only. No runtime behavior was changed in this pass.
|
|
|
|
## Executive Summary
|
|
|
|
The biggest performance costs in Planarchy currently come from three patterns:
|
|
|
|
1. Broad data fetches followed by repeated in-memory filtering/grouping.
|
|
2. Expensive client-side derivations on screens with large datasets, especially Timeline.
|
|
3. Broad cache invalidation that forces heavy queries to refetch and recompute after small mutations.
|
|
|
|
The highest-value optimization target is the Timeline stack. After that, the best wins are server-side aggregation for chargeability/dashboard workloads and narrowing resource/report queries to visible scope.
|
|
|
|
## Main Hotspots
|
|
|
|
### 1. Timeline
|
|
|
|
Relevant files:
|
|
|
|
- [TimelineContext.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/timeline/TimelineContext.tsx)
|
|
- [TimelineView.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/timeline/TimelineView.tsx)
|
|
- [TimelineResourcePanel.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/timeline/TimelineResourcePanel.tsx)
|
|
- [TimelineProjectPanel.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/timeline/TimelineProjectPanel.tsx)
|
|
- [timeline.ts](/home/hartmut/Documents/Copilot/planarchy/packages/api/src/router/timeline.ts)
|
|
|
|
Observed issues:
|
|
|
|
- `TimelineContext` derives multiple large collections in the client from the same raw payload.
|
|
- Resource and project filtering is partly pushed to the backend, but large read-model payloads are still constructed and further filtered/grouped in the browser.
|
|
- Timeline mutations invalidate multiple heavy queries at once.
|
|
- Timeline SSE events also trigger broad invalidations.
|
|
|
|
Impact:
|
|
|
|
- Slow initial render.
|
|
- Lag on interaction and hover.
|
|
- Slow recovery after edits because the full timeline dataset is often fetched again.
|
|
|
|
### 2. Chargeability Report
|
|
|
|
Relevant files:
|
|
|
|
- [ChargeabilityReportClient.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/reports/ChargeabilityReportClient.tsx)
|
|
- [chargeability-report.ts](/home/hartmut/Documents/Copilot/planarchy/packages/api/src/router/chargeability-report.ts)
|
|
|
|
Observed issues:
|
|
|
|
- The server loads matching resources, then all bookings in range, then repeatedly filters bookings and vacations per resource and per month.
|
|
- The client renders a wide report grid and still performs filtering/grouping locally.
|
|
- Public holiday support is still marked as TODO in the route, so future correctness work will likely add more cost unless the data model is optimized first.
|
|
|
|
Impact:
|
|
|
|
- CPU-heavy report generation.
|
|
- Larger-than-needed payloads.
|
|
- Increased render cost on wide month ranges.
|
|
|
|
### 3. Dashboard Chargeability / Analytics Widgets
|
|
|
|
Relevant files:
|
|
|
|
- [dashboard.ts](/home/hartmut/Documents/Copilot/planarchy/packages/api/src/router/dashboard.ts)
|
|
- [get-chargeability-overview.ts](/home/hartmut/Documents/Copilot/planarchy/packages/application/src/use-cases/dashboard/get-chargeability-overview.ts)
|
|
- [get-overview.ts](/home/hartmut/Documents/Copilot/planarchy/packages/application/src/use-cases/dashboard/get-overview.ts)
|
|
|
|
Observed issues:
|
|
|
|
- `getDashboardChargeabilityOverview()` fetches all relevant resources and all bookings for the month, then filters the full booking set again for each resource.
|
|
- Similar patterns exist in dashboard summary use cases where relatively small widgets are backed by broad read logic.
|
|
|
|
Impact:
|
|
|
|
- Avoidable server CPU and latency.
|
|
- Widgets become more expensive as the database grows even if the UI remains small.
|
|
|
|
### 4. Resources Screen
|
|
|
|
Relevant files:
|
|
|
|
- [ResourcesClient.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/app/(app)/resources/ResourcesClient.tsx)
|
|
- [resource.ts](/home/hartmut/Documents/Copilot/planarchy/packages/api/src/router/resource.ts)
|
|
|
|
Observed issues:
|
|
|
|
- Infinite scrolling is in place, which is good, but the screen still does substantial client-side sort/filter/order work.
|
|
- Separate global stats queries can be expensive when only the currently visible rows need enriched values.
|
|
- Large dropdown/filter datasets are fully loaded to support selection UIs.
|
|
|
|
Impact:
|
|
|
|
- Good enough at small scale, but it will degrade with larger teams and imported planning data.
|
|
|
|
### 5. Shared Booking Read Model
|
|
|
|
Relevant files:
|
|
|
|
- [list-assignment-bookings.ts](/home/hartmut/Documents/Copilot/planarchy/packages/application/src/use-cases/allocation/list-assignment-bookings.ts)
|
|
|
|
Observed issues:
|
|
|
|
- This is a clean shared entry point, but it returns broad booking rows and leaves most grouping/aggregation to callers.
|
|
- Several heavy endpoints call this and then repeatedly scan the returned array.
|
|
|
|
Impact:
|
|
|
|
- Logic is easy to reuse, but performance scales poorly because each consumer re-aggregates independently.
|
|
|
|
## Current Database Index Baseline
|
|
|
|
Relevant schema:
|
|
|
|
- [schema.prisma](/home/hartmut/Documents/Copilot/planarchy/packages/db/prisma/schema.prisma)
|
|
|
|
Good coverage already exists for:
|
|
|
|
- `Assignment`: `resourceId`, `projectId`, `status`, date ranges, and composite indices.
|
|
- `DemandRequirement`: `projectId`, `status`, date ranges.
|
|
- `Vacation`: `resourceId`, `status`, date ranges.
|
|
- `Resource`: `chapter`, `isActive`, `countryId`, `orgUnitId`, `resourceType`.
|
|
- `Project`: `status`, date range, `orderType`, `clientId`.
|
|
|
|
Likely gaps for current query patterns:
|
|
|
|
- `Resource` filters used together in analytics screens: `isActive`, `chgResponsibility`, `departed`, `rolledOff`, `countryId`, `managementLevelGroupId`.
|
|
- `Project` filters used in timeline/report contexts: `clientId` combined with active date windows.
|
|
- Potential overlap-heavy access paths where PostgreSQL may benefit more from specialized query shapes or materialized read models than from adding more conventional B-tree indexes.
|
|
|
|
Conclusion:
|
|
|
|
The schema is not unindexed. The main issue is query shape and repeated in-memory work, not just missing indexes. Index work should support query redesign, not replace it.
|
|
|
|
## Recommended Optimizations
|
|
|
|
### A. Move Timeline Derivations Closer to the Backend
|
|
|
|
Proposal:
|
|
|
|
- Add a timeline read endpoint that returns data already grouped for the active view mode and active filters.
|
|
- Return normalized maps and compact slices instead of large repeated nested objects.
|
|
- Keep the raw endpoint only if another screen truly needs it.
|
|
|
|
Pros:
|
|
|
|
- Largest likely user-visible win.
|
|
- Reduces browser CPU, garbage collection, and hover lag.
|
|
- Shrinks payload size if redundant nested fields are removed.
|
|
|
|
Cons:
|
|
|
|
- More backend read-model code.
|
|
- Slightly higher coupling between API shape and timeline UI needs.
|
|
- Requires careful migration to avoid breaking existing interactions.
|
|
|
|
Recommended priority: P0
|
|
|
|
Estimated effort: medium to large
|
|
|
|
### B. Build Indexed Maps Once Instead of Repeated Array Scans
|
|
|
|
Proposal:
|
|
|
|
- In timeline/report/dashboard code, replace repeated `.filter()`, `.find()`, and `.some()` passes with prebuilt maps keyed by `resourceId`, `projectId`, `monthKey`, and status where appropriate.
|
|
- Standardize this as shared helper utilities for read-model construction.
|
|
|
|
Pros:
|
|
|
|
- Low-risk improvement.
|
|
- Can be introduced incrementally.
|
|
- Helps both client and server hot paths.
|
|
|
|
Cons:
|
|
|
|
- Does not solve oversized payloads by itself.
|
|
- Adds some code complexity if done ad hoc instead of through shared helpers.
|
|
|
|
Recommended priority: P0
|
|
|
|
Estimated effort: small to medium
|
|
|
|
### C. Replace Broad Invalidations with Scoped Cache Updates
|
|
|
|
Proposal:
|
|
|
|
- Audit timeline mutations and SSE handlers that currently invalidate `getEntries`, `getEntriesView`, `getProjectContext`, and `getBudgetStatus` together.
|
|
- Invalidate only the affected query keys.
|
|
- Where practical, patch query cache locally after small edits instead of refetching everything.
|
|
|
|
Pros:
|
|
|
|
- Faster feedback after drag/drop and edit flows.
|
|
- Less network churn.
|
|
- Lower server load during active planning sessions.
|
|
|
|
Cons:
|
|
|
|
- Cache correctness becomes more complex.
|
|
- Needs strong regression coverage around drag/drop and split allocation flows.
|
|
|
|
Recommended priority: P0
|
|
|
|
Estimated effort: medium
|
|
|
|
### D. Push Chargeability Aggregation into Dedicated Server Read Models
|
|
|
|
Proposal:
|
|
|
|
- Replace repeated per-resource filtering of a flat booking array with server-side grouped structures.
|
|
- Precompute `bookingsByResource` and `vacationsByResource` once before month iteration.
|
|
- Consider a dedicated monthly chargeability read model for reports and dashboard widgets.
|
|
|
|
Pros:
|
|
|
|
- Strong improvement for reports and widgets.
|
|
- Eliminates duplicate aggregation logic across dashboard/resource/report endpoints.
|
|
- Better foundation for public holiday integration.
|
|
|
|
Cons:
|
|
|
|
- Requires consolidating overlapping business logic.
|
|
- Read-model duplication is possible if not designed carefully.
|
|
|
|
Recommended priority: P1
|
|
|
|
Estimated effort: medium
|
|
|
|
### E. Narrow Data Fetches to Visible Scope
|
|
|
|
Proposal:
|
|
|
|
- Avoid loading large supporting datasets such as `resource.list({ limit: 500 })` or “all projects” queries when only label resolution for selected IDs is required.
|
|
- Add lightweight lookup endpoints like `getByIds()` or `resolveLabels()`.
|
|
- On the resources page, fetch chargeability/stat enrichments only for the visible page or current filtered result slice.
|
|
|
|
Pros:
|
|
|
|
- Smaller payloads.
|
|
- Faster filter bar and screen startup.
|
|
- Simple, targeted changes.
|
|
|
|
Cons:
|
|
|
|
- More API endpoints or variants.
|
|
- Some UI components become slightly more stateful.
|
|
|
|
Recommended priority: P1
|
|
|
|
Estimated effort: small to medium
|
|
|
|
### F. Add Read-Optimized Analytics Endpoints or Materialized Views
|
|
|
|
Proposal:
|
|
|
|
- For expensive monthly analytics, introduce a read-optimized table or materialized view refreshed on schedule or after import batches.
|
|
- Use this for dashboard and chargeability summaries, not for transactional edit screens.
|
|
|
|
Pros:
|
|
|
|
- Best long-term scalability for imported dispo-scale data.
|
|
- Predictable response times for reports.
|
|
- Reduces repeated heavy joins and calculations.
|
|
|
|
Cons:
|
|
|
|
- More infrastructure and refresh logic.
|
|
- Risk of stale analytics if refresh semantics are unclear.
|
|
- Higher implementation complexity than direct query improvements.
|
|
|
|
Recommended priority: P2
|
|
|
|
Estimated effort: large
|
|
|
|
### G. Add Composite Indexes Only Where Query Patterns Justify Them
|
|
|
|
Proposal:
|
|
|
|
- Review actual PostgreSQL query plans for:
|
|
- resource analytics filters
|
|
- timeline date-overlap filters
|
|
- project/client/date combinations
|
|
- Add composite indexes only after measuring the slow plans.
|
|
|
|
Pros:
|
|
|
|
- Cheap improvement when aligned to real plans.
|
|
- Complements query redesign.
|
|
|
|
Cons:
|
|
|
|
- Limited value if broad in-memory processing remains.
|
|
- Too many indexes can slow writes/imports.
|
|
|
|
Recommended priority: P1
|
|
|
|
Estimated effort: small
|
|
|
|
Suggested candidates to measure first:
|
|
|
|
- `Resource(isActive, departed, rolledOff, chgResponsibility, countryId)`
|
|
- `Resource(isActive, managementLevelGroupId, countryId)`
|
|
- `Project(clientId, status, startDate, endDate)`
|
|
|
|
### H. Consider Row Virtualization for Heavy Tables
|
|
|
|
Proposal:
|
|
|
|
- Add virtualization for very large row-based screens if profiling confirms DOM/render cost remains high after data/query optimizations.
|
|
- Candidate screens: resources table, chargeability report table, timeline side panels.
|
|
|
|
Pros:
|
|
|
|
- Strong render win when many rows are mounted.
|
|
- Independent of backend changes.
|
|
|
|
Cons:
|
|
|
|
- More UI complexity.
|
|
- Can complicate drag/drop, sticky headers, and keyboard navigation.
|
|
- Should not be the first fix for timeline if the main problem is data churn.
|
|
|
|
Recommended priority: P2
|
|
|
|
Estimated effort: medium
|
|
|
|
## Priority Order
|
|
|
|
### Phase 1
|
|
|
|
- A. Move timeline derivations closer to the backend.
|
|
- B. Replace repeated array scans with indexed maps.
|
|
- C. Scope cache invalidation for timeline mutations and SSE.
|
|
|
|
Expected result:
|
|
|
|
- Best chance of making timeline interaction materially faster without removing functionality.
|
|
|
|
### Phase 2
|
|
|
|
- D. Server-side chargeability read models.
|
|
- E. Narrow data fetches to visible scope.
|
|
- G. Add measured composite indexes.
|
|
|
|
Expected result:
|
|
|
|
- Faster reports, dashboard widgets, and filter-heavy screens.
|
|
|
|
### Phase 3
|
|
|
|
- F. Materialized analytics/read models.
|
|
- H. Virtualization where profiling still shows render bottlenecks.
|
|
|
|
Expected result:
|
|
|
|
- Better scalability once data volume grows further.
|
|
|
|
## Suggested Measurement Plan
|
|
|
|
Before implementation:
|
|
|
|
- Capture browser performance traces for:
|
|
- timeline initial load
|
|
- timeline hover/selection
|
|
- timeline mutation save path
|
|
- chargeability report load
|
|
- resources page load and scroll
|
|
- Capture server timings for:
|
|
- `timeline.getEntriesView`
|
|
- `chargeabilityReport.getReport`
|
|
- `dashboard.getChargeabilityOverview`
|
|
- `resource.getChargeabilityStats`
|
|
- Record PostgreSQL query plans for the slowest endpoints.
|
|
|
|
During implementation:
|
|
|
|
- Compare payload sizes before and after endpoint changes.
|
|
- Track React commit duration on Timeline and Resources screens.
|
|
- Measure query invalidation counts after common edits.
|
|
|
|
Success criteria:
|
|
|
|
- Timeline hover and selection feel immediate under imported dispo-scale data.
|
|
- Timeline mutation flows no longer trigger full-screen refetch storms.
|
|
- Chargeability report load time drops materially for multi-month ranges.
|
|
- Dashboard widgets stop scanning large booking arrays per resource.
|
|
|
|
## No-Regression Guidance
|
|
|
|
Do not trade away any of the following:
|
|
|
|
- Existing planning semantics for assignments, proposed work, and demand visibility.
|
|
- Timeline drag/drop correctness.
|
|
- Tooltip, right-click, and selection behavior.
|
|
- Anonymization behavior.
|
|
- Filter parity between resources, timeline, and reports.
|
|
|
|
Recommended safeguards:
|
|
|
|
- Add endpoint-level benchmarks for the heavy routes.
|
|
- Add UI regression checks around timeline interaction paths.
|
|
- Roll out timeline changes behind a temporary feature flag if the payload shape changes significantly.
|
|
- Compare old and new aggregation outputs on the same imported dataset before removing legacy paths.
|
|
|
|
## Suggested Ticket Breakdown
|
|
|
|
1. Profile and baseline the four slowest endpoints plus timeline render path.
|
|
2. Refactor timeline read model to return pre-grouped data for current filters/view.
|
|
3. Replace timeline broad invalidations with scoped invalidation and selective cache patching.
|
|
4. Refactor chargeability report aggregation into grouped server-side structures.
|
|
5. Narrow lookup/filter support queries to selected IDs or visible pages.
|
|
6. Review PostgreSQL query plans and add only measured composite indexes.
|
|
7. Re-profile and decide whether virtualization or materialized analytics are still needed.
|
|
|
|
## Bottom Line
|
|
|
|
This codebase can be made substantially faster without removing functionality. The main gains will not come from cosmetic micro-optimizations. They will come from changing where data is shaped, reducing repeated scans, and stopping broad cache churn on heavy views.
|