Polling lives in internal/ext/watch, per the human's call to keep core under its ceiling rather than raise it a second time — which is what ADR-0041 said a second raise would mean. It is a poller, deletable without trace, and core stayed at 2671/2800. A settled change calls the same `rebuilder` that startup calls, because a reload path that differs from the startup path is a reload path that drifts. The index is an atomic.Pointer swapped whole, so a request reads the site that was current when it arrived instead of one being rebuilt underneath it — the alternative, mutating in place, is a data race with every in-flight request. Names, sizes and modification times, not contents: reading every file to detect a change costs more than the rebuild it triggers. Editor droppings are excluded, because saving in vim writes a swap file, a backup and the number 4913, and each would otherwise look like a change. A change must hold still for a moment first, since one save is often several operations. Verified against the running binary: a page 404s, the file appears, and five seconds later it serves — one "site root changed" in the log. Then three droppings written at once produced no rebuild at all. Two warnings fired and were fixed rather than silenced: `runServe` gave up the rebuild closure to `rebuilder`, and the fingerprint walk gave up its body to `record`, where three exclusions read as a list instead of as nesting. The Dockerfile ships the binary alone. The site root arrives as a volume and is never copied in — it is somebody's content repository with its own history (ADR-0011), so the image is the same for every site.
92 lines
2.2 KiB
Go
92 lines
2.2 KiB
Go
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(Fixed(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/")
|
|
}
|
|
}
|