Skip to content

Commit d45a485

Browse files
committed
test: add double-length arrow benchmark test
1 parent cbb54b5 commit d45a485

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package benchmarks
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/authzed/spicedb/internal/datastore/common"
11+
"github.com/authzed/spicedb/internal/datastore/memdb"
12+
"github.com/authzed/spicedb/pkg/datalayer"
13+
"github.com/authzed/spicedb/pkg/datastore"
14+
"github.com/authzed/spicedb/pkg/query"
15+
"github.com/authzed/spicedb/pkg/schema/v2"
16+
"github.com/authzed/spicedb/pkg/schemadsl/compiler"
17+
"github.com/authzed/spicedb/pkg/schemadsl/input"
18+
"github.com/authzed/spicedb/pkg/tuple"
19+
)
20+
21+
// BenchmarkCheckDoubleWideArrow benchmarks permission checking through two consecutive
22+
// arrow hops with wide fan-out at each level.
23+
//
24+
// The hierarchy is: file -> org -> group -> user
25+
//
26+
// - 5 files, each belonging to 3 orgs
27+
// - 23 orgs (prime), each containing 7 groups
28+
// - 97 groups (prime), each with 15 members
29+
// - 997 users (prime)
30+
//
31+
// The schema:
32+
//
33+
// definition group { relation member: user }
34+
// definition org { relation group: group; permission member = group->member }
35+
// definition file { relation org: org; relation view: user;
36+
// permission viewer = view + org->member }
37+
//
38+
// Checking viewer on a file requires two arrow traversals:
39+
// 1. file->org (fanout: orgs per file)
40+
// 2. org->member which resolves to org->group->member (double fan-out)
41+
//
42+
// Four sub-benchmarks are run:
43+
// - plain: compile the outline directly and run Check each iteration
44+
// - advised: seed a CountAdvisor from a single warm-up run, apply it to the
45+
// canonical outline, compile once, then run Check each iteration
46+
// - plain_delay: same as plain, but with networkDelay latency per datastore call
47+
// - advised_delay: same as advised, but with networkDelay latency per datastore call
48+
func BenchmarkCheckDoubleWideArrow(b *testing.B) {
49+
const (
50+
numFiles = 5
51+
numOrgs = 97 // prime
52+
numGroups = 299 // prime
53+
numUsers = 499 // prime
54+
orgsPerFile = 20
55+
groupsPerOrg = 10
56+
usersPerGroup = 20
57+
)
58+
59+
// ---- shared setup ----
60+
61+
rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
62+
require.NoError(b, err)
63+
64+
ctx := context.Background()
65+
66+
schemaText := `
67+
definition user {}
68+
69+
definition group {
70+
relation member: user
71+
}
72+
73+
definition org {
74+
relation group: group
75+
permission member = group->member
76+
}
77+
78+
definition file {
79+
relation org: org
80+
relation view: user
81+
permission viewer = view + org->member
82+
}
83+
`
84+
85+
compiled, err := compiler.Compile(compiler.InputSchema{
86+
Source: input.Source("benchmark"),
87+
SchemaString: schemaText,
88+
}, compiler.AllowUnprefixedObjectType())
89+
require.NoError(b, err)
90+
91+
_, err = rawDS.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
92+
return rwt.LegacyWriteNamespaces(ctx, compiled.ObjectDefinitions...)
93+
})
94+
require.NoError(b, err)
95+
96+
relationships := make([]tuple.Relationship, 0,
97+
numFiles*orgsPerFile+numOrgs*groupsPerOrg+numGroups*usersPerGroup)
98+
99+
// file:fileN#org@org:orgM
100+
for fileID := 0; fileID < numFiles; fileID++ {
101+
step := fileID + 1
102+
for i := 0; i < orgsPerFile; i++ {
103+
orgID := (i * step) % numOrgs
104+
rel := fmt.Sprintf("file:file%d#org@org:org%d", fileID, orgID)
105+
relationships = append(relationships, tuple.MustParse(rel))
106+
}
107+
}
108+
109+
// org:orgN#group@group:groupM
110+
for orgID := 0; orgID < numOrgs; orgID++ {
111+
step := orgID + 1
112+
for i := 0; i < groupsPerOrg; i++ {
113+
groupID := (i * step) % numGroups
114+
rel := fmt.Sprintf("org:org%d#group@group:group%d", orgID, groupID)
115+
relationships = append(relationships, tuple.MustParse(rel))
116+
}
117+
}
118+
119+
// group:groupN#member@user:userM
120+
for groupID := 0; groupID < numGroups; groupID++ {
121+
step := groupID + 1
122+
for i := 0; i < usersPerGroup; i++ {
123+
userID := (i * step) % numUsers
124+
rel := fmt.Sprintf("group:group%d#member@user:user%d", groupID, userID)
125+
relationships = append(relationships, tuple.MustParse(rel))
126+
}
127+
}
128+
129+
revision, err := common.WriteRelationships(ctx, rawDS, tuple.UpdateOperationCreate, relationships...)
130+
require.NoError(b, err)
131+
132+
dsSchema, err := schema.BuildSchemaFromDefinitions(compiled.ObjectDefinitions, nil)
133+
require.NoError(b, err)
134+
135+
// Build the canonical outline once; all sub-benchmarks derive from it.
136+
canonicalOutline, err := query.BuildOutlineFromSchema(dsSchema, "file", "viewer")
137+
require.NoError(b, err)
138+
139+
// The resource and subject are the same for all sub-benchmarks.
140+
resources := query.NewObjects("file", "file0")
141+
subject := query.NewObject("user", "user181").WithEllipses()
142+
143+
// Base reader (no simulated latency).
144+
reader := query.NewQueryDatastoreReader(datalayer.NewDataLayer(rawDS).SnapshotReader(revision))
145+
146+
// Delay reader wrapping the base reader with simulated network latency.
147+
delayReader := query.NewDelayReader(networkDelay, reader)
148+
149+
// buildAdvisedIterator seeds a CountAdvisor from a single warm-up run using the
150+
// provided reader and returns the compiled advised iterator.
151+
buildAdvisedIterator := func(b *testing.B, r query.QueryDatastoreReader) query.Iterator {
152+
b.Helper()
153+
obs := query.NewCountObserver()
154+
warmIt, err := canonicalOutline.Compile()
155+
require.NoError(b, err)
156+
warmCtx := query.NewLocalContext(ctx,
157+
query.WithReader(r),
158+
query.WithObserver(obs),
159+
)
160+
seq, err := warmCtx.Check(warmIt, resources, subject)
161+
require.NoError(b, err)
162+
_, err = query.CollectAll(seq)
163+
require.NoError(b, err)
164+
165+
advisor := query.NewCountAdvisor(obs.GetStats())
166+
advisedCO, err := query.ApplyAdvisor(canonicalOutline, advisor)
167+
require.NoError(b, err)
168+
advisedIt, err := advisedCO.Compile()
169+
require.NoError(b, err)
170+
return advisedIt
171+
}
172+
173+
// ---- plain sub-benchmark ----
174+
175+
b.Run("plain", func(b *testing.B) {
176+
it, err := canonicalOutline.Compile()
177+
require.NoError(b, err)
178+
179+
b.Log("plain explain:\n", it.Explain())
180+
181+
queryCtx := query.NewLocalContext(ctx, query.WithReader(reader))
182+
183+
b.ResetTimer()
184+
for b.Loop() {
185+
seq, err := queryCtx.Check(it, resources, subject)
186+
require.NoError(b, err)
187+
paths, err := query.CollectAll(seq)
188+
require.NoError(b, err)
189+
require.NotEmpty(b, paths)
190+
}
191+
})
192+
193+
// ---- advised sub-benchmark ----
194+
195+
b.Run("advised", func(b *testing.B) {
196+
advisedIt := buildAdvisedIterator(b, reader)
197+
198+
b.Log("advised explain:\n", advisedIt.Explain())
199+
200+
queryCtx := query.NewLocalContext(ctx, query.WithReader(reader))
201+
202+
b.ResetTimer()
203+
for b.Loop() {
204+
seq, err := queryCtx.Check(advisedIt, resources, subject)
205+
require.NoError(b, err)
206+
paths, err := query.CollectAll(seq)
207+
require.NoError(b, err)
208+
require.NotEmpty(b, paths)
209+
}
210+
})
211+
212+
// ---- plain_delay sub-benchmark ----
213+
214+
b.Run("plain_delay", func(b *testing.B) {
215+
it, err := canonicalOutline.Compile()
216+
require.NoError(b, err)
217+
218+
queryCtx := query.NewLocalContext(ctx, query.WithReader(delayReader))
219+
220+
b.ResetTimer()
221+
for b.Loop() {
222+
seq, err := queryCtx.Check(it, resources, subject)
223+
require.NoError(b, err)
224+
paths, err := query.CollectAll(seq)
225+
require.NoError(b, err)
226+
require.NotEmpty(b, paths)
227+
}
228+
})
229+
230+
// ---- advised_delay sub-benchmark ----
231+
232+
b.Run("advised_delay", func(b *testing.B) {
233+
advisedIt := buildAdvisedIterator(b, delayReader)
234+
queryCtx := query.NewLocalContext(ctx, query.WithReader(delayReader))
235+
236+
b.ResetTimer()
237+
for b.Loop() {
238+
seq, err := queryCtx.Check(advisedIt, resources, subject)
239+
require.NoError(b, err)
240+
paths, err := query.CollectAll(seq)
241+
require.NoError(b, err)
242+
require.NotEmpty(b, paths)
243+
}
244+
})
245+
}

0 commit comments

Comments
 (0)