Skip to content

Commit 1831d25

Browse files
committed
Optimize DOM updates in graph rendering for improved performance
1 parent 82b5483 commit 1831d25

2 files changed

Lines changed: 75 additions & 26 deletions

File tree

README.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,38 @@ the map already scanned.
150150

151151
## Language support
152152

153-
| Language | Parser | Extracted |
154-
|---|---|---|
155-
| Python | stdlib `ast` (precise) | functions, classes, params/annotations, docstrings, Flask/FastAPI/Django routes, SQLAlchemy/Django models, raw SQL, `requests`/`httpx` calls |
156-
| JavaScript / TypeScript / JSX / TSX / Vue / Svelte | structural regex + brace matching | functions, arrow fns, classes, Express/Nest/Fastify routes, `fetch`/`axios` calls, Mongoose/Prisma/Sequelize/Knex/TypeORM, raw SQL |
157-
| Java, C#, Go, PHP, Ruby | structural regex | functions/methods, classes, Spring/ASP.NET/Laravel/Rails/Gin routes, raw SQL |
158-
| SQL | regex | `CREATE TABLE` schemas with columns |
159-
| Config (`.env`, json/yaml/toml/ini, settings.py…) | key scan | DB connection settings — **credentials always masked** |
160-
| Anything else (Rust, Kotlin, Swift, C/C++…) || shown in the UI as "⚠ Unsupported — parsing skipped", never silently dropped |
153+
Parser used per language, then exactly what gets extracted — checked feature
154+
by feature against the actual parser code, not a marketing table:
155+
156+
| Language | Parser | Functions / methods | Classes / structs | Backend routes | Outgoing API calls | ORM / DB models | Raw SQL | Call graph | Page-nav links |
157+
|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
158+
| Python | stdlib `ast` (precise) ||| ✅ Flask / FastAPI / Django |`requests` / `httpx` / `aiohttp` / `urllib` | ✅ SQLAlchemy / Django / peewee / tortoise ||||
159+
| JavaScript / TypeScript / JSX / TSX | regex + brace matching ||| ✅ Express / Fastify / Koa / NestJS |`fetch` / `axios` | ✅ Mongoose / Prisma / Sequelize / Knex / TypeORM ||||
160+
| Vue / Svelte | regex, `<script>` block only¹ ||| ✅ (same as JS) | ✅ (same as JS) | ✅ (same as JS) ||||
161+
| Java | regex ||| ✅ Spring `@*Mapping` ||||||
162+
| C# | regex ||| ✅ ASP.NET `[Http*]` ||||||
163+
| Go | regex || ✅ (structs) | ✅ Gin / net-http style ||||||
164+
| PHP | regex ||| ✅ Laravel `Route::` ||||||
165+
| Ruby | regex ||| ✅ Rails / Sinatra ||||||
166+
| SQL (`.sql` files) | regex |||||`CREATE TABLE` schemas + columns ||||
167+
| Config (`.env`, json/yaml/toml/ini/xml…)² | key scan |||||||||
168+
| Anything else (Rust, Kotlin, Swift, C/C++, Scala, Elixir, Erlang, Lua, R, Perl, Dart, Zig…) ||||||||||
169+
170+
Unsupported languages still show up in the Explorer tree with an
171+
"⚠ Unsupported — parsing skipped" badge — never silently dropped — they just
172+
have no functions/edges to draw.
173+
174+
¹ Vue/Svelte only parse the `<script>` block — template-only bindings
175+
(`@click`, `v-on`, Svelte reactive markup) aren't extracted.
176+
² Config-format files get a DB-connection-settings scan instead of code
177+
extraction — **credentials always masked**, see [export.py](codebread/export.py)-embedded
178+
source too. Files like `settings.py` or `config.js` are parsed as their real
179+
language (full extraction above), not this bucket — only non-code formats
180+
(json/yaml/toml/ini/`.env`/xml/properties) land here.
181+
182+
Only Python's `ast`-based parser captures real parameter type annotations,
183+
return types, and docstrings — every regex-based parser above extracts
184+
parameter *names* only.
161185

