Skip to content

Commit 93c0634

Browse files
chandrasekharan-zipstackclaudekirtimanmishrazipstack
authored
[MISC] Optimize prompt studio list endpoint with lightweight serializer (#1901)
* [PERF] Optimize prompt studio list endpoint with lightweight serializer Reduce list endpoint queries from O(tools × (4 + prompts)) to ~1 by: - New CustomToolListSerializer skips profile lookups, prompt fetching, coverage calculation - Added get_serializer_class() to route list action to lightweight serializer - Optimized get_queryset() with select_related("created_by") + Subquery annotation for prompt_count Tested: 4.6x faster (0.057s vs 0.263s with 9 tools). No regressions on detail/retrieve/create/update/delete. Consumers verified: ListOfTools.jsx (OSS), LinkProjectModal.jsx (Cloud lookups). * Address review comments: warn on N+1 fallback, use Count("id") - Add logger.warning when _prompt_count annotation is missing to surface misuse instead of silently degrading to per-instance queries - Use Count("id") instead of Count("*") to match codebase convention Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Kirtiman Mishra <110175055+kirtimanmishrazipstack@users.noreply.github.com>
1 parent 28a206b commit 93c0634

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

backend/prompt_studio/prompt_studio_core_v2/serializers.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,47 @@
3434
from file_management.constants import FileInformationKey as FileKey
3535

3636

37+
class CustomToolListSerializer(serializers.ModelSerializer):
38+
"""Lightweight serializer for the list endpoint.
39+
40+
Avoids the O(tools x prompts) queries that CustomToolSerializer.to_representation
41+
causes by skipping profile lookups, prompt fetching, and coverage calculation.
42+
"""
43+
44+
created_by_email = serializers.SerializerMethodField()
45+
prompt_count = serializers.SerializerMethodField()
46+
47+
class Meta:
48+
model = CustomTool
49+
fields = [
50+
"tool_id",
51+
"tool_name",
52+
"description",
53+
"author",
54+
"created_by",
55+
"created_at",
56+
"modified_at",
57+
"shared_to_org",
58+
"icon",
59+
"created_by_email",
60+
"prompt_count",
61+
]
62+
63+
def get_created_by_email(self, instance):
64+
return instance.created_by.email if instance.created_by else ""
65+
66+
def get_prompt_count(self, instance):
67+
if hasattr(instance, "_prompt_count"):
68+
return instance._prompt_count or 0
69+
# Fallback triggers a per-instance query if annotation is missing
70+
logger.warning(
71+
"CustomToolListSerializer used without _prompt_count annotation "
72+
"for tool %s — falling back to per-instance query",
73+
instance.tool_id,
74+
)
75+
return instance.mapped_prompt.count()
76+
77+
3778
class CustomToolSerializer(IntegrityErrorMixin, AuditSerializer):
3879
shared_users = serializers.PrimaryKeyRelatedField(
3980
queryset=User.objects.filter(is_service_account=False),

backend/prompt_studio/prompt_studio_core_v2/views.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from celery import signature
1313
from celery.result import AsyncResult
1414
from django.db import IntegrityError
15-
from django.db.models import QuerySet
15+
from django.db.models import Count, OuterRef, QuerySet, Subquery
1616
from django.http import HttpRequest, HttpResponse
1717
from file_management.constants import FileInformationKey as FileKey
1818
from file_management.exceptions import FileNotFound
@@ -77,6 +77,7 @@
7777

7878
from .models import CustomTool
7979
from .serializers import (
80+
CustomToolListSerializer,
8081
CustomToolSerializer,
8182
FileInfoIdeSerializer,
8283
FileUploadIdeSerializer,
@@ -95,14 +96,32 @@ class PromptStudioCoreView(viewsets.ModelViewSet):
9596

9697
serializer_class = CustomToolSerializer
9798

99+
def get_serializer_class(self):
100+
if self.action == "list":
101+
return CustomToolListSerializer
102+
return CustomToolSerializer
103+
98104
def get_permissions(self) -> list[Any]:
99105
if self.action == "destroy":
100106
return [IsOwner()]
101107

102108
return [IsOwnerOrSharedUserOrSharedToOrg()]
103109

104110
def get_queryset(self) -> QuerySet | None:
105-
return CustomTool.objects.for_user(self.request.user)
111+
qs = CustomTool.objects.for_user(self.request.user)
112+
if self.action == "list":
113+
# Subquery avoids conflict with distinct("tool_id") from for_user()
114+
prompt_count_sq = (
115+
ToolStudioPrompt.objects.filter(tool_id=OuterRef("pk"))
116+
.order_by()
117+
.values("tool_id")
118+
.annotate(cnt=Count("id"))
119+
.values("cnt")
120+
)
121+
qs = qs.select_related("created_by").annotate(
122+
_prompt_count=Subquery(prompt_count_sq)
123+
)
124+
return qs
106125

107126
def get_object(self):
108127
"""Override get_object to trigger lazy migration when accessing tools."""

0 commit comments

Comments
 (0)