This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+97 -19
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package zstdframe
@@ -10,6 +10,7 @@ import (
"github.com/klauspost/compress/zstd"
"tailscale.com/util/must"
"tailscale.com/util/testenv"
)
// Option is an option that can be passed to [AppendEncode] or [AppendDecode].
@@ -104,6 +105,17 @@ func (lowMemory) isOption() {}
// By default, more memory used for better speed.
func LowMemory(low bool) Option { return lowMemory(low) }
// poolCoders reports whether encoders and decoders should be pooled for
// reuse across calls.
//
// Pooling is disabled within testing/synctest bubbles. The zstd Encoder and
// Decoder types use channels internally, and the Go runtime kills the
// process when a channel created within a bubble is used from outside that
// bubble. A process-wide pool shared between bubbled and non-bubbled
// goroutines does exactly that, so bubbled goroutines instead construct a
// fresh coder per call and let the GC reclaim it.
func poolCoders() bool { return !testenv.InSynctestBubble() }
var encoderPools sync.Map // map[encoderOptions]*sync.Pool -> *zstd.Encoder
type encoderOptions struct {
@@ -113,12 +125,8 @@ type encoderOptions struct {
lowMemory bool
}
type encoder struct {
pool *sync.Pool
*zstd.Encoder
}
func getEncoder(opts ...Option) encoder {
// parseEncoderOptions applies opts on top of the default encoder options.
func parseEncoderOptions(opts []Option) encoderOptions {
eopts := encoderOptions{level: zstd.SpeedDefault, checksum: true}
for _, opt := range opts {
switch opt := opt.(type) {
@@ -132,13 +140,71 @@ func getEncoder(opts ...Option) encoder {
eopts.lowMemory = bool(opt)
}
}
return eopts
}
vpool, ok := encoderPools.Load(eopts)
if !ok {
vpool, _ = encoderPools.LoadOrStore(eopts, new(sync.Pool))
type encoder struct {
pool *sync.Pool
*zstd.Encoder
}
var streamingEncoderPools sync.Map // map[encoderOptions]*sync.Pool -> *zstd.Encoder
// GetStreamingEncoder returns an encoder from the shared pool, configured
// for streaming (stateful) use: it maintains compression context across
// Write calls, and Close finishes the frame. The caller must call
// enc.Reset with the destination writer before first use, and enc.Close
// when done writing. Call the returned put function exactly once
// afterwards to return the encoder to the pool; the encoder must not be
// used after that, and must not be used concurrently.
func GetStreamingEncoder(opts ...Option) (enc *zstd.Encoder, put func()) {
eopts := parseEncoderOptions(opts)
var pool *sync.Pool
if poolCoders() {
vpool, ok := streamingEncoderPools.Load(eopts)
if !ok {
vpool, _ = streamingEncoderPools.LoadOrStore(eopts, new(sync.Pool))
}
pool = vpool.(*sync.Pool)
enc, _ = pool.Get().(*zstd.Encoder)
}
if enc == nil {
// Unlike the stateless encoders above, streaming encoders must not
// use SingleSegment framing: the total content size is not known up
// front, and the window size must be communicated to decoders.
zopts := []zstd.EOption{
// Set concurrency=1 to ensure synchronous operation.
zstd.WithEncoderConcurrency(1),
zstd.WithEncoderLevel(eopts.level),
zstd.WithEncoderCRC(eopts.checksum),
zstd.WithLowerEncoderMem(eopts.lowMemory),
}
if eopts.maxWindowLog2 > 0 {
zopts = append(zopts, zstd.WithWindowSize(1<<eopts.maxWindowLog2))
}
enc = must.Get(zstd.NewWriter(nil, zopts...))
}
return enc, func() {
if pool != nil {
pool.Put(enc)
}
}
}
func getEncoder(opts ...Option) encoder {
eopts := parseEncoderOptions(opts)
var pool *sync.Pool
var enc *zstd.Encoder
if poolCoders() {
vpool, ok := encoderPools.Load(eopts)
if !ok {
vpool, _ = encoderPools.LoadOrStore(eopts, new(sync.Pool))
}
pool = vpool.(*sync.Pool)
enc, _ = pool.Get().(*zstd.Encoder)
}
pool := vpool.(*sync.Pool)
enc, _ := pool.Get().(*zstd.Encoder)
if enc == nil {
var noopts int
zopts := [...]zstd.EOption{
@@ -167,7 +233,11 @@ func getEncoder(opts ...Option) encoder {
return encoder{pool, enc}
}
func putEncoder(e encoder) { e.pool.Put(e.Encoder) }
func putEncoder(e encoder) {
if e.pool != nil {
e.pool.Put(e.Encoder)
}
}
var decoderPools sync.Map // map[decoderOptions]*sync.Pool -> *zstd.Decoder
@@ -205,12 +275,16 @@ func getDecoder(opts ...Option) decoder {
}
}
vpool, ok := decoderPools.Load(dopts)
if !ok {
vpool, _ = decoderPools.LoadOrStore(dopts, new(sync.Pool))
var pool *sync.Pool
var dec *zstd.Decoder
if poolCoders() {
vpool, ok := decoderPools.Load(dopts)
if !ok {
vpool, _ = decoderPools.LoadOrStore(dopts, new(sync.Pool))
}
pool = vpool.(*sync.Pool)
dec, _ = pool.Get().(*zstd.Decoder)
}
pool := vpool.(*sync.Pool)
dec, _ := pool.Get().(*zstd.Decoder)
if dec == nil {
var noopts int
zopts := [...]zstd.DOption{
@@ -231,7 +305,11 @@ func getDecoder(opts ...Option) decoder {
return decoder{pool, dec, maxSize}
}
func putDecoder(d decoder) { d.pool.Put(d.Decoder) }
func putDecoder(d decoder) {
if d.pool != nil {
d.pool.Put(d.Decoder)
}
}
func (d decoder) DecodeAll(src, dst []byte) ([]byte, error) {
// We only configure DecodeAll to enforce MaxDecodedSize by powers-of-two.
+12 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package zstdframe provides functionality for encoding and decoding
@@ -72,6 +72,17 @@ func AppendDecode(dst, src []byte, opts ...Option) ([]byte, error) {
return dec.DecodeAll(src, dst)
}
// GetDecoder returns a decoder from the shared decoder pool for streaming
// (io.Reader-style) use, configured with opts. The caller must call
// dec.Reset on it before first use and whenever its underlying reader
// changes. When done with the decoder, the caller must call the returned
// put function exactly once to return it to the pool. The decoder must not
// be used after put is called, and must not be used concurrently.
func GetDecoder(opts ...Option) (dec *zstd.Decoder, put func()) {
d := getDecoder(opts...)
return d.Decoder, func() { putDecoder(d) }
}
// NextSize parses the next frame (regardless of whether it is a
// data frame or a metadata frame) and returns the total size of the frame.
// The frame can be skipped by slicing n bytes from b (e.g., b[n:]).