Skip to content

Commit a3b2b54

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 eb498d1 commit a3b2b54

6 files changed

Lines changed: 343 additions & 46 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: 128 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
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

10+
type QueueListEngine = Pick<RunEngine, "lengthOfQueues" | "currentConcurrencyOfQueues">;
11+
812
const DEFAULT_ITEMS_PER_PAGE = 25;
913
const MAX_ITEMS_PER_PAGE = 100;
1014

@@ -13,12 +17,54 @@ const typeToDBQueueType: Record<"task" | "custom", TaskQueueType> = {
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+
function buildQueueListWhere(
36+
environmentId: string,
37+
query: string | undefined,
38+
type: "task" | "custom" | undefined
39+
): Prisma.TaskQueueWhereInput {
40+
const trimmedQuery = query?.trim();
41+
42+
return {
43+
runtimeEnvironmentId: environmentId,
44+
version: "V2",
45+
name: trimmedQuery
46+
? {
47+
contains: trimmedQuery,
48+
mode: "insensitive",
49+
}
50+
: undefined,
51+
type: type ? typeToDBQueueType[type] : undefined,
52+
};
53+
}
54+
1655
export class QueueListPresenter extends BasePresenter {
1756
private readonly perPage: number;
57+
private readonly engineClient: QueueListEngine;
1858

19-
constructor(perPage: number = DEFAULT_ITEMS_PER_PAGE) {
20-
super();
59+
constructor(
60+
perPage: number = DEFAULT_ITEMS_PER_PAGE,
61+
prismaClient?: PrismaClientOrTransaction,
62+
replicaClient?: PrismaClientOrTransaction,
63+
engineClient: QueueListEngine = engine
64+
) {
65+
super(prismaClient, replicaClient);
2166
this.perPage = Math.min(perPage, MAX_ITEMS_PER_PAGE);
67+
this.engineClient = engineClient;
2268
}
2369

2470
public async call({
@@ -33,26 +79,14 @@ export class QueueListPresenter extends BasePresenter {
3379
perPage?: number;
3480
type?: "task" | "custom";
3581
}) {
36-
const hasFilters = (query !== undefined && query.length > 0) || type !== undefined;
82+
const hasFilters = Boolean(query?.trim()) || type !== undefined;
3783

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-
});
52-
53-
//check the engine is the correct version
5484
const engineVersion = await determineEngineVersion({ environment });
5585
if (engineVersion === "V1") {
86+
const totalQueues = await this._replica.taskQueue.count({
87+
where: buildQueueListWhere(environment.id, query, type),
88+
});
89+
5690
if (totalQueues === 0) {
5791
const oldQueue = await this._replica.taskQueue.findFirst({
5892
where: {
@@ -78,10 +112,30 @@ export class QueueListPresenter extends BasePresenter {
78112
};
79113
}
80114

115+
if (hasFilters) {
116+
const { queues, hasMore } = await this.getFilteredQueues(environment, query, page, type);
117+
118+
return {
119+
success: true as const,
120+
queues,
121+
pagination: {
122+
mode: "filtered" as const,
123+
currentPage: page,
124+
hasMore,
125+
},
126+
hasFilters,
127+
};
128+
}
129+
130+
const totalQueues = await this._replica.taskQueue.count({
131+
where: buildQueueListWhere(environment.id, query, type),
132+
});
133+
81134
return {
82135
success: true as const,
83-
queues: await this.getQueuesWithPagination(environment, query, page, type),
136+
queues: await this.getUnfilteredQueues(environment, page, type),
84137
pagination: {
138+
mode: "unfiltered" as const,
85139
currentPage: page,
86140
totalPages: Math.ceil(totalQueues / this.perPage),
87141
count: totalQueues,
@@ -91,24 +145,47 @@ export class QueueListPresenter extends BasePresenter {
91145
};
92146
}
93147

94-
private async getQueuesWithPagination(
148+
private async getFilteredQueues(
95149
environment: AuthenticatedEnvironment,
96150
query: string | undefined,
97151
page: number,
98152
type: "task" | "custom" | undefined
99153
) {
100154
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,
155+
where: buildQueueListWhere(environment.id, query, type),
156+
select: {
157+
friendlyId: true,
158+
name: true,
159+
orderableName: true,
160+
concurrencyLimit: true,
161+
concurrencyLimitBase: true,
162+
concurrencyLimitOverriddenAt: true,
163+
concurrencyLimitOverriddenBy: true,
164+
type: true,
165+
paused: true,
166+
},
167+
orderBy: {
168+
orderableName: "asc",
111169
},
170+
skip: (page - 1) * this.perPage,
171+
take: this.perPage + 1,
172+
});
173+
174+
const hasMore = queues.length > this.perPage;
175+
176+
return {
177+
queues: await this.enrichQueues(environment, queues.slice(0, this.perPage)),
178+
hasMore,
179+
};
180+
}
181+
182+
private async getUnfilteredQueues(
183+
environment: AuthenticatedEnvironment,
184+
page: number,
185+
type: "task" | "custom" | undefined
186+
) {
187+
const queues = await this._replica.taskQueue.findMany({
188+
where: buildQueueListWhere(environment.id, undefined, type),
112189
select: {
113190
friendlyId: true,
114191
name: true,
@@ -127,12 +204,29 @@ export class QueueListPresenter extends BasePresenter {
127204
take: this.perPage,
128205
});
129206

207+
return this.enrichQueues(environment, queues);
208+
}
209+
210+
private async enrichQueues(
211+
environment: AuthenticatedEnvironment,
212+
queues: {
213+
friendlyId: string;
214+
name: string;
215+
orderableName: string | null;
216+
concurrencyLimit: number | null;
217+
concurrencyLimitBase: number | null;
218+
concurrencyLimitOverriddenAt: Date | null;
219+
concurrencyLimitOverriddenBy: string | null;
220+
type: TaskQueueType;
221+
paused: boolean;
222+
}[]
223+
) {
130224
const results = await Promise.all([
131-
engine.lengthOfQueues(
225+
this.engineClient.lengthOfQueues(
132226
environment,
133227
queues.map((q) => q.name)
134228
),
135-
engine.currentConcurrencyOfQueues(
229+
this.engineClient.currentConcurrencyOfQueues(
136230
environment,
137231
queues.map((q) => q.name)
138232
),
@@ -149,7 +243,6 @@ export class QueueListPresenter extends BasePresenter {
149243

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

152-
// Transform queues to include running and queued counts
153246
return queues.map((queue) =>
154247
toQueueItem({
155248
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/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)