mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
fix(sessions): don't evict loaded images whose waiters haven't taken them
With every cached image in use, a newly loaded entry (refs 0) was the only evictable one and got revoked before its waiting hook acquired it, leaving the image stuck on the loading placeholder; two loads finishing together could also evict each other. Count pending waiters per image and skip images that still have one when evicting.
This commit is contained in:
1 parent
f5a4bdc977
commit
3abbdf28a1
2 files changed
+69
-3
No files matched your search
@@ -292,12 +292,26 @@ interface CachedImage {
|
||||
|
||||
const imageCache = new Map<string, CachedImage>();
|
||||
const imageLoads = new Map<string, Promise<string>>();
|
||||
/** 每张图还在等加载结果、尚未占用的等待方个数;有等待方的图不淘汰 */
|
||||
const imageWaiters = new Map<string, number>();
|
||||
|
||||
/** 超出上限时按最久未用淘汰没人在用的图并 revoke */
|
||||
const startWaiting = (key: string) =>
|
||||
imageWaiters.set(key, (imageWaiters.get(key) ?? 0) + 1);
|
||||
|
||||
const stopWaiting = (key: string) => {
|
||||
const left = (imageWaiters.get(key) ?? 0) - 1;
|
||||
if (left > 0) imageWaiters.set(key, left);
|
||||
else imageWaiters.delete(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* 超出上限时按最久未用淘汰没人在用的图并 revoke。
|
||||
* 加载完、等待方还没来得及占用的图也算在用(几张图同时加载完时会互相淘汰)
|
||||
*/
|
||||
const evictImages = () => {
|
||||
for (const [key, entry] of imageCache) {
|
||||
if (imageCache.size <= SESSION_IMAGE_CACHE_SIZE) return;
|
||||
if (entry.refs > 0) continue;
|
||||
if (entry.refs > 0 || imageWaiters.has(key)) continue;
|
||||
URL.revokeObjectURL(entry.url);
|
||||
imageCache.delete(key);
|
||||
}
|
||||
@@ -347,6 +361,7 @@ export const clearSessionImageCache = () => {
|
||||
imageCache.forEach((entry) => URL.revokeObjectURL(entry.url));
|
||||
imageCache.clear();
|
||||
imageLoads.clear();
|
||||
imageWaiters.clear();
|
||||
};
|
||||
|
||||
export interface SessionImageResult {
|
||||
@@ -387,11 +402,16 @@ export const useSessionImage = (
|
||||
take();
|
||||
} else {
|
||||
setState({ url: null, isLoading: true, error: null });
|
||||
startWaiting(key);
|
||||
loadImageUrl(key, providerId!, sourcePath!, image!).then(
|
||||
() => {
|
||||
if (!cancelled) take();
|
||||
stopWaiting(key);
|
||||
// 已卸载就不占用;不再等它了,补一次淘汰以免超出上限
|
||||
if (cancelled) evictImages();
|
||||
else take();
|
||||
},
|
||||
(error: unknown) => {
|
||||
stopWaiting(key);
|
||||
if (!cancelled) setState({ url: null, isLoading: false, error });
|
||||
},
|
||||
);
|
||||
|
||||
@@ -413,6 +413,52 @@ describe("useSessionImage", () => {
|
||||
held.unmount();
|
||||
});
|
||||
|
||||
it("上限内的图全在用时,新加载的那张也拿得到 URL,不会被立刻淘汰", async () => {
|
||||
const mounted = [];
|
||||
for (let i = 0; i < SESSION_IMAGE_CACHE_SIZE; i += 1) {
|
||||
const hook = renderHook(() =>
|
||||
useSessionImage("codex", "/a", image(`/busy-${i}.png`)),
|
||||
);
|
||||
await waitFor(() => expect(hook.result.current.url).toBeTruthy());
|
||||
mounted.push(hook);
|
||||
}
|
||||
const extra = renderHook(() =>
|
||||
useSessionImage("codex", "/a", image("/extra.png")),
|
||||
);
|
||||
await waitFor(() => expect(extra.result.current.isLoading).toBe(false));
|
||||
expect(extra.result.current.url).toBe(
|
||||
`blob:${SESSION_IMAGE_CACHE_SIZE + 1}`,
|
||||
);
|
||||
expect(revokeObjectURL).not.toHaveBeenCalled();
|
||||
extra.unmount();
|
||||
mounted.forEach((hook) => hook.unmount());
|
||||
});
|
||||
|
||||
it("上限内的图全在用时,两张同时加载完也都拿得到 URL,不会互相淘汰", async () => {
|
||||
const mounted = [];
|
||||
for (let i = 0; i < SESSION_IMAGE_CACHE_SIZE; i += 1) {
|
||||
const hook = renderHook(() =>
|
||||
useSessionImage("codex", "/a", image(`/busy-${i}.png`)),
|
||||
);
|
||||
await waitFor(() => expect(hook.result.current.url).toBeTruthy());
|
||||
mounted.push(hook);
|
||||
}
|
||||
const resolvers: Array<(value: ArrayBuffer) => void> = [];
|
||||
api.getImage.mockImplementation(
|
||||
() => new Promise<ArrayBuffer>((ok) => resolvers.push(ok)),
|
||||
);
|
||||
const x = renderHook(() => useSessionImage("codex", "/a", image("/x.png")));
|
||||
const y = renderHook(() => useSessionImage("codex", "/a", image("/y.png")));
|
||||
await act(async () => {
|
||||
resolvers.forEach((resolve) => resolve(new ArrayBuffer(3)));
|
||||
});
|
||||
await waitFor(() => expect(y.result.current.isLoading).toBe(false));
|
||||
expect(x.result.current.url).toBeTruthy();
|
||||
expect(y.result.current.url).toBeTruthy();
|
||||
expect(revokeObjectURL).not.toHaveBeenCalled();
|
||||
[x, y, ...mounted].forEach((hook) => hook.unmount());
|
||||
});
|
||||
|
||||
it("加载中卸载:不再更新状态", async () => {
|
||||
let resolve: (value: ArrayBuffer) => void = () => {};
|
||||
let reject: (error: Error) => void = () => {};
|
||||
|
||||
Reference in new issue
Block a user