Conversation
5e5d36c to
69115b2
Compare
af7e6a5 to
934a475
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
| scope :ordered_by_relevance_for, ->(project) { | ||
| used_in_project = Labeling | ||
| .where(labelable_type: WorkPackage.name) | ||
| .where("labelings.label_id = labels.id") | ||
| .joins("INNER JOIN work_packages ON work_packages.id = labelings.labelable_id") | ||
| .where(work_packages: { project_id: project }) | ||
| .arel | ||
| .exists | ||
|
|
||
| reorder(used_in_project.desc) | ||
| .order(Arel.sql("#{USAGE_COUNT_SQL} DESC")) | ||
| .order("LOWER(labels.name) ASC") |
There was a problem hiding this comment.
🤖 Benchmark: Label.ordered_by_relevance_for
The labels dropdown calls this endpoint with pageSize=-1 (clamped to apiv3_max_page_size, 1000 by default) and re-queries with a name ~ filter on each keystroke, so I benchmarked the exact SQL the index endpoint runs at three data sizes. Data was bulk-inserted with a skewed labeling distribution, the target project holding ~1-2% of work packages. Timings are server-side query times in ms (median / p95 over 19 warm runs).
| Scenario (labels / labelings / WPs) | Unfiltered | Filtered | total count |
Alphabetical baseline |
|---|---|---|---|---|
| Small (100 / 10k / 5k) | 4.7 / 68.7 | 0.7 / 1.9 | 0.3 | 1.9 |
| Medium (1k / 100k / 50k) | 97.2 / 114.5 | 1.2 / 2.6 | 0.3 | 31.6 |
| Large (10k / 1M / 500k) | 627.5 / 841.8 | 5.0 / 5.9 | 0.6 | 274.2 |
Where the time goes. The "used in this project" EXISTS is cheap: Postgres turns it into a hashed subplan that runs once (loops=1). The cost is the correlated usage_count subquery, which runs once per label (loops=10000, ~1M buffer hits at the large size). The name filter is applied before those subqueries are evaluated, so the per-keystroke filtered path stays in single-digit ms even at the large size. Only the first, unfiltered open of the dropdown gets slow, and only on instances with thousands of labels and ~1M labelings.
Alternatives tried (large size only):
| Rewrite | Unfiltered | Filtered |
|---|---|---|
EXISTS as a CTE |
~623 | not run |
usage_count via one GROUP BY |
~227 | not run |
| Both combined | ~90 | ~87 |
The combined rewrite returns the same order and brings the unfiltered open under 100ms, but it makes every filtered keystroke ~17x slower (5ms to 87ms), because aggregating over all labelings is a fixed cost the name filter can't reduce.
Decision: keeping the current query. The keystroke path is fast at every size, and the slow case is a first open on very large instances. If that becomes a real problem, the GROUP BY rewrite can be applied to the unfiltered request only.
There was a problem hiding this comment.
Nice. This gives confidence. Thanks for making it.
Labels are global and unbounded, so allowed values are linked to a project-scoped endpoint rather than embedded. The flag joins the schema cache dependencies so toggling it does not serve stale schemas.
The work package labels dropdown needs labels already used in the current workspace first, then the most used ones, so the schema points at this endpoint instead of the global listing.
Assigning labels does not touch the work package row, so without this the cached JSON representation kept serving the previous labels.
The form attribute list is derived from the schema representer's static definitions, where show_if never runs, so exports and the type form configuration listed labels with the flag off and printed the raw association with it on.
Nothing reads usage_count from ordered_by_relevance_for, and plain SQL strings read easier than the Arel expressions here.
The shared index endpoint already handles offset and pageSize; reorder lets the relevance ordering replace the query's default name order on merge.
New nested resources only get workspace-scoped path helpers; the projects route remains an undocumented alias.
c1f84a6 to
389bd60
Compare
brunopagno
left a comment
There was a problem hiding this comment.
Nice job. This looks really good 👍
Approving, as it can go through, but left a comment in case you want to think about naming shenanigans 😁
Cheers 🎉 🎉 🎉
| scope :ordered_by_relevance_for, ->(project) { | ||
| used_in_project = Labeling | ||
| .where(labelable_type: WorkPackage.name) | ||
| .where("labelings.label_id = labels.id") | ||
| .joins("INNER JOIN work_packages ON work_packages.id = labelings.labelable_id") | ||
| .where(work_packages: { project_id: project }) | ||
| .arel | ||
| .exists | ||
|
|
||
| reorder(used_in_project.desc) | ||
| .order(Arel.sql("#{USAGE_COUNT_SQL} DESC")) | ||
| .order("LOWER(labels.name) ASC") |
There was a problem hiding this comment.
Nice. This gives confidence. Thanks for making it.
| select("labels.*, #{USAGE_COUNT_SQL} AS usage_count") | ||
| } | ||
|
|
||
| scope :ordered_by_relevance_for, ->(project) { |
There was a problem hiding this comment.
🟢 I am debating this with myself, but I feel there's something a bit off
relevanceis ambiguous, it can mean anything and it does not explain what the sorting is doing- reading the code explains it, but it takes a minute to parse everything
- the product solution is not really tested and might change in the future
For me either add an explanation comment...
# Sorts first by locality (labels already used in the current project come first)
# then by frequency (most used first).... or make the scope name overexplain itself
scope :ordered_by_locality_and_usage_for, -> (project) {but if this changes in the future we need to change the method signature 👀
https://community.openproject.org/wp/COMMS-1046
Adds
labelsto the work package schema behind thework_package_labelsflag, linking its allowed values to a newGET /api/v3/workspaces/:id/labelsthat orders labels by relevance for the workspace: labels already used there first, then the most used overall, then by name.Label assignments now take part in the work package cache checksum, so a labels-only change is no longer served stale, and exports print label names while the flag hides the attribute from form configuration and exports when off. Nested collection endpoints also honour
offsetandpageSize, which they silently ignored before.AI involvement
Collaborative – AI generated a substantial part of the code; I reviewed and understand every line.