perf(import): adjustable concurrency and skip redundant SWF downloads
CI / check (push) Successful in 2m22s
CI / deploy (push) Successful in 1m12s
CI / publish-container (push) Successful in 44s

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.
This commit is contained in:
openhands committed 2026-09-13 12:14:52 +02:00
1 parent 641b6b8cb8
commit 2a708b01b1
5 files changed
+42 -5

No files matched your search

@@ -43,7 +43,7 @@ export const POST = withAdmin(
async (request, ctx) => { async (request, ctx) => {
const body = await request.json(); const body = await request.json();
const rawItems: BatchItem[] = body.items || []; 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; const sourceId: string | undefined = body.sourceId;
if (rawItems.length === 0) return apiError("No items to import", 400); if (rawItems.length === 0) return apiError("No items to import", 400);
+31 -2
View File
@@ -269,6 +269,8 @@ export function StudioClient({
null, null,
); );
const batchAbortRef = useRef<AbortController | null>(null); const batchAbortRef = useRef<AbortController | null>(null);
// How many items the batch endpoints process in parallel.
const [concurrency, setConcurrency] = useState(4);
// Nitro editor // Nitro editor
const [editingClassname, setEditingClassname] = useState<string | null>(null); const [editingClassname, setEditingClassname] = useState<string | null>(null);
@@ -834,7 +836,7 @@ export function StudioClient({
revision: i.revision, revision: i.revision,
category: i.category, category: i.category,
})), })),
concurrency: 3, concurrency,
translate, translate,
langs: langs:
translate && translateLangs.length > 0 ? translateLangs : undefined, translate && translateLangs.length > 0 ? translateLangs : undefined,
@@ -979,7 +981,7 @@ export function StudioClient({
body: JSON.stringify({ body: JSON.stringify({
sourceId: activeSource, sourceId: activeSource,
classnames: failed, classnames: failed,
concurrency: 10, concurrency,
final: true, final: true,
}), }),
signal: abort.signal, signal: abort.signal,
@@ -1561,6 +1563,33 @@ export function StudioClient({
</summary> </summary>
<div className="flex max-h-48 flex-wrap items-center gap-3 overflow-auto py-3"> <div className="flex max-h-48 flex-wrap items-center gap-3 overflow-auto py-3">
{" "} {" "}
{/* ── Import speed (parallel items per batch) ─────────── */}
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[var(--admin-text-muted)]">
Speed
</span>
<Select
value={String(concurrency)}
onValueChange={(value) =>
setConcurrency(Math.max(1, Number(value)))
}
>
<SelectTrigger
aria-label="Import speed"
className="h-8 w-24 text-xs"
>
<SelectValue>{`${concurrency} parallel`}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 (gentle)</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="4">4 (recommended)</SelectItem>
<SelectItem value="6">6</SelectItem>
<SelectItem value="8">8</SelectItem>
<SelectItem value="12">12 (turbo)</SelectItem>
</SelectContent>
</Select>
</div>
{/* ── Translate control (per-import) ─────────────── */} {/* ── Translate control (per-import) ─────────────── */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex h-8 items-center gap-1.5 rounded-md border border-[var(--admin-border)] px-2.5"> <div className="flex h-8 items-center gap-1.5 rounded-md border border-[var(--admin-border)] px-2.5">
@@ -152,6 +152,7 @@ describe("completion of existing furniture", () => {
it("imports recycler_kintsugiB with an allocated ID throughout SQL and furnidata", async () => { it("imports recycler_kintsugiB with an allocated ID throughout SQL and furnidata", async () => {
vi.clearAllMocks(); vi.clearAllMocks();
mocks.nitroExists = false;
const selections = [[], [{ itemName: "recycler_kintsugi" }]]; const selections = [[], [{ itemName: "recycler_kintsugi" }]];
mocks.select.mockImplementation(() => ({ mocks.select.mockImplementation(() => ({
from: () => ({ where: () => ({ limit: async () => selections.shift() }) }), from: () => ({ where: () => ({ limit: async () => selections.shift() }) }),
+8 -1
View File
@@ -711,7 +711,13 @@ export async function importSingleFurni(params: {
iconFailure = detail; iconFailure = detail;
warnings.push(`Icon download failed: ${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) ? Promise.resolve(false)
: (preservingExisting || params.preserveAssets) && existsSync(swfPath) : (preservingExisting || params.preserveAssets) && existsSync(swfPath)
? Promise.resolve(true) ? Promise.resolve(true)
@@ -766,6 +772,7 @@ export async function importSingleFurni(params: {
!swfOk && !swfOk &&
!nitroDownloadOk && !nitroDownloadOk &&
!params.providedNitro && !params.providedNitro &&
!existsSync(/*turbopackIgnore: true*/ nitroPath) &&
!(params.preserveAssets && existsSync(nitroPath)) !(params.preserveAssets && existsSync(nitroPath))
) )
warnings.push(`SWF download failed: ${swfFailure || "unknown cause"}`); warnings.push(`SWF download failed: ${swfFailure || "unknown cause"}`);
+1 -1
View File
@@ -33,7 +33,7 @@ export interface RunSseBatchOptions<T> {
*/ */
export function runSseBatch<T>(opts: RunSseBatchOptions<T>): Response { export function runSseBatch<T>(opts: RunSseBatchOptions<T>): Response {
const { items, labelOf, worker, signal, flush } = opts; 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 encoder = new TextEncoder();
const ac = new AbortController(); const ac = new AbortController();