Skip to content

Commit 39ca6c2

Browse files
committed
fluent: add qp, a different spin on quip
This is what I came up with, building on top of Eric's quip. I don't want to waste too much time naming this, and I like two-letter package names in place of dot-imports, so "qp" seems good enough for now. They are the "strong" consonants when one says "Quick iPld". First, move the benchmarks comparing all fluent packages to the root fluent package, to keep things a bit more tidy. Second, make all the benchmarks report their allocation stats, without having to always remember to use the -benchmem flag. Third, add a qp benchmark. Fourth, notice a couple of potential bugs in the quip benchmarks, and add TODOs for them. Finally, add the qp API. It differs from quip in a few external ways: 1) No error pointers. Instead, it uses panics which are recovered at the top-level API layer. This reduces verbosity, removes the "forgot to handle an error" type of mistake, and does not affect performance thanks to the defers being statically allocated in the stack. 2) Supposed better composition. For example, one can use MapEntry along with Map to have a map inside another map. In contrast, quip requires either an extra layer of func literals, or extra API like AssignMapEntryString. 3) Thanks to the points above, the API is significantly smaller. Note that some helper APIs like Bool are missing, but even when added, qp should expose about half the API funcs taht quip does. This is the first proof of concept. I'll probably finish adding the rest of the API helpers when I find the first use case for qp. Benchmark numbers, with perflock and benchstat on my i5-8350u laptop: name time/op Quip-8 1.39µs ± 1% QuipWithoutScalarFuncs-8 1.42µs ± 2% Qp-8 1.46µs ± 2% name alloc/op Quip-8 912B ± 0% QuipWithoutScalarFuncs-8 912B ± 0% Qp-8 912B ± 0% name allocs/op Quip-8 18.0 ± 0% QuipWithoutScalarFuncs-8 18.0 ± 0% Qp-8 18.0 ± 0%
1 parent bf0cbde commit 39ca6c2

File tree

3 files changed

+203
-2
lines changed

3 files changed

+203
-2
lines changed

fluent/quip/quip_bench_test.go fluent/bench_test.go

+52-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package quip_test
1+
package fluent_test
22

