Skip to content

Commit 697b310

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 697b310

7 files changed

Lines changed: 418 additions & 49 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: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,23 @@ import { LinkButton } from "./Buttons";
77
export function PaginationControls({
88
currentPage,
99
totalPages,
10+
hasNextPage,
1011
showPageNumbers = true,
1112
}: {
1213
currentPage: number;
1314
totalPages: number;
15+
/** When set, Next/visibility use this instead of totalPages (filtered lists without a total count). */
16+
hasNextPage?: boolean;
1417
showPageNumbers?: boolean;
1518
}) {
1619
const location = useLocation();
17-
if (totalPages <= 1) {
20+
const isFilteredMode = hasNextPage !== undefined;
21+
const showPagination = isFilteredMode
22+
? currentPage > 1 || hasNextPage
23+
: totalPages > 1;
24+
const nextDisabled = isFilteredMode ? !hasNextPage : currentPage === totalPages;
25+
26+
if (!showPagination) {
1827
return null;
1928
}
2029

@@ -42,8 +51,8 @@ export function PaginationControls({
4251
TrailingIcon={ChevronRightIcon}
4352
shortcut={{ key: "k" }}
4453
tooltip="Next"
45-
disabled={currentPage === totalPages}
46-
className={cn("px-2", currentPage !== totalPages ? "group" : "")}
54+
disabled={nextDisabled}
55+
className={cn("px-2", !nextDisabled ? "group" : "")}
4756
/>
4857
</>
4958
) : (
@@ -66,23 +75,21 @@ export function PaginationControls({
6675
<div
6776
className={cn(
6877
"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"
78+
currentPage === 1 && nextDisabled && "opacity-30"
7079
)}
7180
/>
7281

73-
<div
74-
className={cn("peer/next order-3", currentPage === totalPages && "pointer-events-none")}
75-
>
82+
<div className={cn("peer/next order-3", nextDisabled && "pointer-events-none")}>
7683
<LinkButton
7784
to={pageUrl(location, currentPage + 1)}
7885
variant="secondary/small"
7986
TrailingIcon={ChevronRightIcon}
8087
shortcut={{ key: "k" }}
8188
tooltip="Next"
82-
disabled={currentPage === totalPages}
89+
disabled={nextDisabled}
8390
className={cn(
8491
"flex items-center rounded-l-none border-l-0 pl-[0.5625rem] pr-2",
85-
currentPage === totalPages && "cursor-not-allowed opacity-50"
92+
nextDisabled && "cursor-not-allowed opacity-50"
8693
)}
8794
/>
8895
</div>

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

Lines changed: 158 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,99 @@
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+
function buildQueueListWhere(
65+
environmentId: string,
66+
query: string | undefined,
67+
type: "task" | "custom" | undefined
68+
): Prisma.TaskQueueWhereInput {
69+
const trimmedQuery = query?.trim();
70+
71+
return {
72+
runtimeEnvironmentId: environmentId,
73+
version: "V2",
74+
name: trimmedQuery
75+
? {
76+
contains: trimmedQuery,
77+
mode: "insensitive",
78+
}
79+
: undefined,
80+
type: type ? typeToDBQueueType[type] : undefined,
81+
};
82+
}
83+
1684
export class QueueListPresenter extends BasePresenter {
1785
private readonly perPage: number;
86+
private readonly engineClient: QueueListEngine;
1887

19-
constructor(perPage: number = DEFAULT_ITEMS_PER_PAGE) {
20-
super();
88+
constructor(
89+
perPage: number = QUEUE_LIST_DEFAULT_ITEMS_PER_PAGE,
90+
prismaClient?: PrismaClientOrTransaction,
91+
replicaClient?: PrismaClientOrTransaction,
92+
engineClient: QueueListEngine = engine
93+
) {
94+
super(prismaClient, replicaClient);
2195
this.perPage = Math.min(perPage, MAX_ITEMS_PER_PAGE);
96+
this.engineClient = engineClient;
2297
}
2398

2499
public async call({
@@ -33,26 +108,14 @@ export class QueueListPresenter extends BasePresenter {
33108
perPage?: number;
34109
type?: "task" | "custom";
35110
}) {
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-
});
111+
const hasFilters = Boolean(query?.trim()) || type !== undefined;
52112

53-
//check the engine is the correct version
54113
const engineVersion = await determineEngineVersion({ environment });
55114
if (engineVersion === "V1") {
115+
const totalQueues = await this._replica.taskQueue.count({
116+
where: buildQueueListWhere(environment.id, query, type),
117+
});
118+
56119
if (totalQueues === 0) {
57120
const oldQueue = await this._replica.taskQueue.findFirst({
58121
where: {
@@ -78,10 +141,30 @@ export class QueueListPresenter extends BasePresenter {
78141
};
79142
}
80143

144+
if (hasFilters) {
145+
const { queues, hasMore } = await this.getFilteredQueues(environment, query, page, type);
146+
147+
return {
148+
success: true as const,
149+
queues,
150+
pagination: {
151+
mode: "filtered" as const,
152+
currentPage: page,
153+
hasMore,
154+
},
155+
hasFilters,
156+
};
157+
}
158+
159+
const totalQueues = await this._replica.taskQueue.count({
160+
where: buildQueueListWhere(environment.id, query, type),
161+
});
162+
81163
return {
82164
success: true as const,
83-
queues: await this.getQueuesWithPagination(environment, query, page, type),
165+
queues: await this.getUnfilteredQueues(environment, page, type),
84166
pagination: {
167+
mode: "unfiltered" as const,
85168
currentPage: page,
86169
totalPages: Math.ceil(totalQueues / this.perPage),
87170
count: totalQueues,
@@ -91,24 +174,47 @@ export class QueueListPresenter extends BasePresenter {
91174
};
92175
}
93176

94-
private async getQueuesWithPagination(
177+
private async getFilteredQueues(
95178
environment: AuthenticatedEnvironment,
96179
query: string | undefined,
97180
page: number,
98181
type: "task" | "custom" | undefined
99182
) {
100183
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,
184+
where: buildQueueListWhere(environment.id, query, type),
185+
select: {
186+
friendlyId: true,
187+
name: true,
188+
orderableName: true,
189+
concurrencyLimit: true,
190+
concurrencyLimitBase: true,
191+
concurrencyLimitOverriddenAt: true,
192+
concurrencyLimitOverriddenBy: true,
193+
type: true,
194+
paused: true,
111195
},
196+
orderBy: {
197+
orderableName: "asc",
198+
},
199+
skip: (page - 1) * this.perPage,
200+
take: this.perPage + 1,
201+
});
202+
203+
const hasMore = queues.length > this.perPage;
204+
205+
return {
206+
queues: await this.enrichQueues(environment, queues.slice(0, this.perPage)),
207+
hasMore,
208+
};
209+
}
210+
211+
private async getUnfilteredQueues(
212+
environment: AuthenticatedEnvironment,
213+
page: number,
214+
type: "task" | "custom" | undefined
215+
) {
216+
const queues = await this._replica.taskQueue.findMany({
217+
where: buildQueueListWhere(environment.id, undefined, type),
112218
select: {
113219
friendlyId: true,
114220
name: true,
@@ -127,12 +233,29 @@ export class QueueListPresenter extends BasePresenter {
127233
take: this.perPage,
128234
});
129235

236+
return this.enrichQueues(environment, queues);
237+
}
238+
239+
private async enrichQueues(
240+
environment: AuthenticatedEnvironment,
241+
queues: {
242+
friendlyId: string;
243+
name: string;
244+
orderableName: string | null;
245+
concurrencyLimit: number | null;
246+
concurrencyLimitBase: number | null;
247+
concurrencyLimitOverriddenAt: Date | null;
248+
concurrencyLimitOverriddenBy: string | null;
249+
type: TaskQueueType;
250+
paused: boolean;
251+
}[]
252+
) {
130253
const results = await Promise.all([
131-
engine.lengthOfQueues(
254+
this.engineClient.lengthOfQueues(
132255
environment,
133256
queues.map((q) => q.name)
134257
),
135-
engine.currentConcurrencyOfQueues(
258+
this.engineClient.currentConcurrencyOfQueues(
136259
environment,
137260
queues.map((q) => q.name)
138261
),
@@ -149,7 +272,6 @@ export class QueueListPresenter extends BasePresenter {
149272

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

152-
// Transform queues to include running and queued counts
153275
return queues.map((queue) =>
154276
toQueueItem({
155277
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 });

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
6464
paused: queue.paused,
6565
})),
6666
currentPage: result.pagination.currentPage,
67-
hasMore: result.pagination.currentPage < result.pagination.totalPages,
67+
hasMore: result.pagination.mode === "filtered" ? result.pagination.hasMore : false,
6868
hasFilters: result.hasFilters,
6969
};
7070
}

0 commit comments

Comments
 (0)