perf(B05): 화면에 들어올 때마다 지표면을 다시 파싱하던 것 제거
B05 는 해시가 바뀔 때마다 renderB05Route 로 통째로 다시 조립되어 뷰어 상태가 비워짐. 그래서 8MB 지표면을 진입할 때마다 다시 읽고 다시 파싱했고, 그것이 진입을 잡는 단일 동기 블록 14.4초였음(공용 브라우저 3왕복 실측: 14,708 / 1,060 / 14,841ms). - 파싱해 둔 지형을 모듈 단위 cachedTerrain 에 한 벌 보관 — 같은 모델이면 새 장면에 그대로 붙임. 다른 모델을 부르면 옛것을 버려 GPU 버퍼가 안 쌓임. - 장면에서 뗄 때 보관본은 disposeObject 하지 않음. - window.__surfaceTiming 디버그 훅 추가 — 단계별 시간(fetch·parse·fit·contours). 자체검증(공용 브라우저 3왕복) — B05 진입 683 / 474 / 639ms (전 14,708 / 1,060 / 14,841). surfaceTiming: reuse 0ms · fit+markers 35ms · contours 183ms · total 222ms. B06 진입은 2.4~2.7초로 변화 없음. 시험 395 통과·17 건너뜀, typecheck 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,29 @@ type ViewKind = "iso" | "top" | "front" | "side";
|
||||
const LIGHT_VIEWER_BACKGROUND = 0xf5f7fa;
|
||||
const DARK_VIEWER_BACKGROUND = 0x251f38;
|
||||
|
||||
/**
|
||||
* 파싱해 둔 지표면 한 벌 — **뷰어보다 오래 산다**.
|
||||
*
|
||||
* B05 는 해시가 바뀔 때마다 `renderB05Route` 로 통째로 다시 조립되므로 뷰어 안의 상태는
|
||||
* 매번 비워진다. 그러면 8MB 짜리 지표면을 화면에 들어올 때마다 다시 읽고 다시 파싱하는데,
|
||||
* 실측에서 그 값이 **단일 동기 블록 14.4초**였다(2026-09-06 공용 브라우저 3왕복).
|
||||
* 같은 모델이면 이 자리에 둔 것을 새 장면에 그대로 붙인다.
|
||||
*
|
||||
* 한 벌만 쥔다 — 다른 모델을 부르면 옛것을 버린다(GPU 버퍼가 쌓이지 않게).
|
||||
* 장면에서 뗄 때도 이 객체는 `disposeObject` 하지 않는다.
|
||||
*/
|
||||
const cachedTerrain: { key: string | null; object: THREE.Object3D | null } = {
|
||||
key: null,
|
||||
object: null,
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/** 지표면 적재 단계별 시간(ms) — 화면 밖에서 수치로 확인하는 디버그 훅. */
|
||||
__surfaceTiming?: Array<{ step: string; ms: number }>;
|
||||
}
|
||||
}
|
||||
|
||||
/** 서피스 삼각형 수 — 클리핑이 실제로 걷어냈는지 확인하는 계측용. */
|
||||
function countTriangles(root: THREE.Object3D | null): number {
|
||||
let total = 0;
|
||||
@@ -184,6 +207,9 @@ export function createRouteViewer(): RouteViewer {
|
||||
|
||||
let terrain: THREE.Object3D | null = null;
|
||||
// 예상형상(코리도) 상태 — 원본/클리핑 지형 두 벌 유지·스왑(2026-08-23).
|
||||
//
|
||||
// 지형은 아래 모듈 단위 `cachedTerrain` 이 한 벌 쥐고 있어, B05 를 드나들어도 다시
|
||||
// 파싱하지 않는다(2026-09-06 실측: 재진입마다 14.4초짜리 단일 동기 블록이 있었다).
|
||||
let corridorBuild: CorridorBuildResult | null = null;
|
||||
let corridorGroup: THREE.Group | null = null;
|
||||
let clippedTerrain: THREE.Object3D | null = null;
|
||||
@@ -609,38 +635,66 @@ export function createRouteViewer(): RouteViewer {
|
||||
markers,
|
||||
structurePick,
|
||||
async loadSurface(projectId, modelId, method, smooth, interval, nextBounds) {
|
||||
const step = (label: string, from: number): void => {
|
||||
timing.push({ step: label, ms: Math.round(performance.now() - from) });
|
||||
};
|
||||
const timing: Array<{ step: string; ms: number }> = [];
|
||||
const started = performance.now();
|
||||
bounds = nextBounds;
|
||||
current = { projectId, modelId, smooth, interval };
|
||||
if (terrain) {
|
||||
scene.remove(terrain);
|
||||
disposeObject(terrain);
|
||||
if (terrain !== cachedTerrain.object) disposeObject(terrain);
|
||||
}
|
||||
heightIndex = null; // 지형이 바뀌면 높이 색인·밴드 분할본도 새로 만든다.
|
||||
bandSplit?.dispose();
|
||||
bandSplit = null;
|
||||
const url = `${API_BASE_URL}/projects/${projectId}/surface/models/${modelId}/preview?smooth=${smooth}`;
|
||||
// 브라우저 보관함에 있으면 그대로 쓰고, 없을 때만 내려받는다(새로고침이 빨라진다).
|
||||
const buffer = await fetchCachedBytes(projectId, url);
|
||||
terrain = await new Promise<THREE.Object3D>((resolve, reject) => {
|
||||
if (method === "meshfree") {
|
||||
const geometry = new PLYLoader().parse(buffer);
|
||||
resolve(new THREE.Points(geometry, new THREE.PointsMaterial({ size: 0.35 })));
|
||||
} else {
|
||||
new GLTFLoader().parse(buffer, "", (gltf) => resolve(gltf.scene), reject);
|
||||
const key = `${projectId}:${modelId}:${method}:${smooth}`;
|
||||
if (cachedTerrain.key === key && cachedTerrain.object) {
|
||||
// 같은 지표면 모델을 이미 파싱해 뒀다 — 다시 읽지도 파싱하지도 않는다(2026-09-06).
|
||||
terrain = cachedTerrain.object;
|
||||
step("reuse", started);
|
||||
} else {
|
||||
const fetched = performance.now();
|
||||
// 브라우저 보관함에 있으면 그대로 쓰고, 없을 때만 내려받는다(새로고침이 빨라진다).
|
||||
const buffer = await fetchCachedBytes(projectId, url);
|
||||
step("fetch", fetched);
|
||||
const parsed = performance.now();
|
||||
terrain = await new Promise<THREE.Object3D>((resolve, reject) => {
|
||||
if (method === "meshfree") {
|
||||
const geometry = new PLYLoader().parse(buffer);
|
||||
resolve(new THREE.Points(geometry, new THREE.PointsMaterial({ size: 0.35 })));
|
||||
} else {
|
||||
new GLTFLoader().parse(buffer, "", (gltf) => resolve(gltf.scene), reject);
|
||||
}
|
||||
});
|
||||
terrain.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) child.material.side = THREE.DoubleSide;
|
||||
});
|
||||
step("parse", parsed);
|
||||
// 한 벌만 쥔다 — 다른 모델로 바뀌면 옛것을 버린다.
|
||||
if (cachedTerrain.object && cachedTerrain.object !== terrain) {
|
||||
disposeObject(cachedTerrain.object);
|
||||
}
|
||||
});
|
||||
terrain.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) child.material.side = THREE.DoubleSide;
|
||||
});
|
||||
cachedTerrain.key = key;
|
||||
cachedTerrain.object = terrain;
|
||||
}
|
||||
scene.add(terrain);
|
||||
// 흑백 토글이 켜진 채 모델을 다시 불러와도 상태를 유지한다.
|
||||
applySurfaceGrayscale();
|
||||
// 지형·bounds가 준비된 시점에 코리도를 다시 조립한다 — 초기 진입은 종횡단
|
||||
// 로드가 지형보다 먼저 끝나 setCorridor가 그룹 생성을 미뤄뒀을 수 있다.
|
||||
if (corridorBuild) setCorridor(corridorBuild);
|
||||
const fitted = performance.now();
|
||||
fit("top");
|
||||
markers.renderMarkers();
|
||||
step("fit+markers", fitted);
|
||||
const contoured = performance.now();
|
||||
await reloadContours(interval);
|
||||
step("contours", contoured);
|
||||
step("total", started);
|
||||
window.__surfaceTiming = timing;
|
||||
// 로딩이 끝나면 안내문을 지운다 — 조작법 설명이 화면에 계속 떠 있을 이유가
|
||||
// 없다(2026-08-19 사용자 지시). 로딩·이동 중 안내는 그대로 쓴다.
|
||||
status.textContent = "";
|
||||
|
||||
Reference in New Issue
Block a user