Files
netbird/client/vnc/server/rfb_bench_test.go
2026-05-17 15:48:15 +02:00

365 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build !js && !ios && !android
package server
import (
"image"
"math/rand"
"testing"
)
// Representative frame sizes.
var benchRects = []struct {
name string
w, h int
}{
{"1080p_full", 1920, 1080},
{"720p_full", 1280, 720},
{"256x256_tile", 256, 256},
{"64x64_tile", 64, 64},
}
func makeBenchImage(w, h int, seed int64) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, w, h))
r := rand.New(rand.NewSource(seed))
_, _ = r.Read(img.Pix)
// Force alpha byte so the fast path and slow path produce identical output.
for i := 3; i < len(img.Pix); i += 4 {
img.Pix[i] = 0xff
}
return img
}
func makeBenchImagePartial(w, h, changedRows int) (*image.RGBA, *image.RGBA) {
prev := makeBenchImage(w, h, 1)
cur := image.NewRGBA(prev.Rect)
copy(cur.Pix, prev.Pix)
if changedRows > h {
changedRows = h
}
// Dirty the first `changedRows` rows.
r := rand.New(rand.NewSource(2))
_, _ = r.Read(cur.Pix[:changedRows*cur.Stride])
for i := 3; i < len(cur.Pix); i += 4 {
cur.Pix[i] = 0xff
}
return prev, cur
}
func BenchmarkEncodeRawRect(b *testing.B) {
pf := defaultClientPixelFormat()
for _, r := range benchRects {
img := makeBenchImage(r.w, r.h, 1)
b.Run(r.name, func(b *testing.B) {
b.SetBytes(int64(r.w * r.h * 4))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = encodeRawRect(img, pf, 0, 0, r.w, r.h)
}
})
}
}
func BenchmarkEncodeTightRect(b *testing.B) {
pf := defaultClientPixelFormat()
for _, r := range benchRects {
img := makeBenchImage(r.w, r.h, 1)
t := newTightState()
b.Run(r.name, func(b *testing.B) {
b.SetBytes(int64(r.w * r.h * 4))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = encodeTightRect(img, pf, 0, 0, r.w, r.h, t)
}
})
}
}
// BenchmarkWritePixels isolates the per-pixel pack loop from the allocation
// and FramebufferUpdate-header overhead.
func BenchmarkWritePixels(b *testing.B) {
pf := defaultClientPixelFormat()
for _, r := range benchRects {
img := makeBenchImage(r.w, r.h, 1)
dst := make([]byte, r.w*r.h*4)
b.Run(r.name, func(b *testing.B) {
b.SetBytes(int64(r.w * r.h * 4))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
writePixels(dst, img, pf, rect{0, 0, r.w, r.h})
}
})
}
}
func BenchmarkSwizzleBGRAtoRGBA(b *testing.B) {
for _, r := range benchRects {
size := r.w * r.h * 4
src := make([]byte, size)
dst := make([]byte, size)
rng := rand.New(rand.NewSource(1))
_, _ = rng.Read(src)
b.Run(r.name, func(b *testing.B) {
b.SetBytes(int64(size))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
swizzleBGRAtoRGBA(dst, src)
}
})
}
}
// BenchmarkSwizzleBGRAtoRGBANaive is the naive byte-by-byte implementation
// that the Linux SHM capturer used before the uint32 rewrite, kept here so
// we can compare the cost directly.
func BenchmarkSwizzleBGRAtoRGBANaive(b *testing.B) {
for _, r := range benchRects {
size := r.w * r.h * 4
src := make([]byte, size)
dst := make([]byte, size)
rng := rand.New(rand.NewSource(1))
_, _ = rng.Read(src)
b.Run(r.name, func(b *testing.B) {
b.SetBytes(int64(size))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for j := 0; j < size; j += 4 {
dst[j+0] = src[j+2]
dst[j+1] = src[j+1]
dst[j+2] = src[j+0]
dst[j+3] = 0xff
}
}
})
}
}
// BenchmarkEncodeUniformTile_TightFill measures the fast path for a uniform
// 64×64 tile via Tight's Fill subencoding (16 wire bytes regardless of size).
func BenchmarkEncodeUniformTile_TightFill(b *testing.B) {
pf := defaultClientPixelFormat()
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
for i := 0; i < len(img.Pix); i += 4 {
img.Pix[i+0] = 0x33
img.Pix[i+1] = 0x66
img.Pix[i+2] = 0x99
img.Pix[i+3] = 0xff
}
t := newTightState()
b.ReportAllocs()
var bytesOut int
for i := 0; i < b.N; i++ {
out := encodeTightRect(img, pf, 0, 0, 64, 64, t)
bytesOut = len(out)
}
b.ReportMetric(float64(bytesOut), "wire_bytes")
}
func BenchmarkTileIsUniform(b *testing.B) {
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
for i := 0; i < len(img.Pix); i += 4 {
img.Pix[i+3] = 0xff
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = tileIsUniform(img, 0, 0, 64, 64)
}
}
// BenchmarkEncodeManyTilesVsFullFrame exercises the bandwidth + CPU
// trade-off that motivates the full-frame promotion path: encoding a burst
// of N dirty 64×64 tiles as separate Tight rects vs emitting one big Tight
// rect for the whole frame.
func BenchmarkEncodeManyTilesVsFullFrame(b *testing.B) {
pf := defaultClientPixelFormat()
const w, h = 1920, 1080
img := makeBenchImage(w, h, 1)
// Build the list of every tile in the frame (worst case: entire screen dirty).
var tiles [][4]int
for ty := 0; ty < h; ty += tileSize {
th := tileSize
if ty+th > h {
th = h - ty
}
for tx := 0; tx < w; tx += tileSize {
tw := tileSize
if tx+tw > w {
tw = w - tx
}
tiles = append(tiles, [4]int{tx, ty, tw, th})
}
}
nTiles := len(tiles)
b.Run("per_tile_tight", func(b *testing.B) {
t := newTightState()
b.SetBytes(int64(w * h * 4))
b.ReportAllocs()
var totalOut int
for i := 0; i < b.N; i++ {
totalOut = 0
for _, r := range tiles {
out := encodeTightRect(img, pf, r[0], r[1], r[2], r[3], t)
totalOut += len(out)
}
}
b.ReportMetric(float64(totalOut), "wire_bytes")
b.ReportMetric(float64(nTiles), "tiles")
})
b.Run("full_frame_tight", func(b *testing.B) {
t := newTightState()
b.SetBytes(int64(w * h * 4))
b.ReportAllocs()
var totalOut int
for i := 0; i < b.N; i++ {
out := encodeTightRect(img, pf, 0, 0, w, h, t)
totalOut = len(out)
}
b.ReportMetric(float64(totalOut), "wire_bytes")
})
}
// BenchmarkShouldPromoteToFullFrame verifies the threshold check itself is
// cheap. It runs on every frame, so regressions here hit all workloads.
func BenchmarkShouldPromoteToFullFrame(b *testing.B) {
const w, h = 1920, 1080
s := &session{serverW: w, serverH: h}
// Build a worst-case rect list (every tile dirty, 510 entries).
var rects [][4]int
for ty := 0; ty < h; ty += tileSize {
th := tileSize
if ty+th > h {
th = h - ty
}
for tx := 0; tx < w; tx += tileSize {
tw := tileSize
if tx+tw > w {
tw = w - tx
}
rects = append(rects, [4]int{tx, ty, tw, th})
}
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = s.shouldPromoteToFullFrame(rects)
}
}
// BenchmarkEncodeCoalescedVsPerTile compares per-tile encoding vs the
// coalesced rect list emitted by diffRects, on a horizontal-band dirty
// pattern (e.g. a scrolling status bar) where coalescing pays off.
func BenchmarkEncodeCoalescedVsPerTile(b *testing.B) {
pf := defaultClientPixelFormat()
const w, h = 1920, 1080
img := makeBenchImage(w, h, 1)
// Dirty band: rows 200..264 (one tile-row), full width.
var perTile [][4]int
for tx := 0; tx < w; tx += tileSize {
tw := tileSize
if tx+tw > w {
tw = w - tx
}
perTile = append(perTile, [4]int{tx, 200, tw, tileSize})
}
coalesced := coalesceRects(append([][4]int(nil), perTile...))
b.Run("per_tile", func(b *testing.B) {
t := newTightState()
b.ReportAllocs()
var bytesOut int
for i := 0; i < b.N; i++ {
bytesOut = 0
for _, r := range perTile {
out := encodeTightRect(img, pf, r[0], r[1], r[2], r[3], t)
bytesOut += len(out)
}
}
b.ReportMetric(float64(bytesOut), "wire_bytes")
b.ReportMetric(float64(len(perTile)), "rects")
})
b.Run("coalesced", func(b *testing.B) {
t := newTightState()
b.ReportAllocs()
var bytesOut int
for i := 0; i < b.N; i++ {
bytesOut = 0
for _, r := range coalesced {
out := encodeTightRect(img, pf, r[0], r[1], r[2], r[3], t)
bytesOut += len(out)
}
}
b.ReportMetric(float64(bytesOut), "wire_bytes")
b.ReportMetric(float64(len(coalesced)), "rects")
})
}
func BenchmarkCoalesceRects(b *testing.B) {
const w, h = 1920, 1080
// Worst case: every tile dirty.
var allTiles [][4]int
for ty := 0; ty < h; ty += tileSize {
th := tileSize
if ty+th > h {
th = h - ty
}
for tx := 0; tx < w; tx += tileSize {
tw := tileSize
if tx+tw > w {
tw = w - tx
}
allTiles = append(allTiles, [4]int{tx, ty, tw, th})
}
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
in := make([][4]int, len(allTiles))
copy(in, allTiles)
_ = coalesceRects(in)
}
}
// BenchmarkEncodeTight_Photo measures Tight on random/photographic content.
// The internal sampledColorCount gate routes large many-colour rects to JPEG
// at quality 70.
func BenchmarkEncodeTight_Photo(b *testing.B) {
pf := defaultClientPixelFormat()
for _, r := range []struct {
name string
w, h int
}{
{"256x256", 256, 256},
{"512x512", 512, 512},
{"1080p", 1920, 1080},
} {
img := makeBenchImage(r.w, r.h, 1)
b.Run(r.name+"/tight", func(b *testing.B) {
t := newTightState()
b.SetBytes(int64(r.w * r.h * 4))
b.ReportAllocs()
var bytesOut int
for i := 0; i < b.N; i++ {
out := encodeTightRect(img, pf, 0, 0, r.w, r.h, t)
bytesOut = len(out)
}
b.ReportMetric(float64(bytesOut), "wire_bytes")
})
}
}
func BenchmarkDiffRects(b *testing.B) {
for _, r := range benchRects {
prev, cur := makeBenchImagePartial(r.w, r.h, 100)
b.Run(r.name, func(b *testing.B) {
b.SetBytes(int64(r.w * r.h * 4))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = diffRects(prev, cur, r.w, r.h, tileSize)
}
})
}
}