162186
## How it classifies layers
163187

codebread/web/app.js

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -348,36 +348,61 @@ function visibleEdges() {
348348
}
349349

350350
/* ---------------- DOM (re)build ---------------- */
351+
/* Incremental: only the nodes/edges that actually entered or left the
352+
visible set touch the DOM. A full teardown+rebuild here would mean every
353+
single click in a large, densely-linked graph re-creates *everything*
354+
already on screen (elements + listeners) just to add one more node —
355+
that's the "too many links = lag" complaint this replaced. */
351356
function rebuild() {
352-
nodesG.innerHTML = ""; edgesG.innerHTML = "";
353-
S.elNodes.clear(); S.elEdges.clear();
354-
S.drawnEdges = [];
357+
const newEdges = visibleEdges();
358+
const newEdgeKeys = new Set(newEdges.map(edgeKey));
355359

356-
for (const e of visibleEdges()) {
360+
for (const { key } of S.drawnEdges) {
361+
if (newEdgeKeys.has(key)) continue;
362+
const els = S.elEdges.get(key);
363+
if (!els) continue;
364+
els.vis.remove(); els.hit.remove();
365+
S.elEdges.delete(key);
366+
}
367+
368+
const drawnEdges = [];
369+
for (const e of newEdges) {
357370
const key = edgeKey(e);
358-
const vis = document.createElementNS(SVGNS, "path");
359-
vis.setAttribute("class", "edge k-" + e.kind + (e.cycle ? " cycle" : ""));
360-
const hit = document.createElementNS(SVGNS, "path");
361-
hit.setAttribute("class", "edge-hit");
362-
hit.addEventListener("mousemove", (ev) => showEdgeTooltip(e, ev));
363-
hit.addEventListener("mouseleave", hideTooltip);
364-
hit.addEventListener("contextmenu", (ev) => {
365-
ev.preventDefault(); ev.stopPropagation();
366-
onEdgeContextMenu(e, ev);
367-
});
368-
edgesG.appendChild(vis); edgesG.appendChild(hit);
369-
S.elEdges.set(key, { vis, hit });
370-
S.drawnEdges.push({ edge: e, key });
371+
let els = S.elEdges.get(key);
372+
if (!els) {
373+
const vis = document.createElementNS(SVGNS, "path");
374+
vis.setAttribute("class", "edge k-" + e.kind + (e.cycle ? " cycle" : ""));
375+
const hit = document.createElementNS(SVGNS, "path");
376+
hit.setAttribute("class", "edge-hit");
377+
hit.addEventListener("mousemove", (ev) => showEdgeTooltip(e, ev));
378+
hit.addEventListener("mouseleave", hideTooltip);
379+
hit.addEventListener("contextmenu", (ev) => {
380+
ev.preventDefault(); ev.stopPropagation();
381+
onEdgeContextMenu(e, ev);
382+
});
383+
edgesG.appendChild(vis); edgesG.appendChild(hit);
384+
els = { vis, hit };
385+
S.elEdges.set(key, els);
386+
}
387+
drawnEdges.push({ edge: e, key });
371388
}
389+
S.drawnEdges = drawnEdges;
372390

391+
for (const [id, g] of S.elNodes) {
392+
const n = S.nodesById.get(id);
393+
if (S.visible.has(id) && n && passesFilter(n)) continue;
394+
g.remove();
395+
S.elNodes.delete(id);
396+
}
373397
for (const id of S.visible) {
374398
const n = S.nodesById.get(id);
375-
if (!n || !passesFilter(n)) continue;
399+
if (!n || !passesFilter(n) || S.elNodes.has(id)) continue;
376400
ensurePos(id);
377401
const g = makeNodeEl(n);
378402
nodesG.appendChild(g);
379403
S.elNodes.set(id, g);
380404
}
405+
381406
applyHighlight();
382407
updatePositions();
383408
renderMinimap();

0 commit comments

Comments
 (0)