From 2a708b01b1945988faa76b536bd9a48929f703a0 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 13 Sep 2026 12:14:52 +0200 Subject: [PATCH] perf(import): adjustable concurrency and skip redundant SWF downloads Raise batch concurrency (furni 3->12, clone 10->12) with a Speed control next to Translate. Skip the SWF download when a .nitro bundle already exists on disk (color variants share the base nitro), and stop flagging that as a failed download. --- src/app/api/admin/import/furni/batch/route.ts | 2 +- src/components/admin/studio/studio-client.tsx | 33 +++++++++++++++++-- src/lib/services/furni-import-repair.test.ts | 1 + src/lib/services/furni-import.ts | 9 ++++- src/lib/services/import/core/sse-batch.ts | 2 +- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/app/api/admin/import/furni/batch/route.ts b/src/app/api/admin/import/furni/batch/route.ts index dc571216..5cbfd36d 100644 --- a/src/app/api/admin/import/furni/batch/route.ts +++ b/src/app/api/admin/import/furni/batch/route.ts @@ -43,7 +43,7 @@ export const POST = withAdmin( async (request, ctx) => { const body = await request.json(); const rawItems: BatchItem[] = body.items || []; - const concurrency = Math.min(Math.max(body.concurrency || 1, 1), 3); + const concurrency = Math.min(Math.max(body.concurrency || 3, 1), 12); const sourceId: string | undefined = body.sourceId; if (rawItems.length === 0) return apiError("No items to import", 400); diff --git a/src/components/admin/studio/studio-client.tsx b/src/components/admin/studio/studio-client.tsx index 1b1823b7..20236d83 100644 --- a/src/components/admin/studio/studio-client.tsx +++ b/src/components/admin/studio/studio-client.tsx @@ -269,6 +269,8 @@ export function StudioClient({ null, ); const batchAbortRef = useRef(null); + // How many items the batch endpoints process in parallel. + const [concurrency, setConcurrency] = useState(4); // Nitro editor const [editingClassname, setEditingClassname] = useState(null); @@ -834,7 +836,7 @@ export function StudioClient({ revision: i.revision, category: i.category, })), - concurrency: 3, + concurrency, translate, langs: translate && translateLangs.length > 0 ? translateLangs : undefined, @@ -979,7 +981,7 @@ export function StudioClient({ body: JSON.stringify({ sourceId: activeSource, classnames: failed, - concurrency: 10, + concurrency, final: true, }), signal: abort.signal, @@ -1561,6 +1563,33 @@ export function StudioClient({
{" "} + {/* ── Import speed (parallel items per batch) ─────────── */} +
+ + Speed + + +
{/* ── Translate control (per-import) ─────────────── */}
diff --git a/src/lib/services/furni-import-repair.test.ts b/src/lib/services/furni-import-repair.test.ts index 5c24a05d..e11112c9 100644 --- a/src/lib/services/furni-import-repair.test.ts +++ b/src/lib/services/furni-import-repair.test.ts @@ -152,6 +152,7 @@ describe("completion of existing furniture", () => { it("imports recycler_kintsugiB with an allocated ID throughout SQL and furnidata", async () => { vi.clearAllMocks(); + mocks.nitroExists = false; const selections = [[], [{ itemName: "recycler_kintsugi" }]]; mocks.select.mockImplementation(() => ({ from: () => ({ where: () => ({ limit: async () => selections.shift() }) }), diff --git a/src/lib/services/furni-import.ts b/src/lib/services/furni-import.ts index 9104e534..247afeb8 100644 --- a/src/lib/services/furni-import.ts +++ b/src/lib/services/furni-import.ts @@ -711,7 +711,13 @@ export async function importSingleFurni(params: { iconFailure = detail; warnings.push(`Icon download failed: ${detail}`); }), - params.providedNitro || (params.preserveAssets && existsSync(nitroPath)) + // Skip the SWF download entirely when a .nitro bundle already exists on + // disk — this happens for colour variants that share the base classname's + // nitro file. The SWF is only used for Nitro conversion (not needed) and + // icon extraction (the nitro bundle is used as a fallback for that below). + existsSync(/*turbopackIgnore: true*/ nitroPath) || + params.providedNitro || + (params.preserveAssets && existsSync(nitroPath)) ? Promise.resolve(false) : (preservingExisting || params.preserveAssets) && existsSync(swfPath) ? Promise.resolve(true) @@ -766,6 +772,7 @@ export async function importSingleFurni(params: { !swfOk && !nitroDownloadOk && !params.providedNitro && + !existsSync(/*turbopackIgnore: true*/ nitroPath) && !(params.preserveAssets && existsSync(nitroPath)) ) warnings.push(`SWF download failed: ${swfFailure || "unknown cause"}`); diff --git a/src/lib/services/import/core/sse-batch.ts b/src/lib/services/import/core/sse-batch.ts index 98e1de0f..5a83dde6 100644 --- a/src/lib/services/import/core/sse-batch.ts +++ b/src/lib/services/import/core/sse-batch.ts @@ -33,7 +33,7 @@ export interface RunSseBatchOptions { */ export function runSseBatch(opts: RunSseBatchOptions): Response { const { items, labelOf, worker, signal, flush } = opts; - const concurrency = Math.min(Math.max(opts.concurrency || 3, 1), 10); + const concurrency = Math.min(Math.max(opts.concurrency || 3, 1), 12); const encoder = new TextEncoder(); const ac = new AbortController();