perf: offload SWF→Nitro conversion to a worker-thread pool
The synchronous convertSwfToNitro / extractIconFromSwf calls (pure CPU, no DB/network) now run in a small worker pool so the main Node event loop stays free during imports — the dominant import-time CPU spike is moved off the request thread. The pool degrades gracefully to a synchronous fallback if workers can't be created, and is skipped in the test environment.
This commit is contained in:
1 parent
7116f49e2b
commit
8d1b09f151
6 files changed
+240
-14
No files matched your search
@@ -24,10 +24,20 @@ vi.mock("@/lib/services/import/core/download", () => ({
|
|||||||
validateSwfBytes: vi.fn(),
|
validateSwfBytes: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/services/swf-to-nitro", () => ({
|
vi.mock("@/lib/services/swf-to-nitro", () => {
|
||||||
convertSwfToNitro: vi.fn(),
|
const convertSwfToNitro = vi.fn();
|
||||||
extractIconFromSwf: vi.fn(),
|
const extractIconFromSwf = vi.fn();
|
||||||
}));
|
return {
|
||||||
|
convertSwfToNitro,
|
||||||
|
extractIconFromSwf,
|
||||||
|
convertSwfToNitroAsync: vi.fn(async (buffer: Buffer, classname: string) =>
|
||||||
|
convertSwfToNitro(buffer, classname),
|
||||||
|
),
|
||||||
|
extractIconFromSwfAsync: vi.fn(async (buffer: Buffer, classname: string) =>
|
||||||
|
extractIconFromSwf(buffer, classname),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import {
|
import {
|
||||||
autoPriceFurni,
|
autoPriceFurni,
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import {
|
|||||||
} from "@/lib/services/import/core/download";
|
} from "@/lib/services/import/core/download";
|
||||||
import {
|
import {
|
||||||
type ConversionResult,
|
type ConversionResult,
|
||||||
convertSwfToNitro,
|
convertSwfToNitroAsync,
|
||||||
extractIconFromSwf,
|
extractIconFromSwfAsync,
|
||||||
parseNitroBundle,
|
parseNitroBundle,
|
||||||
} from "@/lib/services/swf-to-nitro";
|
} from "@/lib/services/swf-to-nitro";
|
||||||
import type { ImportSingleResult } from "@/types/furni";
|
import type { ImportSingleResult } from "@/types/furni";
|
||||||
@@ -766,7 +766,7 @@ export async function importSingleFurni(params: {
|
|||||||
if (!iconOk && (swfOk || existsSync(/*turbopackIgnore: true*/ swfPath))) {
|
if (!iconOk && (swfOk || existsSync(/*turbopackIgnore: true*/ swfPath))) {
|
||||||
try {
|
try {
|
||||||
const swfBuf = await fs.readFile(/*turbopackIgnore: true*/ swfPath);
|
const swfBuf = await fs.readFile(/*turbopackIgnore: true*/ swfPath);
|
||||||
const iconPng = extractIconFromSwf(swfBuf, baseClassname);
|
const iconPng = await extractIconFromSwfAsync(swfBuf, baseClassname);
|
||||||
if (iconPng) {
|
if (iconPng) {
|
||||||
await fs.writeFile(/*turbopackIgnore: true*/ iconPath, iconPng);
|
await fs.writeFile(/*turbopackIgnore: true*/ iconPath, iconPng);
|
||||||
iconOk = true;
|
iconOk = true;
|
||||||
@@ -795,7 +795,10 @@ export async function importSingleFurni(params: {
|
|||||||
try {
|
try {
|
||||||
if (existsSync(/*turbopackIgnore: true*/ swfPath)) {
|
if (existsSync(/*turbopackIgnore: true*/ swfPath)) {
|
||||||
const swfBuffer = await fs.readFile(/*turbopackIgnore: true*/ swfPath);
|
const swfBuffer = await fs.readFile(/*turbopackIgnore: true*/ swfPath);
|
||||||
conversionResult = convertSwfToNitro(swfBuffer, baseClassname);
|
conversionResult = await convertSwfToNitroAsync(
|
||||||
|
swfBuffer,
|
||||||
|
baseClassname,
|
||||||
|
);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
/*turbopackIgnore: true*/ nitroPath,
|
/*turbopackIgnore: true*/ nitroPath,
|
||||||
conversionResult.bundle,
|
conversionResult.bundle,
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ vi.mock("@/lib/services/import/core/download", async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("@/lib/services/swf-to-nitro", () => ({
|
vi.mock("@/lib/services/swf-to-nitro", () => {
|
||||||
convertSwfToNitro: vi.fn(() => ({
|
const convertSwfToNitro = vi.fn((_buffer: Buffer, _classname: string) => ({
|
||||||
bundle: Buffer.alloc(256, 0x42),
|
bundle: Buffer.alloc(256, 0x42),
|
||||||
classname: "n/a",
|
classname: "n/a",
|
||||||
dimensions: { x: 1, y: 1, z: 1 },
|
dimensions: { x: 1, y: 1, z: 1 },
|
||||||
@@ -61,10 +61,23 @@ vi.mock("@/lib/services/swf-to-nitro", () => ({
|
|||||||
defaultdir: 0,
|
defaultdir: 0,
|
||||||
partcolors: { color: [] },
|
partcolors: { color: [] },
|
||||||
},
|
},
|
||||||
})),
|
}));
|
||||||
extractIconFromSwf: vi.fn(() => Buffer.from("fake-icon")),
|
const extractIconFromSwf = vi.fn((_buffer: Buffer, _classname: string) =>
|
||||||
parseNitroBundle: vi.fn(() => ({ json: null })),
|
Buffer.from("fake-icon"),
|
||||||
}));
|
);
|
||||||
|
const parseNitroBundle = vi.fn(() => ({ json: null }));
|
||||||
|
return {
|
||||||
|
convertSwfToNitro,
|
||||||
|
extractIconFromSwf,
|
||||||
|
parseNitroBundle,
|
||||||
|
convertSwfToNitroAsync: vi.fn(async (buffer: Buffer, classname: string) =>
|
||||||
|
convertSwfToNitro(buffer, classname),
|
||||||
|
),
|
||||||
|
extractIconFromSwfAsync: vi.fn(async (buffer: Buffer, classname: string) =>
|
||||||
|
extractIconFromSwf(buffer, classname),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("@/lib/services/habbo-furnidata-cache", () => ({
|
vi.mock("@/lib/services/habbo-furnidata-cache", () => ({
|
||||||
lookupOfficialHabboFurni: vi.fn(async () => null),
|
lookupOfficialHabboFurni: vi.fn(async () => null),
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,55 @@
|
|||||||
|
import { parentPort } from "node:worker_threads";
|
||||||
|
import { convertSwfToNitro, extractIconFromSwf } from "./index";
|
||||||
|
|
||||||
|
export interface ConvertRequest {
|
||||||
|
id: number;
|
||||||
|
type: "convert";
|
||||||
|
buffer: Buffer;
|
||||||
|
classname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IconRequest {
|
||||||
|
id: number;
|
||||||
|
type: "icon";
|
||||||
|
buffer: Buffer;
|
||||||
|
classname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkerRequest = ConvertRequest | IconRequest;
|
||||||
|
|
||||||
|
export type WorkerResponse =
|
||||||
|
| {
|
||||||
|
id: number;
|
||||||
|
ok: true;
|
||||||
|
result?: import("./index").ConversionResult;
|
||||||
|
icon?: Buffer | null;
|
||||||
|
}
|
||||||
|
| { id: number; ok: false; error: string };
|
||||||
|
|
||||||
|
if (parentPort) {
|
||||||
|
parentPort.on("message", (msg: WorkerRequest) => {
|
||||||
|
try {
|
||||||
|
if (msg.type === "convert") {
|
||||||
|
const result = convertSwfToNitro(msg.buffer, msg.classname);
|
||||||
|
parentPort!.postMessage({
|
||||||
|
id: msg.id,
|
||||||
|
ok: true,
|
||||||
|
result,
|
||||||
|
} satisfies WorkerResponse);
|
||||||
|
} else {
|
||||||
|
const icon = extractIconFromSwf(msg.buffer, msg.classname);
|
||||||
|
parentPort!.postMessage({
|
||||||
|
id: msg.id,
|
||||||
|
ok: true,
|
||||||
|
icon: icon ?? null,
|
||||||
|
} satisfies WorkerResponse);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
parentPort!.postMessage({
|
||||||
|
id: msg.id,
|
||||||
|
ok: false,
|
||||||
|
error: (err as Error).message,
|
||||||
|
} satisfies WorkerResponse);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -50,6 +50,11 @@ export type {
|
|||||||
ConversionResult,
|
ConversionResult,
|
||||||
FurniMetadata,
|
FurniMetadata,
|
||||||
} from "@/types/furni";
|
} from "@/types/furni";
|
||||||
|
// Worker-pool wrappers — run the CPU-heavy conversion off the main thread.
|
||||||
|
export {
|
||||||
|
convertSwfToNitroAsync,
|
||||||
|
extractIconFromSwfAsync,
|
||||||
|
} from "./conversion-pool";
|
||||||
// Re-export public API from submodules
|
// Re-export public API from submodules
|
||||||
export { createNitroBundle, parseNitroBundle } from "./nitro-builder";
|
export { createNitroBundle, parseNitroBundle } from "./nitro-builder";
|
||||||
export { computeDefaultDir, extractPartColors } from "./xml-processor";
|
export { computeDefaultDir, extractPartColors } from "./xml-processor";
|
||||||
|
|||||||
Reference in new issue
Block a user