112 lines
11 KiB
TypeScript
112 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import path from "node:path";
|
|
import { performance } from "node:perf_hooks";
|
|
import { describe, it } from "node:test";
|
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
import { createDatabaseContext } from "../src/db/database-context.js";
|
|
import { ExternalInitialImportProjectCommandRepository } from "../src/db/repositories/external-initial-import-project-command.repository.js";
|
|
import { ExternalFollowUpImportProjectCommandRepository } from "../src/db/repositories/external-follow-up-import-project-command.repository.js";
|
|
import { ExternalModelStateRepository } from "../src/db/repositories/external-model-state.repository.js";
|
|
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
|
|
import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/project-state-restore-command.repository.js";
|
|
import { ProjectTransferRepository } from "../src/db/repositories/project-transfer.repository.js";
|
|
import { readProjectStateSnapshot } from "../src/db/repositories/project-state-snapshot.persistence.js";
|
|
import { externalModelConfigurations } from "../src/db/schema/external-model-configurations.js";
|
|
import { projects } from "../src/db/schema/projects.js";
|
|
import { createEmptyExternalModelState, createExternalInitialImportProjectCommand } from "../src/domain/models/external-initial-import-project-command.model.js";
|
|
import { createExternalFollowUpImportProjectCommand } from "../src/domain/models/external-follow-up-import-project-command.model.js";
|
|
import { createExternalXlsxFollowUpImportPlan } from "../src/external-model/application/external-xlsx-follow-up-import.js";
|
|
import { buildExternalXlsxFollowUpTarget } from "../src/external-model/application/external-xlsx-follow-up-target.js";
|
|
import { buildExternalXlsxInitialImportTarget } from "../src/external-model/application/external-xlsx-initial-import-target.js";
|
|
import { createExternalXlsxInitialImportPlan } from "../src/external-model/application/external-xlsx-initial-import.js";
|
|
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js";
|
|
import { createVirtualRevitObjectWindow, filterAndSortRevitObjects } from "../src/frontend/components/revit-object-table-model.js";
|
|
import type { ExternalProjectObjectDto } from "../src/frontend/types.js";
|
|
import { createLargeRevitXlsxFixture, externalXlsxTestConfiguration } from "./fixtures/revit-xlsx-fixtures.js";
|
|
|
|
const objectCount = 5_000;
|
|
const deviceTypeId = "10000000-0000-4000-8000-000000000001";
|
|
const configuration = {
|
|
...createDefaultExternalModelConfiguration(),
|
|
...externalXlsxTestConfiguration,
|
|
deviceTypes: [{ id: deviceTypeId, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" } }],
|
|
familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId, quantityRule: "socketCount" as const }],
|
|
};
|
|
|
|
describe("Linear XLSX scale", () => {
|
|
it("imports, persists, filters and virtualizes 5,000 synthetic objects within bounded resources", async () => {
|
|
const heapBefore = process.memoryUsage().heapUsed;
|
|
const bytes = await createLargeRevitXlsxFixture(objectCount);
|
|
const parseStarted = performance.now();
|
|
const { plan, document } = await createExternalXlsxInitialImportPlan({ fileName: "large.xlsx", bytes, configuration, catalogs: { floors: [], rooms: [], distributionBoards: [], projectDevices: [] } });
|
|
const parseMs = performance.now() - parseStarted;
|
|
assert.equal(plan.objectCount, objectCount);
|
|
assert.ok(bytes.byteLength < 10 * 1024 * 1024);
|
|
|
|
let sequence = 0;
|
|
const target = buildExternalXlsxInitialImportTarget({ projectId: "project-scale", expectedRevision: 0, sourceName: "Revit", importedAtIso: "2026-09-30T12:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document, plan, roomDecisions: plan.sourceRooms.map((room) => ({ sourceRoomKey: room.sourceRoomKey, roomId: null, defaultDistributionBoardId: null })), familyProjectDeviceDecisions: plan.familyGroups.map((family) => ({ familyAndType: family.familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: null, createId: () => `30000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
|
const configurationSnapshot = { id: "config-scale", projectId: "project-scale", configuration };
|
|
const command = createExternalInitialImportProjectCommand(configurationSnapshot, configurationSnapshot, createEmptyExternalModelState(), target);
|
|
const commandBytes = Buffer.byteLength(JSON.stringify(command));
|
|
|
|
const context = createDatabaseContext(":memory:");
|
|
try {
|
|
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
|
|
context.db.insert(projects).values({ id: "project-scale", name: "Lasttest" }).run();
|
|
context.db.insert(externalModelConfigurations).values(configurationSnapshot).run();
|
|
const transactionStarted = performance.now();
|
|
new ExternalInitialImportProjectCommandRepository(context.db).execute({ projectId: "project-scale", expectedRevision: 0, source: "user", command });
|
|
const transactionMs = performance.now() - transactionStarted;
|
|
let snapshot = readProjectStateSnapshot(context.db, "project-scale");
|
|
assert.ok(snapshot);
|
|
assert.equal(snapshot.state.externalModel.objects.length, objectCount);
|
|
|
|
const currentState = new ExternalModelStateRepository(context.db).getByProject("project-scale").state;
|
|
const returnValues = new Map(currentState.objects.map((object) => [object.id, { circuitCombined: object.acceptedSourceValues.circuitCombined, boardName: object.acceptedSourceValues.boardName, circuitIdentifier: object.acceptedSourceValues.circuitIdentifier, designation: object.acceptedSourceValues.designation, electricalPowerW: object.acceptedSourceValues.electricalPowerW, voltageV: object.acceptedSourceValues.voltageV, currentA: object.acceptedSourceValues.currentA }]));
|
|
const followParseStarted = performance.now();
|
|
const follow = await createExternalXlsxFollowUpImportPlan({ fileName: "large-follow.xlsx", bytes, configuration, catalogs: { floors: [], rooms: [], distributionBoards: [], projectDevices: [] }, currentState, currentReturnValuesByObjectId: returnValues });
|
|
const followParseMs = performance.now() - followParseStarted;
|
|
const followTarget = buildExternalXlsxFollowUpTarget({ projectId: "project-scale", expectedRevision: 1, importedAtIso: "2026-09-30T13:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document: follow.document, plan: follow.plan, currentState, sourceChangeDecisions: [], roomDecisions: [], familyProjectDeviceDecisions: [], objectsWithoutRoomDefaultDistributionBoardId: null, rowsById: new Map(), createId: () => `40000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
|
const followCommand = createExternalFollowUpImportProjectCommand({ expectedConfiguration: configurationSnapshot, targetConfiguration: configurationSnapshot, expectedState: currentState, targetState: followTarget.targetState, rows: followTarget.rowTransitions });
|
|
const followTransactionStarted = performance.now();
|
|
new ExternalFollowUpImportProjectCommandRepository(context.db).execute({ projectId: "project-scale", expectedRevision: 1, source: "user", command: followCommand });
|
|
const followTransactionMs = performance.now() - followTransactionStarted;
|
|
const followedState = new ExternalModelStateRepository(context.db).getByProject("project-scale").state;
|
|
assert.equal(followedState.importBatches.length, 2);
|
|
assert.equal(followedState.importBatches[0]!.document, null);
|
|
assert.equal(followedState.objects.length, objectCount);
|
|
|
|
const namedSnapshot = new ProjectSnapshotRepository(context.db).createNamed({ projectId: "project-scale", expectedRevision: 2, name: "Lasttest" });
|
|
assert.ok(namedSnapshot);
|
|
const preparedRestore = new ProjectSnapshotRepository(context.db).prepareRestore("project-scale", namedSnapshot.id);
|
|
assert.ok(preparedRestore);
|
|
new ProjectStateRestoreCommandRepository(context.db).execute({ projectId: "project-scale", expectedRevision: 2, source: "restore", command: preparedRestore.command });
|
|
const transferRepository = new ProjectTransferRepository(context.db);
|
|
const transfer = transferRepository.exportProject("project-scale");
|
|
assert.ok(transfer);
|
|
const duplicate = transferRepository.importDuplicate(transfer);
|
|
assert.equal(readProjectStateSnapshot(context.db, duplicate.projectId)!.state.externalModel.objects.length, objectCount);
|
|
snapshot = readProjectStateSnapshot(context.db, "project-scale");
|
|
assert.ok(snapshot);
|
|
|
|
const clientRows: ExternalProjectObjectDto[] = target.objects.map((object) => ({ id: object.id, worksharingId: object.worksharingId, ifcGuid: object.ifcGuid, deviceTypeId: object.deviceTypeId, familyAndType: object.acceptedSourceValues.familyAndType, selectionMarker: String(object.acceptedSourceValues.selectionMarker ?? ""), sourceCircuitIdentifier: "", sourceRoomNumber: String(object.acceptedSourceValues.roomNumber ?? ""), sourceRoomName: String(object.acceptedSourceValues.roomName ?? ""), roomId: null, distributionBoardId: object.distributionBoardId, linkedProjectDeviceId: object.linkedProjectDeviceId, circuitDeviceRowId: object.circuitDeviceRowId, presenceStatus: object.presenceStatus, planningValues: object.planningValues }));
|
|
const clientStarted = performance.now();
|
|
const filtered = filterAndSortRevitObjects(clientRows, { search: "Raum 42", boardFilter: "all", assignmentFilter: "all", sort: "worksharing" });
|
|
const window = createVirtualRevitObjectWindow(filtered, 4_400, 44, 520);
|
|
const clientMs = performance.now() - clientStarted;
|
|
assert.ok(filtered.length > 0);
|
|
assert.ok(window.rows.length <= 22);
|
|
|
|
const heapDeltaBytes = Math.max(0, process.memoryUsage().heapUsed - heapBefore);
|
|
const snapshotBytes = Buffer.byteLength(snapshot.payloadJson);
|
|
assert.ok(parseMs < 15_000, `XLSX parse took ${parseMs.toFixed(0)} ms`);
|
|
assert.ok(transactionMs < 15_000, `Import transaction took ${transactionMs.toFixed(0)} ms`);
|
|
assert.ok(followParseMs < 15_000, `Follow-up parse took ${followParseMs.toFixed(0)} ms`);
|
|
assert.ok(followTransactionMs < 15_000, `Follow-up transaction took ${followTransactionMs.toFixed(0)} ms`);
|
|
assert.ok(clientMs < 1_000, `Client projection took ${clientMs.toFixed(0)} ms`);
|
|
assert.ok(heapDeltaBytes < 768 * 1024 * 1024, `Heap grew by ${heapDeltaBytes} bytes`);
|
|
assert.ok(commandBytes < 100 * 1024 * 1024);
|
|
console.info("R10 scale metrics", { objectCount, workbookBytes: bytes.byteLength, commandBytes, snapshotBytes, parseMs: Math.round(parseMs), transactionMs: Math.round(transactionMs), followParseMs: Math.round(followParseMs), followTransactionMs: Math.round(followTransactionMs), clientMs: Math.round(clientMs), heapDeltaBytes });
|
|
} finally { context.close(); }
|
|
});
|
|
});
|