Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/fix-queue-search-performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---

Speed up queue search by skipping count on filtered queries and using hasMore pagination
24 changes: 15 additions & 9 deletions apps/webapp/app/components/primitives/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ import { Link, useLocation } from "@remix-run/react";
import { cn } from "~/utils/cn";
import { LinkButton } from "./Buttons";

/** Pass `hasNextPage` when the total page count is unknown; use `showPageNumbers={false}` in that case. */
export function PaginationControls({
currentPage,
totalPages,
hasNextPage,
showPageNumbers = true,
}: {
currentPage: number;
totalPages: number;
/** When set, navigation uses this instead of `totalPages`. */
hasNextPage?: boolean;
showPageNumbers?: boolean;
}) {
const location = useLocation();
if (totalPages <= 1) {
const hasNextMode = hasNextPage !== undefined;
const showPagination = hasNextMode ? currentPage > 1 || hasNextPage : totalPages > 1;
const nextDisabled = hasNextMode ? !hasNextPage : currentPage === totalPages;

if (!showPagination) {
return null;
}

Expand Down Expand Up @@ -42,8 +50,8 @@ export function PaginationControls({
TrailingIcon={ChevronRightIcon}
shortcut={{ key: "k" }}
tooltip="Next"
disabled={currentPage === totalPages}
className={cn("px-2", currentPage !== totalPages ? "group" : "")}
disabled={nextDisabled}
className={cn("px-2", !nextDisabled ? "group" : "")}
/>
</>
) : (
Expand All @@ -66,23 +74,21 @@ export function PaginationControls({
<div
className={cn(
"order-2 h-6 w-px bg-charcoal-600 transition-colors peer-hover/next:bg-charcoal-550 peer-hover/prev:bg-charcoal-550",
currentPage === 1 && currentPage === totalPages && "opacity-30"
currentPage === 1 && nextDisabled && "opacity-30"
)}
/>

<div
className={cn("peer/next order-3", currentPage === totalPages && "pointer-events-none")}
>
<div className={cn("peer/next order-3", nextDisabled && "pointer-events-none")}>
<LinkButton
to={pageUrl(location, currentPage + 1)}
variant="secondary/small"
TrailingIcon={ChevronRightIcon}
shortcut={{ key: "k" }}
tooltip="Next"
disabled={currentPage === totalPages}
disabled={nextDisabled}
className={cn(
"flex items-center rounded-l-none border-l-0 pl-[0.5625rem] pr-2",
currentPage === totalPages && "cursor-not-allowed opacity-50"
nextDisabled && "cursor-not-allowed opacity-50"
)}
/>
</div>
Expand Down
171 changes: 121 additions & 50 deletions apps/webapp/app/presenters/v3/QueueListPresenter.server.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,68 @@
import { TaskQueueType } from "@trigger.dev/database";
import type { RunEngine } from "@internal/run-engine";
import { Prisma, TaskQueueType } from "@trigger.dev/database";
import { type PrismaClientOrTransaction } from "~/db.server";
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { determineEngineVersion } from "~/v3/engineVersion.server";
import { engine } from "~/v3/runEngine.server";
import { BasePresenter } from "./basePresenter.server";
import { toQueueItem } from "./QueueRetrievePresenter.server";
import type { QueueListPagination } from "./queueListPagination.server";

const DEFAULT_ITEMS_PER_PAGE = 25;
type QueueListEngine = Pick<RunEngine, "lengthOfQueues" | "currentConcurrencyOfQueues">;

export const QUEUE_LIST_DEFAULT_ITEMS_PER_PAGE = 25;
const MAX_ITEMS_PER_PAGE = 100;

const typeToDBQueueType: Record<"task" | "custom", TaskQueueType> = {
task: TaskQueueType.VIRTUAL,
custom: TaskQueueType.NAMED,
};

const queueListSelect = {
friendlyId: true,
name: true,
orderableName: true,
concurrencyLimit: true,
concurrencyLimitBase: true,
concurrencyLimitOverriddenAt: true,
concurrencyLimitOverriddenBy: true,
type: true,
paused: true,
} satisfies Prisma.TaskQueueSelect;

function buildQueueListWhere(
environmentId: string,
query: string | undefined,
type: "task" | "custom" | undefined
): Prisma.TaskQueueWhereInput {
const trimmedQuery = query?.trim();

return {
runtimeEnvironmentId: environmentId,
version: "V2",
name: trimmedQuery
? {
contains: trimmedQuery,
mode: "insensitive",
}
: undefined,
type: type ? typeToDBQueueType[type] : undefined,
};
}

export class QueueListPresenter extends BasePresenter {
private readonly perPage: number;
private readonly engineClient: QueueListEngine;

constructor(perPage: number = DEFAULT_ITEMS_PER_PAGE) {
super();
constructor(
perPage: number = QUEUE_LIST_DEFAULT_ITEMS_PER_PAGE,
prismaClient?: PrismaClientOrTransaction,
replicaClient?: PrismaClientOrTransaction,
engineClient: QueueListEngine = engine
) {
super(prismaClient, replicaClient);
this.perPage = Math.min(perPage, MAX_ITEMS_PER_PAGE);
this.engineClient = engineClient;
}

public async call({
Expand All @@ -33,26 +77,14 @@ export class QueueListPresenter extends BasePresenter {
perPage?: number;
type?: "task" | "custom";
}) {
const hasFilters = (query !== undefined && query.length > 0) || type !== undefined;

// Get total count for pagination
const totalQueues = await this._replica.taskQueue.count({
where: {
runtimeEnvironmentId: environment.id,
version: "V2",
name: query
? {
contains: query,
mode: "insensitive",
}
: undefined,
type: type ? typeToDBQueueType[type] : undefined,
},
});
const hasFilters = Boolean(query?.trim()) || type !== undefined;

//check the engine is the correct version
const engineVersion = await determineEngineVersion({ environment });
if (engineVersion === "V1") {
const totalQueues = await this._replica.taskQueue.count({
where: buildQueueListWhere(environment.id, query, type),
});

if (totalQueues === 0) {
const oldQueue = await this._replica.taskQueue.findFirst({
where: {
Expand All @@ -78,10 +110,30 @@ export class QueueListPresenter extends BasePresenter {
};
}

if (hasFilters) {
const { queues, hasMore } = await this.getFilteredQueues(environment, query, page, type);

return {
success: true as const,
queues,
pagination: {
mode: "filtered" as const,
currentPage: page,
hasMore,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
hasFilters,
};
}

const totalQueues = await this._replica.taskQueue.count({
where: buildQueueListWhere(environment.id, query, type),
});

return {
success: true as const,
queues: await this.getQueuesWithPagination(environment, query, page, type),
queues: await this.getUnfilteredQueues(environment, page, type),
pagination: {
mode: "unfiltered" as const,
currentPage: page,
totalPages: Math.ceil(totalQueues / this.perPage),
count: totalQueues,
Expand All @@ -91,48 +143,68 @@ export class QueueListPresenter extends BasePresenter {
};
}

private async getQueuesWithPagination(
private async getFilteredQueues(
environment: AuthenticatedEnvironment,
query: string | undefined,
page: number,
type: "task" | "custom" | undefined
) {
const queues = await this._replica.taskQueue.findMany({
where: {
runtimeEnvironmentId: environment.id,
version: "V2",
name: query
? {
contains: query,
mode: "insensitive",
}
: undefined,
type: type ? typeToDBQueueType[type] : undefined,
},
select: {
friendlyId: true,
name: true,
orderableName: true,
concurrencyLimit: true,
concurrencyLimitBase: true,
concurrencyLimitOverriddenAt: true,
concurrencyLimitOverriddenBy: true,
type: true,
paused: true,
where: buildQueueListWhere(environment.id, query, type),
select: queueListSelect,
orderBy: {
orderableName: "asc",
},
skip: (page - 1) * this.perPage,
take: this.perPage + 1,
});

const hasMore = queues.length > this.perPage;

return {
queues: await this.enrichQueues(environment, queues.slice(0, this.perPage)),
hasMore,
};
}

private async getUnfilteredQueues(
environment: AuthenticatedEnvironment,
page: number,
type: "task" | "custom" | undefined
) {
const queues = await this._replica.taskQueue.findMany({
where: buildQueueListWhere(environment.id, undefined, type),
select: queueListSelect,
orderBy: {
orderableName: "asc",
},
skip: (page - 1) * this.perPage,
take: this.perPage,
});

const results = await Promise.all([
engine.lengthOfQueues(
return this.enrichQueues(environment, queues);
}

private async enrichQueues(
environment: AuthenticatedEnvironment,
queues: {
friendlyId: string;
name: string;
orderableName: string | null;
concurrencyLimit: number | null;
concurrencyLimitBase: number | null;
concurrencyLimitOverriddenAt: Date | null;
concurrencyLimitOverriddenBy: string | null;
type: TaskQueueType;
paused: boolean;
}[]
) {
const [queuedByQueue, runningByQueue] = await Promise.all([
this.engineClient.lengthOfQueues(
environment,
queues.map((q) => q.name)
),
engine.currentConcurrencyOfQueues(
this.engineClient.currentConcurrencyOfQueues(
environment,
queues.map((q) => q.name)
),
Expand All @@ -149,14 +221,13 @@ export class QueueListPresenter extends BasePresenter {

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

// Transform queues to include running and queued counts
return queues.map((queue) =>
toQueueItem({
friendlyId: queue.friendlyId,
name: queue.name,
type: queue.type,
running: results[1][queue.name] ?? 0,
queued: results[0][queue.name] ?? 0,
running: runningByQueue[queue.name] ?? 0,
queued: queuedByQueue[queue.name] ?? 0,
concurrencyLimit: queue.concurrencyLimit ?? null,
concurrencyLimitBase: queue.concurrencyLimitBase ?? null,
concurrencyLimitOverriddenAt: queue.concurrencyLimitOverriddenAt ?? null,
Expand Down
43 changes: 43 additions & 0 deletions apps/webapp/app/presenters/v3/queueListPagination.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export type QueueListFilteredPagination = {
mode: "filtered";
currentPage: number;
hasMore: boolean;
};

export type QueueListUnfilteredPagination = {
mode: "unfiltered";
currentPage: number;
totalPages: number;
count: number;
};

export type QueueListPagination = QueueListFilteredPagination | QueueListUnfilteredPagination;

export type OffsetLimitPagination = {
currentPage: number;
totalPages: number;
count: number;
};

/** Maps presenter pagination to the public API / SDK offset-limit contract. */
export function toOffsetLimitQueueListPagination(
pagination: QueueListPagination,
options: { itemsOnPage: number; perPage: number }
): OffsetLimitPagination {
if (pagination.mode === "unfiltered") {
return {
currentPage: pagination.currentPage,
totalPages: pagination.totalPages,
count: pagination.count,
};
}

return {
currentPage: pagination.currentPage,
totalPages: pagination.hasMore ? pagination.currentPage + 1 : pagination.currentPage,
count:
(pagination.currentPage - 1) * options.perPage +
options.itemsOnPage +
(pagination.hasMore ? 1 : 0),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,10 @@ export default function Page() {
<QueueFilters />
<PaginationControls
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
totalPages={pagination.mode === "unfiltered" ? pagination.totalPages : 1}
hasNextPage={
pagination.mode === "filtered" ? pagination.hasMore : undefined
}
showPageNumbers={false}
/>
</div>
Expand Down
Loading