Kill the GraphRAG flash: warm the view up, and never paint it white

Two causes, both dealt with.

A fresh QWebEngineView paints white, and _ensure_web put it on screen empty —
so on a dark theme that white rectangle sat there for the whole first scan.
It is now blanked to the app's own background colour the moment it is created,
before it is ever shown.

And the work itself moved off the click. MainWindow warms GraphRAG up 3s after
the window appears — building the browser view (~140ms) and the first graph
(~485ms) while nothing is waiting on them. Clicking GraphRAG then costs 78ms
with zero scans and zero setHtml calls, where before it was ~625ms of empty
view.

This spends the memory the lazy construction was saving, a few seconds after
startup rather than never — which is the trade you asked for. Startup itself is
untouched: the checker asserts neither the view nor the graph exists at the
moment the window opens.

check_graphrag_rescan now covers the warm-up too, and fails if it stops running.

24/24 checkers pass; check_combo_popup took a teardown segfault in the suite and
passed 3/3 standalone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nam Pham Dinh Thanh
2026-08-18 20:46:10 +09:00
co-authored by Claude Opus 5
parent d1e3be0feb
commit c4e9158779
3 changed files with 55 additions and 4 deletions
+15
View File
@@ -416,6 +416,21 @@ class MainWindow(QMainWindow):
self._update_dock_guard()
self.help_agent.reposition()
self.help_agent.raise_()
# Build GraphRAG's browser view and first graph once the window is up
# and idle, so clicking GraphRAG does not sit on an empty view while
# both happen. 3s is after the first paint and any startup refresh.
if not getattr(self, "_graph_prewarmed", False):
self._graph_prewarmed = True
QTimer.singleShot(3000, self._prewarm_graph)
def _prewarm_graph(self) -> None:
view = getattr(self, "structure", None)
if view is None or not hasattr(view, "prewarm"):
return
try:
view.prewarm()
except Exception: # noqa: BLE001 — a warm-up must never break the app
pass
# ---- i18n ----------------------------------------------------------
def _retranslate(self) -> None:
+19 -4
View File
@@ -50,24 +50,39 @@ def main() -> int:
win.show()
app.processEvents()
st, w = win.structure, win.workspace
fails = []
fails: list[str] = []
# first visit builds the view and scans once
win._goto(win._ROW_WORKSPACE, w._graphrag_tab_idx)
# startup itself must not build any of it
if st.web is not None or st._graph is not None:
fails.append("dung san do thi ngay luc khoi dong — startup phai nhe")
# the idle warm-up builds the browser view and the first graph off the
# click path (MainWindow fires this on a 3s timer; call it directly here)
win._prewarm_graph()
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")
print("FAIL lam nong khong dung duoc do thi")
sys.stdout.flush()
os._exit(1)
print(f"sau khi lam nong: web={'co' if st.web else 'chua'} do thi={'co' if st._graph else 'chua'}")
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]
# ...so the click itself does nothing but show it — this is the flash
t0 = time.monotonic()
win._goto(win._ROW_WORKSPACE, w._graphrag_tab_idx)
app.processEvents()
print(f"bam vao GraphRAG: {(time.monotonic() - t0) * 1000:.0f}ms "
f"_scan={calls['scan']} setHtml={calls['html']}")
if calls["scan"] or calls["html"]:
fails.append("bam vao GraphRAG van phai quet/nap lai trang — con chop trang")
# re-entering an unchanged graph must not rebuild anything
for _ in range(3):
win._goto(win._ROW_WORKSPACE, w._project_tab_idx)
+21
View File
@@ -495,10 +495,31 @@ class StructureGraphView(QWidget):
f'<pre style="white-space:pre-wrap; font-family:Consolas,monospace; '
f'font-size:12px;">{html.escape(text)}</pre>')
def prewarm(self) -> None:
"""Pay for the graph view before it is clicked on, not during.
Opening GraphRAG built a QWebEngineView (~140ms) and scanned the project
(~485ms) while an empty browser sat on screen — long enough, and white
enough, to read as the app restarting itself. Called from an idle timer
after the window is up, so startup itself is unaffected; the memory the
lazy construction was saving is spent a few seconds later instead.
"""
if not _HAS_WEB or self.web is not None:
return
self._ensure_web()
if self._graph is None and self.path_edit.text().strip():
self._needs_scan = False
self._scan() # runs on a worker thread
def _ensure_web(self) -> None:
if self.web is not None or not _HAS_WEB:
return
self.web = QWebEngineView()
# Blank the page in the app's own background first. A fresh
# QWebEngineView paints white, and on a dark theme that white rectangle
# WAS the flash — it showed for as long as the first scan took.
self.web.setHtml(
f"<body style='margin:0;background:{current_palette().bg}'></body>")
self._bridge = _Bridge()
self._channel = QWebChannel()
self._channel.registerObject("py", self._bridge)