-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Speed up screenshots with chrome-headless-shell #2559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
509088e
6e09825
9137eec
a7bd8b6
973542f
4e98d97
ac4f888
ac51b5c
da14e20
0dbc874
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| package runner | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "strings" | ||
| "sync" | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| const spaHydratedMarker = "hydrated-ok" | ||
|
|
||
| // spaHTML is a tiny SPA-style page: it paints a boot screen first, then | ||
| // fetches /app.js which hydrates #root after a short delay. Early captures | ||
| // only contain "boot"; a correct wait must include spaHydratedMarker. | ||
| const spaHTML = `<!doctype html> | ||
| <html><head><title>spa-bench</title></head> | ||
| <body> | ||
| <div id="root">boot</div> | ||
| <script>fetch("/app.js").then(function(r){return r.text()}).then(eval);</script> | ||
| </body></html>` | ||
|
|
||
| const spaJS = ` | ||
| setTimeout(function(){ | ||
| document.getElementById("root").textContent = "` + spaHydratedMarker + `"; | ||
| }, 80); | ||
| ` | ||
|
|
||
| func startSPAScreenshotServer(t testing.TB) *httptest.Server { | ||
| t.Helper() | ||
| mux := http.NewServeMux() | ||
| mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||
| w.Header().Set("Content-Type", "text/html; charset=utf-8") | ||
| _, _ = w.Write([]byte(spaHTML)) | ||
| }) | ||
| mux.HandleFunc("/app.js", func(w http.ResponseWriter, r *http.Request) { | ||
| time.Sleep(50 * time.Millisecond) | ||
| w.Header().Set("Content-Type", "application/javascript") | ||
| _, _ = w.Write([]byte(spaJS)) | ||
| }) | ||
| srv := httptest.NewServer(mux) | ||
| t.Cleanup(srv.Close) | ||
| return srv | ||
| } | ||
|
|
||
| func newScreenshotBrowser(t testing.TB, useLocal bool) *Browser { | ||
| t.Helper() | ||
| browser, err := NewBrowser("", useLocal, nil) | ||
| if err != nil { | ||
| t.Skipf("headless browser unavailable: %v", err) | ||
| } | ||
| t.Cleanup(browser.Close) | ||
| return browser | ||
| } | ||
|
|
||
| func captureSPA(t testing.TB, browser *Browser, url string) (screenshot []byte, body string) { | ||
| t.Helper() | ||
| screenshot, body, _, err := browser.ScreenshotWithBody(url, 15*time.Second, 200*time.Millisecond, nil, false, nil) | ||
| if err != nil { | ||
| t.Fatalf("screenshot: %v", err) | ||
| } | ||
| return screenshot, body | ||
| } | ||
|
|
||
| func TestScreenshotSPAWaitsForHydration(t *testing.T) { | ||
| srv := startSPAScreenshotServer(t) | ||
| browser := newScreenshotBrowser(t, false) | ||
|
|
||
| screenshot, body := captureSPA(t, browser, srv.URL) | ||
| if len(screenshot) == 0 { | ||
| t.Fatal("empty screenshot") | ||
| } | ||
| if !strings.Contains(body, spaHydratedMarker) { | ||
| t.Fatalf("early capture: body missing %q\n%s", spaHydratedMarker, body) | ||
| } | ||
| } | ||
|
|
||
| func TestScreenshotSPAConcurrentNoEarlyCapture(t *testing.T) { | ||
| const workers = 8 | ||
| srv := startSPAScreenshotServer(t) | ||
| browser := newScreenshotBrowser(t, false) | ||
|
|
||
| var wg sync.WaitGroup | ||
| errs := make(chan error, workers) | ||
| for i := 0; i < workers; i++ { | ||
| wg.Add(1) | ||
| go func() { | ||
| defer wg.Done() | ||
| screenshot, body, _, err := browser.ScreenshotWithBody(srv.URL, 15*time.Second, 200*time.Millisecond, nil, false, nil) | ||
| if err != nil { | ||
| errs <- err | ||
| return | ||
| } | ||
| if len(screenshot) == 0 { | ||
| errs <- fmt.Errorf("empty screenshot") | ||
| return | ||
| } | ||
| if !strings.Contains(body, spaHydratedMarker) { | ||
| errs <- fmt.Errorf("early capture: body missing %q", spaHydratedMarker) | ||
| } | ||
| }() | ||
| } | ||
| wg.Wait() | ||
| close(errs) | ||
| for err := range errs { | ||
| t.Error(err) | ||
| } | ||
| } | ||
|
|
||
| // BenchmarkScreenshotSPA times concurrent captures of the local SPA. | ||
| // | ||
| // On linux/amd64, /default uses chrome-headless-shell when available and | ||
| // /local forces a system Chrome via NewBrowser(..., useLocal=true). Compare: | ||
| // | ||
| // go test -bench=BenchmarkScreenshotSPA -benchtime=20s -count=5 ./runner | ||
| func BenchmarkScreenshotSPA(b *testing.B) { | ||
| for _, useLocal := range []bool{false, true} { | ||
| name := "default" | ||
| if useLocal { | ||
| name = "local" | ||
| } | ||
| b.Run(name, func(b *testing.B) { | ||
| srv := startSPAScreenshotServer(b) | ||
| browser := newScreenshotBrowser(b, useLocal) | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| screenshot, body, _, err := browser.ScreenshotWithBody(srv.URL, 15*time.Second, 200*time.Millisecond, nil, false, nil) | ||
| if err != nil { | ||
| b.Fatalf("screenshot: %v", err) | ||
| } | ||
| if len(screenshot) == 0 || !strings.Contains(body, spaHydratedMarker) { | ||
| b.Fatal("early or empty capture") | ||
| } | ||
| } | ||
|
Comment on lines
+128
to
+136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win Keep the concurrent benchmark scenario. This loop runs screenshot captures sequentially. It no longer measures the concurrent SPA workload described by the PR objective and cannot reproduce the reported concurrent speedup. Restore a parallel benchmark, or add a separate concurrent sub-benchmark and label this loop as the sequential baseline. 🤖 Prompt for AI Agents |
||
| }) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.