Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/stale-brooms-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": minor
---

feat: Improve search speed by chunking long time range searches into smaller incremental search windows.
8 changes: 8 additions & 0 deletions packages/app/src/components/DBRowTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export const RawLogTable = memo(
error,
columnTypeMap,
dateRange,
loadingDate,
}: {
wrapLines: boolean;
displayedColumns: string[];
Expand Down Expand Up @@ -266,6 +267,7 @@ export const RawLogTable = memo(
isError?: boolean;
error?: ClickHouseQueryError | Error;
dateRange?: [Date, Date];
loadingDate?: Date;
}) => {
const generateRowMatcher = generateRowId;

Expand Down Expand Up @@ -770,6 +772,11 @@ export const RawLogTable = memo(
<div className="spin-animate d-inline-block">
<i className="bi bi-arrow-repeat" />
</div>{' '}
{loadingDate != null && (
<>
Searched <FormatTime value={loadingDate} />.{' '}
</>
)}
Loading results
{dateRange?.[0] != null && dateRange?.[1] != null ? (
<>
Expand Down Expand Up @@ -1174,6 +1181,7 @@ function DBSqlRowTableComponent({
error={error ?? undefined}
columnTypeMap={columnMap}
dateRange={config.dateRange}
loadingDate={data?.window?.startTime}
/>
</>
);
Expand Down
166 changes: 138 additions & 28 deletions packages/app/src/hooks/useOffsetPaginatedQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,122 @@ function queryKeyFn(
return [prefix, config, queryTimeout];
}

type TPageParam = number;
// Time window configuration - progressive bucketing strategy
const TIME_WINDOWS_MS = [
6 * 60 * 60 * 1000, // 6h
6 * 60 * 60 * 1000, // 6h
12 * 60 * 60 * 1000, // 12h
24 * 60 * 60 * 1000, // 24h
];

type TimeWindow = {
startTime: Date;
endTime: Date;
windowIndex: number;
};

type TPageParam = {
windowIndex: number;
offset: number;
};

type TQueryFnData = {
data: Record<string, any>[];
meta: ColumnMetaType[];
chSql: ChSql;
window: TimeWindow;
};

type TData = {
pages: TQueryFnData[];
pageParams: TPageParam[];
};

const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
// Generate time windows from date range using progressive bucketing
function generateTimeWindows(startDate: Date, endDate: Date): TimeWindow[] {
const windows: TimeWindow[] = [];
let currentEnd = new Date(endDate);
let windowIndex = 0;

while (currentEnd > startDate) {
const windowSize =
TIME_WINDOWS_MS[windowIndex] ||
TIME_WINDOWS_MS[TIME_WINDOWS_MS.length - 1]; // use largest window size
const windowStart = new Date(
Math.max(currentEnd.getTime() - windowSize, startDate.getTime()),
);

windows.push({
endTime: new Date(currentEnd),
startTime: windowStart,
windowIndex,
});

currentEnd = windowStart;
windowIndex++;
}

return windows;
}

// Get time window from page param
function getTimeWindowFromPageParam(
config: ChartConfigWithDateRange,
pageParam: TPageParam,
): TimeWindow {
const [startDate, endDate] = config.dateRange;
const windows = generateTimeWindows(startDate, endDate);
const window = windows[pageParam.windowIndex];
if (window == null) {
throw new Error('Invalid time window for page param');
Comment on lines +102 to +103
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is too generic. It should include the windowIndex value to help with debugging: Invalid time window for page param with windowIndex: ${pageParam.windowIndex}

Suggested change
if (window == null) {
throw new Error('Invalid time window for page param');
throw new Error(`Invalid time window for page param with windowIndex: ${pageParam.windowIndex}`);

Copilot uses AI. Check for mistakes.
}
return window;
}

// Calculate next page param based on current results and window
function getNextPageParam(
lastPage: TQueryFnData | null,
allPages: TQueryFnData[],
config: ChartConfigWithDateRange,
): TPageParam | undefined {
if (lastPage == null) {
return undefined;
}

const [startDate, endDate] = config.dateRange;
const windows = generateTimeWindows(startDate, endDate);
const currentWindow = lastPage.window;

// Calculate total results from all pages in current window
const currentWindowPages = allPages.filter(
p => p.window.windowIndex === currentWindow.windowIndex,
);
const currentWindowResults = currentWindowPages.reduce(
(sum, page) => sum + page.data.length,
0,
);

// If we have results in the current window, continue paginating within it
if (lastPage.data.length > 0) {
return {
windowIndex: currentWindow.windowIndex,
offset: currentWindowResults,
};
}

// If no more results in current window, move to next window
const nextWindowIndex = currentWindow.windowIndex + 1;
if (nextWindowIndex < windows.length) {
return {
windowIndex: nextWindowIndex,
offset: 0,
};
}

return undefined;
}

const queryFn: QueryFunction<TQueryFnData, TQueryKey, TPageParam> = async ({
queryKey,
pageParam,
signal,
Expand All @@ -57,19 +161,27 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
// Only stream incrementally if this is a fresh query with no previous
// response or if it's a paginated query
// otherwise we'll flicker the UI with streaming data
const isStreamingIncrementally = !meta.hasPreviousQueries || pageParam > 0;
const isStreamingIncrementally =
!meta.hasPreviousQueries ||
pageParam.offset > 0 ||
pageParam.windowIndex > 0;

const config = queryKey[1];
const query = await renderChartConfig(
{
...config,
limit: {
limit: config.limit?.limit,
offset: pageParam,
},

// Get the time window for this page
const timeWindow = getTimeWindowFromPageParam(config, pageParam);

// Create config with windowed date range
const windowedConfig = {
...config,
dateRange: [timeWindow.startTime, timeWindow.endTime] as [Date, Date],
limit: {
limit: config.limit?.limit,
offset: pageParam.offset,
},
getMetadata(),
);
};

const query = await renderChartConfig(windowedConfig, getMetadata());

const queryTimeout = queryKey[2];
const clickhouseClient = getClickhouseClient({ queryTimeout });
Expand All @@ -94,6 +206,7 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
data: [],
meta: [],
chSql: { sql: '', params: {} },
window: timeWindow,
};
if (oldData == null) {
return {
Expand Down Expand Up @@ -161,7 +274,14 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
queryClient.setQueryData<TData>(queryKey, oldData => {
if (oldData == null) {
return {
pages: [{ data: rowObjs, meta: queryResultMeta, chSql: query }],
pages: [
{
data: rowObjs,
meta: queryResultMeta,
chSql: query,
window: timeWindow,
},
],
pageParams: [pageParam],
};
}
Expand All @@ -177,6 +297,7 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
data: [...(page.data ?? []), ...rowObjs],
meta: queryResultMeta,
chSql: query,
window: timeWindow,
},
],
pageParams: oldData.pageParams,
Expand Down Expand Up @@ -215,6 +336,7 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
data: queryResultData,
meta: queryResultMeta,
chSql: query,
window: timeWindow,
};
}

Expand Down Expand Up @@ -244,6 +366,7 @@ function flattenData(data: TData | undefined): TQueryFnData | null {
meta: data.pages[0].meta,
data: flattenPages(data.pages),
chSql: data.pages[0].chSql,
window: data.pages[data.pages.length - 1].window,
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the last page's window in flattenData may not represent the correct time window for the flattened data. Consider using the first page's window or creating a merged window that spans from the first to last page's time range.

Copilot uses AI. Check for mistakes.
};
}

Expand Down Expand Up @@ -290,22 +413,9 @@ export default function useOffsetPaginatedQuery(
return isLive ? prev : undefined;
},
enabled,
initialPageParam: 0,
// TODO: Use initialData derived from cache to do a smarter time range fetch
initialPageParam: { windowIndex: 0, offset: 0 } as TPageParam,
getNextPageParam: (lastPage, allPages) => {
if (lastPage == null) {
return undefined;
}

const len = lastPage.data.length;
if (len === 0) {
return undefined;
}

const data = flattenPages(allPages);

// TODO: Need to configure overlap and account for granularity
return data.length;
return getNextPageParam(lastPage, allPages, config);
},
staleTime: Infinity, // TODO: Pick a correct time
meta: {
Expand Down