mirror of
https://github.com/YuzuZensai/TrollSSH.git
synced 2026-09-13 15:29:00 +00:00
🚀 perf: make render-cache compression opt-in, default off
This commit is contained in:
@@ -31,6 +31,9 @@ MAX_TERMINAL_CELLS=256000
|
||||
|
||||
# Memory budget in MB for the rendered-frame cache (0 disables caching).
|
||||
RENDER_CACHE_MB=256
|
||||
# Compress cached frames (flate). Cuts cache RAM ~2x but decompresses on every
|
||||
# cache hit, so CPU rises with concurrent sessions. Off = zero-copy reads.
|
||||
RENDER_CACHE_COMPRESS=false
|
||||
|
||||
# Go soft memory limit; set below your container limit to avoid OOM kills.
|
||||
GOMEMLIMIT=1GiB
|
||||
|
||||
@@ -33,6 +33,7 @@ type Config struct {
|
||||
MaxTerminalCells int
|
||||
SessionTimeout time.Duration
|
||||
RenderCacheMB int
|
||||
RenderCacheCompress bool
|
||||
BrightnessThreshold int
|
||||
Charset string
|
||||
Invert bool
|
||||
@@ -133,6 +134,7 @@ func Load() Config {
|
||||
MaxTerminalCells: envInt("MAX_TERMINAL_CELLS", 500*512, 1, maxInt),
|
||||
SessionTimeout: envDurationMs("SESSION_TIMEOUT", 10*time.Minute),
|
||||
RenderCacheMB: envInt("RENDER_CACHE_MB", 256, 0, maxInt),
|
||||
RenderCacheCompress: envBool("RENDER_CACHE_COMPRESS", false),
|
||||
BrightnessThreshold: envInt("BRIGHTNESS_THRESHOLD", 40, 0, 100),
|
||||
Charset: envString("CHARSET", "detailed"),
|
||||
Invert: envBool("INVERT", false),
|
||||
|
||||
@@ -29,6 +29,7 @@ type cacheShard struct {
|
||||
|
||||
type Cache struct {
|
||||
shards []cacheShard
|
||||
compress bool
|
||||
size atomic.Int64
|
||||
hits atomic.Uint64
|
||||
misses atomic.Uint64
|
||||
@@ -83,12 +84,12 @@ func decompressAscii(src []byte, origLen int) []byte {
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func NewCache(maxBytes int64) *Cache {
|
||||
func NewCache(maxBytes int64, compress bool) *Cache {
|
||||
if maxBytes <= 0 {
|
||||
return nil
|
||||
}
|
||||
shardCount := int(min(int64(16), max(int64(1), maxBytes/(1<<20))))
|
||||
cache := &Cache{shards: make([]cacheShard, shardCount)}
|
||||
cache := &Cache{shards: make([]cacheShard, shardCount), compress: compress}
|
||||
for i := range cache.shards {
|
||||
cache.shards[i] = cacheShard{
|
||||
maxBytes: maxBytes / int64(shardCount),
|
||||
@@ -130,6 +131,9 @@ func (c *Cache) get(key cacheKey) ([]byte, bool) {
|
||||
data, origLen := entry.data, entry.origLen
|
||||
shard.mu.Unlock()
|
||||
c.hits.Add(1)
|
||||
if !c.compress {
|
||||
return data, true
|
||||
}
|
||||
return decompressAscii(data, origLen), true
|
||||
}
|
||||
|
||||
@@ -138,8 +142,11 @@ func (c *Cache) put(key cacheKey, ascii []byte) {
|
||||
return
|
||||
}
|
||||
shard := c.shard(key)
|
||||
compressed := compressAscii(ascii)
|
||||
cost := entryCost(key, compressed)
|
||||
data, origLen := ascii, 0
|
||||
if c.compress {
|
||||
data, origLen = compressAscii(ascii), len(ascii)
|
||||
}
|
||||
cost := entryCost(key, data)
|
||||
if cost > shard.maxBytes {
|
||||
c.rejections.Add(1)
|
||||
return
|
||||
@@ -149,7 +156,7 @@ func (c *Cache) put(key cacheKey, ascii []byte) {
|
||||
if _, ok := shard.entries[key]; ok {
|
||||
return
|
||||
}
|
||||
entry := &cacheEntry{key: key, data: compressed, origLen: len(ascii), cost: cost}
|
||||
entry := &cacheEntry{key: key, data: data, origLen: origLen, cost: cost}
|
||||
shard.entries[key] = shard.order.PushBack(entry)
|
||||
shard.size += cost
|
||||
c.size.Add(cost)
|
||||
|
||||
@@ -43,27 +43,35 @@ func BenchmarkRenderGray(b *testing.B) { benchRender(b, ColorTierNone, 120,
|
||||
func BenchmarkRenderTrueBig(b *testing.B) { benchRender(b, ColorTierTrueColor, 240, 70) }
|
||||
|
||||
func BenchmarkRenderCachedParallel(b *testing.B) {
|
||||
fc := loadBenchSet(b)
|
||||
r := NewRenderer(0, fc.ColorFrames, Options{
|
||||
BrightnessThreshold: 40,
|
||||
Charset: "detailed",
|
||||
}, NewCache(8<<20))
|
||||
frame, err := r.Render(0, 120, 40, false, ColorTierTrueColor)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.SetBytes(int64(len(frame)))
|
||||
b.ReportAllocs()
|
||||
var failures atomic.Uint64
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
if _, err := r.Render(0, 120, 40, false, ColorTierTrueColor); err != nil {
|
||||
failures.Add(1)
|
||||
}
|
||||
for _, compress := range []bool{false, true} {
|
||||
name := "uncompressed"
|
||||
if compress {
|
||||
name = "compressed"
|
||||
}
|
||||
})
|
||||
if failures.Load() != 0 {
|
||||
b.Fatalf("render failures: %d", failures.Load())
|
||||
b.Run(name, func(b *testing.B) {
|
||||
fc := loadBenchSet(b)
|
||||
r := NewRenderer(0, fc.ColorFrames, Options{
|
||||
BrightnessThreshold: 40,
|
||||
Charset: "detailed",
|
||||
}, NewCache(8<<20, compress))
|
||||
frame, err := r.Render(0, 120, 40, false, ColorTierTrueColor)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.SetBytes(int64(len(frame)))
|
||||
b.ReportAllocs()
|
||||
var failures atomic.Uint64
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
if _, err := r.Render(0, 120, 40, false, ColorTierTrueColor); err != nil {
|
||||
failures.Add(1)
|
||||
}
|
||||
}
|
||||
})
|
||||
if failures.Load() != 0 {
|
||||
b.Fatalf("render failures: %d", failures.Load())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestRenderConcurrentSameKey(t *testing.T) {
|
||||
r := NewRenderer(0, [][]byte{jpegBuf.Bytes()}, Options{
|
||||
BrightnessThreshold: 40,
|
||||
Charset: "standard",
|
||||
}, NewCache(1<<20))
|
||||
}, NewCache(1<<20, false))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([][]byte, 32)
|
||||
@@ -104,7 +104,7 @@ func TestRenderCacheEvictsByBytes(t *testing.T) {
|
||||
key := func(index int) cacheKey { return cacheKey{index: index} }
|
||||
payload := incompressible(1000)
|
||||
budget := 3 * entryCost(key(0), compressAscii(payload))
|
||||
c := NewCache(budget)
|
||||
c := NewCache(budget, true)
|
||||
for i := range 5 {
|
||||
c.put(key(i), payload)
|
||||
}
|
||||
@@ -120,20 +120,22 @@ func TestRenderCacheEvictsByBytes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRenderCacheRoundTrips(t *testing.T) {
|
||||
c := NewCache(1 << 20)
|
||||
want := incompressible(4096)
|
||||
c.put(cacheKey{}, want)
|
||||
got, ok := c.get(cacheKey{})
|
||||
if !ok {
|
||||
t.Fatal("entry should be cached")
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatal("decompressed entry does not match original")
|
||||
for _, compress := range []bool{false, true} {
|
||||
c := NewCache(1<<20, compress)
|
||||
want := incompressible(4096)
|
||||
c.put(cacheKey{}, want)
|
||||
got, ok := c.get(cacheKey{})
|
||||
if !ok {
|
||||
t.Fatalf("compress=%v: entry should be cached", compress)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("compress=%v: entry does not match original", compress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCacheDisabled(t *testing.T) {
|
||||
c := NewCache(0)
|
||||
c := NewCache(0, false)
|
||||
if c != nil {
|
||||
t.Fatal("zero budget should disable the cache")
|
||||
}
|
||||
@@ -144,7 +146,7 @@ func TestRenderCacheDisabled(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRenderCacheRejectsOversizedEntry(t *testing.T) {
|
||||
c := NewCache(256)
|
||||
c := NewCache(256, true)
|
||||
c.put(cacheKey{}, incompressible(10_000))
|
||||
if _, ok := c.get(cacheKey{}); ok {
|
||||
t.Error("entry larger than budget should not be cached")
|
||||
@@ -155,7 +157,7 @@ func TestRenderCacheRejectsOversizedEntry(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRenderCacheAccountsCompressedSize(t *testing.T) {
|
||||
cache := NewCache(512)
|
||||
cache := NewCache(512, true)
|
||||
value := make([]byte, 1, 4096)
|
||||
cache.put(cacheKey{}, value)
|
||||
if _, ok := cache.get(cacheKey{}); !ok {
|
||||
|
||||
@@ -232,7 +232,7 @@ func clampTermSize(cols, rows, maxDimension, maxCells, quantum int) (int, int) {
|
||||
func New(deps ServerDeps) *Server {
|
||||
cfg := deps.Config
|
||||
|
||||
cache := render.NewCache(int64(cfg.RenderCacheMB) << 20)
|
||||
cache := render.NewCache(int64(cfg.RenderCacheMB)<<20, cfg.RenderCacheCompress)
|
||||
sets := make([]frameSet, len(deps.VideoSets))
|
||||
for i, data := range deps.VideoSets {
|
||||
sets[i] = frameSet{
|
||||
|
||||
Reference in New Issue
Block a user