Skip to content

perf(api): improve performance on states and modules endpoints#9275

Open
p11o wants to merge 5 commits into
makeplane:previewfrom
p11o:preview
Open

perf(api): improve performance on states and modules endpoints#9275
p11o wants to merge 5 commits into
makeplane:previewfrom
p11o:preview

Conversation

@p11o

@p11o p11o commented Jun 19, 2026

Copy link
Copy Markdown

Description

These perf updates amount to a roughly 15s latency reduction as the api calls are sequential:

  Wave 1 (config):  ████████ 3s
  Wave 2 (issues):          ████████ 3s
  Wave 3 (details):                 ████████ 3s
  Wave 4 (...):                             ████████ 3s
  Wave 5 (...):                                     ████████ 3s
                                                             = ~15s

/api/projects/{id}/states/

The get_queryset filtered membership via a JOIN through project__project_projectmember, which multiplied rows and required .distinct() to deduplicate. Replaced with an Exists() correlated subquery on ProjectMember — same semantics, no row inflation.

/api/projects/{id}/modules/

Five of the six issue-count annotations used Count("issue_module__issue__state__group", distinct=True), traversing a 3-table join path per annotation. Replaced with Coalesce(Subquery(...annotate(count=Count("id")))) — the same pattern used in #8889 (2.6s -> 0.07s improvement on sub-issues).

Also extracted the three identical get_queryset bodies from ModuleListCreateAPIEndpoint, ModuleDetailAPIEndpoint, and ModuleArchiveUnarchiveAPIEndpoint into shared helpers.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Test Scenarios

Ran the full Docker test suite (docker compose -f docker-compose-test.yml up --build --abort-on-container-exit): 345 passed, 8 pre-existing failures in TestMagicSign*

References

#8889 — prior art for the Coalesce(Subquery) pattern in this codebase

Summary by CodeRabbit

Summary of changes

  • New Features
    • Added contract regression coverage to prevent cross-project leakage for issue activity and comments.
  • Refactor
    • Reworked module list/detail data loading to use consistent issue-count annotations across views.
    • Centralized project-scoped permission filtering for issue-related endpoints using more reliable membership checks.
  • Bug Fixes
    • Strengthened project membership enforcement for state access and issue activity/comment visibility.
    • Improved issue view filtering to avoid duplicate results while preserving existing access rules.

p11o added 2 commits June 19, 2026 16:26
…yset

Filtering via project__project_projectmember caused a JOIN that
multiplied rows, requiring a DISTINCT to deduplicate. Replace with
an Exists() correlated subquery on ProjectMember, which avoids the
row inflation entirely and makes DISTINCT unnecessary.
…dule queryset

The five state-group counts used Count("issue_module__issue__state__group",
distinct=True) which traverses a 3-table join path per annotation and forces
the DB to deduplicate the inflated result set. Replace with Coalesce(Subquery)
per the pattern already established in sub_issue.py (PR makeplane#8889).

Also extract the duplicated get_queryset body from ModuleListCreateAPIEndpoint,
ModuleDetailAPIEndpoint, and ModuleArchiveUnarchiveAPIEndpoint into shared
helper functions to eliminate the copy-paste.
@CLAassistant

CLAassistant commented Jun 19, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a7fdbe08-e09c-402d-be69-93cc5633d854

📥 Commits

Reviewing files that changed from the base of the PR and between e41ad2d and 23c8d0c.

📒 Files selected for processing (2)
  • apps/api/plane/app/views/issue/activity.py
  • apps/api/plane/tests/contract/app/test_issue_activity.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/plane/app/views/issue/activity.py

📝 Walkthrough

Walkthrough

The PR centralizes module queryset construction into shared helpers and changes state, issue activity, and issue view query filtering to use Exists/OuterRef membership subqueries instead of join-based ProjectMember filtering.

Changes

Module queryset centralization

Layer / File(s) Summary
Issue-count subquery and base queryset helpers
apps/api/plane/api/views/module.py
Expands ORM imports for subquery/count construction and adds _module_issue_count_subquery(state_group=None) plus _build_module_queryset(slug, project_id, order_by="-created_at") with select/prefetch setup, issue-count annotations, and ordering.
Endpoint get_queryset delegation
apps/api/plane/api/views/module.py
ModuleListCreateAPIEndpoint, ModuleDetailAPIEndpoint, and ModuleArchiveUnarchiveAPIEndpoint now call _build_module_queryset; the archive endpoint still filters archived modules afterward.

Project membership Exists filters

Layer / File(s) Summary
ProjectMember Exists subquery in State endpoints
apps/api/plane/api/views/state.py
Adds Exists, OuterRef, and ProjectMember imports, then rewrites both state endpoint querysets to filter membership with filter(Exists(member_check)) keyed by project_id and user membership.
Issue activity and view permission filters
apps/api/plane/app/views/issue/activity.py, apps/api/plane/app/views/view/base.py
Adds Exists, OuterRef, and ProjectMember imports where needed, then applies membership checks in IssueActivityEndpoint.get and IssueViewViewSet.get_queryset using subqueries instead of joined member paths; the issue view queryset keeps its archived-project exclusion, access predicate, favorite annotation, ordering, and no longer calls distinct().

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through queries, swift and bright,
With subqueries tucked in left and right.
No joins to tangle, no distinct() fog—
Just clean little checks in the rabbit log.
The clover grows in orderly rows 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change as API performance improvements for states and modules endpoints.
Description check ✅ Passed The description follows the template and includes a detailed summary, change type, test results, and references.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/api/plane/api/views/state.py (1)

172-186: 💤 Low value

Consider extracting a shared helper to reduce duplication.

The get_queryset implementations in StateListCreateAPIEndpoint (lines 49-61) and StateDetailAPIEndpoint (lines 173-185) are identical. Following the same pattern applied in module.py where _build_module_queryset was extracted, a shared helper could consolidate this logic.

♻️ Optional helper extraction
def _build_state_queryset(slug, project_id, user):
    """Build the base State queryset with membership check."""
    member_check = ProjectMember.objects.filter(
        project_id=OuterRef("project_id"),
        member=user,
        is_active=True,
        deleted_at__isnull=True,
    )
    return (
        State.objects.filter(workspace__slug=slug)
        .filter(project_id=project_id)
        .filter(Exists(member_check))
        .filter(is_triage=False)
        .filter(project__archived_at__isnull=True)
        .select_related("project", "workspace")
    )

Then both endpoints can use:

def get_queryset(self):
    return _build_state_queryset(
        self.kwargs.get("slug"),
        self.kwargs.get("project_id"),
        self.request.user,
    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/api/views/state.py` around lines 172 - 186, Extract the
duplicated queryset building logic from both StateListCreateAPIEndpoint and
StateDetailAPIEndpoint get_queryset methods into a shared helper function called
_build_state_queryset. This helper should accept slug, project_id, and user as
parameters and contain the ProjectMember membership check and all the filtering
logic that builds the State queryset. Then update both get_queryset methods in
StateListCreateAPIEndpoint and StateDetailAPIEndpoint to call this new helper
function instead of duplicating the entire queryset construction logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/api/plane/api/views/state.py`:
- Around line 172-186: Extract the duplicated queryset building logic from both
StateListCreateAPIEndpoint and StateDetailAPIEndpoint get_queryset methods into
a shared helper function called _build_state_queryset. This helper should accept
slug, project_id, and user as parameters and contain the ProjectMember
membership check and all the filtering logic that builds the State queryset.
Then update both get_queryset methods in StateListCreateAPIEndpoint and
StateDetailAPIEndpoint to call this new helper function instead of duplicating
the entire queryset construction logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 191d05e1-ca58-45fa-a3f1-de10936682ea

📥 Commits

Reviewing files that changed from the base of the PR and between 53a323d and 21ec42d.

📒 Files selected for processing (2)
  • apps/api/plane/api/views/module.py
  • apps/api/plane/api/views/state.py

CI requires 80% docstring coverage. Added one-line docstrings to all
functions introduced or modified by the perf fixes: _build_module_queryset,
and the get_queryset methods on ModuleListCreateAPIEndpoint,
ModuleDetailAPIEndpoint, ModuleArchiveUnarchiveAPIEndpoint, and both
State endpoint classes.
@p11o p11o changed the title perf(api): fix slow project config endpoints — states and modules perf(api): improve performance on states and modules endpoints Jun 19, 2026
…ctivity querysets

Eliminates row-inflating JOINs through project_projectmember in
IssueViewViewSet and IssueActivityEndpoint, replacing them with
correlated Exists() subqueries. Removes the forced .distinct() from
the views queryset.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/plane/app/views/issue/activity.py`:
- Around line 35-59: The membership filter in the issue activity query is only
tied to the URL project_id, while IssueActivity and IssueComment rows are not
constrained to that same project, which can allow cross-project reads. Update
the queries in the activity view to bind returned rows to the same project as
member_check by adding a project-level constraint on IssueActivity and
IssueComment (using the existing issue_id/slug filters plus the project
identifier from the request), so the membership check and returned data both
refer to the same project. Use the existing symbols member_check,
issue_activities, issue_comments, and the surrounding filter/Exists chain to
apply the fix consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5001af35-eb6a-45b5-8849-4688fac4724d

📥 Commits

Reviewing files that changed from the base of the PR and between a58d466 and e41ad2d.

📒 Files selected for processing (2)
  • apps/api/plane/app/views/issue/activity.py
  • apps/api/plane/app/views/view/base.py

Comment thread apps/api/plane/app/views/issue/activity.py
…tyEndpoint

Binds IssueActivity and IssueComment queries to project_id from the URL
and uses OuterRef("project_id") in the membership Exists() subquery, so
membership in the URL project no longer grants read access to rows from
a different project sharing the same issue_id.

Adds regression tests covering both the activity and comment branches.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants