Skip to content

Commit 64eb638

Browse files
authored
feat: Improve search speed by chunking long time range searches into smaller incremental search windows. (#1125)
Resolves HDX-1035
1 parent 88f3caf commit 64eb638

3 files changed

Lines changed: 151 additions & 28 deletions

File tree

‎.changeset/stale-brooms-sparkle.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": minor
3+
---
4+
5+
feat: Improve search speed by chunking long time range searches into smaller incremental search windows.

‎packages/app/src/components/DBRowTable.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export const RawLogTable = memo(
238238
error,
239239
columnTypeMap,
240240
dateRange,
241+
loadingDate,
241242
}: {
242243
wrapLines: boolean;
243244
displayedColumns: string[];
@@ -266,6 +267,7 @@ export const RawLogTable = memo(
266267
isError?: boolean;
267268
error?: ClickHouseQueryError | Error;
268269
dateRange?: [Date, Date];
270+
loadingDate?: Date;
269271
}) => {
270272
const generateRowMatcher = generateRowId;
271273

@@ -770,6 +772,11 @@ export const RawLogTable = memo(
770772
<div className="spin-animate d-inline-block">
771773
<i className="bi bi-arrow-repeat" />
772774
</div>{' '}
775+
{loadingDate != null && (
776+
<>
777+
Searched <FormatTime value={loadingDate} />.{' '}
778+
</>
779+
)}
773780
Loading results
774781
{dateRange?.[0] != null && dateRange?.[1] != null ? (
775782
<>
@@ -1174,6 +1181,7 @@ function DBSqlRowTableComponent({
11741181
error={error ?? undefined}
11751182
columnTypeMap={columnMap}
11761183
dateRange={config.dateRange}
1184+
loadingDate={data?.window?.startTime}
11771185
/>
11781186
</>
11791187
);

‎packages/app/src/hooks/useOffsetPaginatedQuery.tsx‎

Lines changed: 138 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,122 @@ function queryKeyFn(
3333
return [prefix, config, queryTimeout];
3434
}
3535

36-
type TPageParam = number;
36+
// Time window configuration - progressive bucketing strategy
37+
const TIME_WINDOWS_MS = [
38+
6 * 60 * 60 * 1000, // 6h
39+
6 * 60 * 60 * 1000, // 6h
40+
12 * 60 * 60 * 1000, // 12h
41+
24 * 60 * 60 * 1000, // 24h
42+
];
43+
44+
type TimeWindow = {
45+
startTime: Date;
46+
endTime: Date;
47+
windowIndex: number;
48+
};
49+
50+
type TPageParam = {
51+
windowIndex: number;
52+
offset: number;
53+
};
54+
3755
type TQueryFnData = {
3856
data: Record<string, any>[];
3957
meta: ColumnMetaType[];
4058
chSql: ChSql;
59+
window: TimeWindow;
4160
};
61+
4262
type TData = {
4363
pages: TQueryFnData[];
4464
pageParams: TPageParam[];
4565
};
4666

47-
const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
67+
// Generate time windows from date range using progressive bucketing
68+
function generateTimeWindows(startDate: Date, endDate: Date): TimeWindow[] {
69+
const windows: TimeWindow[] = [];
70+
let currentEnd = new Date(endDate);
71+
let windowIndex = 0;
72+
73+
while (currentEnd > startDate) {
74+
const windowSize =
75+
TIME_WINDOWS_MS[windowIndex] ||
76+
TIME_WINDOWS_MS[TIME_WINDOWS_MS.length - 1]; // use largest window size
77+
const windowStart = new Date(
78+
Math.max(currentEnd.getTime() - windowSize, startDate.getTime()),
79+
);
80+
81+
windows.push({
82+
endTime: new Date(currentEnd),
83+
startTime: windowStart,
84+
windowIndex,
85+
});
86+
87+
currentEnd = windowStart;
88+
windowIndex++;
89+
}
90+
91+
return windows;
92+
}
93+
94+
// Get time window from page param
95+
function getTimeWindowFromPageParam(
96+
config: ChartConfigWithDateRange,
97+
pageParam: TPageParam,
98+
): TimeWindow {
99+
const [startDate, endDate] = config.dateRange;
100+
const windows = generateTimeWindows(startDate, endDate);
101+
const window = windows[pageParam.windowIndex];
102+
if (window == null) {
103+
throw new Error('Invalid time window for page param');
104+
}
105+
return window;
106+
}
107+
108+
// Calculate next page param based on current results and window
109+
function getNextPageParam(
110+
lastPage: TQueryFnData | null,
111+
allPages: TQueryFnData[],
112+
config: ChartConfigWithDateRange,
113+
): TPageParam | undefined {
114+
if (lastPage == null) {
115+
return undefined;
116+
}
117+
118+
const [startDate, endDate] = config.dateRange;
119+
const windows = generateTimeWindows(startDate, endDate);
120+
const currentWindow = lastPage.window;
121+
122+
// Calculate total results from all pages in current window
123+
const currentWindowPages = allPages.filter(
124+
p => p.window.windowIndex === currentWindow.windowIndex,
125+
);
126+
const currentWindowResults = currentWindowPages.reduce(
127+
(sum, page) => sum + page.data.length,
128+
0,
129+
);
130+
131+
// If we have results in the current window, continue paginating within it
132+
if (lastPage.data.length > 0) {
133+
return {
134+
windowIndex: currentWindow.windowIndex,
135+
offset: currentWindowResults,
136+
};
137+
}
138+
139+
// If no more results in current window, move to next window
140+
const nextWindowIndex = currentWindow.windowIndex + 1;
141+
if (nextWindowIndex < windows.length) {
142+
return {
143+
windowIndex: nextWindowIndex,
144+
offset: 0,
145+
};
146+
}
147+
148+
return undefined;
149+
}
150+
151+
const queryFn: QueryFunction<TQueryFnData, TQueryKey, TPageParam> = async ({
48152
queryKey,
49153
pageParam,
50154
signal,
@@ -57,19 +161,27 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
57161
// Only stream incrementally if this is a fresh query with no previous
58162
// response or if it's a paginated query
59163
// otherwise we'll flicker the UI with streaming data
60-
const isStreamingIncrementally = !meta.hasPreviousQueries || pageParam > 0;
164+
const isStreamingIncrementally =
165+
!meta.hasPreviousQueries ||
166+
pageParam.offset > 0 ||
167+
pageParam.windowIndex > 0;
61168

62169
const config = queryKey[1];
63-
const query = await renderChartConfig(
64-
{
65-
...config,
66-
limit: {
67-
limit: config.limit?.limit,
68-
offset: pageParam,
69-
},
170+
171+
// Get the time window for this page
172+
const timeWindow = getTimeWindowFromPageParam(config, pageParam);
173+
174+
// Create config with windowed date range
175+
const windowedConfig = {
176+
...config,
177+
dateRange: [timeWindow.startTime, timeWindow.endTime] as [Date, Date],
178+
limit: {
179+
limit: config.limit?.limit,
180+
offset: pageParam.offset,
70181
},
71-
getMetadata(),
72-
);
182+
};
183+
184+
const query = await renderChartConfig(windowedConfig, getMetadata());
73185

74186
const queryTimeout = queryKey[2];
75187
const clickhouseClient = getClickhouseClient({ queryTimeout });
@@ -94,6 +206,7 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
94206
data: [],
95207
meta: [],
96208
chSql: { sql: '', params: {} },
209+
window: timeWindow,
97210
};
98211
if (oldData == null) {
99212
return {
@@ -161,7 +274,14 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
161274
queryClient.setQueryData<TData>(queryKey, oldData => {
162275
if (oldData == null) {
163276
return {
164-
pages: [{ data: rowObjs, meta: queryResultMeta, chSql: query }],
277+
pages: [
278+
{
279+
data: rowObjs,
280+
meta: queryResultMeta,
281+
chSql: query,
282+
window: timeWindow,
283+
},
284+
],
165285
pageParams: [pageParam],
166286
};
167287
}
@@ -177,6 +297,7 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
177297
data: [...(page.data ?? []), ...rowObjs],
178298
meta: queryResultMeta,
179299
chSql: query,
300+
window: timeWindow,
180301
},
181302
],
182303
pageParams: oldData.pageParams,
@@ -215,6 +336,7 @@ const queryFn: QueryFunction<TQueryFnData, TQueryKey, number> = async ({
215336
data: queryResultData,
216337
meta: queryResultMeta,
217338
chSql: query,
339+
window: timeWindow,
218340
};
219341
}
220342

@@ -244,6 +366,7 @@ function flattenData(data: TData | undefined): TQueryFnData | null {
244366
meta: data.pages[0].meta,
245367
data: flattenPages(data.pages),
246368
chSql: data.pages[0].chSql,
369+
window: data.pages[data.pages.length - 1].window,
247370
};
248371
}
249372

@@ -290,22 +413,9 @@ export default function useOffsetPaginatedQuery(
290413
return isLive ? prev : undefined;
291414
},
292415
enabled,
293-
initialPageParam: 0,
294-
// TODO: Use initialData derived from cache to do a smarter time range fetch
416+
initialPageParam: { windowIndex: 0, offset: 0 } as TPageParam,
295417
getNextPageParam: (lastPage, allPages) => {
296-
if (lastPage == null) {
297-
return undefined;
298-
}
299-
300-
const len = lastPage.data.length;
301-
if (len === 0) {
302-
return undefined;
303-
}
304-
305-
const data = flattenPages(allPages);
306-
307-
// TODO: Need to configure overlap and account for granularity
308-
return data.length;
418+
return getNextPageParam(lastPage, allPages, config);
309419
},
310420
staleTime: Infinity, // TODO: Pick a correct time
311421
meta: {

0 commit comments

Comments
 (0)