Skip to content

Commit aa743c7

Browse files
committed
sstable: add code generator for sstable properties
The code to load/encode properties uses a mixture of relfection and unsafe code and is fairly hard to understand. This commit switches to using generated code instead. The generated code is straightforward to understand.
1 parent 635423e commit aa743c7

File tree

20 files changed

+967
-372
lines changed

20 files changed

+967
-372
lines changed

go.mod

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ require (
2727
github.com/stretchr/testify v1.9.0
2828
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
2929
golang.org/x/perf v0.0.0-20230113213139-801c7ef9e5c5
30-
golang.org/x/sync v0.7.0
31-
golang.org/x/sys v0.18.0
30+
golang.org/x/sync v0.16.0
31+
golang.org/x/sys v0.34.0
32+
golang.org/x/tools v0.35.0
3233
)
3334

3435
require (
@@ -48,9 +49,12 @@ require (
4849
github.com/prometheus/procfs v0.10.1 // indirect
4950
github.com/rogpeppe/go-internal v1.9.0 // indirect
5051
github.com/spf13/pflag v1.0.5 // indirect
52+
golang.org/x/mod v0.26.0 // indirect
5153
golang.org/x/text v0.14.0 // indirect
5254
google.golang.org/protobuf v1.33.0 // indirect
5355
gopkg.in/yaml.v3 v3.0.1 // indirect
5456
)
5557

56-
go 1.23
58+
go 1.23.0
59+
60+
toolchain go1.23.6

go.sum

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
111111
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
112112
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
113113
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
114-
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
115-
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
114+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
115+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
116116
github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU=
117117
github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
118118
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@@ -253,6 +253,8 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc
253253
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
254254
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
255255
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
256+
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
257+
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
256258
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
257259
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
258260
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -273,8 +275,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
273275
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
274276
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
275277
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
276-
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
277-
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
278+
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
279+
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
278280
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
279281
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
280282
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -284,8 +286,8 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w
284286
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
285287
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
286288
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
287-
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
288-
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
289+
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
290+
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
289291
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
290292
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
291293
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -304,6 +306,8 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn
304306
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
305307
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
306308
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
309+
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
310+
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
307311
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
308312
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
309313
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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 main
6+
7+
import (
8+
"bytes"
9+
"go/ast"
10+
"go/format"
11+
"go/token"
12+
"log"
13+
"os"
14+
"path/filepath"
15+
"reflect"
16+
"slices"
17+
"strings"
18+
"text/template"
19+
20+
"golang.org/x/tools/go/packages"
21+
)
22+
23+
// Field represents a struct field tagged with `prop:"..."`.
24+
type Field struct {
25+
Name string // Go identifier
26+
Tag string // tag value
27+
Kind string // bool | uint32 | uint64 | string
28+
EncodeEmpty bool // whether to encode empty a zero value
29+
}
30+
31+
// Template for sstable/properties_gen.go.
32+
const tmpl = `// Code generated by genprops; DO NOT EDIT.
33+
package sstable
34+
35+
import (
36+
"bytes"
37+
"encoding/binary"
38+
"fmt"
39+
"iter"
40+
"maps"
41+
"slices"
42+
"strings"
43+
44+
"github.com/cockroachdb/pebble/internal/intern"
45+
)
46+
47+
// load populates *Properties from an iterator and records which fields were
48+
// present using the bit‑vector.
49+
func (p *Properties) load(i iter.Seq2[[]byte, []byte]) error {
50+
p.Loaded = 0
51+
for k, v := range i {
52+
switch string(k) {
53+
{{- range .Fields }}
54+
case "{{ .Tag }}":
55+
p.Loaded |= 1 << _bit_{{ .Name }}
56+
{{- if eq .Kind "bool" }}
57+
p.{{ .Name }} = len(v) == 1 && v[0] == '1'
58+
{{- else if eq .Kind "uint32" }}
59+
p.{{ .Name }} = binary.LittleEndian.Uint32(v)
60+
{{- else if eq .Kind "uint64" }}
61+
n, _ := binary.Uvarint(v)
62+
p.{{ .Name }} = n
63+
{{- else if eq .Kind "string" }}
64+
p.{{ .Name }} = string(v)
65+
{{- end }}
66+
{{- end }}
67+
default:
68+
if _, denied := ignoredInternalProperties[string(k)]; !denied {
69+
if p.UserProperties == nil {
70+
p.UserProperties = make(map[string]string)
71+
}
72+
p.UserProperties[intern.Bytes(k)] = string(v)
73+
}
74+
}
75+
}
76+
return nil
77+
}
78+
79+
// encodeAll returns a map of property keys and encoded values.
80+
func (p *Properties) encodeAll() map[string][]byte {
81+
m := make(map[string][]byte, _numPropBits+len(p.UserProperties))
82+
var allocBuf []byte
83+
alloc := func(n int) []byte {
84+
if len(allocBuf) < n {
85+
allocBuf = make([]byte, n + 512)
86+
}
87+
res := allocBuf[:n]
88+
allocBuf = allocBuf[n:]
89+
return res
90+
}
91+
92+
{{- range .Fields }}
93+
{{- if .EncodeEmpty }}
94+
if true {
95+
{{- else }}
96+
if p.{{ .Name }} != {{ zeroVal .Kind }} {
97+
{{- end }}
98+
{{- if eq .Kind "bool" }}
99+
val := alloc(1)
100+
val[0] = '0'
101+
if p.{{ .Name }} {
102+
val[0] = '1'
103+
}
104+
{{- else if eq .Kind "uint32" }}
105+
val := alloc(4)
106+
binary.LittleEndian.PutUint32(val, p.{{ .Name }})
107+
{{- else if eq .Kind "uint64" }}
108+
val := alloc(10)
109+
n := binary.PutUvarint(val, p.{{ .Name }})
110+
val = val[:n]
111+
{{- else if eq .Kind "string" }}
112+
val := alloc(len(p.{{ .Name }}))
113+
copy(val, p.{{ .Name }})
114+
{{- end }}
115+
m["{{ .Tag }}"] = val
116+
}
117+
{{- end }}
118+
return m
119+
}
120+
121+
// isLoaded returns true if the bit corresponding to field bit is set.
122+
func (p *Properties) isLoaded(bit int) bool { return p.Loaded&(1<<bit) != 0 }
123+
124+
// String writes a human‑readable representation of Properties, matching the
125+
// previous reflection‑based output.
126+
func (p *Properties) String() string {
127+
var buf bytes.Buffer
128+
{{- range .Fields }}
129+
if p.{{ .Name }} != {{ zeroVal .Kind }} || p.isLoaded(_bit_{{ .Name }}) {
130+
fmt.Fprintf(&buf, "%s: %v\n", "{{ .Tag }}", p.{{ .Name }})
131+
}
132+
{{- end }}
133+
if len(p.UserProperties) > 0 {
134+
// Print the user properties in alphabetical order.
135+
for _, k := range slices.Sorted(maps.Keys(p.UserProperties)) {
136+
v := p.UserProperties[k]
137+
if strings.IndexFunc(v, func(r rune) bool { return r < ' ' || r > '~' }) != -1 {
138+
fmt.Fprintf(&buf, "%s: hex:%x\n", k, v)
139+
} else {
140+
fmt.Fprintf(&buf, "%s: %s\n", k, v)
141+
}
142+
}
143+
}
144+
return buf.String()
145+
}
146+
147+
// Bit positions for property field.
148+
const (
149+
{{- range $i, $f := .Fields }}
150+
_bit_{{$f.Name}} = {{$i}}
151+
{{- end }}
152+
_numPropBits = {{ len .Fields }}
153+
)
154+
` // end template
155+
156+
// zeroVal returns a literal zero value string for the given kind.
157+
func zeroVal(kind string) string {
158+
switch kind {
159+
case "bool":
160+
return "false"
161+
case "uint32", "uint64":
162+
return "0"
163+
case "string":
164+
return `""`
165+
default:
166+
return "nil"
167+
}
168+
}
169+
170+
func main() {
171+
cfg := &packages.Config{
172+
Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedDeps |
173+
packages.NeedTypesInfo | packages.NeedFiles | packages.NeedName,
174+
}
175+
176+
pkgs, err := packages.Load(cfg, "github.com/cockroachdb/pebble/sstable")
177+
if err != nil {
178+
log.Fatalf("loading packages: %v", err)
179+
}
180+
181+
var fields []Field
182+
for _, pkg := range pkgs {
183+
for _, file := range pkg.Syntax {
184+
for _, decl := range file.Decls {
185+
gd, ok := decl.(*ast.GenDecl)
186+
if !ok || gd.Tok != token.TYPE {
187+
continue
188+
}
189+
for _, spec := range gd.Specs {
190+
ts := spec.(*ast.TypeSpec)
191+
st, ok := ts.Type.(*ast.StructType)
192+
if !ok {
193+
continue
194+
}
195+
qname := pkg.Types.Path() + "." + ts.Name.Name
196+
if qname != "github.com/cockroachdb/pebble/sstable.Properties" &&
197+
qname != "github.com/cockroachdb/pebble/sstable.CommonProperties" {
198+
continue
199+
}
200+
for _, f := range st.Fields.List {
201+
if f.Tag == nil || len(f.Names) == 0 {
202+
continue
203+
}
204+
tags := reflect.StructTag(strings.Trim(f.Tag.Value, "`"))
205+
tag := tags.Get("prop")
206+
if tag == "" {
207+
continue
208+
}
209+
options := strings.Split(tags.Get("options"), ",")
210+
name := f.Names[0].Name
211+
typ := pkg.TypesInfo.Types[f.Type].Type.String()
212+
var kind string
213+
switch typ {
214+
case "bool":
215+
kind = "bool"
216+
case "uint32":
217+
kind = "uint32"
218+
case "uint64":
219+
kind = "uint64"
220+
case "string":
221+
kind = "string"
222+
default:
223+
log.Fatalf("unsupported property type %s", typ)
224+
}
225+
// We always encode some properties, even if they are zero.
226+
encodeEmpty := slices.Contains(options, "encodeempty")
227+
fields = append(fields, Field{Name: name, Tag: tag, Kind: kind, EncodeEmpty: encodeEmpty})
228+
}
229+
}
230+
}
231+
}
232+
}
233+
234+
if len(fields) > 64 {
235+
log.Fatalf("too many prop fields (%d), exceeds 64‑bit bitfield", len(fields))
236+
}
237+
var sstableDir string
238+
for _, pkg := range pkgs {
239+
if pkg.PkgPath == "github.com/cockroachdb/pebble/sstable" {
240+
sstableDir = filepath.Dir(pkg.GoFiles[0])
241+
break
242+
}
243+
}
244+
if sstableDir == "" {
245+
log.Fatalf("sstable package not found")
246+
}
247+
outputFile := filepath.Join(sstableDir, "properties_gen.go")
248+
249+
t := template.Must(template.New("gen").Funcs(template.FuncMap{
250+
"zeroVal": zeroVal,
251+
}).Parse(tmpl))
252+
253+
var buf bytes.Buffer
254+
if err := t.Execute(&buf, map[string]any{"Fields": fields}); err != nil {
255+
log.Fatalf("executing template: %v", err)
256+
}
257+
258+
src, err := format.Source(buf.Bytes())
259+
if err != nil {
260+
_ = os.WriteFile(outputFile, buf.Bytes(), 0o644)
261+
log.Fatalf("formatting source: %v\n", err)
262+
}
263+
264+
if err := os.WriteFile(outputFile, src, 0o644); err != nil {
265+
log.Fatalf("writing generated file: %v", err)
266+
}
267+
}

0 commit comments

Comments
 (0)