diff --git a/components/ledger/Earthfile b/components/ledger/Earthfile index e77a937f92..867009c8b5 100644 --- a/components/ledger/Earthfile +++ b/components/ledger/Earthfile @@ -112,7 +112,7 @@ bench: benchstat: FROM core+builder-image DO --pass-args core+GO_INSTALL --package=golang.org/x/perf/cmd/benchstat@latest - COPY --pass-args +bench/results.txt /tmp/branch.txt ARG compareAgainstRevision=main COPY --pass-args github.com/formancehq/stack/components/ledger:$compareAgainstRevision+bench/results.txt /tmp/main.txt + COPY --pass-args +bench/results.txt /tmp/branch.txt RUN benchstat /tmp/main.txt /tmp/branch.txt diff --git a/components/ledger/go.mod b/components/ledger/go.mod index a542898304..2e29ec8de0 100644 --- a/components/ledger/go.mod +++ b/components/ledger/go.mod @@ -16,6 +16,7 @@ require ( github.com/jackc/pgx/v5 v5.3.0 github.com/lib/pq v1.10.7 github.com/logrusorgru/aurora v2.0.3+incompatible + github.com/ory/dockertest/v3 v3.9.1 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 @@ -30,6 +31,7 @@ require ( go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/metric v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 + go.uber.org/atomic v1.10.0 go.uber.org/fx v1.19.2 go.uber.org/mock v0.3.0 gopkg.in/segmentio/analytics-go.v3 v3.1.0 @@ -80,20 +82,19 @@ require ( github.com/jcmturner/gokrb5/v8 v8.4.3 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/klauspost/compress v1.15.15 // indirect + github.com/klauspost/compress v1.16.7 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect - github.com/nats-io/nats.go v1.23.0 // indirect - github.com/nats-io/nkeys v0.3.0 // indirect + github.com/nats-io/nats.go v1.28.0 // indirect + github.com/nats-io/nkeys v0.4.6 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.5 // indirect - github.com/ory/dockertest/v3 v3.9.1 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -142,15 +143,14 @@ require ( go.opentelemetry.io/otel/sdk v1.16.0 // indirect go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect - go.uber.org/atomic v1.10.0 // indirect go.uber.org/dig v1.16.1 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.9.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.56.3 // indirect diff --git a/components/ledger/go.sum b/components/ledger/go.sum index 089ec32fb3..badfc7d9f5 100644 --- a/components/ledger/go.sum +++ b/components/ledger/go.sum @@ -297,8 +297,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -332,12 +332,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI= -github.com/nats-io/nats-server/v2 v2.9.8 h1:jgxZsv+A3Reb3MgwxaINcNq/za8xZInKhDg9Q0cGN1o= -github.com/nats-io/nats.go v1.23.0 h1:lR28r7IX44WjYgdiKz9GmUeW0uh/m33uD3yEjLZ2cOE= -github.com/nats-io/nats.go v1.23.0/go.mod h1:ki/Scsa23edbh8IRZbCuNXR9TDcbvfaSijKtaqQgw+Q= -github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= -github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak= +github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU= +github.com/nats-io/nats.go v1.28.0 h1:Th4G6zdsz2d0OqXdfzKLClo6bOfoI/b1kInhRtFIy5c= +github.com/nats-io/nats.go v1.28.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc= +github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= +github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -554,13 +554,12 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -712,8 +711,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -726,12 +725,12 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/components/ledger/internal/storage/ledgerstore/store_benchmarks_test.go b/components/ledger/internal/storage/ledgerstore/store_benchmarks_test.go index 9b5d8d9204..2a5525512d 100644 --- a/components/ledger/internal/storage/ledgerstore/store_benchmarks_test.go +++ b/components/ledger/internal/storage/ledgerstore/store_benchmarks_test.go @@ -2,13 +2,18 @@ package ledgerstore import ( "context" + "encoding/json" "flag" "fmt" + "github.com/formancehq/stack/libs/go-libs/bun/bunexplain" + "github.com/formancehq/stack/libs/go-libs/pointer" "math/big" + "os" "testing" + "text/tabwriter" + "time" ledger "github.com/formancehq/ledger/internal" - "github.com/formancehq/stack/libs/go-libs/bun/bunexplain" "github.com/formancehq/stack/libs/go-libs/logging" "github.com/formancehq/stack/libs/go-libs/metadata" "github.com/formancehq/stack/libs/go-libs/query" @@ -19,6 +24,40 @@ import ( var nbTransactions = flag.Int("transactions", 10000, "number of transactions to create") var batch = flag.Int("batch", 1000, "logs batching") +type bunContextHook struct{} + +func (b bunContextHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) context.Context { + hooks := ctx.Value("hooks") + if hooks == nil { + return ctx + } + + for _, hook := range hooks.([]bun.QueryHook) { + ctx = hook.BeforeQuery(ctx, event) + } + + return ctx +} + +func (b bunContextHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) { + hooks := ctx.Value("hooks") + if hooks == nil { + return + } + + for _, hook := range hooks.([]bun.QueryHook) { + hook.AfterQuery(ctx, event) + } + + return +} + +var _ bun.QueryHook = &bunContextHook{} + +func contextWithHook(ctx context.Context, hooks ...bun.QueryHook) context.Context { + return context.WithValue(ctx, "hooks", hooks) +} + type scenarioInfo struct { nbAccounts int } @@ -28,6 +67,8 @@ type scenario struct { setup func(ctx context.Context, b *testing.B, store *Store) *scenarioInfo } +var now = ledger.Now() + var scenarios = []scenario{ { name: "nominal", @@ -46,23 +87,29 @@ var scenarios = []scenario{ fees := itemPrice.Div(itemPrice, big.NewInt(100)) // 1% appendLog(ledger.NewTransactionLog( - ledger.NewTransaction().WithPostings(ledger.NewPosting( - "world", fmt.Sprintf("player:%d", j/2), "USD/2", provision, - )).WithID(big.NewInt(int64(i*(*batch)+j))), + ledger.NewTransaction(). + WithPostings(ledger.NewPosting( + "world", fmt.Sprintf("player:%d", j/2), "USD/2", provision, + )). + WithID(big.NewInt(int64(i*(*batch)+j))). + WithDate(now.Add(time.Minute*time.Duration(i*(*batch)+j))), map[string]metadata.Metadata{}, )) appendLog(ledger.NewTransactionLog( - ledger.NewTransaction().WithPostings( - ledger.NewPosting(fmt.Sprintf("player:%d", j/2), "seller", "USD/2", itemPrice), - ledger.NewPosting("seller", "fees", "USD/2", fees), - ).WithID(big.NewInt(int64(i*(*batch)+j+1))), + ledger.NewTransaction(). + WithPostings( + ledger.NewPosting(fmt.Sprintf("player:%d", j/2), "seller", "USD/2", itemPrice), + ledger.NewPosting("seller", "fees", "USD/2", fees), + ). + WithID(big.NewInt(int64(i*(*batch)+j+1))). + WithDate(now.Add(time.Minute*time.Duration(i*(*batch)+j))), map[string]metadata.Metadata{}, )) status := "pending" if j%8 == 0 { status = "terminated" } - appendLog(ledger.NewSetMetadataLog(ledger.Now(), ledger.SetMetadataLogPayload{ + appendLog(ledger.NewSetMetadataLog(now.Add(time.Minute*time.Duration(i*(*batch)+j)), ledger.SetMetadataLogPayload{ TargetType: ledger.MetaTargetTypeTransaction, TargetID: big.NewInt(int64(i*(*batch) + j + 1)), Metadata: map[string]string{ @@ -76,7 +123,7 @@ var scenarios = []scenario{ nbAccounts := *batch / 2 for i := 0; i < nbAccounts; i++ { - lastLog = ledger.NewSetMetadataLog(ledger.Now(), ledger.SetMetadataLogPayload{ + lastLog = ledger.NewSetMetadataLog(now, ledger.SetMetadataLogPayload{ TargetType: ledger.MetaTargetTypeAccount, TargetID: fmt.Sprintf("player:%d", i), Metadata: map[string]string{ @@ -93,36 +140,141 @@ var scenarios = []scenario{ }, } +func reportMetrics(ctx context.Context, b *testing.B, store *Store) { + type stat struct { + RelID string `bun:"relid"` + IndexRelID string `bun:"indexrelid"` + RelName string `bun:"relname"` + IndexRelName string `bun:"indexrelname"` + IdxScan int `bun:"idxscan"` + IdxTupRead int `bun:"idx_tup_read"` + IdxTupFetch int `bun:"idx_tup_fetch"` + } + ret := make([]stat, 0) + err := store.db.NewSelect(). + Table("pg_stat_user_indexes"). + Where("schemaname = ?", store.name). + Scan(ctx, &ret) + require.NoError(b, err) + + tabWriter := tabwriter.NewWriter(os.Stdout, 8, 8, 0, '\t', 0) + defer func() { + require.NoError(b, tabWriter.Flush()) + }() + _, err = fmt.Fprintf(tabWriter, "IndexRelName\tIdxScan\tIdxTypRead\tIdxTupFetch\r\n") + require.NoError(b, err) + + _, err = fmt.Fprintf(tabWriter, "---\t---\r\n") + require.NoError(b, err) + + for _, s := range ret { + _, err := fmt.Fprintf(tabWriter, "%s\t%d\t%d\t%d\r\n", s.IndexRelName, s.IdxScan, s.IdxTupRead, s.IdxTupFetch) + require.NoError(b, err) + } +} + +func reportTableSizes(ctx context.Context, b *testing.B, store *Store) { + + tabWriter := tabwriter.NewWriter(os.Stdout, 12, 8, 0, '\t', 0) + defer func() { + require.NoError(b, tabWriter.Flush()) + }() + _, err := fmt.Fprintf(tabWriter, "Table\tTotal size\tTable size\tRelation size\tIndexes size\tMain size\tFSM size\tVM size\tInit size\r\n") + require.NoError(b, err) + + _, err = fmt.Fprintf(tabWriter, "---\t---\t---\t---\t---\t---\t---\t---\r\n") + require.NoError(b, err) + + for _, table := range []string{ + "transactions", "accounts", "moves", "logs", "transactions_metadata", "accounts_metadata", + } { + totalRelationSize := "" + err := store.db.DB.QueryRowContext(ctx, fmt.Sprintf(`select pg_size_pretty(pg_total_relation_size('%s'))`, table)). + Scan(&totalRelationSize) + require.NoError(b, err) + + tableSize := "" + err = store.db.DB.QueryRowContext(ctx, fmt.Sprintf(`select pg_size_pretty(pg_table_size('%s'))`, table)). + Scan(&tableSize) + require.NoError(b, err) + + relationSize := "" + err = store.db.DB.QueryRowContext(ctx, fmt.Sprintf(`select pg_size_pretty(pg_relation_size('%s'))`, table)). + Scan(&relationSize) + require.NoError(b, err) + + indexesSize := "" + err = store.db.DB.QueryRowContext(ctx, fmt.Sprintf(`select pg_size_pretty(pg_indexes_size('%s'))`, table)). + Scan(&indexesSize) + require.NoError(b, err) + + mainSize := "" + err = store.db.DB.QueryRowContext(ctx, fmt.Sprintf(`select pg_size_pretty(pg_relation_size('%s', 'main'))`, table)). + Scan(&mainSize) + require.NoError(b, err) + + fsmSize := "" + err = store.db.DB.QueryRowContext(ctx, fmt.Sprintf(`select pg_size_pretty(pg_relation_size('%s', 'fsm'))`, table)). + Scan(&fsmSize) + require.NoError(b, err) + + vmSize := "" + err = store.db.DB.QueryRowContext(ctx, fmt.Sprintf(`select pg_size_pretty(pg_relation_size('%s', 'vm'))`, table)). + Scan(&vmSize) + require.NoError(b, err) + + initSize := "" + err = store.db.DB.QueryRowContext(ctx, fmt.Sprintf(`select pg_size_pretty(pg_relation_size('%s', 'init'))`, table)). + Scan(&initSize) + require.NoError(b, err) + + _, err = fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\r\n", + table, totalRelationSize, tableSize, relationSize, indexesSize, mainSize, fsmSize, vmSize, initSize) + require.NoError(b, err) + } +} + func BenchmarkList(b *testing.B) { ctx := logging.TestingContext() - hooks := make([]bun.QueryHook, 0) - if testing.Verbose() { - hooks = append(hooks, bunexplain.NewExplainHook()) - } for _, scenario := range scenarios { b.Run(scenario.name, func(b *testing.B) { - store := newLedgerStore(b, hooks...) + store := newLedgerStore(b, &bunContextHook{}) info := scenario.setup(ctx, b, store) - _, err := store.db.Exec("VACUUM ANALYZE") + defer func() { + if testing.Verbose() { + reportMetrics(ctx, b, store) + reportTableSizes(ctx, b, store) + } + }() + + _, err := store.db.Exec("VACUUM FULL ANALYZE") require.NoError(b, err) - b.Run("transactions", func(b *testing.B) { - benchmarksReadTransactions(b, ctx, store, info) - }) - b.Run("accounts", func(b *testing.B) { - benchmarksReadAccounts(b, ctx, store) - }) - b.Run("aggregates", func(b *testing.B) { - benchmarksGetAggregatedBalances(b, ctx, store) + runAllWithPIT := func(b *testing.B, pit *ledger.Time) { + b.Run("transactions", func(b *testing.B) { + benchmarksReadTransactions(b, ctx, store, info, pit) + }) + b.Run("accounts", func(b *testing.B) { + benchmarksReadAccounts(b, ctx, store, pit) + }) + b.Run("aggregates", func(b *testing.B) { + benchmarksGetAggregatedBalances(b, ctx, store, pit) + }) + } + runAllWithPIT(b, nil) + b.Run("using pit", func(b *testing.B) { + // Use pit with the more recent, this way we force the storage to use a join + // Doing this allowing to test the worst case + runAllWithPIT(b, pointer.For(now.Add(time.Minute*time.Duration(*nbTransactions)))) }) }) } } -func benchmarksReadTransactions(b *testing.B, ctx context.Context, store *Store, info *scenarioInfo) { +func benchmarksReadTransactions(b *testing.B, ctx context.Context, store *Store, info *scenarioInfo, pit *ledger.Time) { type testCase struct { name string query query.Builder @@ -170,10 +322,16 @@ func benchmarksReadTransactions(b *testing.B, ctx context.Context, store *Store, for _, t := range testCases { t := t b.Run(t.name, func(b *testing.B) { + var q GetTransactionsQuery for i := 0; i < b.N; i++ { - q := NewGetTransactionsQuery(PaginatedQueryOptions[PITFilterWithVolumes]{ - PageSize: 10, + q = NewGetTransactionsQuery(PaginatedQueryOptions[PITFilterWithVolumes]{ + PageSize: 100, QueryBuilder: t.query, + Options: PITFilterWithVolumes{ + PITFilter: PITFilter{ + PIT: pit, + }, + }, }) if t.expandVolumes { q = q.WithExpandVolumes() @@ -187,11 +345,16 @@ func benchmarksReadTransactions(b *testing.B, ctx context.Context, store *Store, require.Fail(b, "response should not be empty") } } + + explainRequest(ctx, b, func(ctx context.Context) { + _, err := store.GetTransactions(ctx, q) + require.NoError(b, err) + }) }) } } -func benchmarksReadAccounts(b *testing.B, ctx context.Context, store *Store) { +func benchmarksReadAccounts(b *testing.B, ctx context.Context, store *Store, pit *ledger.Time) { type testCase struct { name string query query.Builder @@ -224,10 +387,16 @@ func benchmarksReadAccounts(b *testing.B, ctx context.Context, store *Store) { for _, t := range testCases { t := t b.Run(t.name, func(b *testing.B) { + var q GetAccountsQuery for i := 0; i < b.N; i++ { - q := NewGetAccountsQuery(PaginatedQueryOptions[PITFilterWithVolumes]{ - PageSize: 10, + q = NewGetAccountsQuery(PaginatedQueryOptions[PITFilterWithVolumes]{ + PageSize: 100, QueryBuilder: t.query, + Options: PITFilterWithVolumes{ + PITFilter: PITFilter{ + PIT: pit, + }, + }, }) if t.expandVolumes { q = q.WithExpandVolumes() @@ -241,11 +410,16 @@ func benchmarksReadAccounts(b *testing.B, ctx context.Context, store *Store) { require.Fail(b, "response should not be empty") } } + + explainRequest(ctx, b, func(ctx context.Context) { + _, err := store.GetAccountsWithVolumes(ctx, q) + require.NoError(b, err) + }) }) } } -func benchmarksGetAggregatedBalances(b *testing.B, ctx context.Context, store *Store) { +func benchmarksGetAggregatedBalances(b *testing.B, ctx context.Context, store *Store, pit *ledger.Time) { type testCase struct { name string query query.Builder @@ -273,16 +447,55 @@ func benchmarksGetAggregatedBalances(b *testing.B, ctx context.Context, store *S for _, t := range testCases { t := t b.Run(t.name, func(b *testing.B) { + var q GetAggregatedBalanceQuery for i := 0; i < b.N; i++ { - ret, err := store.GetAggregatedBalances(ctx, NewGetAggregatedBalancesQuery(PaginatedQueryOptions[PITFilter]{ - PageSize: 10, + q = NewGetAggregatedBalancesQuery(PaginatedQueryOptions[PITFilter]{ + PageSize: 100, QueryBuilder: t.query, - })) + Options: PITFilter{ + PIT: pit, + }, + }) + ret, err := store.GetAggregatedBalances(ctx, q) require.NoError(b, err) if !t.allowEmptyResponse && len(ret) == 0 { require.Fail(b, "response should not be empty") } } + + explainRequest(ctx, b, func(ctx context.Context) { + _, err := store.GetAggregatedBalances(ctx, q) + require.NoError(b, err) + }) }) } } + +func explainRequest(ctx context.Context, b *testing.B, f func(ctx context.Context)) { + var ( + explained string + jsonExplained string + ) + additionalHooks := make([]bun.QueryHook, 0) + if testing.Verbose() { + additionalHooks = append(additionalHooks, bunexplain.NewExplainHook(bunexplain.WithListener(func(data string) { + explained = data + }))) + } + additionalHooks = append(additionalHooks, bunexplain.NewExplainHook( + bunexplain.WithListener(func(data string) { + jsonExplained = data + }), + bunexplain.WithJSONFormat(), + )) + ctx = contextWithHook(ctx, additionalHooks...) + f(ctx) + + if testing.Verbose() { + fmt.Println(explained) + } + jsonQueryPlan := make([]any, 0) + + require.NoError(b, json.Unmarshal([]byte(jsonExplained), &jsonQueryPlan)) + b.ReportMetric(jsonQueryPlan[0].(map[string]any)["Plan"].(map[string]any)["Total Cost"].(float64), "cost") +} diff --git a/components/ledger/libs/bun/bunexplain/explain_hook.go b/components/ledger/libs/bun/bunexplain/explain_hook.go index 1f36874f05..94d23acb90 100644 --- a/components/ledger/libs/bun/bunexplain/explain_hook.go +++ b/components/ledger/libs/bun/bunexplain/explain_hook.go @@ -1,6 +1,7 @@ package bunexplain import ( + "bytes" "context" "database/sql" "fmt" @@ -9,8 +10,13 @@ import ( "github.com/uptrace/bun" ) +type listener func(data string) + //nolint:unused -type explainHook struct{} +type explainHook struct { + listener listener + json bool +} //nolint:unused func (h *explainHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {} @@ -24,19 +30,34 @@ func (h *explainHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) co } if err := event.DB.RunInTx(context.Background(), &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { - rows, err := tx.Query("explain analyze verbose " + event.Query) + query := "explain " + if h.json { + query += "(format json) " + } else { + query += "analyze verbose " + } + query += event.Query + rows, err := tx.Query(query) if err != nil { return err } - defer rows.Next() + defer func() { + _ = rows.Close() + }() + data := bytes.NewBufferString("") for rows.Next() { var line string if err := rows.Scan(&line); err != nil { return err } - fmt.Println(line) + data.WriteString(line) + data.WriteString("\r\n") + } + if rows.Err() != nil { + return rows.Err() } + h.listener(data.String()) return tx.Rollback() @@ -47,6 +68,30 @@ func (h *explainHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) co return ctx } -func NewExplainHook() *explainHook { - return &explainHook{} +func NewExplainHook(opts ...option) *explainHook { + ret := &explainHook{} + for _, opt := range append(defaultOptions, opts...) { + opt(ret) + } + return ret +} + +type option func(hook *explainHook) + +var defaultOptions = []option{ + WithListener(func(data string) { + fmt.Println(data) + }), +} + +var WithListener = func(w listener) option { + return func(hook *explainHook) { + hook.listener = w + } +} + +var WithJSONFormat = func() option { + return func(hook *explainHook) { + hook.json = true + } } diff --git a/libs/go-libs/bun/bunexplain/explain_hook.go b/libs/go-libs/bun/bunexplain/explain_hook.go index 1f36874f05..94d23acb90 100644 --- a/libs/go-libs/bun/bunexplain/explain_hook.go +++ b/libs/go-libs/bun/bunexplain/explain_hook.go @@ -1,6 +1,7 @@ package bunexplain import ( + "bytes" "context" "database/sql" "fmt" @@ -9,8 +10,13 @@ import ( "github.com/uptrace/bun" ) +type listener func(data string) + //nolint:unused -type explainHook struct{} +type explainHook struct { + listener listener + json bool +} //nolint:unused func (h *explainHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {} @@ -24,19 +30,34 @@ func (h *explainHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) co } if err := event.DB.RunInTx(context.Background(), &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { - rows, err := tx.Query("explain analyze verbose " + event.Query) + query := "explain " + if h.json { + query += "(format json) " + } else { + query += "analyze verbose " + } + query += event.Query + rows, err := tx.Query(query) if err != nil { return err } - defer rows.Next() + defer func() { + _ = rows.Close() + }() + data := bytes.NewBufferString("") for rows.Next() { var line string if err := rows.Scan(&line); err != nil { return err } - fmt.Println(line) + data.WriteString(line) + data.WriteString("\r\n") + } + if rows.Err() != nil { + return rows.Err() } + h.listener(data.String()) return tx.Rollback() @@ -47,6 +68,30 @@ func (h *explainHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) co return ctx } -func NewExplainHook() *explainHook { - return &explainHook{} +func NewExplainHook(opts ...option) *explainHook { + ret := &explainHook{} + for _, opt := range append(defaultOptions, opts...) { + opt(ret) + } + return ret +} + +type option func(hook *explainHook) + +var defaultOptions = []option{ + WithListener(func(data string) { + fmt.Println(data) + }), +} + +var WithListener = func(w listener) option { + return func(hook *explainHook) { + hook.listener = w + } +} + +var WithJSONFormat = func() option { + return func(hook *explainHook) { + hook.json = true + } }