mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Benchmarks for searchv2 (#60730)
* bench-test * cleanup * more simplification * fix tests * correct wrong argument ordering & use constant * fix issues with tests setup * add benchmark results * reuse Gabriel's concurrent setup code * correct error logs for ac benchmarks
This commit is contained in:
parent
d0e95f8c95
commit
1865205d68
@ -3,13 +3,13 @@ package acimpl
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
@ -17,64 +17,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const concurrency = 10
|
|
||||||
const batchSize = 1000
|
|
||||||
|
|
||||||
type bounds struct {
|
|
||||||
start, end int
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentBatch spawns the requested amount of workers then ask them to run eachFn on chunks of the requested size
|
|
||||||
func concurrentBatch(workers, count, size int, eachFn func(start, end int) error) error {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
alldone := make(chan bool) // Indicates that all workers have finished working
|
|
||||||
chunk := make(chan bounds) // Gives the workers the bounds they should work with
|
|
||||||
ret := make(chan error) // Allow workers to notify in case of errors
|
|
||||||
defer close(ret)
|
|
||||||
|
|
||||||
// Launch all workers
|
|
||||||
for x := 0; x < workers; x++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for ck := range chunk {
|
|
||||||
if err := eachFn(ck.start, ck.end); err != nil {
|
|
||||||
ret <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// Tell the workers the chunks they have to work on
|
|
||||||
for i := 0; i < count; {
|
|
||||||
end := i + size
|
|
||||||
if end > count {
|
|
||||||
end = count
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk <- bounds{start: i, end: end}
|
|
||||||
|
|
||||||
i = end
|
|
||||||
}
|
|
||||||
close(chunk)
|
|
||||||
|
|
||||||
// Wait for the workers
|
|
||||||
wg.Wait()
|
|
||||||
close(alldone)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// wait for an error or for all workers to be done
|
|
||||||
select {
|
|
||||||
case err := <-ret:
|
|
||||||
return err
|
|
||||||
case <-alldone:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupBenchEnv will create userCount users, userCount managed roles with resourceCount managed permission each
|
// setupBenchEnv will create userCount users, userCount managed roles with resourceCount managed permission each
|
||||||
// Example: setupBenchEnv(b, 2, 3):
|
// Example: setupBenchEnv(b, 2, 3):
|
||||||
// - will create 2 users and assign them 2 managed roles
|
// - will create 2 users and assign them 2 managed roles
|
||||||
@ -102,7 +44,7 @@ func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.S
|
|||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
|
|
||||||
// Populate users, roles and assignments
|
// Populate users, roles and assignments
|
||||||
if errInsert := concurrentBatch(concurrency, usersCount, batchSize, func(start, end int) error {
|
if errInsert := actest.ConcurrentBatch(actest.Concurrency, usersCount, actest.BatchSize, func(start, end int) error {
|
||||||
n := end - start
|
n := end - start
|
||||||
users := make([]user.User, 0, n)
|
users := make([]user.User, 0, n)
|
||||||
orgUsers := make([]org.OrgUser, 0, n)
|
orgUsers := make([]org.OrgUser, 0, n)
|
||||||
@ -157,13 +99,13 @@ func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.S
|
|||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}); errInsert != nil {
|
}); errInsert != nil {
|
||||||
require.NoError(b, err, "could not insert users and roles")
|
require.NoError(b, errInsert, "could not insert users and roles")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate permissions
|
// Populate permissions
|
||||||
action2 := "resources:action2"
|
action2 := "resources:action2"
|
||||||
if errInsert := concurrentBatch(concurrency, resourceCount*usersCount, batchSize, func(start, end int) error {
|
if errInsert := actest.ConcurrentBatch(actest.Concurrency, resourceCount*usersCount, actest.BatchSize, func(start, end int) error {
|
||||||
permissions := make([]accesscontrol.Permission, 0, end-start)
|
permissions := make([]accesscontrol.Permission, 0, end-start)
|
||||||
for i := start; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
permissions = append(permissions, accesscontrol.Permission{
|
permissions = append(permissions, accesscontrol.Permission{
|
||||||
@ -180,7 +122,7 @@ func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.S
|
|||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}); errInsert != nil {
|
}); errInsert != nil {
|
||||||
require.NoError(b, err, "could not insert permissions")
|
require.NoError(b, errInsert, "could not insert permissions")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
pkg/services/accesscontrol/actest/common.go
Normal file
61
pkg/services/accesscontrol/actest/common.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package actest
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
const Concurrency = 10
|
||||||
|
const BatchSize = 1000
|
||||||
|
|
||||||
|
type bounds struct {
|
||||||
|
start, end int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConcurrentBatch spawns the requested amount of workers then ask them to run eachFn on chunks of the requested size
|
||||||
|
func ConcurrentBatch(workers, count, size int, eachFn func(start, end int) error) error {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
alldone := make(chan bool) // Indicates that all workers have finished working
|
||||||
|
chunk := make(chan bounds) // Gives the workers the bounds they should work with
|
||||||
|
ret := make(chan error) // Allow workers to notify in case of errors
|
||||||
|
defer close(ret)
|
||||||
|
|
||||||
|
// Launch all workers
|
||||||
|
for x := 0; x < workers; x++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for ck := range chunk {
|
||||||
|
if err := eachFn(ck.start, ck.end); err != nil {
|
||||||
|
ret <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Tell the workers the chunks they have to work on
|
||||||
|
for i := 0; i < count; {
|
||||||
|
end := i + size
|
||||||
|
if end > count {
|
||||||
|
end = count
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk <- bounds{start: i, end: end}
|
||||||
|
|
||||||
|
i = end
|
||||||
|
}
|
||||||
|
close(chunk)
|
||||||
|
|
||||||
|
// Wait for the workers
|
||||||
|
wg.Wait()
|
||||||
|
close(alldone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for an error or for all workers to be done
|
||||||
|
select {
|
||||||
|
case err := <-ret:
|
||||||
|
return err
|
||||||
|
case <-alldone:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
211
pkg/services/searchV2/service_bench_test.go
Normal file
211
pkg/services/searchV2/service_bench_test.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package searchV2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||||
|
"github.com/grafana/grafana/pkg/services/querylibrary/querylibraryimpl"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store"
|
||||||
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupBenchEnv will set up a database with folderCount folders and dashboardsPerFolder dashboards per folder
|
||||||
|
// It will also set up and run the search service
|
||||||
|
// and create a signed in user object with explicit permissions on each dashboard and folder.
|
||||||
|
func setupBenchEnv(b *testing.B, folderCount, dashboardsPerFolder int) (*StandardSearchService, *user.SignedInUser, error) {
|
||||||
|
sqlStore := db.InitTestDB(b)
|
||||||
|
err := populateDB(folderCount, dashboardsPerFolder, sqlStore)
|
||||||
|
require.NoError(b, err, "error when populating the database")
|
||||||
|
|
||||||
|
// load all dashboards and folders
|
||||||
|
dbLoadingBatchSize := (dashboardsPerFolder + 1) * folderCount
|
||||||
|
cfg := &setting.Cfg{Search: setting.SearchSettings{DashboardLoadingBatchSize: dbLoadingBatchSize}}
|
||||||
|
features := featuremgmt.WithFeatures()
|
||||||
|
orgSvc := &orgtest.FakeOrgService{
|
||||||
|
ExpectedOrgs: []*org.OrgDTO{{ID: 1}},
|
||||||
|
}
|
||||||
|
querySvc := querylibraryimpl.ProvideService(cfg, features)
|
||||||
|
searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{},
|
||||||
|
tracing.InitializeTracerForTest(), features, orgSvc, nil, querySvc).(*StandardSearchService)
|
||||||
|
require.True(b, ok)
|
||||||
|
|
||||||
|
err = runSearchService(searchService)
|
||||||
|
require.NoError(b, err, "error when running search service")
|
||||||
|
|
||||||
|
user := getSignedInUser(folderCount, dashboardsPerFolder)
|
||||||
|
|
||||||
|
return searchService, user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a signed in user object with permissions on all dashboards and folders
|
||||||
|
func getSignedInUser(folderCount, dashboardsPerFolder int) *user.SignedInUser {
|
||||||
|
folderScopes := make([]string, folderCount)
|
||||||
|
for i := 1; i <= folderCount; i++ {
|
||||||
|
folderScopes[i-1] = dashboards.ScopeFoldersProvider.GetResourceScopeUID(fmt.Sprintf("folder%d", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
dashScopes := make([]string, folderCount*dashboardsPerFolder)
|
||||||
|
for i := folderCount + 1; i <= (folderCount * (dashboardsPerFolder + 1)); i++ {
|
||||||
|
dashScopes[i-(folderCount+1)] = dashboards.ScopeDashboardsProvider.GetResourceScopeUID(fmt.Sprintf("dashboard%d", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &user.SignedInUser{
|
||||||
|
UserID: 1,
|
||||||
|
OrgID: 1,
|
||||||
|
Permissions: map[int64]map[string][]string{
|
||||||
|
1: {
|
||||||
|
dashboards.ActionDashboardsRead: dashScopes,
|
||||||
|
dashboards.ActionFoldersRead: folderScopes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs initial indexing of search service
|
||||||
|
func runSearchService(searchService *StandardSearchService) error {
|
||||||
|
if err := searchService.dashboardIndex.buildInitialIndexes(context.Background(), []int64{int64(1)}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
searchService.dashboardIndex.initialIndexingComplete = true
|
||||||
|
|
||||||
|
// Required for sync that is called during dashboard search
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
doneCh := <-searchService.dashboardIndex.syncCh
|
||||||
|
close(doneCh)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populates database with dashboards and folders
|
||||||
|
func populateDB(folderCount, dashboardsPerFolder int, sqlStore *sqlstore.SQLStore) error {
|
||||||
|
// Insert folders
|
||||||
|
offset := 1
|
||||||
|
if errInsert := actest.ConcurrentBatch(actest.Concurrency, folderCount, actest.BatchSize, func(start, end int) error {
|
||||||
|
n := end - start
|
||||||
|
folders := make([]dashboards.Dashboard, 0, n)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for u := start; u < end; u++ {
|
||||||
|
folderID := int64(u + offset)
|
||||||
|
folders = append(folders, dashboards.Dashboard{
|
||||||
|
ID: folderID,
|
||||||
|
UID: fmt.Sprintf("folder%v", folderID),
|
||||||
|
Title: fmt.Sprintf("folder%v", folderID),
|
||||||
|
IsFolder: true,
|
||||||
|
OrgID: 1,
|
||||||
|
Created: now,
|
||||||
|
Updated: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||||
|
if _, err := sess.Insert(folders); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}); errInsert != nil {
|
||||||
|
return errInsert
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert dashboards
|
||||||
|
offset += folderCount
|
||||||
|
if errInsert := actest.ConcurrentBatch(actest.Concurrency, dashboardsPerFolder*folderCount, actest.BatchSize, func(start, end int) error {
|
||||||
|
n := end - start
|
||||||
|
dbs := make([]dashboards.Dashboard, 0, n)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for u := start; u < end; u++ {
|
||||||
|
dashID := int64(u + offset)
|
||||||
|
folderID := int64((u+offset)%folderCount + 1)
|
||||||
|
dbs = append(dbs, dashboards.Dashboard{
|
||||||
|
ID: dashID,
|
||||||
|
UID: fmt.Sprintf("dashboard%v", dashID),
|
||||||
|
Title: fmt.Sprintf("dashboard%v", dashID),
|
||||||
|
IsFolder: false,
|
||||||
|
FolderID: folderID,
|
||||||
|
OrgID: 1,
|
||||||
|
Created: now,
|
||||||
|
Updated: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||||
|
if _, err := sess.Insert(dbs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}); errInsert != nil {
|
||||||
|
return errInsert
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchSearchV2(b *testing.B, folderCount, dashboardsPerFolder int) {
|
||||||
|
searchService, testUser, err := setupBenchEnv(b, folderCount, dashboardsPerFolder)
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
expectedResultCount := (dashboardsPerFolder + 1) * folderCount
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
result := searchService.doDashboardQuery(context.Background(), testUser, 1, DashboardQuery{Limit: expectedResultCount})
|
||||||
|
require.NoError(b, result.Error)
|
||||||
|
require.NotZero(b, len(result.Frames))
|
||||||
|
for _, field := range result.Frames[0].Fields {
|
||||||
|
if field.Name == "uid" {
|
||||||
|
require.Equal(b, expectedResultCount, field.Len())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with some dashboards and some folders
|
||||||
|
func BenchmarkSearchV2_10_10(b *testing.B) {
|
||||||
|
benchSearchV2(b, 10, 10)
|
||||||
|
} // ~0.0002 s/op
|
||||||
|
func BenchmarkSearchV2_10_100(b *testing.B) {
|
||||||
|
benchSearchV2(b, 10, 100)
|
||||||
|
} // ~0.002 s/op
|
||||||
|
|
||||||
|
// Test with many dashboards and only one folder
|
||||||
|
func BenchmarkSearchV2_1_1k(b *testing.B) {
|
||||||
|
benchSearchV2(b, 1, 1000)
|
||||||
|
} // ~0.002 s/op
|
||||||
|
func BenchmarkSearchV2_1_10k(b *testing.B) {
|
||||||
|
benchSearchV2(b, 1, 10000)
|
||||||
|
} // ~0.019 s/op
|
||||||
|
|
||||||
|
// Test with a large number of dashboards and folders
|
||||||
|
func BenchmarkSearchV2_100_100(b *testing.B) {
|
||||||
|
benchSearchV2(b, 100, 100)
|
||||||
|
} // ~0.02 s/op
|
||||||
|
func BenchmarkSearchV2_100_1k(b *testing.B) {
|
||||||
|
benchSearchV2(b, 100, 1000)
|
||||||
|
} // ~0.22 s/op
|
||||||
|
func BenchmarkSearchV2_1k_100(b *testing.B) {
|
||||||
|
benchSearchV2(b, 1000, 100)
|
||||||
|
} // ~0.22 s/op
|
Loading…
Reference in New Issue
Block a user