Repository navigation
perf: batch the appointment list's per-row queries and memoize the notifier - #295
Merged
Merged
Conversation
A wave prepares one notification per recipient, and the notifier looked the appointment up for every one of them just to decide whether "Maybe" is offered — 80 identical queries for 80 recipients — and resolved the app icon URL each time. Both are now remembered per request. Dev instance, 80 recipients: creating an appointment drops from 429 to 353 queries, moving one from 351 to 272.
The list endpoint issued ten-plus queries per appointment: the response summary re-read the appointment and its responses, resolved the section groups' members anew for every row (the group manager drops a group's cached members whenever getUserGroups() hands out a fresh group object, which it does once per responder), the attachments and the series size were read per row, the check-in counts re-read appointment and responses once more from the controller, and the scheduling filters read every row's responses before paging. Now the page reads everybody's responses, attachments, series sizes and bookings once each, hands the entity and its responses to the summary and check-in tallies, and VisibilityService keeps group members, the group list and "everyone" for the request. The single-appointment endpoint takes the same route. Payloads are unchanged — verified byte-identical on the dev instance for admin and member views. Dev instance (61 upcoming appointments, 12 users): the unpaged list drops from 691 to 65 queries, a 20-row page from 263 to 65, the "all, only mine" view from 1132 to 65, the single appointment from 60 to 56. The psalm baseline loses the entries that the now-typed AppointmentAttachmentMapper::findByAppointment() return fixes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two performance fixes, found by profiling the dev instance with the profiler app. No API change: payloads are byte-identical, so web and Flutter clients are unaffected.
Appointment list: one query per page instead of ten per row
GET /api/appointmentsissued ten-plus queries per appointment: the response summary re-read the appointment and its responses, resolved the section groups' members anew for every row (the group manager drops a group's cached members whenevergetUserGroups()hands out a fresh group object, which it does once per responder), attachments and series sizes were read per row, the check-in counts re-read appointment and responses once more from the controller, and the scheduling filters read every row's responses before paging.Now the page reads everybody's responses, attachments, series sizes and bookings once each, hands the entity and its responses to the summary and check-in tallies, and
VisibilityServicekeeps group members, the group list and "everyone" for the request. The single-appointment endpoint takes the same route.limit=20timeframe=all&onlyForMe=truelimit=20Verified byte-identical responses (JSON with sorted keys) for admin and member, list and single appointment, before and after.
Notification waves: read the appointment once
The notifier looked the appointment up for every recipient of a wave just to decide whether "Maybe" is offered, and resolved the icon URL each time. Both are remembered per request. With 80 recipients, creating an appointment drops from 429 to 353 queries, moving one from 351 to 272. The remaining per-recipient cost is Nextcloud's own (notification insert, user lookups, push signing) plus the push proxy round trip.
Cache lifetime
All new caches are plain fields on request-scoped services (
VisibilityService,Notifier): they live for one web request, one cron run or one occ command — the same lifetime the existing team-member and permission caches have. Nothing is written to Redis or APCu.Follow-ups, deliberately out of scope
occupancy(),standingOf(),confirmedIds());CapacityService::split()already offers the pre-loaded seam.getUpcomingAppointmentsForWidget()(fixed five rows) still reads per row.AppointmentService::resolveAudience()/getAllWhitelistedUsers(),PermissionService::collectGroupMembers()andStatisticsService::groupMembers()still expand groups through the group manager instead ofVisibilityService::getGroupMembers().Checks
./scripts/check.shis green (eslint, stylelint, php-cs-fixer, psalm, phpunit, vite build, l10n, OpenAPI spec unchanged). The psalm baseline loses the entries that the now-typedAppointmentAttachmentMapper::findByAppointment()return fixes.🤖 Generated with Claude Code