detached 0eeac95

perf(headless): encode each screenshot frame once with fast PNG compression

commit
0eeac95b8149301ee569458a4584cfed7811fd39
author
brice · · 2026-10-21 17:54 UTC
committer
brice · · 2026-10-22 04:47 UTC
parent
3a0a985
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +147 -2
M internal/adapters/headless/output.go +44 -2
@@ -8,6 +8,7 @@ import (
88 "image/png"
99 "os"
1010 "path/filepath"
11+ "sync"
1112 "time"
1213
1314 "golang.org/x/sys/unix"
@@ -583,7 +584,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
583584 if !written {
584585 continue
585586 }
586- written, err = writePNGSecureResult(filepath.Join(opts.ScreenshotDir, "latest.png"), shot, opts.Security, scene.Security)
587+ // latest.png is the frame just written, linked rather than encoded again.
588+ written, err = linkPNGSecure(filepath.Join(opts.ScreenshotDir, name), filepath.Join(opts.ScreenshotDir, "latest.png"), shot, opts.Security, scene.Security)
587589 if err != nil {
588590 return fmt.Errorf("latest screenshot: %w", err)
589591 }
@@ -640,6 +642,21 @@ func writePNGSecure(path string, img image.Image, security ports.SessionSecurity
640642 return err
641643 }
642644
645+// screenshotPNG encodes screenshot frames: encoding is by far the largest cost
646+// of a headless frame, so it trades file size for speed. Its buffers are
647+// reused between frames.
648+var screenshotPNG = png.Encoder{CompressionLevel: png.BestSpeed, BufferPool: new(pngBuffers)}
649+
650+// pngBuffers is a png.EncoderBufferPool.
651+type pngBuffers struct{ pool sync.Pool }
652+
653+func (p *pngBuffers) Get() *png.EncoderBuffer {
654+ b, _ := p.pool.Get().(*png.EncoderBuffer)
655+ return b
656+}
657+
658+func (p *pngBuffers) Put(b *png.EncoderBuffer) { p.pool.Put(b) }
659+
643660 // writePNGSecureResult distinguishes a security skip from a published PNG.
644661 func writePNGSecureResult(path string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
645662 if security != nil && (state.Protected || security.Snapshot() != state) {
@@ -650,7 +667,7 @@ func writePNGSecureResult(path string, img image.Image, security ports.SessionSe
650667 return false, err
651668 }
652669 defer os.Remove(f.Name())
653- if err := png.Encode(f, img); err != nil {
670+ if err := screenshotPNG.Encode(f, img); err != nil {
654671 _ = f.Close()
655672 return false, err
656673 }
@@ -666,6 +683,31 @@ func writePNGSecureResult(path string, img image.Image, security ports.SessionSe
666683 return true, nil
667684 }
668685
686+// linkPNGSecure publishes the PNG already written at src as dst without
687+// encoding it again: a hard link under a temporary name, renamed over dst, so
688+// readers see the old or the new frame, never a partial one. Frame files are
689+// never modified after they are published, which makes the shared file safe.
690+// It gates on security like writePNGSecureResult, and falls back to encoding
691+// img where the file system cannot link.
692+func linkPNGSecure(src, dst string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
693+ if security != nil && (state.Protected || security.Snapshot() != state) {
694+ return false, nil
695+ }
696+ tmp := dst + ".tmp"
697+ _ = os.Remove(tmp)
698+ if err := os.Link(src, tmp); err != nil {
699+ return writePNGSecureResult(dst, img, security, state)
700+ }
701+ defer os.Remove(tmp)
702+ if security != nil && security.Snapshot() != state {
703+ return false, nil
704+ }
705+ if err := os.Rename(tmp, dst); err != nil {
706+ return false, err
707+ }
708+ return true, nil
709+}
710+
669711 // report queues what the output read, with flip for a drawn frame, and
670712 // pushes it to the sink.
671713 func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) {
@@ -8,6 +8,7 @@ import (
8 "image/png"8 "image/png"
9 "os"9 "os"
10 "path/filepath"10 "path/filepath"
11+ "sync"
11 "time"12 "time"
12 13
13 "golang.org/x/sys/unix"14 "golang.org/x/sys/unix"
@@ -583,7 +584,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
583 if !written {584 if !written {
584 continue585 continue
585 }586 }
586- written, err = writePNGSecureResult(filepath.Join(opts.ScreenshotDir, "latest.png"), shot, opts.Security, scene.Security)587+ // latest.png is the frame just written, linked rather than encoded again.
588+ written, err = linkPNGSecure(filepath.Join(opts.ScreenshotDir, name), filepath.Join(opts.ScreenshotDir, "latest.png"), shot, opts.Security, scene.Security)
587 if err != nil {589 if err != nil {
588 return fmt.Errorf("latest screenshot: %w", err)590 return fmt.Errorf("latest screenshot: %w", err)
589 }591 }
@@ -640,6 +642,21 @@ func writePNGSecure(path string, img image.Image, security ports.SessionSecurity
640 return err642 return err
641 }643 }
642 644
645+// screenshotPNG encodes screenshot frames: encoding is by far the largest cost
646+// of a headless frame, so it trades file size for speed. Its buffers are
647+// reused between frames.
648+var screenshotPNG = png.Encoder{CompressionLevel: png.BestSpeed, BufferPool: new(pngBuffers)}
649+
650+// pngBuffers is a png.EncoderBufferPool.
651+type pngBuffers struct{ pool sync.Pool }
652+
653+func (p *pngBuffers) Get() *png.EncoderBuffer {
654+ b, _ := p.pool.Get().(*png.EncoderBuffer)
655+ return b
656+}
657+
658+func (p *pngBuffers) Put(b *png.EncoderBuffer) { p.pool.Put(b) }
659+
643 // writePNGSecureResult distinguishes a security skip from a published PNG.660 // writePNGSecureResult distinguishes a security skip from a published PNG.
644 func writePNGSecureResult(path string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {661 func writePNGSecureResult(path string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
645 if security != nil && (state.Protected || security.Snapshot() != state) {662 if security != nil && (state.Protected || security.Snapshot() != state) {
@@ -650,7 +667,7 @@ func writePNGSecureResult(path string, img image.Image, security ports.SessionSe
650 return false, err667 return false, err
651 }668 }
652 defer os.Remove(f.Name())669 defer os.Remove(f.Name())
653- if err := png.Encode(f, img); err != nil {670+ if err := screenshotPNG.Encode(f, img); err != nil {
654 _ = f.Close()671 _ = f.Close()
655 return false, err672 return false, err
656 }673 }
@@ -666,6 +683,31 @@ func writePNGSecureResult(path string, img image.Image, security ports.SessionSe
666 return true, nil683 return true, nil
667 }684 }
668 685
686+// linkPNGSecure publishes the PNG already written at src as dst without
687+// encoding it again: a hard link under a temporary name, renamed over dst, so
688+// readers see the old or the new frame, never a partial one. Frame files are
689+// never modified after they are published, which makes the shared file safe.
690+// It gates on security like writePNGSecureResult, and falls back to encoding
691+// img where the file system cannot link.
692+func linkPNGSecure(src, dst string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
693+ if security != nil && (state.Protected || security.Snapshot() != state) {
694+ return false, nil
695+ }
696+ tmp := dst + ".tmp"
697+ _ = os.Remove(tmp)
698+ if err := os.Link(src, tmp); err != nil {
699+ return writePNGSecureResult(dst, img, security, state)
700+ }
701+ defer os.Remove(tmp)
702+ if security != nil && security.Snapshot() != state {
703+ return false, nil
704+ }
705+ if err := os.Rename(tmp, dst); err != nil {
706+ return false, err
707+ }
708+ return true, nil
709+}
710+
669 // report queues what the output read, with flip for a drawn frame, and711 // report queues what the output read, with flip for a drawn frame, and
670 // pushes it to the sink.712 // pushes it to the sink.
671 func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) {713 func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[ports.WindowID]uint64) {
M internal/adapters/headless/output_test.go +103 -0
@@ -269,6 +269,109 @@ func TestRunTrimsRendererOnTick(t *testing.T) {
269269 }
270270 }
271271
272+// latest.png is the frame file linked, not encoded again: the same pixels,
273+// published atomically, and nothing left behind.
274+func TestLinkPNGSecure(t *testing.T) {
275+ dir := t.TempDir()
276+ img := image.NewRGBA(image.Rect(0, 0, 3, 2))
277+ for i := range img.Pix {
278+ img.Pix[i] = byte(i * 7)
279+ }
280+ frame, latest := filepath.Join(dir, "frame-000001.png"), filepath.Join(dir, "latest.png")
281+ if err := writePNGSecure(frame, img, nil, ports.SecurityState{}); err != nil {
282+ t.Fatal(err)
283+ }
284+ // An older latest.png is replaced.
285+ if err := os.WriteFile(latest, []byte("old"), 0o600); err != nil {
286+ t.Fatal(err)
287+ }
288+ if written, err := linkPNGSecure(frame, latest, img, nil, ports.SecurityState{}); err != nil || !written {
289+ t.Fatalf("link = %v, %v", written, err)
290+ }
291+ a, err := os.ReadFile(frame)
292+ if err != nil {
293+ t.Fatal(err)
294+ }
295+ b, err := os.ReadFile(latest)
296+ if err != nil {
297+ t.Fatal(err)
298+ }
299+ if !bytes.Equal(a, b) {
300+ t.Fatal("latest.png differs from the frame")
301+ }
302+ got, err := png.Decode(bytes.NewReader(b))
303+ if err != nil {
304+ t.Fatal(err)
305+ }
306+ for y := range 2 {
307+ for x := range 3 {
308+ if g, w := color.RGBAModel.Convert(got.At(x, y)), img.RGBAAt(x, y); g != color.Color(w) {
309+ t.Errorf("(%d,%d) = %v, want %v", x, y, g, w)
310+ }
311+ }
312+ }
313+ entries, err := os.ReadDir(dir)
314+ if err != nil {
315+ t.Fatal(err)
316+ }
317+ if len(entries) != 2 {
318+ t.Fatalf("leftover files: %v", entries)
319+ }
320+}
321+
322+// A frame that crossed a security change is not published as latest.png, and
323+// a source that cannot be linked is encoded instead.
324+func TestLinkPNGSecureSkipsAndFallsBack(t *testing.T) {
325+ img := image.NewRGBA(image.Rect(0, 0, 1, 1))
326+ good := ports.SecurityState{Generation: 1}
327+ t.Run("protected", func(t *testing.T) {
328+ dir := t.TempDir()
329+ frame := filepath.Join(dir, "frame-000001.png")
330+ if err := writePNGSecure(frame, img, nil, ports.SecurityState{}); err != nil {
331+ t.Fatal(err)
332+ }
333+ gate := portsmocks.NewMockSessionSecurity(t)
334+ gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true})
335+ if written, err := linkPNGSecure(frame, filepath.Join(dir, "latest.png"), img, gate, good); err != nil || written {
336+ t.Fatalf("link = %v, %v", written, err)
337+ }
338+ if entries, _ := os.ReadDir(dir); len(entries) != 1 {
339+ t.Fatalf("latest.png or a temp file delivered: %v", entries)
340+ }
341+ })
342+ t.Run("changed after the link", func(t *testing.T) {
343+ dir := t.TempDir()
344+ frame := filepath.Join(dir, "frame-000001.png")
345+ if err := writePNGSecure(frame, img, nil, ports.SecurityState{}); err != nil {
346+ t.Fatal(err)
347+ }
348+ gate := portsmocks.NewMockSessionSecurity(t)
349+ gate.EXPECT().Snapshot().Return(good).Once()
350+ gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 2}).Once()
351+ if written, err := linkPNGSecure(frame, filepath.Join(dir, "latest.png"), img, gate, good); err != nil || written {
352+ t.Fatalf("link = %v, %v", written, err)
353+ }
354+ if entries, _ := os.ReadDir(dir); len(entries) != 1 {
355+ t.Fatalf("latest.png or a temp file delivered: %v", entries)
356+ }
357+ })
358+ t.Run("no source file", func(t *testing.T) {
359+ dir := t.TempDir()
360+ latest := filepath.Join(dir, "latest.png")
361+ if written, err := linkPNGSecure(filepath.Join(dir, "missing.png"), latest, img, nil, ports.SecurityState{}); err != nil || !written {
362+ t.Fatalf("link = %v, %v", written, err)
363+ }
364+ f, err := os.Open(latest)
365+ if err != nil {
366+ t.Fatal(err)
367+ }
368+ defer f.Close()
369+ if _, err := png.Decode(f); err != nil {
370+ t.Fatal(err)
371+ }
372+ })
373+}
374+
272375 func TestWritePNGAtomic(t *testing.T) {
273376 dir := t.TempDir()
274377 img := image.NewRGBA(image.Rect(0, 0, 1, 1))
@@ -269,6 +269,109 @@ func TestRunTrimsRendererOnTick(t *testing.T) {
269 }269 }
270 }270 }
271 271
272+// latest.png is the frame file linked, not encoded again: the same pixels,
273+// published atomically, and nothing left behind.
274+func TestLinkPNGSecure(t *testing.T) {
275+ dir := t.TempDir()
276+ img := image.NewRGBA(image.Rect(0, 0, 3, 2))
277+ for i := range img.Pix {
278+ img.Pix[i] = byte(i * 7)
279+ }
280+ frame, latest := filepath.Join(dir, "frame-000001.png"), filepath.Join(dir, "latest.png")
281+ if err := writePNGSecure(frame, img, nil, ports.SecurityState{}); err != nil {
282+ t.Fatal(err)
283+ }
284+ // An older latest.png is replaced.
285+ if err := os.WriteFile(latest, []byte("old"), 0o600); err != nil {
286+ t.Fatal(err)
287+ }
288+ if written, err := linkPNGSecure(frame, latest, img, nil, ports.SecurityState{}); err != nil || !written {
289+ t.Fatalf("link = %v, %v", written, err)
290+ }
291+ a, err := os.ReadFile(frame)
292+ if err != nil {
293+ t.Fatal(err)
294+ }
295+ b, err := os.ReadFile(latest)
296+ if err != nil {
297+ t.Fatal(err)
298+ }
299+ if !bytes.Equal(a, b) {
300+ t.Fatal("latest.png differs from the frame")
301+ }
302+ got, err := png.Decode(bytes.NewReader(b))
303+ if err != nil {
304+ t.Fatal(err)
305+ }
306+ for y := range 2 {
307+ for x := range 3 {
308+ if g, w := color.RGBAModel.Convert(got.At(x, y)), img.RGBAAt(x, y); g != color.Color(w) {
309+ t.Errorf("(%d,%d) = %v, want %v", x, y, g, w)
310+ }
311+ }
312+ }
313+ entries, err := os.ReadDir(dir)
314+ if err != nil {
315+ t.Fatal(err)
316+ }
317+ if len(entries) != 2 {
318+ t.Fatalf("leftover files: %v", entries)
319+ }
320+}
321+
322+// A frame that crossed a security change is not published as latest.png, and
323+// a source that cannot be linked is encoded instead.
324+func TestLinkPNGSecureSkipsAndFallsBack(t *testing.T) {
325+ img := image.NewRGBA(image.Rect(0, 0, 1, 1))
326+ good := ports.SecurityState{Generation: 1}
327+ t.Run("protected", func(t *testing.T) {
328+ dir := t.TempDir()
329+ frame := filepath.Join(dir, "frame-000001.png")
330+ if err := writePNGSecure(frame, img, nil, ports.SecurityState{}); err != nil {
331+ t.Fatal(err)
332+ }
333+ gate := portsmocks.NewMockSessionSecurity(t)
334+ gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true})
335+ if written, err := linkPNGSecure(frame, filepath.Join(dir, "latest.png"), img, gate, good); err != nil || written {
336+ t.Fatalf("link = %v, %v", written, err)
337+ }
338+ if entries, _ := os.ReadDir(dir); len(entries) != 1 {
339+ t.Fatalf("latest.png or a temp file delivered: %v", entries)
340+ }
341+ })
342+ t.Run("changed after the link", func(t *testing.T) {
343+ dir := t.TempDir()
344+ frame := filepath.Join(dir, "frame-000001.png")
345+ if err := writePNGSecure(frame, img, nil, ports.SecurityState{}); err != nil {
346+ t.Fatal(err)
347+ }
348+ gate := portsmocks.NewMockSessionSecurity(t)
349+ gate.EXPECT().Snapshot().Return(good).Once()
350+ gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 2}).Once()
351+ if written, err := linkPNGSecure(frame, filepath.Join(dir, "latest.png"), img, gate, good); err != nil || written {
352+ t.Fatalf("link = %v, %v", written, err)
353+ }
354+ if entries, _ := os.ReadDir(dir); len(entries) != 1 {
355+ t.Fatalf("latest.png or a temp file delivered: %v", entries)
356+ }
357+ })
358+ t.Run("no source file", func(t *testing.T) {
359+ dir := t.TempDir()
360+ latest := filepath.Join(dir, "latest.png")
361+ if written, err := linkPNGSecure(filepath.Join(dir, "missing.png"), latest, img, nil, ports.SecurityState{}); err != nil || !written {
362+ t.Fatalf("link = %v, %v", written, err)
363+ }
364+ f, err := os.Open(latest)
365+ if err != nil {
366+ t.Fatal(err)
367+ }
368+ defer f.Close()
369+ if _, err := png.Decode(f); err != nil {
370+ t.Fatal(err)
371+ }
372+ })
373+}
374+
272 func TestWritePNGAtomic(t *testing.T) {375 func TestWritePNGAtomic(t *testing.T) {
273 dir := t.TempDir()376 dir := t.TempDir()
274 img := image.NewRGBA(image.Rect(0, 0, 1, 1))377 img := image.NewRGBA(image.Rect(0, 0, 1, 1))

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close