Commit 39ca6c26 authored by Daniel Martí's avatar Daniel Martí

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%
parent bf0cbde7
package quip_test
package fluent_test
import (
"strings"
......@@ -7,11 +7,14 @@ import (
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/fluent"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/fluent/quip"
basicnode "github.com/ipld/go-ipld-prime/node/basic"
)
func BenchmarkQuip(b *testing.B) {
b.ReportAllocs()
f2 := func(na ipld.NodeAssembler, a string, b string, c string, d []string) (err error) {
quip.AssembleMap(&err, na, 4, func(ma ipld.MapAssembler) {
quip.AssignMapEntryString(&err, ma, "destination", a)
......@@ -50,6 +53,8 @@ func BenchmarkQuip(b *testing.B) {
}
func BenchmarkQuipWithoutScalarFuncs(b *testing.B) {
b.ReportAllocs()
// This is simply a slightly longer way of writing the same thing.
// Just for curiosity and to track if there's any measureable performance difference.
f2 := func(na ipld.NodeAssembler, a string, b string, c string, d []string) (err error) {
......@@ -79,7 +84,7 @@ func BenchmarkQuipWithoutScalarFuncs(b *testing.B) {
var err error
for i := 0; i < b.N; i++ {
n = quip.BuildList(&err, basicnode.Prototype.Any, -1, func(la ipld.ListAssembler) {
f2(la.AssembleValue(),
f2(la.AssembleValue(), // TODO: forgot to check error?
"/",
"overlay",
"none",
......@@ -97,7 +102,44 @@ func BenchmarkQuipWithoutScalarFuncs(b *testing.B) {
}
}
func BenchmarkQp(b *testing.B) {
b.ReportAllocs()
f2 := func(na ipld.NodeAssembler, a string, b string, c string, d []string) {
qp.Map(4, func(ma ipld.MapAssembler) {
qp.MapEntry(ma, "destination", qp.String(a))
qp.MapEntry(ma, "type", qp.String(b))
qp.MapEntry(ma, "source", qp.String(c))
qp.MapEntry(ma, "options", qp.List(int64(len(d)), func(la ipld.ListAssembler) {
for _, s := range d {
qp.ListEntry(la, qp.String(s))
}
}))
})(na)
}
for i := 0; i < b.N; i++ {
n, err := qp.BuildList(basicnode.Prototype.Any, -1, func(la ipld.ListAssembler) {
f2(la.AssembleValue(), // TODO: forgot to check error?
"/",
"overlay",
"none",
[]string{
"lowerdir=" + "/",
"upperdir=" + "/tmp/overlay-root/upper",
"workdir=" + "/tmp/overlay-root/work",
},
)
})
if err != nil {
b.Fatal(err)
}
_ = n
}
}
func BenchmarkUnmarshal(b *testing.B) {
b.ReportAllocs()
var n ipld.Node
var err error
serial := `[{
......@@ -124,6 +166,8 @@ func BenchmarkUnmarshal(b *testing.B) {
}
func BenchmarkFluent(b *testing.B) {
b.ReportAllocs()
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
......@@ -147,6 +191,8 @@ func BenchmarkFluent(b *testing.B) {
}
func BenchmarkReflect(b *testing.B) {
b.ReportAllocs()
var n ipld.Node
var err error
val := []interface{}{
......@@ -171,6 +217,8 @@ func BenchmarkReflect(b *testing.B) {
}
func BenchmarkReflectIncludingInitialization(b *testing.B) {
b.ReportAllocs()
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
......@@ -194,6 +242,8 @@ func BenchmarkReflectIncludingInitialization(b *testing.B) {
}
func BenchmarkAgonizinglyBare(b *testing.B) {
b.ReportAllocs()
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
......
package qp_test
import (
"os"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/fluent/qp"
basicnode "github.com/ipld/go-ipld-prime/node/basic"
)
// TODO: can we make ListEntry/MapEntry less verbose?
func Example() {
n, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma ipld.MapAssembler) {
qp.MapEntry(ma, "some key", qp.String("some value"))
qp.MapEntry(ma, "another key", qp.String("another value"))
qp.MapEntry(ma, "nested map", qp.Map(2, func(ma ipld.MapAssembler) {
qp.MapEntry(ma, "deeper entries", qp.String("deeper values"))
qp.MapEntry(ma, "more deeper entries", qp.String("more deeper values"))
}))
qp.MapEntry(ma, "nested list", qp.List(2, func(la ipld.ListAssembler) {
qp.ListEntry(la, qp.Int(1))
qp.ListEntry(la, qp.Int(2))
}))
})
if err != nil {
panic(err)
}
dagjson.Encoder(n, os.Stdout)
// Output:
// {
// "some key": "some value",
// "another key": "another value",
// "nested map": {
// "deeper entries": "deeper values",
// "more deeper entries": "more deeper values"
// },
// "nested list": [
// 1,
// 2
// ]
// }
}
// qp is similar to fluent/quip, but with a bit more magic.
package qp
import (
"github.com/ipld/go-ipld-prime"
)
type Assemble = func(ipld.NodeAssembler)
func BuildMap(np ipld.NodePrototype, sizeHint int64, fn func(ipld.MapAssembler)) (_ ipld.Node, err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
nb := np.NewBuilder()
Map(sizeHint, fn)(nb)
return nb.Build(), nil
}
type mapParams struct {
sizeHint int64
fn func(ipld.MapAssembler)
}
func (mp mapParams) Assemble(na ipld.NodeAssembler) {
ma, err := na.BeginMap(mp.sizeHint)
if err != nil {
panic(err)
}
mp.fn(ma)
if err := ma.Finish(); err != nil {
panic(err)
}
}
func Map(sizeHint int64, fn func(ipld.MapAssembler)) Assemble {
return mapParams{sizeHint, fn}.Assemble
}
func MapEntry(ma ipld.MapAssembler, k string, fn Assemble) {
na, err := ma.AssembleEntry(k)
if err != nil {
panic(err)
}
fn(na)
}
func BuildList(np ipld.NodePrototype, sizeHint int64, fn func(ipld.ListAssembler)) (_ ipld.Node, err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
nb := np.NewBuilder()
List(sizeHint, fn)(nb)
return nb.Build(), nil
}
type listParams struct {
sizeHint int64
fn func(ipld.ListAssembler)
}
func (lp listParams) Assemble(na ipld.NodeAssembler) {
la, err := na.BeginList(lp.sizeHint)
if err != nil {
panic(err)
}
lp.fn(la)
if err := la.Finish(); err != nil {
panic(err)
}
}
func List(sizeHint int64, fn func(ipld.ListAssembler)) Assemble {
return listParams{sizeHint, fn}.Assemble
}
func ListEntry(la ipld.ListAssembler, fn Assemble) {
fn(la.AssembleValue())
}
type stringParam string
func (s stringParam) Assemble(na ipld.NodeAssembler) {
if err := na.AssignString(string(s)); err != nil {
panic(err)
}
}
func String(s string) Assemble {
return stringParam(s).Assemble
}
type intParam int64
func (i intParam) Assemble(na ipld.NodeAssembler) {
if err := na.AssignInt(int64(i)); err != nil {
panic(err)
}
}
func Int(i int64) Assemble {
return intParam(i).Assemble
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment