/* * 스케일 테스트 트윈 벤치 — 미국 공장(WMS+MES+YMS) 형태를 대규모로 스케일업, 5종 동기 엔티티 전부 부하. * 동기 엔티티 = locations·items(재고)·equipment·tasks·orders (노드/무버뿐이 아님). area 는 계층(건물>층>존). * 측정(가정 아닌 실측): masterToTwin(구조 생성)·apply(혼합 이벤트 처리량)·snapshot(전체 투영)·buildEntityDeltas(브로드캐스팅, 't1')·heap. * 순수 커널+호스트 매핑(shell·DB 무관) → node:test. area 는 공간층(projector State 밖)이라 처리량엔 무영향 — 구조 생성 비용에만. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { StateProjector } from '@operato/twin-kernel' import { objectEvent, BIZSTEP, DISP, OP_EVENT } from '@operato/ops-contract' import { masterToTwin } from '../server/service/reference/reference-master.ts' import { buildEntityDeltas } from '../shared/entity-delta.ts' const T0 = '2026-07-20T00:00:00.000Z' const MB = (b: number) => Math.round(b / 1048576) /** 미국 공장형 대규모 master 생성: 건물>층>존 area 계층 + 노드(storage/dock/station) + 무버(forklift/cutter/welder/hostler). */ function bigMaster(tier: { buildings: number; floorsPer: number; zonesPer: number; nodesPerZone: number; equipment: number }) { const locations: any[] = [] let nodeN = 0 for (let b = 0; b < tier.buildings; b++) { locations.push({ id: `bld-${b}`, type: 'building', role: 'area', name: `건물 ${b}` }) for (let f = 0; f < tier.floorsPer; f++) { locations.push({ id: `bld-${b}-flr-${f}`, type: 'floor', role: 'area', name: `층 ${b}-${f}`, parentId: `bld-${b}` }) for (let z = 0; z < tier.zonesPer; z++) { const zid = `bld-${b}-flr-${f}-zn-${z}` locations.push({ id: zid, type: 'zone', role: 'area', name: `존 ${b}-${f}-${z}`, parentId: `bld-${b}-flr-${f}` }) for (let n = 0; n < tier.nodesPerZone; n++) { const t = n % 4 === 0 ? 'dock' : n % 4 === 1 ? 'staging' : 'storage' locations.push({ id: `nd-${nodeN}`, type: t, role: 'location', name: `노드 ${nodeN}`, parentId: zid, capacity: 500, footprint: { x: nodeN % 100, y: (nodeN / 100) | 0, w: 1, h: 1 } }) nodeN++ } } } } const KINDS = ['forklift', 'cutter', 'welder', 'hostler'] const equipment = Array.from({ length: tier.equipment }, (_, i) => ({ id: `mv-${i}`, kind: KINDS[i % 4], homeLocationId: `nd-${i % nodeN}` })) return { master: { source: 'scale-us', system: 'wms' as const, siteName: 'US 공장(스케일)', space: { width: 500, depth: 300, unit: 'm', geo: { lat: 32.7, lon: -117.1 } }, locations, equipment }, locationCount: nodeN, areaCount: locations.filter(l => l.role === 'area').length } } function itemEnv(i: number, location: string): any { const ev = objectEvent({ eventTime: T0, action: 'OBSERVE' as any, bizStep: BIZSTEP.storing, disposition: DISP.sellable, epcList: [`urn:epc:id:sscc:0614141.${i}`], readPoint: location }) return { eventId: `i${i}`, eventType: `epcis.${ev.type}`, eventTime: T0, tenantId: 't', data: ev } } const equipmentEnv = (id: string, kind: string, loc: string): any => ({ eventId: id, eventType: OP_EVENT.equipment, eventTime: T0, tenantId: 't', data: { moverId: id, kind, status: 'running', location: loc } }) const taskEnv = (id: string, to: string): any => ({ eventId: id, eventType: OP_EVENT.task, eventTime: T0, tenantId: 't', data: { taskId: id, kind: 'move', status: 'in-progress', toNode: to } }) const orderEnv = (id: string): any => ({ eventId: id, eventType: OP_EVENT.order, eventTime: T0, tenantId: 't', data: { orderId: id, kind: 'inbound', status: 'in-progress', requested: 100, fulfilled: 50 } }) const TIERS = { medium: { buildings: 3, floorsPer: 3, zonesPer: 4, nodesPerZone: 8, equipment: 80, items: 30000, tasks: 2000, orders: 500 }, large: { buildings: 6, floorsPer: 4, zonesPer: 6, nodesPerZone: 12, equipment: 300, items: 120000, tasks: 10000, orders: 2000 } } for (const [name, tier] of Object.entries(TIERS)) { test(`스케일 트윈 [${name}] — 구조 생성 + 5종 엔티티 부하 실측`, () => { // 1) 구조 생성(masterToTwin) — 노드·무버·area 계층 const { master, locationCount, areaCount } = bigMaster(tier) let t = performance.now() const { model, spaceContent } = masterToTwin(master) const tMaster = performance.now() - t console.log(`\n [${name}] 구조: 노드 ${locationCount.toLocaleString()} · 무버 ${tier.equipment} · area ${areaCount}(계층) · masterToTwin ${tMaster.toFixed(0)}ms`) assert.equal(model.locations.length, locationCount) assert.equal((spaceContent.areas ?? []).length, areaCount, 'area 계층 생성') const proj = new StateProjector(model) const equipment = model.equipment as any[] // 2) apply 처리량 — 5종 혼합(items·equipment·tasks·orders) t = performance.now() for (let i = 0; i < tier.items; i++) proj.apply(itemEnv(i, `nd-${i % locationCount}`)) for (let i = 0; i < tier.equipment; i++) proj.apply(equipmentEnv(equipment[i].id, equipment[i].kind, `nd-${i % locationCount}`)) for (let i = 0; i < tier.tasks; i++) proj.apply(taskEnv(`tk-${i}`, `nd-${i % locationCount}`)) for (let i = 0; i < tier.orders; i++) proj.apply(orderEnv(`or-${i}`)) const total = tier.items + tier.equipment + tier.tasks + tier.orders const tApply = performance.now() - t console.log(` [${name}] apply: ${total.toLocaleString()} 이벤트(items+equipment+tasks+orders) ${tApply.toFixed(0)}ms = ${Math.round((total / tApply) * 1000).toLocaleString()} ev/s`) // 3) snapshot(전체 투영) — 5종 전부 let s: any const iters = 30 t = performance.now() for (let k = 0; k < iters; k++) s = proj.snapshot() const tSnap = (performance.now() - t) / iters console.log(` [${name}] snapshot: ${tSnap.toFixed(2)}ms/call (items ${s.items.length.toLocaleString()}·locations ${s.locations.length.toLocaleString()}·equipment ${s.equipment.length}·tasks ${s.tasks.length}·orders ${s.orders.length}) → 최대 ${Math.round(1000 / tSnap).toLocaleString()} snap/s`) // 4) 브로드캐스팅 payload(buildEntityDeltas) t = performance.now() for (let k = 0; k < iters; k++) buildEntityDeltas(s, 't1') const tDelta = (performance.now() - t) / iters console.log(` [${name}] buildEntityDeltas: ${tDelta.toFixed(2)}ms/call`) console.log(` [${name}] heap: ${MB(process.memoryUsage().heapUsed)}MB`) // 정합 sanity assert.equal(s.equipment.length, tier.equipment) assert.equal(s.orders.length, tier.orders) assert.ok(s.items.length > 0) }) }