github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/benchmark/driver_bench_test.go (about)

     1  //go:build ci && docker
     2  // +build ci,docker
     3  
     4  package benchmark
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"math"
    10  	"math/rand"
    11  	"strconv"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/authzed/spicedb/internal/datastore/crdb"
    18  	"github.com/authzed/spicedb/internal/datastore/mysql"
    19  	"github.com/authzed/spicedb/internal/datastore/postgres"
    20  	"github.com/authzed/spicedb/internal/datastore/spanner"
    21  	"github.com/authzed/spicedb/internal/testfixtures"
    22  	testdatastore "github.com/authzed/spicedb/internal/testserver/datastore"
    23  	"github.com/authzed/spicedb/internal/testserver/datastore/config"
    24  	dsconfig "github.com/authzed/spicedb/pkg/cmd/datastore"
    25  	"github.com/authzed/spicedb/pkg/datastore"
    26  	"github.com/authzed/spicedb/pkg/datastore/options"
    27  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    28  	"github.com/authzed/spicedb/pkg/tuple"
    29  )
    30  
    31  const (
    32  	numDocuments = 1000
    33  	usersPerDoc  = 5
    34  
    35  	revisionQuantization = 5 * time.Second
    36  	gcWindow             = 2 * time.Hour
    37  	gcInterval           = 1 * time.Hour
    38  	watchBufferLength    = 1000
    39  )
    40  
    41  var drivers = []struct {
    42  	name        string
    43  	suffix      string
    44  	extraConfig []dsconfig.ConfigOption
    45  }{
    46  	{"memory", "", nil},
    47  	{postgres.Engine, "", nil},
    48  	{crdb.Engine, "-overlap-static", []dsconfig.ConfigOption{dsconfig.WithOverlapStrategy("static")}},
    49  	{crdb.Engine, "-overlap-insecure", []dsconfig.ConfigOption{dsconfig.WithOverlapStrategy("insecure")}},
    50  	{mysql.Engine, "", nil},
    51  }
    52  
    53  var skipped = []string{
    54  	spanner.Engine, // Not useful to benchmark a simulator
    55  }
    56  
    57  var sortOrders = map[string]options.SortOrder{
    58  	"ByResource": options.ByResource,
    59  	"BySubject":  options.BySubject,
    60  }
    61  
    62  func BenchmarkDatastoreDriver(b *testing.B) {
    63  	for _, driver := range drivers {
    64  		b.Run(driver.name+driver.suffix, func(b *testing.B) {
    65  			engine := testdatastore.RunDatastoreEngine(b, driver.name)
    66  			ds := engine.NewDatastore(b, config.DatastoreConfigInitFunc(
    67  				b,
    68  				append(driver.extraConfig,
    69  					dsconfig.WithRevisionQuantization(revisionQuantization),
    70  					dsconfig.WithGCWindow(gcWindow),
    71  					dsconfig.WithGCInterval(gcInterval),
    72  					dsconfig.WithWatchBufferLength(watchBufferLength))...,
    73  			))
    74  
    75  			ctx := context.Background()
    76  
    77  			// Write the standard schema
    78  			ds, _ = testfixtures.StandardDatastoreWithSchema(ds, require.New(b))
    79  
    80  			// Write a fair amount of data, much more than a functional test
    81  			for docNum := 0; docNum < numDocuments; docNum++ {
    82  				_, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
    83  					var updates []*core.RelationTupleUpdate
    84  					for userNum := 0; userNum < usersPerDoc; userNum++ {
    85  						updates = append(updates, &core.RelationTupleUpdate{
    86  							Operation: core.RelationTupleUpdate_CREATE,
    87  							Tuple:     docViewer(strconv.Itoa(docNum), strconv.Itoa(userNum)),
    88  						})
    89  					}
    90  
    91  					return rwt.WriteRelationships(ctx, updates)
    92  				})
    93  				require.NoError(b, err)
    94  			}
    95  
    96  			// Sleep to give the datastore time to stabilize after all the writes
    97  			time.Sleep(1 * time.Second)
    98  
    99  			headRev, err := ds.HeadRevision(ctx)
   100  			require.NoError(b, err)
   101  
   102  			b.Run("TestTuple", func(b *testing.B) {
   103  				b.Run("SnapshotRead", func(b *testing.B) {
   104  					for n := 0; n < b.N; n++ {
   105  						randDocNum := rand.Intn(numDocuments)
   106  						iter, err := ds.SnapshotReader(headRev).QueryRelationships(ctx, datastore.RelationshipsFilter{
   107  							OptionalResourceType:     testfixtures.DocumentNS.Name,
   108  							OptionalResourceIds:      []string{strconv.Itoa(randDocNum)},
   109  							OptionalResourceRelation: "viewer",
   110  						})
   111  						require.NoError(b, err)
   112  						var count int
   113  						for rel := iter.Next(); rel != nil; rel = iter.Next() {
   114  							count++
   115  						}
   116  						require.NoError(b, iter.Err())
   117  						iter.Close()
   118  						require.Equal(b, usersPerDoc, count)
   119  					}
   120  				})
   121  				b.Run("SortedSnapshotReadOnlyNamespace", func(b *testing.B) {
   122  					for orderName, order := range sortOrders {
   123  						order := order
   124  						b.Run(orderName, func(b *testing.B) {
   125  							for n := 0; n < b.N; n++ {
   126  								iter, err := ds.SnapshotReader(headRev).QueryRelationships(ctx, datastore.RelationshipsFilter{
   127  									OptionalResourceType: testfixtures.DocumentNS.Name,
   128  								}, options.WithSort(order))
   129  								require.NoError(b, err)
   130  								var count int
   131  								for rel := iter.Next(); rel != nil; rel = iter.Next() {
   132  									count++
   133  								}
   134  								require.NoError(b, iter.Err())
   135  								iter.Close()
   136  							}
   137  						})
   138  					}
   139  				})
   140  				b.Run("SortedSnapshotReadWithRelation", func(b *testing.B) {
   141  					for orderName, order := range sortOrders {
   142  						order := order
   143  						b.Run(orderName, func(b *testing.B) {
   144  							for n := 0; n < b.N; n++ {
   145  								iter, err := ds.SnapshotReader(headRev).QueryRelationships(ctx, datastore.RelationshipsFilter{
   146  									OptionalResourceType:     testfixtures.DocumentNS.Name,
   147  									OptionalResourceRelation: "viewer",
   148  								}, options.WithSort(order))
   149  								require.NoError(b, err)
   150  								var count int
   151  								for rel := iter.Next(); rel != nil; rel = iter.Next() {
   152  									count++
   153  								}
   154  								require.NoError(b, iter.Err())
   155  								iter.Close()
   156  							}
   157  						})
   158  					}
   159  				})
   160  				b.Run("SortedSnapshotReadAllResourceFields", func(b *testing.B) {
   161  					for orderName, order := range sortOrders {
   162  						order := order
   163  						b.Run(orderName, func(b *testing.B) {
   164  							for n := 0; n < b.N; n++ {
   165  								randDocNum := rand.Intn(numDocuments)
   166  								iter, err := ds.SnapshotReader(headRev).QueryRelationships(ctx, datastore.RelationshipsFilter{
   167  									OptionalResourceType:     testfixtures.DocumentNS.Name,
   168  									OptionalResourceIds:      []string{strconv.Itoa(randDocNum)},
   169  									OptionalResourceRelation: "viewer",
   170  								}, options.WithSort(order))
   171  								require.NoError(b, err)
   172  								var count int
   173  								for rel := iter.Next(); rel != nil; rel = iter.Next() {
   174  									count++
   175  								}
   176  								require.NoError(b, iter.Err())
   177  								iter.Close()
   178  							}
   179  						})
   180  					}
   181  				})
   182  				b.Run("SnapshotReverseRead", func(b *testing.B) {
   183  					for n := 0; n < b.N; n++ {
   184  						iter, err := ds.SnapshotReader(headRev).ReverseQueryRelationships(ctx, datastore.SubjectsFilter{
   185  							SubjectType: testfixtures.UserNS.Name,
   186  						}, options.WithSortForReverse(options.ByResource))
   187  						require.NoError(b, err)
   188  						var count int
   189  						for rel := iter.Next(); rel != nil; rel = iter.Next() {
   190  							count++
   191  						}
   192  						require.NoError(b, iter.Err())
   193  						iter.Close()
   194  					}
   195  				})
   196  				b.Run("Touch", buildTupleTest(ctx, ds, core.RelationTupleUpdate_TOUCH))
   197  				b.Run("Create", buildTupleTest(ctx, ds, core.RelationTupleUpdate_CREATE))
   198  				b.Run("CreateAndTouch", func(b *testing.B) {
   199  					const totalRelationships = 1000
   200  					for _, portionCreate := range []float64{0, 0.10, 0.25, 0.50, 1} {
   201  						portionCreate := portionCreate
   202  						b.Run(fmt.Sprintf("%v_", portionCreate), func(b *testing.B) {
   203  							for n := 0; n < b.N; n++ {
   204  								portionCreateIndex := int(math.Floor(portionCreate * totalRelationships))
   205  								mutations := make([]*core.RelationTupleUpdate, 0, totalRelationships)
   206  								for index := 0; index < totalRelationships; index++ {
   207  									if index >= portionCreateIndex {
   208  										stableID := fmt.Sprintf("id-%d", index)
   209  										tpl := docViewer(stableID, stableID)
   210  										mutations = append(mutations, tuple.Touch(tpl))
   211  									} else {
   212  										randomID := testfixtures.RandomObjectID(32)
   213  										tpl := docViewer(randomID, randomID)
   214  										mutations = append(mutations, tuple.Create(tpl))
   215  									}
   216  								}
   217  
   218  								_, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   219  									return rwt.WriteRelationships(ctx, mutations)
   220  								})
   221  								require.NoError(b, err)
   222  							}
   223  						})
   224  					}
   225  				})
   226  			})
   227  		})
   228  	}
   229  }
   230  
   231  func TestAllDriversBenchmarkedOrSkipped(t *testing.T) {
   232  	notBenchmarked := make(map[string]struct{}, len(datastore.Engines))
   233  	for _, name := range datastore.Engines {
   234  		notBenchmarked[name] = struct{}{}
   235  	}
   236  
   237  	for _, driver := range drivers {
   238  		delete(notBenchmarked, driver.name)
   239  	}
   240  	for _, skippedEngine := range skipped {
   241  		delete(notBenchmarked, skippedEngine)
   242  	}
   243  
   244  	require.Empty(t, notBenchmarked)
   245  }
   246  
   247  func buildTupleTest(ctx context.Context, ds datastore.Datastore, op core.RelationTupleUpdate_Operation) func(b *testing.B) {
   248  	return func(b *testing.B) {
   249  		for n := 0; n < b.N; n++ {
   250  			_, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   251  				randomID := testfixtures.RandomObjectID(32)
   252  				return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{
   253  					{
   254  						Operation: op,
   255  						Tuple:     docViewer(randomID, randomID),
   256  					},
   257  				})
   258  			})
   259  			require.NoError(b, err)
   260  		}
   261  	}
   262  }
   263  
   264  func docViewer(documentID, userID string) *core.RelationTuple {
   265  	return &core.RelationTuple{
   266  		ResourceAndRelation: &core.ObjectAndRelation{
   267  			Namespace: testfixtures.DocumentNS.Name,
   268  			ObjectId:  documentID,
   269  			Relation:  "viewer",
   270  		},
   271  		Subject: &core.ObjectAndRelation{
   272  			Namespace: testfixtures.UserNS.Name,
   273  			ObjectId:  userID,
   274  			Relation:  datastore.Ellipsis,
   275  		},
   276  	}
   277  }