Skip to content

Commit cc93f60

Browse files
dmadisettipre-commit-ci[bot]mscolnick
authored
feat: rig up cache api (#6662)
## 📝 Summary exposes `/api/cache/clear` and `/api/cache/info` and creates an experimental caching panel: <img width="408" height="385" alt="image" src="https://github.com/user-attachments/assets/160af229-06e4-4ee8-9fbb-173e44873d17" /> --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Myles Scolnick <[email protected]>
1 parent 5789c10 commit cc93f60

File tree

30 files changed

+882
-4
lines changed

30 files changed

+882
-4
lines changed

frontend/src/__mocks__/requests.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export const MockRequestClient = {
7070
listSecretKeys: vi.fn().mockResolvedValue({ keys: [] }),
7171
writeSecret: vi.fn().mockResolvedValue({}),
7272
invokeAiTool: vi.fn().mockResolvedValue({}),
73+
clearCache: vi.fn().mockResolvedValue(null),
74+
getCacheInfo: vi.fn().mockResolvedValue(null),
7375
...overrides,
7476
};
7577
},
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/* Copyright 2024 Marimo. All rights reserved. */
2+
3+
import { useAtomValue } from "jotai";
4+
import { DatabaseZapIcon, RefreshCwIcon, Trash2Icon } from "lucide-react";
5+
import React, { useState } from "react";
6+
import { useLocale } from "react-aria";
7+
import { Spinner } from "@/components/icons/spinner";
8+
import { Button } from "@/components/ui/button";
9+
import { ConfirmationButton } from "@/components/ui/confirmation-button";
10+
import { toast } from "@/components/ui/use-toast";
11+
import { cacheInfoAtom } from "@/core/cache/requests";
12+
import { useRequestClient } from "@/core/network/requests";
13+
import { useAsyncData } from "@/hooks/useAsyncData";
14+
import { cn } from "@/utils/cn";
15+
import { formatBytes, formatTime } from "@/utils/formatting";
16+
import { prettyNumber } from "@/utils/numbers";
17+
import { PanelEmptyState } from "./empty-state";
18+
19+
const CachePanel = () => {
20+
const { clearCache, getCacheInfo } = useRequestClient();
21+
const cacheInfo = useAtomValue(cacheInfoAtom);
22+
const [purging, setPurging] = useState(false);
23+
const { locale } = useLocale();
24+
25+
const { isPending, isFetching, refetch } = useAsyncData(async () => {
26+
await getCacheInfo();
27+
// Artificially spin the icon if the request is really fast
28+
await new Promise((resolve) => setTimeout(resolve, 500));
29+
}, []);
30+
31+
const handlePurge = async () => {
32+
try {
33+
setPurging(true);
34+
await clearCache();
35+
toast({
36+
title: "Cache purged",
37+
description: "All cached data has been cleared",
38+
});
39+
// Request updated cache info after purge
40+
refetch();
41+
} catch (err) {
42+
toast({
43+
title: "Error",
44+
description:
45+
err instanceof Error ? err.message : "Failed to purge cache",
46+
variant: "danger",
47+
});
48+
} finally {
49+
setPurging(false);
50+
}
51+
};
52+
53+
// Show spinner only on initial load
54+
if (isPending && !cacheInfo) {
55+
return <Spinner size="medium" centered={true} />;
56+
}
57+
58+
const refreshButton = (
59+
<Button variant="outline" size="sm" onClick={refetch} disabled={isFetching}>
60+
{isFetching ? (
61+
<Spinner size="small" className="w-4 h-4 mr-2" />
62+
) : (
63+
<RefreshCwIcon className="w-4 h-4 mr-2" />
64+
)}
65+
Refresh
66+
</Button>
67+
);
68+
69+
if (!cacheInfo) {
70+
return (
71+
<PanelEmptyState
72+
title="No cache data"
73+
description="Cache information is not available."
74+
icon={<DatabaseZapIcon />}
75+
action={refreshButton}
76+
/>
77+
);
78+
}
79+
80+
const totalHits = cacheInfo.hits;
81+
const totalMisses = cacheInfo.misses;
82+
const totalTime = cacheInfo.time;
83+
const diskTotal = cacheInfo.disk_total;
84+
const diskToFree = cacheInfo.disk_to_free;
85+
86+
const totalRequests = totalHits + totalMisses;
87+
const hitRate = totalRequests > 0 ? (totalHits / totalRequests) * 100 : 0;
88+
89+
// Show empty state if no cache activity
90+
if (totalRequests === 0) {
91+
return (
92+
<PanelEmptyState
93+
title="No cache activity"
94+
description="The cache has not been used yet. Cached functions will appear here once they are executed."
95+
icon={<DatabaseZapIcon />}
96+
action={refreshButton}
97+
/>
98+
);
99+
}
100+
101+
return (
102+
<div className="flex flex-col h-full overflow-auto">
103+
<div className="flex flex-col gap-4 p-4 h-full">
104+
{/* Header with Refresh Button */}
105+
<div className="flex items-center justify-end">
106+
<Button
107+
variant="ghost"
108+
size="icon"
109+
className="h-6 w-6"
110+
onClick={refetch}
111+
disabled={isFetching}
112+
>
113+
<RefreshCwIcon
114+
className={cn(
115+
"h-4 w-4 text-muted-foreground hover:text-foreground",
116+
isFetching && "animate-[spin_0.5s]",
117+
)}
118+
/>
119+
</Button>
120+
</div>
121+
122+
{/* Statistics Section */}
123+
<div className="space-y-3">
124+
<h3 className="text-sm font-semibold text-foreground">Statistics</h3>
125+
<div className="grid grid-cols-2 gap-3">
126+
<StatCard
127+
label="Time saved"
128+
value={formatTime(totalTime, locale)}
129+
description="Total execution time saved"
130+
/>
131+
<StatCard
132+
label="Hit rate"
133+
value={
134+
totalRequests > 0 ? `${prettyNumber(hitRate, locale)}%` : "—"
135+
}
136+
description={`${prettyNumber(totalHits, locale)} hits / ${prettyNumber(totalRequests, locale)} total`}
137+
/>
138+
<StatCard
139+
label="Cache hits"
140+
value={prettyNumber(totalHits, locale)}
141+
description="Successful cache retrievals"
142+
/>
143+
<StatCard
144+
label="Cache misses"
145+
value={prettyNumber(totalMisses, locale)}
146+
description="Cache not found"
147+
/>
148+
</div>
149+
</div>
150+
151+
{/* Storage Section */}
152+
{diskTotal > 0 && (
153+
<div className="space-y-3 pt-2 border-t">
154+
<h3 className="text-sm font-semibold text-foreground">Storage</h3>
155+
<div className="grid grid-cols-1 gap-3">
156+
<StatCard
157+
label="Disk usage"
158+
value={formatBytes(diskTotal, locale)}
159+
description={
160+
diskToFree > 0
161+
? `${formatBytes(diskToFree, locale)} can be freed`
162+
: "Cache storage on disk"
163+
}
164+
/>
165+
</div>
166+
</div>
167+
)}
168+
169+
<div className="my-auto" />
170+
171+
{/* Actions Section */}
172+
<div className="pt-2 border-t">
173+
<ConfirmationButton
174+
title="Purge cache?"
175+
description="This will permanently delete all cached data. This action cannot be undone."
176+
confirmText="Purge"
177+
destructive={true}
178+
onConfirm={handlePurge}
179+
>
180+
<Button
181+
variant="outlineDestructive"
182+
size="xs"
183+
disabled={purging}
184+
className="w-full"
185+
>
186+
{purging ? (
187+
<Spinner size="small" className="w-3 h-3 mr-2" />
188+
) : (
189+
<Trash2Icon className="w-3 h-3 mr-2" />
190+
)}
191+
Purge Cache
192+
</Button>
193+
</ConfirmationButton>
194+
</div>
195+
</div>
196+
</div>
197+
);
198+
};
199+
200+
const StatCard: React.FC<{
201+
label: string;
202+
value: string;
203+
description?: string;
204+
}> = ({ label, value, description }) => {
205+
return (
206+
<div className="flex flex-col gap-1 p-3 rounded-lg border bg-card">
207+
<span className="text-xs text-muted-foreground">{label}</span>
208+
<span className="text-lg font-semibold">{value}</span>
209+
{description && (
210+
<span className="text-xs text-muted-foreground">{description}</span>
211+
)}
212+
</div>
213+
);
214+
};
215+
216+
export default CachePanel;

