🚀 perf: CPU optimization

This commit is contained in:
2026-07-14 13:28:47 +07:00
parent dbf318a20e
commit d7e831f373
4 changed files with 170 additions and 43 deletions
+51 -2
View File
@@ -1,10 +1,14 @@
package main
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
@@ -169,7 +173,11 @@ func TestFrameToAscii(t *testing.T) {
// Below threshold -> first ramp char; full brightness -> last.
opts := asciiOptions{brightnessThreshold: 40, charset: "standard"}
ramp := []rune(resolveCharset("standard"))
out := []rune(frameToAscii([]byte{0, 255}, ramp, opts))
img := &image.RGBA{
Pix: []byte{0, 0, 0, 255, 255, 255, 255, 255},
Stride: 8, Rect: image.Rect(0, 0, 2, 1),
}
out := []rune(frameToAscii(img, buildRampLUT(ramp, opts)))
if out[0] != ramp[0] {
t.Errorf("dark px = %q, want %q", out[0], ramp[0])
}
@@ -181,12 +189,53 @@ func TestFrameToAscii(t *testing.T) {
func TestFrameToAsciiInvert(t *testing.T) {
opts := asciiOptions{brightnessThreshold: 40, charset: "standard", invert: true}
ramp := []rune(resolveCharset("standard"))
out := []rune(frameToAscii([]byte{255}, ramp, opts))
img := &image.RGBA{
Pix: []byte{255, 255, 255, 255},
Stride: 4, Rect: image.Rect(0, 0, 1, 1),
}
out := []rune(frameToAscii(img, buildRampLUT(ramp, opts)))
if out[0] != ramp[0] {
t.Errorf("inverted bright = %q, want %q", out[0], ramp[0])
}
}
func TestRenderConcurrentSameKey(t *testing.T) {
var jpegBuf bytes.Buffer
src := image.NewRGBA(image.Rect(0, 0, 16, 16))
for i := range src.Pix {
src.Pix[i] = byte(i * 7)
}
if err := jpeg.Encode(&jpegBuf, src, nil); err != nil {
t.Fatal(err)
}
r := newFrameRenderer(0, [][]byte{jpegBuf.Bytes()}, asciiOptions{
brightnessThreshold: 40,
charset: "standard",
}, newRenderCache(1<<20))
var wg sync.WaitGroup
results := make([]string, 32)
for i := range results {
wg.Add(1)
go func(i int) {
defer wg.Done()
ascii, err := r.render(0, 20, 10, false, colorTierTrueColor)
if err != nil {
t.Error(err)
return
}
results[i] = ascii
}(i)
}
wg.Wait()
for i, got := range results {
if got != results[0] {
t.Fatalf("result %d differs from result 0", i)
}
}
}
func TestRenderCacheEvictsByBytes(t *testing.T) {
budget := 3 * entryCost("k", strings.Repeat("x", 1000))
c := newRenderCache(budget)
+75 -40
View File
@@ -94,23 +94,16 @@ func putOutBuf(b []byte) {
outPool.Put(&b)
}
func resizeFrame(frame, pix []byte, width, height int, keepAspectRatio bool, tier colorTier) (draw.Image, error) {
func resizeFrame(frame, pix []byte, width, height int, keepAspectRatio bool) (*image.RGBA, error) {
src, err := jpeg.Decode(bytes.NewReader(frame))
if err != nil {
return nil, err
}
rect := image.Rect(0, 0, width, height)
var dst draw.Image
var bg color.Color
if tier == colorTierNone {
dst = &image.Gray{Pix: pix[:width*height], Stride: width, Rect: rect}
bg = color.Gray{0}
} else {
dst = &image.NRGBA{Pix: pix[:4*width*height], Stride: 4 * width, Rect: rect}
bg = color.Black
}
dst := &image.RGBA{Pix: pix[:4*width*height], Stride: 4 * width, Rect: rect}
if keepAspectRatio {
draw.Draw(dst, dst.Bounds(), image.NewUniform(bg), image.Point{}, draw.Src)
draw.Draw(dst, dst.Bounds(), image.NewUniform(color.Black), image.Point{}, draw.Src)
sb := src.Bounds()
sw, sh := sb.Dx(), sb.Dy()
scale := min(float64(width)/float64(sw), float64(height)/float64(sh))
@@ -131,6 +124,15 @@ type asciiOptions struct {
invert bool
}
func buildRampLUT(ramp []rune, options asciiOptions) *[101][]byte {
var lut [101][]byte
for b := range lut {
index := rampIndex(b, options.brightnessThreshold, len(ramp), options.invert)
lut[b] = utf8.AppendRune(nil, ramp[index])
}
return &lut
}
func rampIndex(brightness, threshold, total int, invert bool) int {
var index int
if brightness < threshold {
@@ -147,13 +149,12 @@ func rampIndex(brightness, threshold, total int, invert bool) int {
return index
}
func frameToAscii(pixels []byte, ramp []rune, options asciiOptions) string {
total := len(ramp)
buf := getOutBuf(len(pixels) * 4)
for _, p := range pixels {
brightness := int(p) * 100 / 255
index := rampIndex(brightness, options.brightnessThreshold, total, options.invert)
buf = utf8.AppendRune(buf, ramp[index])
func frameToAscii(img *image.RGBA, rampLUT *[101][]byte) string {
pix := img.Pix
buf := getOutBuf(len(pix))
for o := 0; o < len(pix); o += 4 {
brightness := (int(pix[o])*299 + int(pix[o+1])*587 + int(pix[o+2])*114) / 255 / 10
buf = append(buf, rampLUT[brightness]...)
}
ascii := string(buf)
putOutBuf(buf)
@@ -164,11 +165,18 @@ const ansiReset = "\x1b[0m"
var ansi256Levels = [6]int{0, 95, 135, 175, 215, 255}
func quantize256(r, g, b uint8) int {
toLevel := func(v uint8) int {
var decimal = func() (t [256]string) {
for i := range t {
t[i] = strconv.Itoa(i)
}
return
}()
var ansi256Cube = func() (t [256]uint8) {
for v := range t {
best, bestDist := 0, 1<<30
for i, l := range ansi256Levels {
d := int(v) - l
d := v - l
if d < 0 {
d = -d
}
@@ -176,28 +184,31 @@ func quantize256(r, g, b uint8) int {
bestDist, best = d, i
}
}
return best
t[v] = uint8(best)
}
return 16 + 36*toLevel(r) + 6*toLevel(g) + toLevel(b)
return
}()
func quantize256(r, g, b uint8) int {
return 16 + 36*int(ansi256Cube[r]) + 6*int(ansi256Cube[g]) + int(ansi256Cube[b])
}
func appendColor(buf []byte, r, g, b uint8, tier colorTier) []byte {
if tier == colorTierTrueColor {
buf = append(buf, "\x1b[38;2;"...)
buf = strconv.AppendUint(buf, uint64(r), 10)
buf = append(buf, decimal[r]...)
buf = append(buf, ';')
buf = strconv.AppendUint(buf, uint64(g), 10)
buf = append(buf, decimal[g]...)
buf = append(buf, ';')
buf = strconv.AppendUint(buf, uint64(b), 10)
buf = append(buf, decimal[b]...)
} else {
buf = append(buf, "\x1b[38;5;"...)
buf = strconv.AppendUint(buf, uint64(quantize256(r, g, b)), 10)
buf = append(buf, decimal[quantize256(r, g, b)]...)
}
return append(buf, 'm')
}
func frameToAnsi(img *image.NRGBA, ramp []rune, options asciiOptions, tier colorTier) string {
total := len(ramp)
func frameToAnsi(img *image.RGBA, rampLUT *[101][]byte, tier colorTier) string {
bounds := img.Bounds()
buf := getOutBuf(bounds.Dx() * bounds.Dy() * 16)
var lastR, lastG, lastB uint8
@@ -207,13 +218,12 @@ func frameToAnsi(img *image.NRGBA, ramp []rune, options asciiOptions, tier color
o := img.PixOffset(x, y)
r, g, bl := img.Pix[o], img.Pix[o+1], img.Pix[o+2]
brightness := (int(r)*299 + int(g)*587 + int(bl)*114) / 255 / 10
index := rampIndex(brightness, options.brightnessThreshold, total, options.invert)
if first || r != lastR || g != lastG || bl != lastB {
buf = appendColor(buf, r, g, bl, tier)
lastR, lastG, lastB = r, g, bl
first = false
}
buf = utf8.AppendRune(buf, ramp[index])
buf = append(buf, rampLUT[brightness]...)
}
if y < bounds.Max.Y-1 {
buf = append(buf, ansiReset+"\r\n"...)
@@ -296,17 +306,22 @@ type FrameRenderer struct {
setID int
colorFrames [][]byte
options asciiOptions
ramp []rune
rampLUT *[101][]byte
cache *renderCache
inflightMu sync.Mutex
inflight map[string]chan struct{}
}
func newFrameRenderer(setID int, colorFrames [][]byte, options asciiOptions, cache *renderCache) *FrameRenderer {
ramp := []rune(resolveCharset(options.charset))
return &FrameRenderer{
setID: setID,
colorFrames: colorFrames,
options: options,
ramp: []rune(resolveCharset(options.charset)),
rampLUT: buildRampLUT(ramp, options),
cache: cache,
inflight: make(map[string]chan struct{}),
}
}
@@ -316,21 +331,41 @@ func (r *FrameRenderer) render(index, width, height int, keepAspectRatio bool, t
return ascii, nil
}
n := width * height
if tier != colorTierNone {
n *= 4
if r.cache != nil {
for {
r.inflightMu.Lock()
wait, ok := r.inflight[key]
if !ok {
done := make(chan struct{})
r.inflight[key] = done
r.inflightMu.Unlock()
defer func() {
r.inflightMu.Lock()
delete(r.inflight, key)
r.inflightMu.Unlock()
close(done)
}()
break
}
r.inflightMu.Unlock()
<-wait
if ascii, ok := r.cache.get(key); ok {
return ascii, nil
}
}
}
pix := getPixBuf(n)
img, err := resizeFrame(r.colorFrames[index], pix, width, height, keepAspectRatio, tier)
pix := getPixBuf(4 * width * height)
img, err := resizeFrame(r.colorFrames[index], pix, width, height, keepAspectRatio)
if err != nil {
putPixBuf(pix)
return "", err
}
var ascii string
if tier == colorTierNone {
ascii = frameToAscii(img.(*image.Gray).Pix, r.ramp, r.options)
ascii = frameToAscii(img, r.rampLUT)
} else {
ascii = frameToAnsi(img.(*image.NRGBA), r.ramp, r.options, tier)
ascii = frameToAnsi(img, r.rampLUT, tier)
}
putPixBuf(pix)
+38
View File
@@ -0,0 +1,38 @@
package main
import (
"path/filepath"
"testing"
)
func loadBenchSet(b *testing.B) *FramesContainer {
b.Helper()
matches, _ := filepath.Glob("../frames/*.tsf")
if len(matches) == 0 {
b.Skip("no .tsf frame set in ../frames")
}
fc, err := loadTSF(matches[0])
if err != nil {
b.Skip("failed to load frame set:", err)
}
return fc
}
func benchRender(b *testing.B, tier colorTier, w, h int) {
fc := loadBenchSet(b)
r := newFrameRenderer(0, fc.ColorFrames, asciiOptions{
brightnessThreshold: 40,
charset: "detailed",
}, nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := r.render(i%len(fc.ColorFrames), w, h, false, tier); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkRenderTrueColor(b *testing.B) { benchRender(b, colorTierTrueColor, 120, 40) }
func BenchmarkRender256(b *testing.B) { benchRender(b, colorTier256, 120, 40) }
func BenchmarkRenderGray(b *testing.B) { benchRender(b, colorTierNone, 120, 40) }
func BenchmarkRenderTrueBig(b *testing.B) { benchRender(b, colorTierTrueColor, 240, 70) }
+6 -1
View File
@@ -457,6 +457,7 @@ func (s *Server) playVideo(
currentFrame := 0
loopCount := 0
lastW, lastH := 0, 0
var writeBuf []byte
for {
select {
@@ -488,7 +489,11 @@ func (s *Server) playVideo(
prefix = clearScreen
lastW, lastH = w, h
}
if _, err := channel.Write([]byte(syncStart + prefix + ascii + syncEnd)); err != nil {
writeBuf = append(writeBuf[:0], syncStart...)
writeBuf = append(writeBuf, prefix...)
writeBuf = append(writeBuf, ascii...)
writeBuf = append(writeBuf, syncEnd...)
if _, err := channel.Write(writeBuf); err != nil {
closeSession()
return
}