Skip to content

Commit 9431cc0

Browse files
authored
Merge pull request #27 from buckaroo-data/perf/load-expr-result-caching
perf(load-expr): stabilize stat caching + cache result expressions
2 parents 32057df + e4dad24 commit 9431cc0

12 files changed

Lines changed: 601 additions & 110 deletions

File tree

packages/app/src/api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import type {
1313
async function get<T>(url: string): Promise<T> {
1414
const r = await fetch(url);
1515
if (!r.ok) {
16-
const msg = await r.text().catch(() => `HTTP ${r.status}`);
16+
const text = await r.text().catch(() => `HTTP ${r.status}`);
17+
let msg = text;
18+
try { msg = JSON.parse(text).detail ?? text; } catch { /* not JSON */ }
1719
throw new Error(msg);
1820
}
1921
return r.json();

packages/app/src/pages/DiffPage.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,23 @@ export function DiffPage() {
5858
return (
5959
<main>
6060
<section className="panel detail" style={{ margin: "12px 16px" }}>
61-
<h2 style={{ margin: "0 0 4px 0" }}>diff unavailable</h2>
62-
<p className="meta" style={{ color: "#ffb4b4", margin: "0 0 12px 0" }}>{error}</p>
63-
<p className="meta" style={{ margin: 0 }}>
64-
This diff URL doesn&apos;t resolve (the alias, versions, or pairing may be stale).{" "}
61+
<h2 style={{ margin: "0 0 8px 0" }}>diff error</h2>
62+
<p className="meta" style={{ margin: "0 0 8px 0" }}>
6563
<Link to={`/${project}/catalog/${alias}`}>view {alias} in the catalog</Link>
6664
{" · "}
6765
<Link to={`/${project}/catalog`}>back to catalog</Link>
6866
</p>
67+
<pre style={{
68+
background: "#1a1010",
69+
color: "#ff9090",
70+
padding: 12,
71+
borderRadius: 4,
72+
fontSize: 12,
73+
lineHeight: 1.5,
74+
overflow: "auto",
75+
whiteSpace: "pre-wrap",
76+
margin: 0,
77+
}}>{error}</pre>
6978
</section>
7079
</main>
7180
);

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ dependencies = [
2020
# 3.14+ doesn't compose with xorq's under uv's lockfile resolver.
2121
"tornado>=6.0",
2222
"polars",
23-
"buckaroo==0.14.15",
23+
"buckaroo==0.14.17",
2424
"markdown>=3.10.2",
2525
"pygments>=2.20.0",
2626
# No duckdb extra: the only backend is xorq's built-in datafusion
@@ -47,6 +47,7 @@ dev = [
4747
# marimo export in tests; the package generates marimo source without it.
4848
"marimo>=0.23.9",
4949
"twine>=6.2.0",
50+
"playwright>=1.60.0",
5051
]
5152

5253
[build-system]

scripts/profile_urls.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"""
2+
Profile page load times for a sequence of URLs.
3+
Captures: navigation timing, all network requests, console errors.
4+
Specifically surfaces buckaroo-related JS timing via performance marks/measures.
5+
"""
6+
7+
import time
8+
9+
from playwright.sync_api import sync_playwright
10+
11+
URLS = [
12+
"http://localhost:7860/first-project/catalog/6fdd4dad1447",
13+
"http://localhost:7860/first-project/diff/route_distance/1/2",
14+
"http://localhost:7860/first-project/catalog/763193211746",
15+
"http://localhost:7860/first-project/catalog/0d0b75dfa627",
16+
]
17+
18+
# How long to wait after DOMContentLoaded for async renders (buckaroo data fetch + render)
19+
SETTLE_MS = 8000
20+
21+
22+
def fmt_ms(ms):
23+
if ms is None:
24+
return " N/A "
25+
return f"{ms:7.0f}ms"
26+
27+
28+
def profile_url(page, url, index):
29+
print(f"\n{'='*70}")
30+
print(f"[{index+1}/{len(URLS)}] {url}")
31+
print(f"{'='*70}")
32+
33+
requests = []
34+
console_errors = []
35+
36+
def on_request(req):
37+
requests.append({"url": req.url, "method": req.method, "start": time.monotonic()})
38+
39+
def on_response(resp):
40+
for r in reversed(requests):
41+
if r["url"] == resp.url and "end" not in r:
42+
r["end"] = time.monotonic()
43+
r["status"] = resp.status
44+
r["size"] = resp.headers.get("content-length", "?")
45+
break
46+
47+
def on_console(msg):
48+
if msg.type == "error":
49+
console_errors.append(msg.text)
50+
51+
page.on("request", on_request)
52+
page.on("response", on_response)
53+
page.on("console", on_console)
54+
55+
t_nav_start = time.monotonic()
56+
page.goto(url, wait_until="domcontentloaded")
57+
t_dom = time.monotonic()
58+
59+
# Wait for the page to settle (network idle or timeout)
60+
try:
61+
page.wait_for_load_state("networkidle", timeout=SETTLE_MS)
62+
except Exception:
63+
pass
64+
t_idle = time.monotonic()
65+
66+
# Grab Navigation Timing from the browser
67+
nav_timing = page.evaluate("""() => {
68+
const e = performance.getEntriesByType('navigation')[0];
69+
if (!e) return null;
70+
return {
71+
dns: e.domainLookupEnd - e.domainLookupStart,
72+
tcp: e.connectEnd - e.connectStart,
73+
ttfb: e.responseStart - e.requestStart,
74+
response_download: e.responseEnd - e.responseStart,
75+
dom_interactive: e.domInteractive,
76+
dom_complete: e.domComplete,
77+
load_event: e.loadEventEnd,
78+
};
79+
}""")
80+
81+
# Grab all resource entries (JS, API calls)
82+
resources = page.evaluate("""() => {
83+
return performance.getEntriesByType('resource').map(e => ({
84+
name: e.name,
85+
type: e.initiatorType,
86+
duration: e.duration,
87+
transfer_size: e.transferSize,
88+
start: e.startTime,
89+
}));
90+
}""")
91+
92+
# Any perf marks/measures (buckaroo may emit these)
93+
marks_measures = page.evaluate("""() => {
94+
const marks = performance.getEntriesByType('mark').map(e => ({kind:'mark', name:e.name, start:e.startTime}));
95+
const measures = performance.getEntriesByType('measure').map(
96+
e => ({kind:'measure', name:e.name, start:e.startTime, duration:e.duration}));
97+
return [...marks, ...measures].sort((a,b) => a.start - b.start);
98+
}""")
99+
100+
# --- Print Navigation Timing ---
101+
if nav_timing:
102+
print("\n Navigation Timing:")
103+
print(f" DNS lookup: {fmt_ms(nav_timing['dns'])}")
104+
print(f" TCP connect: {fmt_ms(nav_timing['tcp'])}")
105+
print(f" TTFB: {fmt_ms(nav_timing['ttfb'])}")
106+
print(f" Response download: {fmt_ms(nav_timing['response_download'])}")
107+
print(f" DOM interactive: {fmt_ms(nav_timing['dom_interactive'])}")
108+
print(f" DOM complete: {fmt_ms(nav_timing['dom_complete'])}")
109+
print(f" Load event end: {fmt_ms(nav_timing['load_event'])}")
110+
111+
wall_to_dom = (t_dom - t_nav_start) * 1000
112+
wall_to_idle = (t_idle - t_nav_start) * 1000
113+
print("\n Wall-clock:")
114+
print(f" To DOMContentLoaded: {fmt_ms(wall_to_dom)}")
115+
print(f" To network idle: {fmt_ms(wall_to_idle)}")
116+
117+
# --- API requests ---
118+
api_reqs = [r for r in requests if "/api/" in r["url"] or r["url"].endswith(".json")]
119+
if api_reqs:
120+
print(f"\n API requests ({len(api_reqs)}):")
121+
for r in sorted(api_reqs, key=lambda x: x["start"]):
122+
dur = (r.get("end", time.monotonic()) - r["start"]) * 1000
123+
path = r["url"].split("localhost:7860")[-1]
124+
status = r.get("status", "?")
125+
print(f" [{status}] {fmt_ms(dur)} {path}")
126+
127+
# --- Slowest JS/resource loads ---
128+
js_resources = [r for r in resources if r["type"] in ("script", "fetch", "xmlhttprequest")]
129+
if js_resources:
130+
slow = sorted(js_resources, key=lambda x: -x["duration"])[:8]
131+
print("\n Slowest resources (script/fetch, top 8):")
132+
for r in slow:
133+
name = r["name"].split("localhost:7860")[-1]
134+
kb = f"{r['transfer_size']/1024:.1f}kB" if r["transfer_size"] else "cached"
135+
print(f" {fmt_ms(r['duration'])} [{kb:>10}] {name}")
136+
137+
# --- Performance marks/measures (buckaroo + app) ---
138+
if marks_measures:
139+
print(f"\n Perf marks/measures ({len(marks_measures)}):")
140+
for m in marks_measures:
141+
if m["kind"] == "measure":
142+
print(f" MEASURE {fmt_ms(m['duration'])} @{m['start']:.0f}ms {m['name']}")
143+
else:
144+
print(f" mark @{m['start']:.0f}ms {m['name']}")
145+
else:
146+
print("\n No perf marks/measures found.")
147+
148+
if console_errors:
149+
print(f"\n Console errors ({len(console_errors)}):")
150+
for e in console_errors[:5]:
151+
print(f" ERROR: {e[:120]}")
152+
153+
page.remove_listener("request", on_request)
154+
page.remove_listener("response", on_response)
155+
page.remove_listener("console", on_console)
156+
157+
return {
158+
"url": url,
159+
"wall_to_dom_ms": wall_to_dom,
160+
"wall_to_idle_ms": wall_to_idle,
161+
"api_count": len(api_reqs),
162+
}
163+
164+
165+
def main():
166+
with sync_playwright() as p:
167+
browser = p.chromium.launch(headless=True)
168+
context = browser.new_context(
169+
# disable cache so we get real cold-load numbers per page
170+
# (but keep warm across the session so JS bundle only loads once)
171+
)
172+
page = context.new_page()
173+
174+
results = []
175+
for i, url in enumerate(URLS):
176+
result = profile_url(page, url, i)
177+
results.append(result)
178+
time.sleep(0.5)
179+
180+
print(f"\n{'='*70}")
181+
print("SUMMARY")
182+
print(f"{'='*70}")
183+
print(f" {'URL':<45} {'To DOM':>9} {'To Idle':>9} {'API#':>5}")
184+
print(f" {'-'*45} {'-'*9} {'-'*9} {'-'*5}")
185+
for r in results:
186+
path = r["url"].split("localhost:7860")[-1]
187+
print(
188+
f" {path:<45} {fmt_ms(r['wall_to_dom_ms']):>9} "
189+
f"{fmt_ms(r['wall_to_idle_ms']):>9} {r['api_count']:>5}"
190+
)
191+
192+
browser.close()
193+
194+
195+
if __name__ == "__main__":
196+
main()

0 commit comments

Comments
 (0)