measure the render path, then remember pictures instead of caching pages
The entry said to measure first and put the number in the commit, so: a plain page renders in 14µs, a twelve-picture gallery in 1.23ms. Of that, ~102µs per picture was reading, hashing and decoding bytes the previous request had already read. Remembering that one fact — keyed by path, size and modification time — brings the same gallery to 63µs. 19.5× faster, 21× fewer bytes allocated, twenty-odd lines. After which nothing is slow enough to justify caching whole pages, so ADR-0044 declines the page cache and leaves the parked validity model parked, now with a measurement rather than an intuition behind its trigger. That parked model has five axes and was written before any code existed. The problem it would have been built for turned out to be one repeated file read. Benchmarks live in internal/web so they measure through the real handler, which is also what conventions.md wants before any cache goes in the render path. The invalidation risk has its own test: an edited picture is a different key, so the memo cannot serve yesterday's dimensions. Everything runs clean under -race, since the map is read by concurrent requests.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"khosra/internal/content"
|
||||
"khosra/internal/ext/shortcodes"
|
||||
"khosra/internal/render"
|
||||
)
|
||||
|
||||
// photo is a real encoded JPEG, so a benchmark measures decoding rather than a stub.
|
||||
func photo(b *testing.B, width int) []byte {
|
||||
b.Helper()
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, width*2/3))
|
||||
for x := range width {
|
||||
for y := range width * 2 / 3 {
|
||||
img.Set(x, y, color.RGBA{uint8(x % 256), uint8(y % 256), 90, 255})
|
||||
}
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if err := jpeg.Encode(&out, img, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
func benchHandler(b *testing.B, pictures int) http.Handler {
|
||||
b.Helper()
|
||||
fsys := fstest.MapFS{
|
||||
"content/posts/plain.md": {Data: []byte("---\ntitle: Plain\ndate: 2026-01-01\n---\nJust prose, several words of it.\n")},
|
||||
"content/art/set/index.md": {Data: []byte("---\ntitle: Set\ndate: 2026-01-02\n---\n{{< gallery >}}\n")},
|
||||
}
|
||||
shot := photo(b, 1600)
|
||||
for i := range pictures {
|
||||
fsys[fmt.Sprintf("content/art/set/%02d.jpg", i)] = &fstest.MapFile{Data: shot}
|
||||
}
|
||||
bundles, err := content.Scan(fsys)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r, err := render.New(fsys, content.Settings{}, func(p render.Partial) []goldmark.Extender {
|
||||
return []goldmark.Extender{shortcodes.New(p)}
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
return Handler(content.NewSite(bundles), r, fsys, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func serveOnce(b *testing.B, h http.Handler, path string) {
|
||||
b.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
b.Fatalf("GET %s = %d", path, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPlainPage(b *testing.B) {
|
||||
h := benchHandler(b, 0)
|
||||
for b.Loop() {
|
||||
serveOnce(b, h, "/posts/plain/")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGalleryPage(b *testing.B) {
|
||||
for _, n := range []int{1, 6, 12} {
|
||||
b.Run(fmt.Sprintf("pictures=%d", n), func(b *testing.B) {
|
||||
h := benchHandler(b, n)
|
||||
for b.Loop() {
|
||||
serveOnce(b, h, "/art/set/")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkListing(b *testing.B) {
|
||||
h := benchHandler(b, 0)
|
||||
for b.Loop() {
|
||||
serveOnce(b, h, "/posts/")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user