Skip to content

Commit 481b458

Browse files
committed
perf(webapp): skip queue search count
Skip the count query when filtering queues and paginate search results with hasMore instead.
1 parent 8b40571 commit 481b458

7 files changed

Lines changed: 236 additions & 60 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Speed up queue search by skipping count on filtered queries and using hasMore pagination

apps/webapp/app/components/primitives/Pagination.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,27 @@ import { Link, useLocation } from "@remix-run/react";
44
import { cn } from "~/utils/cn";
55
import { LinkButton } from "./Buttons";
66

7+
/** Pass `hasNextPage` for filtered lists without a total count; use `showPageNumbers={false}` in that mode. */
78
export function PaginationControls({
89
currentPage,
910
totalPages,
11+
hasNextPage,
1012
showPageNumbers = true,
1113
}: {
1214
currentPage: number;
1315
totalPages: number;
16+
/** When set, next/prev use this instead of `totalPages`. */
17+
hasNextPage?: boolean;
1418
showPageNumbers?: boolean;
1519
}) {
1620
const location = useLocation();
17-
if (totalPages <= 1) {
21+
const isFilteredMode = hasNextPage !== undefined;
22+
const showPagination = isFilteredMode
23+
? currentPage > 1 || hasNextPage
24+
: totalPages > 1;
25+
const nextDisabled = isFilteredMode ? !hasNextPage : currentPage === totalPages;
26+
27+
if (!showPagination) {
1828
return null;
1929
}
2030

@@ -42,8 +52,8 @@ export function PaginationControls({
4252
TrailingIcon={ChevronRightIcon}
4353
shortcut={{ key: "k" }}
4454
tooltip="Next"
45-
disabled={currentPage === totalPages}
46-
className={cn("px-2", currentPage !== totalPages ? "group" : "")}
55+
disabled={nextDisabled}
56+
className={cn("px-2", !nextDisabled ? "group" : "")}
4757
/>
4858
</>
4959
) : (
@@ -66,23 +76,21 @@ export function PaginationControls({
6676
<div
6777
className={cn(
6878
"order-2 h-6 w-px bg-charcoal-600 transition-colors peer-hover/next:bg-charcoal-550 peer-hover/prev:bg-charcoal-550",
69-
currentPage === 1 && currentPage === totalPages && "opacity-30"
79+
currentPage === 1 && nextDisabled && "opacity-30"
7080
)}
7181
/>
7282

73-
<div
74-
className={cn("peer/next order-3", currentPage === totalPages && "pointer-events-none")}
75-
>
83+
<div className={cn("peer/next order-3", nextDisabled && "pointer-events-none")}>
7684
<LinkButton
7785
to={pageUrl(location, currentPage + 1)}
7886
variant="secondary/small"
7987
TrailingIcon={ChevronRightIcon}
8088
shortcut={{ key: "k" }}
8189
tooltip="Next"
82-
disabled={currentPage === totalPages}
90+
disabled={nextDisabled}
8391
className={cn(
8492
"flex items-center rounded-l-none border-l-0 pl-[0.5625rem] pr-2",
85-
currentPage === totalPages && "cursor-not-allowed opacity-50"
93+
nextDisabled && "cursor-not-allowed opacity-50"
8694
)}
8795
/>
8896
</div>

apps/webapp/app/presenters/v3/QueueListPresenter.server.ts

Lines changed: 161 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,111 @@
1-
import { TaskQueueType } from "@trigger.dev/database";
1+
import type { RunEngine } from "@internal/run-engine";
2+
import { Prisma, TaskQueueType } from "@trigger.dev/database";
3+
import { type PrismaClientOrTransaction } from "~/db.server";
24
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
35
import { determineEngineVersion } from "~/v3/engineVersion.server";
46
import { engine } from "~/v3/runEngine.server";
57
import { BasePresenter } from "./basePresenter.server";
68
import { toQueueItem } from "./QueueRetrievePresenter.server";
79

8-
const DEFAULT_ITEMS_PER_PAGE = 25;
10+
type QueueListEngine = Pick<RunEngine, "lengthOfQueues" | "currentConcurrencyOfQueues">;
11+
12+
export const QUEUE_LIST_DEFAULT_ITEMS_PER_PAGE = 25;
913
const MAX_ITEMS_PER_PAGE = 100;
1014

1115
const typeToDBQueueType: Record<"task" | "custom", TaskQueueType> = {
1216
task: TaskQueueType.VIRTUAL,
1317
custom: TaskQueueType.NAMED,
1418
};
1519

20+
export type QueueListFilteredPagination = {
21+
mode: "filtered";
22+
currentPage: number;
23+
hasMore: boolean;
24+
};
25+
26+
export type QueueListUnfilteredPagination = {
27+
mode: "unfiltered";
28+
currentPage: number;
29+
totalPages: number;
30+
count: number;
31+
};
32+
33+
export type QueueListPagination = QueueListFilteredPagination | QueueListUnfilteredPagination;
34+
35+
export type OffsetLimitPagination = {
36+
currentPage: number;
37+
totalPages: number;
38+
count: number;
39+
};
40+
41+
/** Maps presenter pagination to the public API / SDK offset-limit contract. */
42+
export function toOffsetLimitQueueListPagination(
43+
pagination: QueueListPagination,
44+
options: { itemsOnPage: number; perPage: number }
45+
): OffsetLimitPagination {
46+
if (pagination.mode === "unfiltered") {
47+
return {
48+
currentPage: pagination.currentPage,
49+
totalPages: pagination.totalPages,
50+
count: pagination.count,
51+
};
52+
}
53+
54+
return {
55+
currentPage: pagination.currentPage,
56+
totalPages: pagination.hasMore ? pagination.currentPage + 1 : pagination.currentPage,
57+
count:
58+
(pagination.currentPage - 1) * options.perPage +
59+
options.itemsOnPage +
60+
(pagination.hasMore ? 1 : 0),
61+
};
62+
}
63+
64+
const queueListSelect = {
65+
friendlyId: true,
66+
name: true,
67+
orderableName: true,
68+
concurrencyLimit: true,
69+
concurrencyLimitBase: true,
70+
concurrencyLimitOverriddenAt: true,
71+
concurrencyLimitOverriddenBy: true,
72+
type: true,
73+
paused: true,
74+
} satisfies Prisma.TaskQueueSelect;
75+
76+
function buildQueueListWhere(
77+
environmentId: string,
78+
query: string | undefined,
79+
type: "task" | "custom" | undefined
80+
): Prisma.TaskQueueWhereInput {
81+
const trimmedQuery = query?.trim();
82+
83+
return {
84+
runtimeEnvironmentId: environmentId,
85+
version: "V2",
86+
name: trimmedQuery
87+
? {
88+
contains: trimmedQuery,
89+
mode: "insensitive",
90+
}
91+
: undefined,
92+
type: type ? typeToDBQueueType[type] : undefined,
93+
};
94+
}
95+
1696
export class QueueListPresenter extends BasePresenter {
1797
private readonly perPage: number;
98+
private readonly engineClient: QueueListEngine;
1899

19-
constructor(perPage: number = DEFAULT_ITEMS_PER_PAGE) {
20-
super();
100+
constructor(
101+
perPage: number = QUEUE_LIST_DEFAULT_ITEMS_PER_PAGE,
102+
prismaClient?: PrismaClientOrTransaction,
103+
replicaClient?: PrismaClientOrTransaction,
104+
engineClient: QueueListEngine = engine
105+
) {
106+
super(prismaClient, replicaClient);
21107
this.perPage = Math.min(perPage, MAX_ITEMS_PER_PAGE);
108+
this.engineClient = engineClient;
22109
}
23110

24111
public async call({
@@ -33,26 +120,14 @@ export class QueueListPresenter extends BasePresenter {
33120
perPage?: number;
34121
type?: "task" | "custom";
35122
}) {
36-
const hasFilters = (query !== undefined && query.length > 0) || type !== undefined;
37-
38-
// Get total count for pagination
39-
const totalQueues = await this._replica.taskQueue.count({
40-
where: {
41-
runtimeEnvironmentId: environment.id,
42-
version: "V2",
43-
name: query
44-
? {
45-
contains: query,
46-
mode: "insensitive",
47-
}
48-
: undefined,
49-
type: type ? typeToDBQueueType[type] : undefined,
50-
},
51-
});
123+
const hasFilters = Boolean(query?.trim()) || type !== undefined;
52124

53-
//check the engine is the correct version
54125
const engineVersion = await determineEngineVersion({ environment });
55126
if (engineVersion === "V1") {
127+
const totalQueues = await this._replica.taskQueue.count({
128+
where: buildQueueListWhere(environment.id, query, type),
129+
});
130+
56131
if (totalQueues === 0) {
57132
const oldQueue = await this._replica.taskQueue.findFirst({
58133
where: {
@@ -78,10 +153,30 @@ export class QueueListPresenter extends BasePresenter {
78153
};
79154
}
80155

156+
if (hasFilters) {
157+
const { queues, hasMore } = await this.getFilteredQueues(environment, query, page, type);
158+
159+
return {
160+
success: true as const,
161+
queues,
162+
pagination: {
163+
mode: "filtered" as const,
164+
currentPage: page,
165+
hasMore,
166+
},
167+
hasFilters,
168+
};
169+
}
170+
171+
const totalQueues = await this._replica.taskQueue.count({
172+
where: buildQueueListWhere(environment.id, query, type),
173+
});
174+
81175
return {
82176
success: true as const,
83-
queues: await this.getQueuesWithPagination(environment, query, page, type),
177+
queues: await this.getUnfilteredQueues(environment, page, type),
84178
pagination: {
179+
mode: "unfiltered" as const,
85180
currentPage: page,
86181
totalPages: Math.ceil(totalQueues / this.perPage),
87182
count: totalQueues,
@@ -91,48 +186,68 @@ export class QueueListPresenter extends BasePresenter {
91186
};
92187
}
93188

94-
private async getQueuesWithPagination(
189+
private async getFilteredQueues(
95190
environment: AuthenticatedEnvironment,
96191
query: string | undefined,
97192
page: number,
98193
type: "task" | "custom" | undefined
99194
) {
100195
const queues = await this._replica.taskQueue.findMany({
101-
where: {
102-
runtimeEnvironmentId: environment.id,
103-
version: "V2",
104-
name: query
105-
? {
106-
contains: query,
107-
mode: "insensitive",
108-
}
109-
: undefined,
110-
type: type ? typeToDBQueueType[type] : undefined,
111-
},
112-
select: {
113-
friendlyId: true,
114-
name: true,
115-
orderableName: true,
116-
concurrencyLimit: true,
117-
concurrencyLimitBase: true,
118-
concurrencyLimitOverriddenAt: true,
119-
concurrencyLimitOverriddenBy: true,
120-
type: true,
121-
paused: true,
196+
where: buildQueueListWhere(environment.id, query, type),
197+
select: queueListSelect,
198+
orderBy: {
199+
orderableName: "asc",
122200
},
201+
skip: (page - 1) * this.perPage,
202+
take: this.perPage + 1,
203+
});
204+
205+
const hasMore = queues.length > this.perPage;
206+
207+
return {
208+
queues: await this.enrichQueues(environment, queues.slice(0, this.perPage)),
209+
hasMore,
210+
};
211+
}
212+
213+
private async getUnfilteredQueues(
214+
environment: AuthenticatedEnvironment,
215+
page: number,
216+
type: "task" | "custom" | undefined
217+
) {
218+
const queues = await this._replica.taskQueue.findMany({
219+
where: buildQueueListWhere(environment.id, undefined, type),
220+
select: queueListSelect,
123221
orderBy: {
124222
orderableName: "asc",
125223
},
126224
skip: (page - 1) * this.perPage,
127225
take: this.perPage,
128226
});
129227

228+
return this.enrichQueues(environment, queues);
229+
}
230+
231+
private async enrichQueues(
232+
environment: AuthenticatedEnvironment,
233+
queues: {
234+
friendlyId: string;
235+
name: string;
236+
orderableName: string | null;
237+
concurrencyLimit: number | null;
238+
concurrencyLimitBase: number | null;
239+
concurrencyLimitOverriddenAt: Date | null;
240+
concurrencyLimitOverriddenBy: string | null;
241+
type: TaskQueueType;
242+
paused: boolean;
243+
}[]
244+
) {
130245
const results = await Promise.all([
131-
engine.lengthOfQueues(
246+
this.engineClient.lengthOfQueues(
132247
environment,
133248
queues.map((q) => q.name)
134249
),
135-
engine.currentConcurrencyOfQueues(
250+
this.engineClient.currentConcurrencyOfQueues(
136251
environment,
137252
queues.map((q) => q.name)
138253
),
@@ -149,7 +264,6 @@ export class QueueListPresenter extends BasePresenter {
149264

150265
const overriddenByMap = new Map(overriddenByUsers.map((u) => [u.id, u]));
151266

152-
// Transform queues to include running and queued counts
153267
return queues.map((queue) =>
154268
toQueueItem({
155269
friendlyId: queue.friendlyId,

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,10 @@ export default function Page() {
440440
<QueueFilters />
441441
<PaginationControls
442442
currentPage={pagination.currentPage}
443-
totalPages={pagination.totalPages}
443+
totalPages={pagination.mode === "unfiltered" ? pagination.totalPages : 1}
444+
hasNextPage={
445+
pagination.mode === "filtered" ? pagination.hasMore : undefined
446+
}
444447
showPageNumbers={false}
445448
/>
446449
</div>

apps/webapp/app/routes/api.v1.queues.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { json } from "@remix-run/server-runtime";
22
import { type QueueItem } from "@trigger.dev/core/v3";
33
import { z } from "zod";
4-
import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server";
4+
import {
5+
QUEUE_LIST_DEFAULT_ITEMS_PER_PAGE,
6+
QueueListPresenter,
7+
toOffsetLimitQueueListPagination,
8+
} from "~/presenters/v3/QueueListPresenter.server";
59
import { logger } from "~/services/logger.server";
610
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
711
import { ServiceValidationError } from "~/v3/services/baseService.server";
@@ -30,7 +34,16 @@ export const loader = createLoaderApiRoute(
3034
}
3135

3236
const queues: QueueItem[] = result.queues;
33-
return json({ data: queues, pagination: result.pagination }, { status: 200 });
37+
return json(
38+
{
39+
data: queues,
40+
pagination: toOffsetLimitQueueListPagination(result.pagination, {
41+
itemsOnPage: queues.length,
42+
perPage: searchParams.perPage ?? QUEUE_LIST_DEFAULT_ITEMS_PER_PAGE,
43+
}),
44+
},
45+
{ status: 200 }
46+
);
3447
} catch (error) {
3548
if (error instanceof ServiceValidationError) {
3649
return json({ error: error.message }, { status: 422 });

0 commit comments

Comments
 (0)