Skip to content

Commit af14b37

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 034058b commit af14b37

8 files changed

Lines changed: 246 additions & 63 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: 121 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,68 @@
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";
9+
import type { QueueListPagination } from "./queueListPagination.server";
710

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

1116
const typeToDBQueueType: Record<"task" | "custom", TaskQueueType> = {
1217
task: TaskQueueType.VIRTUAL,
1318
custom: TaskQueueType.NAMED,
1419
};
1520

21+
const queueListSelect = {
22+
friendlyId: true,
23+
name: true,
24+
orderableName: true,
25+
concurrencyLimit: true,
26+
concurrencyLimitBase: true,
27+
concurrencyLimitOverriddenAt: true,
28+
concurrencyLimitOverriddenBy: true,
29+
type: true,
30+
paused: true,
31+
} satisfies Prisma.TaskQueueSelect;
32+
33+
function buildQueueListWhere(
34+
environmentId: string,
35+
query: string | undefined,
36+
type: "task" | "custom" | undefined
37+
): Prisma.TaskQueueWhereInput {
38+
const trimmedQuery = query?.trim();
39+
40+
return {
41+
runtimeEnvironmentId: environmentId,
42+
version: "V2",
43+
name: trimmedQuery
44+
? {
45+
contains: trimmedQuery,
46+
mode: "insensitive",
47+
}
48+
: undefined,
49+
type: type ? typeToDBQueueType[type] : undefined,
50+
};
51+
}
52+
1653
export class QueueListPresenter extends BasePresenter {
1754
private readonly perPage: number;
55+
private readonly engineClient: QueueListEngine;
1856

19-
constructor(perPage: number = DEFAULT_ITEMS_PER_PAGE) {
20-
super();
57+
constructor(
58+
perPage: number = QUEUE_LIST_DEFAULT_ITEMS_PER_PAGE,
59+
prismaClient?: PrismaClientOrTransaction,
60+
replicaClient?: PrismaClientOrTransaction,
61+
engineClient: QueueListEngine = engine
62+
) {
63+
super(prismaClient, replicaClient);
2164
this.perPage = Math.min(perPage, MAX_ITEMS_PER_PAGE);
65+
this.engineClient = engineClient;
2266
}
2367

2468
public async call({
@@ -33,26 +77,14 @@ export class QueueListPresenter extends BasePresenter {
3377
perPage?: number;
3478
type?: "task" | "custom";
3579
}) {
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-
});
80+
const hasFilters = Boolean(query?.trim()) || type !== undefined;
5281

53-
//check the engine is the correct version
5482
const engineVersion = await determineEngineVersion({ environment });
5583
if (engineVersion === "V1") {
84+
const totalQueues = await this._replica.taskQueue.count({
85+
where: buildQueueListWhere(environment.id, query, type),
86+
});
87+
5688
if (totalQueues === 0) {
5789
const oldQueue = await this._replica.taskQueue.findFirst({
5890
where: {
@@ -78,10 +110,30 @@ export class QueueListPresenter extends BasePresenter {
78110
};
79111
}
80112

113+
if (hasFilters) {
114+
const { queues, hasMore } = await this.getFilteredQueues(environment, query, page, type);
115+
116+
return {
117+
success: true as const,
118+
queues,
119+
pagination: {
120+
mode: "filtered" as const,
121+
currentPage: page,
122+
hasMore,
123+
},
124+
hasFilters,
125+
};
126+
}
127+
128+
const totalQueues = await this._replica.taskQueue.count({
129+
where: buildQueueListWhere(environment.id, query, type),
130+
});
131+
81132
return {
82133
success: true as const,
83-
queues: await this.getQueuesWithPagination(environment, query, page, type),
134+
queues: await this.getUnfilteredQueues(environment, page, type),
84135
pagination: {
136+
mode: "unfiltered" as const,
85137
currentPage: page,
86138
totalPages: Math.ceil(totalQueues / this.perPage),
87139
count: totalQueues,
@@ -91,48 +143,68 @@ export class QueueListPresenter extends BasePresenter {
91143
};
92144
}
93145

94-
private async getQueuesWithPagination(
146+
private async getFilteredQueues(
95147
environment: AuthenticatedEnvironment,
96148
query: string | undefined,
97149
page: number,
98150
type: "task" | "custom" | undefined
99151
) {
100152
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,
153+
where: buildQueueListWhere(environment.id, query, type),
154+
select: queueListSelect,
155+
orderBy: {
156+
orderableName: "asc",
122157
},
158+
skip: (page - 1) * this.perPage,
159+
take: this.perPage + 1,
160+
});
161+
162+
const hasMore = queues.length > this.perPage;
163+
164+
return {
165+
queues: await this.enrichQueues(environment, queues.slice(0, this.perPage)),
166+
hasMore,
167+
};
168+
}
169+
170+
private async getUnfilteredQueues(
171+
environment: AuthenticatedEnvironment,
172+
page: number,
173+
type: "task" | "custom" | undefined
174+
) {
175+
const queues = await this._replica.taskQueue.findMany({
176+
where: buildQueueListWhere(environment.id, undefined, type),
177+
select: queueListSelect,
123178
orderBy: {
124179
orderableName: "asc",
125180
},
126181
skip: (page - 1) * this.perPage,
127182
take: this.perPage,
128183
});
129184

130-
const results = await Promise.all([
131-
engine.lengthOfQueues(
185+
return this.enrichQueues(environment, queues);
186+
}
187+
188+
private async enrichQueues(
189+
environment: AuthenticatedEnvironment,
190+
queues: {
191+
friendlyId: string;
192+
name: string;
193+
orderableName: string | null;
194+
concurrencyLimit: number | null;
195+
concurrencyLimitBase: number | null;
196+
concurrencyLimitOverriddenAt: Date | null;
197+
concurrencyLimitOverriddenBy: string | null;
198+
type: TaskQueueType;
199+
paused: boolean;
200+
}[]
201+
) {
202+
const [queuedByQueue, runningByQueue] = await Promise.all([
203+
this.engineClient.lengthOfQueues(
132204
environment,
133205
queues.map((q) => q.name)
134206
),
135-
engine.currentConcurrencyOfQueues(
207+
this.engineClient.currentConcurrencyOfQueues(
136208
environment,
137209
queues.map((q) => q.name)
138210
),
@@ -149,14 +221,13 @@ export class QueueListPresenter extends BasePresenter {
149221

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

152-
// Transform queues to include running and queued counts
153224
return queues.map((queue) =>
154225
toQueueItem({
155226
friendlyId: queue.friendlyId,
156227
name: queue.name,
157228
type: queue.type,
158-
running: results[1][queue.name] ?? 0,
159-
queued: results[0][queue.name] ?? 0,
229+
running: runningByQueue[queue.name] ?? 0,
230+
queued: queuedByQueue[queue.name] ?? 0,
160231
concurrencyLimit: queue.concurrencyLimit ?? null,
161232
concurrencyLimitBase: queue.concurrencyLimitBase ?? null,
162233
concurrencyLimitOverriddenAt: queue.concurrencyLimitOverriddenAt ?? null,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export type QueueListFilteredPagination = {
2+
mode: "filtered";
3+
currentPage: number;
4+
hasMore: boolean;
5+
};
6+
7+
export type QueueListUnfilteredPagination = {
8+
mode: "unfiltered";
9+
currentPage: number;
10+
totalPages: number;
11+
count: number;
12+
};
13+
14+
export type QueueListPagination = QueueListFilteredPagination | QueueListUnfilteredPagination;
15+
16+
export type OffsetLimitPagination = {
17+
currentPage: number;
18+
totalPages: number;
19+
count: number;
20+
};
21+
22+
/** Maps presenter pagination to the public API / SDK offset-limit contract. */
23+
export function toOffsetLimitQueueListPagination(
24+
pagination: QueueListPagination,
25+
options: { itemsOnPage: number; perPage: number }
26+
): OffsetLimitPagination {
27+
if (pagination.mode === "unfiltered") {
28+
return {
29+
currentPage: pagination.currentPage,
30+
totalPages: pagination.totalPages,
31+
count: pagination.count,
32+
};
33+
}
34+
35+
return {
36+
currentPage: pagination.currentPage,
37+
totalPages: pagination.hasMore ? pagination.currentPage + 1 : pagination.currentPage,
38+
count:
39+
(pagination.currentPage - 1) * options.perPage +
40+
options.itemsOnPage +
41+
(pagination.hasMore ? 1 : 0),
42+
};
43+
}

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>

0 commit comments

Comments
 (0)