GraphRAG rebuilds when opened, not on every project switch
Measured what a visit costs: 140ms to construct the QWebEngineView the first time, 485ms to walk the folder and lay the graph out, and a setHtml of the whole D3 page to show it. The setHtml is the flash — the page really is reloaded. Once built it is not repeated: three round trips in and out rescanned nothing. What did repeat was project switching. _refresh_project_combo scanned immediately on any change, and the rail's picker put that one click away from every screen — so the graph was rebuilt again and again for a tab usually not on screen, and the visit after a switch always paid for a reload. It now marks _needs_scan and lets auto_scan_and_fit() pick it up when GraphRAG is actually opened: two switches from Cowork cost 0 scans, and opening it after costs exactly 1. The first visit per run still builds the browser view and scans, which is the lazy construction the code chose deliberately to keep startup light. That one is a trade, not a bug; say if you would rather pay it at startup. check_graphrag_rescan covers all three: no rebuild on an unchanged re-entry, no scan while off-screen, exactly one on opening. 24/24 checkers pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2f73aaa333
commit
d1e3be0feb
@@ -0,0 +1,112 @@
|
||||
"""GraphRAG rebuilds when it is opened, not every time a project is picked.
|
||||
|
||||
Switching project used to rescan the folder, redo the force layout and setHtml
|
||||
the whole D3 page immediately — for a tab usually not on screen. With the rail's
|
||||
picker one click away from anywhere, that fired constantly, and the rebuild you
|
||||
did see on opening GraphRAG read as the page reloading itself.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO.parent))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from capture_screens import ( # noqa: E402
|
||||
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sandbox = _isolate_home()
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
_load_fonts()
|
||||
_freeze_schedulers()
|
||||
_apply_theme(app)
|
||||
|
||||
from cowork_local.config import CONFIG_DIR
|
||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||
|
||||
from seed_demo_data import seed
|
||||
seed()
|
||||
|
||||
from cowork_local.app import MainWindow
|
||||
from cowork_local.config import AppConfig
|
||||
from cowork_local.i18n import set_language
|
||||
from cowork_local.state import AppContext
|
||||
|
||||
set_language("vi")
|
||||
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
|
||||
win.resize(1600, 950)
|
||||
win.show()
|
||||
app.processEvents()
|
||||
st, w = win.structure, win.workspace
|
||||
fails = []
|
||||
|
||||
# first visit builds the view and scans once
|
||||
win._goto(win._ROW_WORKSPACE, w._graphrag_tab_idx)
|
||||
deadline = time.monotonic() + 30
|
||||
while st._graph is None and time.monotonic() < deadline:
|
||||
app.processEvents()
|
||||
time.sleep(0.02)
|
||||
if st._graph is None:
|
||||
print("FAIL lan dau khong dung duoc do thi")
|
||||
sys.stdout.flush()
|
||||
os._exit(1)
|
||||
|
||||
calls = {"scan": 0, "html": 0}
|
||||
real_scan, real_html = st._scan, st._render_d3
|
||||
st._scan = lambda: (calls.__setitem__("scan", calls["scan"] + 1), real_scan())[1]
|
||||
st._render_d3 = lambda: (calls.__setitem__("html", calls["html"] + 1), real_html())[1]
|
||||
|
||||
# re-entering an unchanged graph must not rebuild anything
|
||||
for _ in range(3):
|
||||
win._goto(win._ROW_WORKSPACE, w._project_tab_idx)
|
||||
app.processEvents()
|
||||
win._goto(win._ROW_WORKSPACE, w._graphrag_tab_idx)
|
||||
app.processEvents()
|
||||
print(f"vao lai 3 lan (khong doi gi): _scan={calls['scan']} setHtml={calls['html']}")
|
||||
if calls["scan"] or calls["html"]:
|
||||
fails.append("vao lai GraphRAG van quet/reload du khong co gi doi")
|
||||
|
||||
# switching project off-screen defers the work
|
||||
win._goto(win._ROW_WORKSPACE, w._cowork_tab_idx)
|
||||
app.processEvents()
|
||||
ids = [pid for _n, pid in w.project_choices()]
|
||||
for pid in ids[1:3]:
|
||||
w.choose_project(pid)
|
||||
app.processEvents()
|
||||
print(f"doi {len(ids[1:3])} project khi dang o Cowork: _scan={calls['scan']} "
|
||||
f"setHtml={calls['html']} | can quet lan toi={st._needs_scan}")
|
||||
if calls["scan"]:
|
||||
fails.append("doi project van quet ngay du GraphRAG khong tren man")
|
||||
if len(ids) > 1 and not st._needs_scan:
|
||||
fails.append("doi project ma khong danh dau can quet lai")
|
||||
|
||||
# ...and opening it does the work once
|
||||
win._goto(win._ROW_WORKSPACE, w._graphrag_tab_idx)
|
||||
app.processEvents()
|
||||
print(f"mo GraphRAG: _scan={calls['scan']}")
|
||||
if len(ids) > 1 and calls["scan"] != 1:
|
||||
fails.append(f"mo GraphRAG phai quet dung 1 lan, dang {calls['scan']}")
|
||||
|
||||
print()
|
||||
for f in fails:
|
||||
print("FAIL " + f)
|
||||
print("PASS GraphRAG chi dung lai do thi khi can" if not fails
|
||||
else f"{len(fails)} problem(s)")
|
||||
sys.stdout.flush()
|
||||
os._exit(1 if fails else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -404,10 +404,13 @@ class StructureGraphView(QWidget):
|
||||
if project is not None:
|
||||
self.path_edit.setText(str(project.workspace_dir()))
|
||||
if project_changed:
|
||||
# Mark it and scan on the next visit rather than now. The rail's
|
||||
# project picker made switching a one-click thing from any screen,
|
||||
# and each switch rebuilt this graph — a folder walk plus a force
|
||||
# layout plus a full setHtml of the D3 page — for a tab that was
|
||||
# usually not even on screen. auto_scan_and_fit() picks the flag up
|
||||
# when GraphRAG is actually opened.
|
||||
self._needs_scan = True
|
||||
if self.web is not None:
|
||||
self._needs_scan = False
|
||||
self._scan()
|
||||
|
||||
# ---- helpers -----------------------------------------------------
|
||||
def _pick(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user