perf: lazy-load file content and fix oversized tile labels

- Server now returns metadata-only tree on initial load (no file content
  in the JSON payload); content is served on-demand via the new
  GET /api/repos/{key}/file?path=... endpoint
- Cache still stores full content; strip_content() runs in-memory before
  the response is sent
- Frontend fetches file content lazily in _fetchContent() when a tile
  enters the LOD view, preventing a massive upfront JSON download for
  large repos (e.g. claude code)
- computeColorRanges() is now deferred to first _showCode() call instead
  of running synchronously for every file during load()
- Cap label fontSize at 5 world units to prevent giant text on large tiles
This commit is contained in:
Fam Zheng
2026-04-07 10:37:31 +01:00
committed by Fam Zheng
parent 37d2b33f32
commit 398ae64ed9
3 changed files with 124 additions and 28 deletions

View File

@@ -31,7 +31,7 @@ function showError(msg) {
alert(msg);
}
async function visualize(tree, repoName) {
async function visualize(tree, repoName, cacheKey) {
showLoading("Building layout...");
// Wait for fonts to load so canvas renders them correctly
@@ -50,7 +50,7 @@ async function visualize(tree, repoName) {
showVisualization();
document.getElementById("osd-info").classList.add("active");
const renderer = new RepoRenderer(viewport, repoName || tree.name);
const renderer = new RepoRenderer(viewport, repoName || tree.name, cacheKey);
await renderer.load(leaves, totalWidth, totalHeight);
}
@@ -85,8 +85,8 @@ async function loadCachedRepo(key, name) {
try {
const res = await fetch(`/api/repos/${key}`);
if (!res.ok) throw new Error("Cache expired");
const tree = await res.json();
await visualize(tree, name);
const { cache_key, tree } = await res.json();
await visualize(tree, name, cache_key);
} catch (err) {
showError(err.message);
}
@@ -121,8 +121,8 @@ btnClone.addEventListener("click", async () => {
throw new Error(err.error || "Clone failed");
}
const tree = await res.json();
await visualize(tree);
const { cache_key, tree } = await res.json();
await visualize(tree, undefined, cache_key);
} catch (err) {
showError(err.message);
} finally {
@@ -174,8 +174,8 @@ async function uploadZip(file) {
throw new Error(err.error || "Upload failed");
}
const tree = await res.json();
await visualize(tree);
const { cache_key, tree } = await res.json();
await visualize(tree, undefined, cache_key);
} catch (err) {
showError(err.message);
}

View File

@@ -42,9 +42,10 @@ function buildWatermark(text, cols, rows) {
// ---------- renderer ----------
export class RepoRenderer {
constructor(container, repoName) {
constructor(container, repoName, cacheKey) {
this.container = container;
this.repoName = repoName || "repo";
this.cacheKey = cacheKey || null;
this.tiles = [];
this.bgMeshes = [];
this.raycaster = new THREE.Raycaster();
@@ -226,7 +227,7 @@ export class RepoRenderer {
// --- Label (always visible, cheap — one per file) ---
const label = new Text();
label.text = leaf.name;
label.fontSize = Math.min(leaf.w, leaf.h) * 0.15;
label.fontSize = Math.min(Math.min(leaf.w, leaf.h) * 0.15, 5);
label.color = 0xffffff;
label.anchorX = "center"; label.anchorY = "middle";
label.rotation.x = -Math.PI / 2;
@@ -235,13 +236,12 @@ export class RepoRenderer {
this.scene.add(label);
label.sync();
// Pre-compute syntax highlight ranges (cheap, no GPU)
const colorRanges = computeColorRanges(leaf.content, leaf.name);
this.tiles.push({
bgMesh, label, darkMat, colorRanges,
bgMesh, label, darkMat,
codeMesh: null, watermark: null, darkMesh: null,
data: leaf, showingCode: false, color, dist: Infinity
// colorRanges computed lazily on first _showCode
colorRanges: undefined,
data: leaf, showingCode: false, loading: false, color, dist: Infinity
});
this.bgMeshes.push(bgMesh);
}
@@ -254,10 +254,43 @@ export class RepoRenderer {
this.tooltip = document.getElementById("tooltip");
}
// -------- lazy content fetch --------
async _fetchContent(tile) {
try {
const res = await fetch(
`/api/repos/${encodeURIComponent(this.cacheKey)}/file?path=${encodeURIComponent(tile.data.path)}`
);
if (res.ok) {
const { content } = await res.json();
tile.data.content = content;
// Pre-compute colorRanges right after fetch (off the hot animation path)
tile.colorRanges = computeColorRanges(content, tile.data.name);
}
} catch {
// network error — leave content null, will retry next LOD cycle
} finally {
tile.loading = false;
}
}
// -------- lazy code/watermark creation --------
_showCode(tile) {
const d = tile.data;
// If content hasn't been loaded yet, kick off a fetch and bail
if (!d.content) {
if (!tile.loading) {
tile.loading = true;
if (this.cacheKey) this._fetchContent(tile);
}
return;
}
// Compute colorRanges lazily (only once, synchronous after content is available)
if (tile.colorRanges === undefined) {
tile.colorRanges = computeColorRanges(d.content, d.name);
}
// Dark bg
if (!tile.darkMesh) {
tile.darkMesh = new THREE.Mesh(new THREE.PlaneGeometry(d.w, d.h), tile.darkMat);