mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	Updates #11058 Change-Id: I35e7ef9b90e83cac04ca93fd964ad00ed5b48430 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			239 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			239 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| package zstdframe
 | |
| 
 | |
| import (
 | |
| 	"math/bits"
 | |
| 	"math/rand/v2"
 | |
| 	"os"
 | |
| 	"runtime"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/klauspost/compress/zstd"
 | |
| 	"tailscale.com/util/must"
 | |
| )
 | |
| 
 | |
| // Use the concatenation of all Go source files in zstdframe as testdata.
 | |
| var src = func() (out []byte) {
 | |
| 	for _, de := range must.Get(os.ReadDir(".")) {
 | |
| 		if strings.HasSuffix(de.Name(), ".go") {
 | |
| 			out = append(out, must.Get(os.ReadFile(de.Name()))...)
 | |
| 		}
 | |
| 	}
 | |
| 	return out
 | |
| }()
 | |
| var dst []byte
 | |
| var dsts [][]byte
 | |
| 
 | |
| // zstdEnc is identical to getEncoder without options,
 | |
| // except it relies on concurrency managed by the zstd package itself.
 | |
| var zstdEnc = must.Get(zstd.NewWriter(nil,
 | |
| 	zstd.WithEncoderConcurrency(runtime.NumCPU()),
 | |
| 	zstd.WithSingleSegment(true),
 | |
| 	zstd.WithZeroFrames(true),
 | |
| 	zstd.WithEncoderLevel(zstd.SpeedDefault),
 | |
| 	zstd.WithEncoderCRC(true),
 | |
| 	zstd.WithLowerEncoderMem(false)))
 | |
| 
 | |
| // zstdDec is identical to getDecoder without options,
 | |
| // except it relies on concurrency managed by the zstd package itself.
 | |
| var zstdDec = must.Get(zstd.NewReader(nil,
 | |
| 	zstd.WithDecoderConcurrency(runtime.NumCPU()),
 | |
| 	zstd.WithDecoderMaxMemory(1<<63),
 | |
| 	zstd.IgnoreChecksum(false),
 | |
| 	zstd.WithDecoderLowmem(false)))
 | |
| 
 | |
| var coders = []struct {
 | |
| 	name         string
 | |
| 	appendEncode func([]byte, []byte) []byte
 | |
| 	appendDecode func([]byte, []byte) ([]byte, error)
 | |
| }{{
 | |
| 	name:         "zstd",
 | |
| 	appendEncode: func(dst, src []byte) []byte { return zstdEnc.EncodeAll(src, dst) },
 | |
| 	appendDecode: func(dst, src []byte) ([]byte, error) { return zstdDec.DecodeAll(src, dst) },
 | |
| }, {
 | |
| 	name:         "zstdframe",
 | |
| 	appendEncode: func(dst, src []byte) []byte { return AppendEncode(dst, src) },
 | |
| 	appendDecode: func(dst, src []byte) ([]byte, error) { return AppendDecode(dst, src) },
 | |
| }}
 | |
| 
 | |