33
import (
44
"strings"
@@ -7,11 +7,14 @@ import (
77
"github.com/ipld/go-ipld-prime"
88
"github.com/ipld/go-ipld-prime/codec/dagjson"
99
"github.com/ipld/go-ipld-prime/fluent"
10+
"github.com/ipld/go-ipld-prime/fluent/qp"
1011
"github.com/ipld/go-ipld-prime/fluent/quip"
1112
basicnode "github.com/ipld/go-ipld-prime/node/basic"
1213
)
1314

1415
func BenchmarkQuip(b *testing.B) {
16+
b.ReportAllocs()
17+
1518
f2 := func(na ipld.NodeAssembler, a string, b string, c string, d []string) (err error) {
1619
quip.AssembleMap(&err, na, 4, func(ma ipld.MapAssembler) {
1720
quip.AssignMapEntryString(&err, ma, "destination", a)
@@ -50,6 +53,8 @@ func BenchmarkQuip(b *testing.B) {
5053
}
5154

5255
func BenchmarkQuipWithoutScalarFuncs(b *testing.B) {
56+
b.ReportAllocs()
57+
5358
// This is simply a slightly longer way of writing the same thing.
5459
// Just for curiosity and to track if there's any measureable performance difference.
5560
f2 := func(na ipld.NodeAssembler, a string, b string, c string, d []string) (err error) {
@@ -79,7 +84,7 @@ func BenchmarkQuipWithoutScalarFuncs(b *testing.B) {
7984
var err error
8085
for i := 0; i < b.N; i++ {
8186
n = quip.BuildList(&err, basicnode.Prototype.Any, -1, func(la ipld.ListAssembler) {
82-
f2(la.AssembleValue(),
87+
f2(la.AssembleValue(), // TODO: forgot to check error?
8388
"/",
8489
"overlay",
8590
"none",
@@ -97,7 +102,44 @@ func BenchmarkQuipWithoutScalarFuncs(b *testing.B) {
97102
}
98103
}
99104

105+
func BenchmarkQp(b *testing.B) {
106+
b.ReportAllocs()
107+
108+
f2 := func(na ipld.NodeAssembler, a string, b string, c string, d []string) {
109+
qp.Map(4, func(ma ipld.MapAssembler) {
110+
qp.MapEntry(ma, "destination", qp.String(a))
111+
qp.MapEntry(ma, "type", qp.String(b))
112+
qp.MapEntry(ma, "source", qp.String(c))
113+
qp.MapEntry(ma, "options", qp.List(int64(len(d)), func(la ipld.ListAssembler) {
114+
for _, s := range d {
115+
qp.ListEntry(la, qp.String(s))
116+
}
117+
}))
118+
})(na)
119+
}
120+
for i := 0; i < b.N; i++ {
121+
n, err := qp.BuildList(basicnode.Prototype.Any, -1, func(la ipld.ListAssembler) {
122+
f2(la.AssembleValue(), // TODO: forgot to check error?
123+
"/",
124+
"overlay",
125+
"none",
126+
[]string{
127+
"lowerdir=" + "/",
128+
"upperdir=" + "/tmp/overlay-root/upper",
129+
"workdir=" + "/tmp/overlay-root/work",
130+
},
131+
)
132+
})
133+
if err != nil {
134+
b.Fatal(err)
135+
}
136+
_ = n
137+
}
138+
}
139+
100140
func BenchmarkUnmarshal(b *testing.B) {
141+
b.ReportAllocs()
142+
101143
var n ipld.Node
102144
var err error
103145
serial := `[{
@@ -124,6 +166,8 @@ func BenchmarkUnmarshal(b *testing.B) {
124166
}
125167

126168
func BenchmarkFluent(b *testing.B) {
169+
b.ReportAllocs()
170+
127171
var n ipld.Node
128172
var err error
129173
for i := 0; i < b.N; i++ {
@@ -147,6 +191,8 @@ func BenchmarkFluent(b *testing.B) {
147191
}
148192

149193
func BenchmarkReflect(b *testing.B) {
194+
b.ReportAllocs()
195+
150196
var n ipld.Node
151197
var err error
152198
val := []interface{}{
@@ -171,6 +217,8 @@ func BenchmarkReflect(b *testing.B) {
171217
}
172218

173219
func BenchmarkReflectIncludingInitialization(b *testing.B) {
220+
b.ReportAllocs()
221+
174222
var n ipld.Node
175223
var err error
176224
for i := 0; i < b.N; i++ {
@@ -194,6 +242,8 @@ func BenchmarkReflectIncludingInitialization(b *testing.B) {
194242
}
195243

196244
func BenchmarkAgonizinglyBare(b *testing.B) {
245+
b.ReportAllocs()
246+
197247
var n ipld.Node
198248
var err error
199249
for i := 0; i < b.N; i++ {

fluent/qp/example_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package qp_test
2+
3+
import (
4+
"os"
5+
6+
"github.com/ipld/go-ipld-prime"
7+
"github.com/ipld/go-ipld-prime/codec/dagjson"
8+
"github.com/ipld/go-ipld-prime/fluent/qp"
9+
basicnode "github.com/ipld/go-ipld-prime/node/basic"
10+
)
11+
12+
// TODO: can we make ListEntry/MapEntry less verbose?
13+
14+
func Example() {
15+
n, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma ipld.MapAssembler) {
16+
qp.MapEntry(ma, "some key", qp.String("some value"))
17+
qp.MapEntry(ma, "another key", qp.String("another value"))
18+
qp.MapEntry(ma, "nested map", qp.Map(2, func(ma ipld.MapAssembler) {
19+
qp.MapEntry(ma, "deeper entries", qp.String("deeper values"))
20+
qp.MapEntry(ma, "more deeper entries", qp.String("more deeper values"))
21+
}))
22+
qp.MapEntry(ma, "nested list", qp.List(2, func(la ipld.ListAssembler) {
23+
qp.ListEntry(la, qp.Int(1))
24+
qp.ListEntry(la, qp.Int(2))
25+
}))
26+
})
27+
if err != nil {
28+
panic(err)
29+
}
30+
dagjson.Encoder(n, os.Stdout)
31+
32+
// Output:
33+
// {
34+
// "some key": "some value",
35+
// "another key": "another value",
36+
// "nested map": {
37+
// "deeper entries": "deeper values",
38+
// "more deeper entries": "more deeper values"
39+
// },
40+
// "nested list": [
41+
// 1,
42+
// 2
43+
// ]
44+
// }
45+
}

fluent/qp/qp.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// qp is similar to fluent/quip, but with a bit more magic.
2+
package qp
3+
4+
import (
5+
"github.com/ipld/go-ipld-prime"
6+
)
7+
8+
type Assemble = func(ipld.NodeAssembler)
9+
10+
func BuildMap(np ipld.NodePrototype, sizeHint int64, fn func(ipld.MapAssembler)) (_ ipld.Node, err error) {
11+
defer func() {
12+
if r := recover(); r != nil {
13+
err = r.(error)
14+
}
15+
}()
16+
nb := np.NewBuilder()
17+
Map(sizeHint, fn)(nb)
18+
return nb.Build(), nil
19+
}
20+
21+
type mapParams struct {
22+
sizeHint int64
23+
fn func(ipld.MapAssembler)
24+
}
25+
26+
func (mp mapParams) Assemble(na ipld.NodeAssembler) {
27+
ma, err := na.BeginMap(mp.sizeHint)
28+
if err != nil {
29+
panic(err)
30+
}
31+
mp.fn(ma)
32+
if err := ma.Finish(); err != nil {
33+
panic(err)
34+
}
35+
}
36+
37+
func Map(sizeHint int64, fn func(ipld.MapAssembler)) Assemble {
38+
return mapParams{sizeHint, fn}.Assemble
39+
}
40+
41+
func MapEntry(ma ipld.MapAssembler, k string, fn Assemble) {
42+
na, err := ma.AssembleEntry(k)
43+
if err != nil {
44+
panic(err)
45+
}
46+
fn(na)
47+
}
48+
49+
func BuildList(np ipld.NodePrototype, sizeHint int64, fn func(ipld.ListAssembler)) (_ ipld.Node, err error) {
50+
defer func() {
51+
if r := recover(); r != nil {
52+
err = r.(error)
53+
}
54+
}()
55+
nb := np.NewBuilder()
56+
List(sizeHint, fn)(nb)
57+
return nb.Build(), nil
58+
}
59+
60+
type listParams struct {
61+
sizeHint int64
62+
fn func(ipld.ListAssembler)
63+
}
64+
65+
func (lp listParams) Assemble(na ipld.NodeAssembler) {
66+
la, err := na.BeginList(lp.sizeHint)
67+
if err != nil {
68+
panic(err)
69+
}
70+
lp.fn(la)
71+
if err := la.Finish(); err != nil {
72+
panic(err)
73+
}
74+
}
75+
76+
func List(sizeHint int64, fn func(ipld.ListAssembler)) Assemble {
77+
return listParams{sizeHint, fn}.Assemble
78+
}
79+
80+
func ListEntry(la ipld.ListAssembler, fn Assemble) {
81+
fn(la.AssembleValue())
82+
}
83+
84+
type stringParam string
85+
86+
func (s stringParam) Assemble(na ipld.NodeAssembler) {
87+
if err := na.AssignString(string(s)); err != nil {
88+
panic(err)
89+
}
90+
}
91+
92+
func String(s string) Assemble {
93+
return stringParam(s).Assemble
94+
}
95+
96+
type intParam int64
97+
98+
func (i intParam) Assemble(na ipld.NodeAssembler) {
99+
if err := na.AssignInt(int64(i)); err != nil {
100+
panic(err)
101+
}
102+
}
103+
104+
func Int(i int64) Assemble {
105+
return intParam(i).Assemble
106+
}

0 commit comments

Comments
 (0)