Skip to content

Commit 6debdd2

Browse files
committed
block: add ParseCompressionStats
This will be necessary to read the compression stats from existing tables (after Open).
1 parent b2e038a commit 6debdd2

File tree

4 files changed

+128
-1
lines changed

4 files changed

+128
-1
lines changed

internal/compression/compression.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package compression
66

77
import (
88
"fmt"
9+
"strconv"
10+
"strings"
911

1012
"github.com/minio/minlz"
1113
)
@@ -23,6 +25,8 @@ const (
2325
MinLZ
2426

2527
NumAlgorithms
28+
29+
Unknown Algorithm = 100
2630
)
2731

2832
// String implements fmt.Stringer, returning a human-readable name for the
@@ -37,8 +41,10 @@ func (a Algorithm) String() string {
3741
return "ZSTD"
3842
case MinLZ:
3943
return "MinLZ"
44+
case Unknown:
45+
return "unknown"
4046
default:
41-
return fmt.Sprintf("unknown(%d)", a)
47+
return fmt.Sprintf("invalid(%d)", a)
4248
}
4349
}
4450

@@ -58,6 +64,26 @@ func (s Setting) String() string {
5864
return fmt.Sprintf("%s%d", s.Algorithm, s.Level)
5965
}
6066

67+
// ParseSetting parses the result of Setting.String() back into the setting.
68+
func ParseSetting(s string) (_ Setting, ok bool) {
69+
for i := range NumAlgorithms {
70+
remainder, ok := strings.CutPrefix(s, i.String())
71+
if !ok {
72+
continue
73+
}
74+
var level int
75+
if remainder != "" {
76+
var err error
77+
level, err = strconv.Atoi(remainder)
78+
if err != nil {
79+
continue
80+
}
81+
}
82+
return Setting{Algorithm: i, Level: uint8(level)}, true
83+
}
84+
return Setting{}, false
85+
}
86+
6187
// Setting presets.
6288
var (
6389
None = makePreset(NoCompression, 0)

internal/compression/compression_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,12 @@ func decompress(algo Algorithm, b []byte) ([]byte, error) {
8080
}
8181
return decodedBuf, nil
8282
}
83+
84+
func TestSettingString(t *testing.T) {
85+
for _, s := range presets {
86+
str := s.String()
87+
s2, ok := ParseSetting(str)
88+
require.True(t, ok)
89+
require.Equal(t, s, s2)
90+
}
91+
}

sstable/block/compression_stats.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"slices"
1212
"strings"
1313

14+
"github.com/cockroachdb/errors"
1415
"github.com/cockroachdb/pebble/internal/compression"
1516
"github.com/cockroachdb/pebble/internal/invariants"
1617
)
@@ -126,3 +127,27 @@ func (c CompressionStats) String() string {
126127
}
127128
return buf.String()
128129
}
130+
131+
// ParseCompressionStats parses the output of CompressionStats.String back into CompressionStats.
132+
//
133+
// If the string contains statistics for unknown compression settings, these are
134+
// accumulated under a special "unknown" setting.
135+
func ParseCompressionStats(s string) (CompressionStats, error) {
136+
var stats CompressionStats
137+
for _, a := range strings.Split(s, ",") {
138+
b := strings.Split(a, ":")
139+
if len(b) != 2 {
140+
return CompressionStats{}, errors.Errorf("cannot parse compression stats %q", s)
141+
}
142+
setting, ok := compression.ParseSetting(b[0])
143+
if !ok {
144+
setting = compression.Setting{Algorithm: compression.Unknown, Level: 0}
145+
}
146+
var cs CompressionStatsForSetting
147+
if _, err := fmt.Sscanf(b[1], "%d/%d", &cs.CompressedBytes, &cs.UncompressedBytes); err != nil {
148+
return CompressionStats{}, errors.Errorf("cannot parse compression stats %q", s)
149+
}
150+
stats.add(setting, cs)
151+
}
152+
return stats, nil
153+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2+
// of this source code is governed by a BSD-style license that can be found in
3+
// the LICENSE file.
4+
5+
package block
6+
7+
import (
8+
"math/rand/v2"
9+
"testing"
10+
11+
"github.com/cockroachdb/pebble/internal/compression"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestCompressionStatsString(t *testing.T) {
16+
var stats CompressionStats
17+
18+
stats.add(compression.None, CompressionStatsForSetting{CompressedBytes: 100, UncompressedBytes: 100})
19+
require.Equal(t, "NoCompression:100/100", stats.String())
20+
21+
stats.add(compression.Snappy, CompressionStatsForSetting{CompressedBytes: 100, UncompressedBytes: 200})
22+
require.Equal(t, "NoCompression:100/100,Snappy:100/200", stats.String())
23+
24+
stats.add(compression.Snappy, CompressionStatsForSetting{CompressedBytes: 100, UncompressedBytes: 200})
25+
require.Equal(t, "NoCompression:100/100,Snappy:200/400", stats.String())
26+
27+
stats.add(compression.MinLZFastest, CompressionStatsForSetting{CompressedBytes: 1000, UncompressedBytes: 4000})
28+
require.Equal(t, "MinLZ1:1000/4000,NoCompression:100/100,Snappy:200/400", stats.String())
29+
30+
stats.add(compression.ZstdLevel1, CompressionStatsForSetting{CompressedBytes: 10000, UncompressedBytes: 80000})
31+
require.Equal(t, "MinLZ1:1000/4000,NoCompression:100/100,Snappy:200/400,ZSTD1:10000/80000", stats.String())
32+
33+
stats = CompressionStats{}
34+
stats.add(compression.MinLZFastest, CompressionStatsForSetting{CompressedBytes: 1000, UncompressedBytes: 4000})
35+
require.Equal(t, "MinLZ1:1000/4000", stats.String())
36+
37+
stats = CompressionStats{}
38+
stats.add(compression.Snappy, CompressionStatsForSetting{CompressedBytes: 1000, UncompressedBytes: 4000})
39+
require.Equal(t, "Snappy:1000/4000", stats.String())
40+
}
41+
42+
func TestCompressionStatsRoundtrip(t *testing.T) {
43+
settings := []compression.Setting{compression.None, compression.Snappy, compression.MinLZFastest, compression.ZstdLevel1, compression.ZstdLevel3}
44+
for n := 0; n < 1000; n++ {
45+
var stats CompressionStats
46+
for _, i := range rand.Perm(len(settings))[:rand.IntN(len(settings)+1)] {
47+
compressed := rand.Uint64N(1_000_000)
48+
uncompressed := compressed
49+
if settings[i] != compression.None {
50+
uncompressed += compressed * rand.Uint64N(20) / 10
51+
}
52+
stats.add(settings[i], CompressionStatsForSetting{CompressedBytes: compressed, UncompressedBytes: uncompressed})
53+
str := stats.String()
54+
stats2, err := ParseCompressionStats(str)
55+
require.NoError(t, err)
56+
str2 := stats2.String()
57+
require.Equal(t, str, str2)
58+
}
59+
}
60+
}
61+
62+
func TestParseCompressionStatsUnknown(t *testing.T) {
63+
stats, err := ParseCompressionStats("MinLZ1:100/200,ZSTD9:100/500,MiddleOut10:13/150000,Magic:15/10000000")
64+
require.NoError(t, err)
65+
expected := "MinLZ1:100/200,ZSTD9:100/500,unknown:28/10150000"
66+
require.Equal(t, expected, stats.String())
67+
}

0 commit comments

Comments
 (0)