Skip to content

Commit ef57fcb

Browse files
committed
perf(logs): batch keyset export reads
1 parent 3367f44 commit ef57fcb

2 files changed

Lines changed: 35 additions & 24 deletions

File tree

apps/sim/app/api/logs/export/route.test.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@ describe('GET /api/logs/export', () => {
104104
})
105105

106106
it('materializes one bounded page at a time while preserving CSV row order', async () => {
107-
queueTableRows(workflowExecutionLogs, [logRow(0)])
108-
queueTableRows(workflowExecutionLogs, [logRow(1)])
109-
queueTableRows(workflowExecutionLogs, [logRow(2)])
107+
queueTableRows(workflowExecutionLogs, [logRow(0), logRow(1), logRow(2)])
110108

111109
const response = await GET(makeRequest())
112110
const lines = (await response.text()).trimEnd().split('\n')
@@ -119,23 +117,25 @@ describe('GET /api/logs/export', () => {
119117
})
120118

121119
it('resumes full pages by startedAt and id without using OFFSET', async () => {
122-
const last = logRow(0, { startedAtCursor: '2026-08-23 12:00:00.000123' })
120+
const firstPage = Array.from({ length: 100 }, (_, index) => logRow(index))
121+
firstPage[99] = logRow(99, { startedAtCursor: '2026-08-23 11:58:21.000123' })
122+
const last = firstPage.at(-1)!
123123
const secondPage = [
124-
logRow(1, {
124+
logRow(100, {
125125
id: 'log-0000-second',
126126
startedAt: last.startedAt,
127-
startedAtCursor: '2026-08-23 12:00:00.000122',
127+
startedAtCursor: '2026-08-23 11:58:21.000122',
128128
}),
129129
]
130-
queueTableRows(workflowExecutionLogs, [last])
130+
queueTableRows(workflowExecutionLogs, firstPage)
131131
queueTableRows(workflowExecutionLogs, secondPage)
132132

133133
const response = await GET(makeRequest())
134134
const lines = (await response.text()).trimEnd().split('\n')
135135

136-
expect(lines).toHaveLength(3)
136+
expect(lines).toHaveLength(102)
137137
expect(dbChainMockFns.offset).not.toHaveBeenCalled()
138-
expect(dbChainMockFns.where).toHaveBeenCalledTimes(3)
138+
expect(dbChainMockFns.where).toHaveBeenCalledTimes(2)
139139
expect(dbChainMockFns.orderBy).toHaveBeenNthCalledWith(
140140
1,
141141
expect.objectContaining({
@@ -169,7 +169,10 @@ describe('GET /api/logs/export', () => {
169169
})
170170

171171
it('does not load the next database page until the current row is consumed', async () => {
172-
queueTableRows(workflowExecutionLogs, [logRow(0)])
172+
queueTableRows(
173+
workflowExecutionLogs,
174+
Array.from({ length: 100 }, (_, index) => logRow(index))
175+
)
173176
queueTableRows(workflowExecutionLogs, [logRow(1)])
174177

175178
const response = await GET(makeRequest())

apps/sim/app/api/logs/export/route.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getErrorMessage } from '@sim/utils/errors'
55
import { and, desc, eq, lt, or, type SQL, sql } from 'drizzle-orm'
66
import { type NextRequest, NextResponse } from 'next/server'
77
import { getSession } from '@/lib/auth'
8-
import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency'
8+
import { mapWithConcurrency } from '@/lib/core/utils/concurrency'
99
import { formatCsvValue, toCsvRow } from '@/lib/core/utils/csv'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1111
import { materializeExecutionDataForDisplay } from '@/lib/logs/execution/trace-store'
@@ -14,7 +14,8 @@ import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion'
1414
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1515

1616
const logger = createLogger('LogsExportAPI')
17-
const LOG_EXPORT_PAGE_SIZE = 1
17+
const LOG_EXPORT_PAGE_SIZE = 100
18+
const LOG_EXPORT_MATERIALIZE_CONCURRENCY = 1
1819

1920
export const revalidate = 0
2021

@@ -119,18 +120,25 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
119120

120121
if (!rows.length) break
121122

122-
for (let chunkStart = 0; chunkStart < rows.length; chunkStart += MATERIALIZE_CONCURRENCY) {
123-
const chunk = rows.slice(chunkStart, chunkStart + MATERIALIZE_CONCURRENCY)
124-
const materialized = await mapWithConcurrency(chunk, MATERIALIZE_CONCURRENCY, (row) =>
125-
materializeExecutionDataForDisplay(
126-
row.executionData as Record<string, unknown> | null,
127-
{
128-
workspaceId: params.workspaceId,
129-
workflowId: row.workflowId,
130-
executionId: row.executionId,
131-
userId: session.user.id,
132-
}
133-
)
123+
for (
124+
let chunkStart = 0;
125+
chunkStart < rows.length;
126+
chunkStart += LOG_EXPORT_MATERIALIZE_CONCURRENCY
127+
) {
128+
const chunk = rows.slice(chunkStart, chunkStart + LOG_EXPORT_MATERIALIZE_CONCURRENCY)
129+
const materialized = await mapWithConcurrency(
130+
chunk,
131+
LOG_EXPORT_MATERIALIZE_CONCURRENCY,
132+
(row) =>
133+
materializeExecutionDataForDisplay(
134+
row.executionData as Record<string, unknown> | null,
135+
{
136+
workspaceId: params.workspaceId,
137+
workflowId: row.workflowId,
138+
executionId: row.executionId,
139+
userId: session.user.id,
140+
}
141+
)
134142
)
135143

136144
for (let index = 0; index < chunk.length; index++) {

0 commit comments

Comments
 (0)