| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- // 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() }))
- }
|