Improve performance of the work items REST API listing query
What does this MR do and why?
The REST work items list (GET /api/v4/namespaces/:full_path/-/work_items) is slower than the GraphQL list it replaced in the frontend. This MR closes part of that gap with two independent fixes found while investigating the difference: an N+1 query on milestone rendering, and redundant Ruby work in widget-support checks. The remaining largest REST-only cost in the profile is the per-item read_work_item authorization loop, which is out of scope here.
Preload milestone parents
The milestone feature preload was split by listing context: the group and namespace endpoints preloaded milestone: :group and the project endpoint preloaded milestone: :project. Neither matches what Milestone#web_url needs, and the split does not match the data. When listing a group with include_descendants=true, most work items belong to projects and carry project milestones, so each distinct milestone lazily loaded its project, the project's namespace, and the namespace's route. In a project listing, work items commonly carry milestones from ancestor groups (every gitlab-org/gitlab milestone is a group milestone), so each distinct milestone lazily loaded its group and the group's route. The GraphQL resolver already preloads both milestone parents, so this closes part of the gap between REST and GraphQL.
- Move the milestone preload into the context-independent
FEATURE_PRELOADStable as{ milestone: [{ group: :route }, { project: { namespace: :route } }] }and drop themilestoneentries fromPROJECT_FEATURE_PRELOADSandGROUP_FEATURE_PRELOADS(lib/api/helpers/work_items/preloads.rb). ActiveRecord skips abelongs_topreload query when every foreign key in the batch is nil, so preloading both parents costs nothing when only one kind of milestone is present - Add request specs for the group listing (project milestones in descendant projects) and the project listing (group milestones on project work items) that assert the query count stays flat as more milestones are added, and check the milestone
web_urlvalues are correct
Memoize has_widget? on WorkItem
WorkItem inherited Issue#has_widget?, which calls work_item_type.widget_classes(resource_parent) and rebuilds the licensed and feature-flag-filtered widget list on every call. The REST list entity checks has_widget? once per requested feature per row (the guard in lib/api/entities/work_items/conditional_exposure_helpers.rb), so 12 features across 20 rows meant around 240 rebuilds per request, plus more in preload_hierarchy_authorization.
WorkItemnow overrideshas_widget?to read its already-memoizedwidget_definitions(app/models/work_item.rb).get_widgetandwidgetsalready memoize on the same instance, so no new staleness is introduced- Measured with stackprof, wall mode, warm cache, Bullet disabled, same request:
has_widget?fell from 28% of request wall time to under 4%, and total samples fell from 310 to 236, roughly 24% less wall time - Verified equivalence for all 9 system-defined types x 33 widget types under project and group parents (594 checks, 0 mismatches). The only difference: an unknown widget name now returns false instead of raising
NameError.Issue#has_widget?itself is unchanged, so plainIssueinstances behave as before - Add a spec in
spec/models/work_item_spec.rb,#has_widget?block: true for a supported widget, false for an unknown one, and confirmswork_item_type.widgetsresolves once across repeatedhas_widget?andget_widgetcalls
References
- Related to #621991 (closed)
Screenshots or screen recordings
N/A, backend-only change.
How to set up and validate locally
- Run the milestone preload specs:
bin/rspec ee/spec/requests/api/work_items_spec.rb -e 'N+1 query prevention for project milestones' -e 'N+1 query prevention for group milestones' - Run the has_widget? spec:
bin/rspec spec/models/work_item_spec.rb -e '#has_widget?' - Optional manual check: as a user with access to a group that has subprojects with project milestones assigned to issues, call
curl --header "PRIVATE-TOKEN: <token>" "http://gdk.test:3000/api/v4/namespaces/<group-full-path>/-/work_items?include_descendants=true&features=milestone&fields=author", and watchlog/development.logto see thatprojects,namespaces, androutesare each loaded in one batch query rather than once per milestone. On master you'd see repeated per-milestone lookups instead.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.