Skip to content

Commit a8ee89e

Browse files
author
Dylan Huang
committed
optimize a bit
1 parent 2cabc42 commit a8ee89e

2 files changed

Lines changed: 167 additions & 90 deletions

File tree

vite-app/src/GlobalState.tsx

Lines changed: 99 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { makeAutoObservable, runInAction } from "mobx";
22
import type { EvaluationRow } from "./types/eval-protocol";
33
import type { PivotConfig, FilterGroup } from "./types/filters";
44
import flattenJson from "./util/flatten-json";
5+
import type { FlatJson } from "./util/flatten-json";
56

67
// Default pivot configuration
78
const DEFAULT_PIVOT_CONFIG: PivotConfig = {
@@ -31,17 +32,35 @@ export class GlobalState {
3132
pivotConfig: PivotConfig;
3233
// Table filter configuration
3334
tableFilterConfig: FilterGroup[];
35+
// Debounced, actually applied table filter configuration (for performance while typing)
36+
appliedTableFilterConfig: FilterGroup[];
3437
// Pagination configuration
3538
currentPage: number;
3639
pageSize: number;
3740
// Loading state
3841
isLoading: boolean = true;
3942

43+
// Cached, denormalized data for performance
44+
// rollout_id -> flattened row
45+
private flattenedById: Record<string, FlatJson> = {};
46+
// rollout_id -> created_at timestamp (ms) for cheap sort
47+
private createdAtMsById: Record<string, number> = {};
48+
49+
// Debounce timers for localStorage saves and filter application
50+
private savePivotConfigTimer: ReturnType<typeof setTimeout> | null = null;
51+
private saveTableFilterConfigTimer: ReturnType<typeof setTimeout> | null =
52+
null;
53+
private savePaginationConfigTimer: ReturnType<typeof setTimeout> | null =
54+
null;
55+
private applyTableFilterTimer: ReturnType<typeof setTimeout> | null = null;
56+
4057
constructor() {
4158
// Load pivot config from localStorage or use defaults
4259
this.pivotConfig = this.loadPivotConfig();
4360
// Load table filter config from localStorage or use defaults
4461
this.tableFilterConfig = this.loadTableFilterConfig();
62+
// Initialize applied filter config with current value
63+
this.appliedTableFilterConfig = this.tableFilterConfig.slice();
4564
// Load pagination config from localStorage or use defaults
4665
const paginationConfig = this.loadPaginationConfig();
4766
this.currentPage = paginationConfig.currentPage;
@@ -101,41 +120,55 @@ export class GlobalState {
101120

102121
// Save pivot configuration to localStorage
103122
private savePivotConfig() {
104-
try {
105-
localStorage.setItem("pivotConfig", JSON.stringify(this.pivotConfig));
106-
} catch (error) {
107-
console.warn("Failed to save pivot config to localStorage:", error);
108-
}
123+
if (this.savePivotConfigTimer) clearTimeout(this.savePivotConfigTimer);
124+
this.savePivotConfigTimer = setTimeout(() => {
125+
try {
126+
localStorage.setItem("pivotConfig", JSON.stringify(this.pivotConfig));
127+
} catch (error) {
128+
console.warn("Failed to save pivot config to localStorage:", error);
129+
}
130+
}, 200);
109131
}
110132

111133
// Save table filter configuration to localStorage
112134
private saveTableFilterConfig() {
113-
try {
114-
localStorage.setItem(
115-
"tableFilterConfig",
116-
JSON.stringify(this.tableFilterConfig)
117-
);
118-
} catch (error) {
119-
console.warn(
120-
"Failed to save table filter config to localStorage:",
121-
error
122-
);
123-
}
135+
if (this.saveTableFilterConfigTimer)
136+
clearTimeout(this.saveTableFilterConfigTimer);
137+
this.saveTableFilterConfigTimer = setTimeout(() => {
138+
try {
139+
localStorage.setItem(
140+
"tableFilterConfig",
141+
JSON.stringify(this.tableFilterConfig)
142+
);
143+
} catch (error) {
144+
console.warn(
145+
"Failed to save table filter config to localStorage:",
146+
error
147+
);
148+
}
149+
}, 200);
124150
}
125151

126152
// Save pagination configuration to localStorage
127153
private savePaginationConfig() {
128-
try {
129-
localStorage.setItem(
130-
"paginationConfig",
131-
JSON.stringify({
132-
currentPage: this.currentPage,
133-
pageSize: this.pageSize,
134-
})
135-
);
136-
} catch (error) {
137-
console.warn("Failed to save pagination config to localStorage:", error);
138-
}
154+
if (this.savePaginationConfigTimer)
155+
clearTimeout(this.savePaginationConfigTimer);
156+
this.savePaginationConfigTimer = setTimeout(() => {
157+
try {
158+
localStorage.setItem(
159+
"paginationConfig",
160+
JSON.stringify({
161+
currentPage: this.currentPage,
162+
pageSize: this.pageSize,
163+
})
164+
);
165+
} catch (error) {
166+
console.warn(
167+
"Failed to save pagination config to localStorage:",
168+
error
169+
);
170+
}
171+
}, 200);
139172
}
140173

141174
// Update pivot configuration and save to localStorage
@@ -148,6 +181,12 @@ export class GlobalState {
148181
updateTableFilterConfig(filters: FilterGroup[]) {
149182
this.tableFilterConfig = filters;
150183
this.saveTableFilterConfig();
184+
185+
// Debounce application of filters to avoid re-filtering on every keystroke
186+
if (this.applyTableFilterTimer) clearTimeout(this.applyTableFilterTimer);
187+
this.applyTableFilterTimer = setTimeout(() => {
188+
this.appliedTableFilterConfig = this.tableFilterConfig.slice();
189+
}, 150);
151190
}
152191

153192
// Update pagination configuration and save to localStorage
@@ -175,6 +214,7 @@ export class GlobalState {
175214
// Reset table filter configuration to defaults
176215
resetTableFilterConfig() {
177216
this.tableFilterConfig = [...DEFAULT_TABLE_FILTER_CONFIG];
217+
this.appliedTableFilterConfig = [...DEFAULT_TABLE_FILTER_CONFIG];
178218
this.saveTableFilterConfig();
179219
}
180220

@@ -217,7 +257,13 @@ export class GlobalState {
217257
if (!row.execution_metadata?.rollout_id) {
218258
return;
219259
}
220-
this.dataset[row.execution_metadata.rollout_id] = row;
260+
const rolloutId = row.execution_metadata.rollout_id;
261+
this.dataset[rolloutId] = row;
262+
// Cache created_at in ms for cheap sorts
263+
const createdMs = new Date(row.created_at).getTime();
264+
this.createdAtMsById[rolloutId] = isNaN(createdMs) ? 0 : createdMs;
265+
// Cache flattened row for filtering/pivot keys
266+
this.flattenedById[rolloutId] = flattenJson(row);
221267
});
222268

223269
runInAction(() => {
@@ -253,42 +299,52 @@ export class GlobalState {
253299
}
254300

255301
// Computed values following MobX best practices
256-
get sortedDataset() {
257-
return Object.values(this.dataset).sort(
258-
(a, b) =>
259-
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
302+
get sortedIds() {
303+
return Object.keys(this.dataset).sort(
304+
(a, b) => (this.createdAtMsById[b] ?? 0) - (this.createdAtMsById[a] ?? 0)
260305
);
261306
}
262307

308+
get sortedDataset() {
309+
return this.sortedIds.map((id) => this.dataset[id]);
310+
}
311+
263312
get flattenedDataset() {
264-
return this.sortedDataset.map((row) => flattenJson(row));
313+
return this.sortedIds.map((id) => this.flattenedById[id]);
265314
}
266315

267316
get filteredFlattenedDataset() {
268-
if (this.tableFilterConfig.length === 0) {
317+
if (this.appliedTableFilterConfig.length === 0) {
269318
return this.flattenedDataset;
270319
}
271320

272-
const filterFunction = this.createFilterFunction(this.tableFilterConfig);
321+
const filterFunction = this.createFilterFunction(
322+
this.appliedTableFilterConfig
323+
);
273324
return this.flattenedDataset.filter(filterFunction);
274325
}
275326

276327
get filteredOriginalDataset() {
277-
if (this.tableFilterConfig.length === 0) {
328+
if (this.appliedTableFilterConfig.length === 0) {
278329
return this.sortedDataset;
279330
}
280331

281-
const filterFunction = this.createFilterFunction(this.tableFilterConfig);
282-
return this.sortedDataset.filter((row) => {
283-
const flattened = flattenJson(row);
284-
return filterFunction(flattened);
285-
});
332+
const filterFunction = this.createFilterFunction(
333+
this.appliedTableFilterConfig
334+
);
335+
return this.sortedIds
336+
.filter((id) => filterFunction(this.flattenedById[id]))
337+
.map((id) => this.dataset[id]);
286338
}
287339

288340
get flattenedDatasetKeys() {
289341
const keySet = new Set<string>();
290-
this.flattenedDataset.forEach((row) => {
291-
Object.keys(row).forEach((key) => keySet.add(key));
342+
// Iterate over cached flattened rows to build a unique key list
343+
this.sortedIds.forEach((id) => {
344+
const flat = this.flattenedById[id];
345+
if (flat) {
346+
Object.keys(flat).forEach((key) => keySet.add(key));
347+
}
292348
});
293349
return Array.from(keySet);
294350
}

vite-app/src/components/FilterSelector.tsx

Lines changed: 68 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React, { useCallback, useMemo } from "react";
12
import type { FilterConfig, FilterGroup } from "../types/filters";
23
import SearchableSelect from "./SearchableSelect";
34
import FilterInput from "./FilterInput";
@@ -11,55 +12,76 @@ interface FilterSelectorProps {
1112
title?: string;
1213
}
1314

14-
const FilterSelector = ({
15+
const FilterSelectorComponent = ({
1516
filters,
1617
onFiltersChange,
1718
availableKeys,
1819
title = "Filters",
1920
}: FilterSelectorProps) => {
20-
const addFilterGroup = () => {
21+
const addFilterGroup = useCallback(() => {
2122
onFiltersChange([...filters, { logic: "AND", filters: [] }]);
22-
};
23-
24-
const removeFilterGroup = (index: number) => {
25-
onFiltersChange(filters.filter((_, i) => i !== index));
26-
};
27-
28-
const updateFilterGroupLogic = (index: number, logic: "AND" | "OR") => {
29-
const newFilters = [...filters];
30-
newFilters[index] = { ...newFilters[index], logic };
31-
onFiltersChange(newFilters);
32-
};
33-
34-
const addFilterToGroup = (groupIndex: number) => {
35-
const newFilters = [...filters];
36-
newFilters[groupIndex].filters.push({
37-
field: "",
38-
operator: "contains",
39-
value: "",
40-
type: "text",
41-
});
42-
onFiltersChange(newFilters);
43-
};
44-
45-
const removeFilterFromGroup = (groupIndex: number, filterIndex: number) => {
46-
const newFilters = [...filters];
47-
newFilters[groupIndex].filters.splice(filterIndex, 1);
48-
onFiltersChange(newFilters);
49-
};
50-
51-
const updateFilterInGroup = (
52-
groupIndex: number,
53-
filterIndex: number,
54-
updates: Partial<FilterConfig>
55-
) => {
56-
const newFilters = [...filters];
57-
newFilters[groupIndex].filters[filterIndex] = {
58-
...newFilters[groupIndex].filters[filterIndex],
59-
...updates,
60-
};
61-
onFiltersChange(newFilters);
62-
};
23+
}, [filters, onFiltersChange]);
24+
25+
const removeFilterGroup = useCallback(
26+
(index: number) => {
27+
onFiltersChange(filters.filter((_, i) => i !== index));
28+
},
29+
[filters, onFiltersChange]
30+
);
31+
32+
const updateFilterGroupLogic = useCallback(
33+
(index: number, logic: "AND" | "OR") => {
34+
const newFilters = [...filters];
35+
newFilters[index] = { ...newFilters[index], logic };
36+
onFiltersChange(newFilters);
37+
},
38+
[filters, onFiltersChange]
39+
);
40+
41+
const addFilterToGroup = useCallback(
42+
(groupIndex: number) => {
43+
const newFilters = [...filters];
44+
newFilters[groupIndex].filters.push({
45+
field: "",
46+
operator: "contains",
47+
value: "",
48+
type: "text",
49+
});
50+
onFiltersChange(newFilters);
51+
},
52+
[filters, onFiltersChange]
53+
);
54+
55+
const removeFilterFromGroup = useCallback(
56+
(groupIndex: number, filterIndex: number) => {
57+
const newFilters = [...filters];
58+
newFilters[groupIndex].filters.splice(filterIndex, 1);
59+
onFiltersChange(newFilters);
60+
},
61+
[filters, onFiltersChange]
62+
);
63+
64+
const updateFilterInGroup = useCallback(
65+
(
66+
groupIndex: number,
67+
filterIndex: number,
68+
updates: Partial<FilterConfig>
69+
) => {
70+
const newFilters = [...filters];
71+
newFilters[groupIndex].filters[filterIndex] = {
72+
...newFilters[groupIndex].filters[filterIndex],
73+
...updates,
74+
};
75+
onFiltersChange(newFilters);
76+
},
77+
[filters, onFiltersChange]
78+
);
79+
80+
// Memoize options for available keys so we don't rebuild objects every render
81+
const keyOptions = useMemo(
82+
() => availableKeys.map((key) => ({ value: key, label: key })),
83+
[availableKeys]
84+
);
6385

6486
return (
6587
<div className="mb-3">
@@ -124,10 +146,7 @@ const FilterSelector = ({
124146
operator: operators[0]?.value || "contains",
125147
});
126148
}}
127-
options={availableKeys.map((key) => ({
128-
value: key,
129-
label: key,
130-
}))}
149+
options={keyOptions}
131150
placeholder="Select field..."
132151
size="sm"
133152
className="min-w-40"
@@ -187,4 +206,6 @@ const FilterSelector = ({
187206
);
188207
};
189208

209+
const FilterSelector = React.memo(FilterSelectorComponent);
210+
190211
export default FilterSelector;

0 commit comments

Comments
 (0)