frontend/src/components/editor/chrome/panels/empty-state.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export const PanelEmptyState = ({
1818
<div className="mx-6 my-6 flex flex-col gap-2">
1919
<div className="flex flex-row gap-2 items-center">
2020
{icon &&
21-
React.cloneElement(icon, { className: "text-accent-foreground" })}
21+
React.cloneElement(icon, {
22+
className: "text-accent-foreground flex-shrink-0",
23+
})}
2224
<span className="mt-1 text-accent-foreground">{title}</span>
2325
</div>
2426
<span className="text-muted-foreground text-sm">{description}</span>

frontend/src/components/editor/chrome/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
BotMessageSquareIcon,
66
BoxIcon,
77
DatabaseIcon,
8+
DatabaseZapIcon,
89
FileTextIcon,
910
FolderTreeIcon,
1011
FunctionSquareIcon,
@@ -35,6 +36,7 @@ export type PanelType =
3536
| "scratchpad"
3637
| "chat"
3738
| "agents"
39+
| "cache"
3840
| "secrets"
3941
| "logs";
4042

@@ -77,6 +79,13 @@ export const PANELS: PanelDescriptor[] = [
7779
tooltip: "Manage packages",
7880
position: "sidebar",
7981
},
82+
{
83+
type: "cache",
84+
Icon: DatabaseZapIcon,
85+
tooltip: "Manage cache",
86+
position: "sidebar",
87+
hidden: !getFeatureFlag("cache_panel"),
88+
},
8089
{
8190
type: "outline",
8291
Icon: ScrollTextIcon,

frontend/src/components/editor/chrome/wrapper/app-chrome.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const LazySecretsPanel = React.lazy(() => import("../panels/secrets-panel"));
5656
const LazySnippetsPanel = React.lazy(() => import("../panels/snippets-panel"));
5757
const LazyTracingPanel = React.lazy(() => import("../panels/tracing-panel"));
5858
const LazyVariablePanel = React.lazy(() => import("../panels/variable-panel"));
59+
const LazyCachePanel = React.lazy(() => import("../panels/cache-panel"));
5960

6061
export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
6162
const { isSidebarOpen, isTerminalOpen, selectedPanel } = useChromeState();
@@ -176,6 +177,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
176177
{selectedPanel === "logs" && <LazyLogsPanel />}
177178
{selectedPanel === "tracing" && <LazyTracingPanel />}
178179
{selectedPanel === "secrets" && <LazySecretsPanel />}
180+
{selectedPanel === "cache" && <LazyCachePanel />}
179181
</TooltipProvider>
180182
</Suspense>
181183
</div>

frontend/src/components/ui/button.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ const buttonVariants = cva(
7070
link: "underline-offset-4 hover:underline text-link",
7171
linkDestructive:
7272
"underline-offset-4 hover:underline text-destructive underline-destructive",
73+
outlineDestructive:
74+
"border border-destructive text-destructive hover:bg-destructive/10",
7375
},
7476
size: {
7577
default: "h-10 py-2 px-4",
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* Copyright 2024 Marimo. All rights reserved. */
2+
3+
import React from "react";
4+
import {
5+
AlertDialog,
6+
AlertDialogAction,
7+
AlertDialogCancel,
8+
AlertDialogContent,
9+
AlertDialogDescription,
10+
AlertDialogDestructiveAction,
11+
AlertDialogFooter,
12+
AlertDialogHeader,
13+
AlertDialogTitle,
14+
AlertDialogTrigger,
15+
} from "./alert-dialog";
16+
17+
interface ConfirmationButtonProps {
18+
/**
19+
* The button element to trigger the confirmation dialog
20+
*/
21+
children: React.ReactElement;
22+
/**
23+
* Title of the confirmation dialog
24+
*/
25+
title: string;
26+
/**
27+
* Description/message of the confirmation dialog
28+
*/
29+
description: string;
30+
/**
31+
* Callback when the user confirms the action
32+
*/
33+
onConfirm: () => void;
34+
/**
35+
* Text for the confirm button (default: "Continue")
36+
*/
37+
confirmText?: string;
38+
/**
39+
* Text for the cancel button (default: "Cancel")
40+
*/
41+
cancelText?: string;
42+
/**
43+
* Whether to use destructive styling for the confirm button
44+
*/
45+
destructive?: boolean;
46+
}
47+
48+
export const ConfirmationButton: React.FC<ConfirmationButtonProps> = ({
49+
children,
50+
title,
51+
description,
52+
onConfirm,
53+
confirmText = "Continue",
54+
cancelText = "Cancel",
55+
destructive = false,
56+
}) => {
57+
const [open, setOpen] = React.useState(false);
58+
59+
const handleConfirm = () => {
60+
onConfirm();
61+
setOpen(false);
62+
};
63+
64+
const ActionComponent = destructive
65+
? AlertDialogDestructiveAction
66+
: AlertDialogAction;
67+
68+
return (
69+
<AlertDialog open={open} onOpenChange={setOpen}>
70+
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
71+
<AlertDialogContent>
72+
<AlertDialogHeader>
73+
<AlertDialogTitle>{title}</AlertDialogTitle>
74+
<AlertDialogDescription>{description}</AlertDialogDescription>
75+
</AlertDialogHeader>
76+
<AlertDialogFooter>
77+
<AlertDialogCancel>{cancelText}</AlertDialogCancel>
78+
<ActionComponent onClick={handleConfirm}>
79+
{confirmText}
80+
</ActionComponent>
81+
</AlertDialogFooter>
82+
</AlertDialogContent>
83+
</AlertDialog>
84+
);
85+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* Copyright 2024 Marimo. All rights reserved. */
2+
import { atom } from "jotai";
3+
import type { CacheInfoFetched } from "../kernel/messages";
4+
5+
export const cacheInfoAtom = atom<CacheInfoFetched | null>(null);

frontend/src/core/config/feature-flag.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface ExperimentalFeatures {
1111
rtc_v2: boolean;
1212
performant_table_charts: boolean;
1313
chat_modes: boolean;
14+
cache_panel: boolean;
1415
sql_linter: boolean;
1516
external_agents: boolean;
1617
sql_mode: boolean;
@@ -23,6 +24,7 @@ const defaultValues: ExperimentalFeatures = {
2324
rtc_v2: false,
2425
performant_table_charts: false,
2526
chat_modes: false,
27+
cache_panel: false,
2628
sql_linter: true,
2729
external_agents: import.meta.env.DEV,
2830
sql_mode: false,

0 commit comments

Comments
 (0)