| func TestDecodeMaxSize(t *testing.T) {
 | |
| 	var enc, dec []byte
 | |
| 	zeros := make([]byte, 1<<16, 2<<16)
 | |
| 	check := func(encSize, maxDecSize int) {
 | |
| 		var gotErr, wantErr error
 | |
| 		enc = AppendEncode(enc[:0], zeros[:encSize])
 | |
| 
 | |
| 		// Directly calling zstd.Decoder.DecodeAll may not trigger size check
 | |
| 		// since it only operates on closest power-of-two.
 | |
| 		dec, gotErr = func() ([]byte, error) {
 | |
| 			d := getDecoder(MaxDecodedSize(uint64(maxDecSize)))
 | |
| 			defer putDecoder(d)
 | |
| 			return d.Decoder.DecodeAll(enc, dec[:0]) // directly call zstd.Decoder.DecodeAll
 | |
| 		}()
 | |
| 		if encSize > 1<<(64-bits.LeadingZeros64(uint64(maxDecSize)-1)) {
 | |
| 			wantErr = zstd.ErrDecoderSizeExceeded
 | |
| 		}
 | |
| 		if gotErr != wantErr {
 | |
| 			t.Errorf("DecodeAll(AppendEncode(%d), %d) error = %v, want %v", encSize, maxDecSize, gotErr, wantErr)
 | |
| 		}
 | |
| 
 | |
| 		// Calling AppendDecode should perform the exact size check.
 | |
| 		dec, gotErr = AppendDecode(dec[:0], enc, MaxDecodedSize(uint64(maxDecSize)))
 | |
| 		if encSize > maxDecSize {
 | |
| 			wantErr = zstd.ErrDecoderSizeExceeded
 | |
| 		}
 | |
| 		if gotErr != wantErr {
 | |
| 			t.Errorf("AppendDecode(AppendEncode(%d), %d) error = %v, want %v", encSize, maxDecSize, gotErr, wantErr)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	rn := rand.New(rand.NewPCG(0, 0))
 | |
| 	for n := 1 << 10; n <= len(zeros); n <<= 1 {
 | |
| 		nl := rn.IntN(n + 1)
 | |
| 		check(nl, nl)
 | |
| 		check(nl, nl-1)
 | |
| 		check(nl, (n+nl)/2)
 | |
| 		check(nl, n)
 | |
| 		check((n+nl)/2, n)
 | |
| 		check(n-1, n-1)
 | |
| 		check(n-1, n)
 | |
| 		check(n-1, n+1)
 | |
| 		check(n, n-1)
 | |
| 		check(n, n)
 | |
| 		check(n, n+1)
 | |
| 		check(n+1, n-1)
 | |
| 		check(n+1, n)
 | |
| 		check(n+1, n+1)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func BenchmarkEncode(b *testing.B) {
 | |
| 	options := []struct {
 | |
| 		name string
 | |
| 		opts []Option
 | |
| 	}{
 | |
| 		{name: "Best", opts: []Option{BestCompression}},
 | |
| 		{name: "Better", opts: []Option{BetterCompression}},
 | |
| 		{name: "Default", opts: []Option{DefaultCompression}},
 | |
| 		{name: "Fastest", opts: []Option{FastestCompression}},
 | |
| 		{name: "FastestLowMemory", opts: []Option{FastestCompression, LowMemory(true)}},
 | |
| 		{name: "FastestWindowSize", opts: []Option{FastestCompression, MaxWindowSize(1 << 10)}},
 | |
| 		{name: "FastestNoChecksum", opts: []Option{FastestCompression, WithChecksum(false)}},
 | |
| 	}
 | |
| 	for _, bb := range options {
 | |
| 		b.Run(bb.name, func(b *testing.B) {
 | |
| 			b.ReportAllocs()
 | |
| 			b.SetBytes(int64(len(src)))
 | |
| 			for range b.N {
 | |
| 				dst = AppendEncode(dst[:0], src, bb.opts...)
 | |
| 			}
 | |
| 		})
 | |
| 		if testing.Verbose() {
 | |
| 			ratio := float64(len(src)) / float64(len(dst))
 | |
| 			b.Logf("ratio:  %0.3fx", ratio)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func BenchmarkDecode(b *testing.B) {
 | |
| 	options := []struct {
 | |
| 		name string
 | |
| 		opts []Option
 | |
| 	}{
 | |
| 		{name: "Checksum", opts: []Option{WithChecksum(true)}},
 | |
| 		{name: "NoChecksum", opts: []Option{WithChecksum(false)}},
 | |
| 		{name: "LowMemory", opts: []Option{LowMemory(true)}},
 | |
| 	}
 | |
| 	src := AppendEncode(nil, src)
 | |
| 	for _, bb := range options {
 | |
| 		b.Run(bb.name, func(b *testing.B) {
 | |
| 			b.ReportAllocs()
 | |
| 			b.SetBytes(int64(len(src)))
 | |
| 			for range b.N {
 | |
| 				dst = must.Get(AppendDecode(dst[:0], src, bb.opts...))
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func BenchmarkEncodeParallel(b *testing.B) {
 | |
| 	numCPU := runtime.NumCPU()
 | |
| 	for _, coder := range coders {
 | |
| 		dsts = dsts[:0]
 | |
| 		for range numCPU {
 | |
| 			dsts = append(dsts, coder.appendEncode(nil, src))
 | |
| 		}
 | |
| 		b.Run(coder.name, func(b *testing.B) {
 | |
| 			b.ReportAllocs()
 | |
| 			for range b.N {
 | |
| 				var group sync.WaitGroup
 | |
| 				for j := 0; j < numCPU; j++ {
 | |
| 					group.Add(1)
 | |
| 					go func(j int) {
 | |
| 						defer group.Done()
 | |
| 						dsts[j] = coder.appendEncode(dsts[j][:0], src)
 | |
| 					}(j)
 | |
| 				}
 | |
| 				group.Wait()
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func BenchmarkDecodeParallel(b *testing.B) {
 | |
| 	numCPU := runtime.NumCPU()
 | |
| 	for _, coder := range coders {
 | |
| 		dsts = dsts[:0]
 | |
| 		src := AppendEncode(nil, src)
 | |
| 		for range numCPU {
 | |
| 			dsts = append(dsts, must.Get(coder.appendDecode(nil, src)))
 | |
| 		}
 | |
| 		b.Run(coder.name, func(b *testing.B) {
 | |
| 			b.ReportAllocs()
 | |
| 			for range b.N {
 | |
| 				var group sync.WaitGroup
 | |
| 				for j := 0; j < numCPU; j++ {
 | |
| 					group.Add(1)
 | |
| 					go func(j int) {
 | |
| 						defer group.Done()
 | |
| 						dsts[j] = must.Get(coder.appendDecode(dsts[j][:0], src))
 | |
| 					}(j)
 | |
| 				}
 | |
| 				group.Wait()
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var opt Option
 | |
| 
 | |
| func TestOptionAllocs(t *testing.T) {
 | |
| 	t.Run("EncoderLevel", func(t *testing.T) {
 | |
| 		t.Log(testing.AllocsPerRun(1e3, func() { opt = EncoderLevel(zstd.SpeedFastest) }))
 | |
| 	})
 | |
| 	t.Run("MaxDecodedSize/PowerOfTwo", func(t *testing.T) {
 | |
| 		t.Log(testing.AllocsPerRun(1e3, func() { opt = MaxDecodedSize(1024) }))
 | |
| 	})
 | |
| 	t.Run("MaxDecodedSize/Prime", func(t *testing.T) {
 | |
| 		t.Log(testing.AllocsPerRun(1e3, func() { opt = MaxDecodedSize(1021) }))
 | |
| 	})
 | |
| 	t.Run("MaxWindowSize", func(t *testing.T) {
 | |
| 		t.Log(testing.AllocsPerRun(1e3, func() { opt = MaxWindowSize(1024) }))
 | |
| 	})
 | |
| 	t.Run("LowMemory", func(t *testing.T) {
 | |
| 		t.Log(testing.AllocsPerRun(1e3, func() { opt = LowMemory(true) }))
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestGetDecoderAllocs(t *testing.T) {
 | |
| 	t.Log(testing.AllocsPerRun(1e3, func() { getDecoder() }))
 | |
| }
 | |
| 
 | |
| func TestGetEncoderAllocs(t *testing.T) {
 | |
| 	t.Log(testing.AllocsPerRun(1e3, func() { getEncoder() }))
 | |
| }
 |