Skip to content

Commit 28052da

Browse files
authored
Faster jobs page (#879)
* Optional next/previous with the list pagination * Defer the loading of the runs table * Only return the latest run in a separate query, then do a join in code * Added a composite index to speed up getting the latest run from a job id
1 parent 364c8c5 commit 28052da

File tree

5 files changed

+80
-29
lines changed

5 files changed

+80
-29
lines changed

apps/webapp/app/components/runs/RunsTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ export function RunsTable({
7777
<TableBody>
7878
{total === 0 && !hasFilters ? (
7979
<TableBlankRow colSpan={showJob ? 10 : 9}>
80-
<NoRuns title="No runs found" />
80+
{!isLoading && <NoRuns title="No runs found" />}
8181
</TableBlankRow>
8282
) : runs.length === 0 ? (
8383
<TableBlankRow colSpan={showJob ? 10 : 9}>
84-
<NoRuns title="No runs match your filters" />
84+
{!isLoading && <NoRuns title="No runs match your filters" />}
8585
</TableBlankRow>
8686
) : (
8787
runs.map((run) => {

apps/webapp/app/presenters/JobListPresenter.server.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Project } from "~/models/project.server";
99
import { User } from "~/models/user.server";
1010
import { z } from "zod";
1111
import { projectPath } from "~/utils/pathBuilder";
12+
import { JobRunStatus } from "@trigger.dev/database";
1213

1314
export type ProjectJob = Awaited<ReturnType<JobListPresenter["call"]>>[0];
1415

@@ -43,14 +44,6 @@ export class JobListPresenter {
4344
id: true,
4445
slug: true,
4546
title: true,
46-
runs: {
47-
select: {
48-
createdAt: true,
49-
status: true,
50-
},
51-
take: 1,
52-
orderBy: [{ createdAt: "desc" }],
53-
},
5447
integrations: {
5548
select: {
5649
key: true,
@@ -105,6 +98,28 @@ export class JobListPresenter {
10598
orderBy: [{ title: "asc" }],
10699
});
107100

101+
const latestRuns = await this.#prismaClient.$queryRaw<
102+
{
103+
createdAt: Date;
104+
status: JobRunStatus;
105+
jobId: string;
106+
rn: BigInt;
107+
}[]
108+
>`
109+
SELECT * FROM (
110+
SELECT
111+
"id",
112+
"createdAt",
113+
"status",
114+
"jobId",
115+
ROW_NUMBER() OVER(PARTITION BY "jobId" ORDER BY "createdAt" DESC) as rn
116+
FROM
117+
"public"."JobRun"
118+
WHERE
119+
"jobId" IN (${Prisma.join(jobs.map((j) => j.id))})
120+
) t
121+
WHERE rn = 1;`;
122+
108123
return jobs
109124
.flatMap((job) => {
110125
const version = job.versions.at(0);
@@ -132,6 +147,8 @@ export class JobListPresenter {
132147
properties = [...properties, ...versionProperties];
133148
}
134149

150+
const latestRun = latestRuns.find((r) => r.jobId === job.id);
151+
135152
return [
136153
{
137154
id: job.id,
@@ -155,7 +172,7 @@ export class JobListPresenter {
155172
(i) => i.setupStatus === "MISSING_FIELDS"
156173
),
157174
environment: version.environment,
158-
lastRun: job.runs.at(0),
175+
lastRun: latestRun,
159176
properties,
160177
projectSlug: job.project.slug,
161178
},

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/ListPagination.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { cn } from "~/utils/cn";
55

66
type List = {
77
pagination: {
8-
next: string | undefined;
9-
previous: string | undefined;
8+
next?: string | undefined;
9+
previous?: string | undefined;
1010
};
1111
};
1212

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.runs/route.tsx

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useLocation, useNavigate, useNavigation } from "@remix-run/react";
2-
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
1+
import { Await, useLoaderData, useLocation, useNavigate, useNavigation } from "@remix-run/react";
2+
import { LoaderFunctionArgs, defer } from "@remix-run/server-runtime";
33
import { typedjson, useTypedLoaderData } from "remix-typedjson";
44
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
55
import { LinkButton } from "~/components/primitives/Buttons";
@@ -20,6 +20,8 @@ import { ProjectParamSchema, docsPath, projectPath } from "~/utils/pathBuilder";
2020
import { ListPagination } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/ListPagination";
2121
import { RunListSearchSchema } from "~/components/runs/RunStatuses";
2222
import { RunsFilters } from "~/components/runs/RunFilters";
23+
import { Suspense } from "react";
24+
import { Spinner } from "~/components/primitives/Spinner";
2325

2426
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
2527
const userId = await requireUserId(request);
@@ -31,7 +33,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
3133

3234
const presenter = new RunListPresenter();
3335

34-
const list = await presenter.call({
36+
const list = presenter.call({
3537
userId,
3638
filterEnvironment: searchParams.environment,
3739
filterStatus: searchParams.status,
@@ -44,13 +46,13 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
4446
to: searchParams.to,
4547
});
4648

47-
return typedjson({
49+
return defer({
4850
list,
4951
});
5052
};
5153

5254
export default function Page() {
53-
const { list } = useTypedLoaderData<typeof loader>();
55+
const { list } = useLoaderData<typeof loader>();
5456
const navigation = useNavigation();
5557
const isLoading = navigation.state !== "idle";
5658
const organization = useOrganization();
@@ -79,18 +81,49 @@ export default function Page() {
7981
<div className="h-full overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
8082
<div className="mb-2 flex items-center justify-between gap-x-2">
8183
<RunsFilters />
82-
<ListPagination list={list} />
84+
<Suspense fallback={<></>}>
85+
<Await resolve={list}>{(data) => <ListPagination list={data} />}</Await>
86+
</Suspense>
8387
</div>
84-
<RunsTable
85-
total={list.runs.length}
86-
hasFilters={false}
87-
showJob={true}
88-
runs={list.runs}
89-
isLoading={isLoading}
90-
runsParentPath={projectPath(organization, project)}
91-
currentUser={user}
92-
/>
93-
<ListPagination list={list} className="mt-2 justify-end" />
88+
<Suspense
89+
fallback={
90+
<RunsTable
91+
total={0}
92+
hasFilters={false}
93+
showJob={true}
94+
runs={[]}
95+
isLoading={true}
96+
runsParentPath={projectPath(organization, project)}
97+
currentUser={user}
98+
/>
99+
}
100+
>
101+
<Await resolve={list}>
102+
{(data) => {
103+
const runs = data.runs.map((run) => ({
104+
...run,
105+
startedAt: run.startedAt ? new Date(run.startedAt) : null,
106+
completedAt: run.completedAt ? new Date(run.completedAt) : null,
107+
createdAt: new Date(run.createdAt),
108+
}));
109+
110+
return (
111+
<>
112+
<RunsTable
113+
total={data.runs.length}
114+
hasFilters={false}
115+
showJob={true}
116+
runs={runs}
117+
isLoading={isLoading}
118+
runsParentPath={projectPath(organization, project)}
119+
currentUser={user}
120+
/>
121+
<ListPagination list={data} className="mt-2 justify-end" />
122+
</>
123+
);
124+
}}
125+
</Await>
126+
</Suspense>
94127
</div>
95128
</PageBody>
96129
</PageContainer>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CREATE INDEX idx_jobrun_jobId_createdAt ON "public"."JobRun" ("jobId", "createdAt" DESC);

0 commit comments

Comments
 (0)