썸네일을 일괄적으로 생성하는 경우 발생하던 Out of Memory 문제 해결

This commit is contained in:
static
2025-07-08 04:31:19 +09:00
parent 69b31ad9af
commit 18660844e6
2 changed files with 62 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
<script module lang="ts"> <script module lang="ts">
const subtexts = { const subtexts = {
queued: "대기 중",
"generation-pending": "준비 중", "generation-pending": "준비 중",
generating: "생성하는 중", generating: "생성하는 중",
"upload-pending": "업로드를 기다리는 중", "upload-pending": "업로드를 기다리는 중",

View File

@@ -8,6 +8,7 @@ import type { FileThumbnailUploadRequest } from "$lib/server/schemas";
import { requestFileDownload } from "$lib/services/file"; import { requestFileDownload } from "$lib/services/file";
export type GenerationStatus = export type GenerationStatus =
| "queued"
| "generation-pending" | "generation-pending"
| "generating" | "generating"
| "upload-pending" | "upload-pending"
@@ -23,6 +24,10 @@ interface File {
const workingFiles = new Map<number, Writable<GenerationStatus>>(); const workingFiles = new Map<number, Writable<GenerationStatus>>();
let queue: (() => void)[] = [];
let memoryUsage = 0;
const MEMORY_LIMIT = 100 * 1024 * 1024; // 100 MiB
export const persistentStates = $state({ export const persistentStates = $state({
files: [] as File[], files: [] as File[],
}); });
@@ -86,18 +91,66 @@ const requestThumbnailUpload = limitFunction(
{ concurrency: 4 }, { concurrency: 4 },
); );
const enqueue = async (
status: Writable<GenerationStatus> | undefined,
fileInfo: FileInfo,
priority = false,
) => {
if (status) {
status.set("queued");
} else {
status = writable("queued");
workingFiles.set(fileInfo.id, status);
persistentStates.files = persistentStates.files.map((file) =>
file.id === fileInfo.id ? { ...file, status } : file,
);
}
let resolver;
const promise = new Promise((resolve) => {
resolver = resolve;
});
if (priority) {
queue = [() => resolver!(), ...queue];
} else {
queue.push(resolver!);
}
await promise;
};
export const requestThumbnailGeneration = async (fileInfo: FileInfo) => { export const requestThumbnailGeneration = async (fileInfo: FileInfo) => {
let status = workingFiles.get(fileInfo.id); let status = workingFiles.get(fileInfo.id);
if (status && get(status) !== "error") return; if (status && get(status) !== "error") return;
status = writable("generation-pending"); if (workingFiles.values().some((status) => get(status) !== "error")) {
workingFiles.set(fileInfo.id, status); await enqueue(status, fileInfo);
persistentStates.files = persistentStates.files.map((file) => }
file.id === fileInfo.id ? { ...file, status } : file, while (memoryUsage >= MEMORY_LIMIT) {
); await enqueue(status, fileInfo, true);
}
if (status) {
status.set("generation-pending");
} else {
status = writable("generation-pending");
workingFiles.set(fileInfo.id, status);
persistentStates.files = persistentStates.files.map((file) =>
file.id === fileInfo.id ? { ...file, status } : file,
);
}
let fileSize = 0;
try { try {
const file = await requestFileDownload(fileInfo.id, fileInfo.contentIv!, fileInfo.dataKey!); const file = await requestFileDownload(fileInfo.id, fileInfo.contentIv!, fileInfo.dataKey!);
fileSize = file.byteLength;
memoryUsage += fileSize;
if (memoryUsage < MEMORY_LIMIT) {
queue.shift()?.();
}
const thumbnail = await generateThumbnail( const thumbnail = await generateThumbnail(
status, status,
file, file,
@@ -110,5 +163,8 @@ export const requestThumbnailGeneration = async (fileInfo: FileInfo) => {
} }
} catch { } catch {
status.set("error"); status.set("error");
} finally {
memoryUsage -= fileSize;
queue.shift()?.();
} }
}; };