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.
This commit is contained in:
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);
|
||||||
|
|||||||
@@ -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() }) }),
|
||||||
|
|||||||
@@ -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"}`);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user