2023-01-27 09:42:08 -06:00
|
|
|
package searchV2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2023-01-30 02:57:50 -06:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2023-01-27 09:42:08 -06:00
|
|
|
"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/sqlstore"
|
|
|
|
"github.com/grafana/grafana/pkg/services/store"
|
|
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
|
|
)
|
|
|
|
|
|
|
|
// 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}},
|
|
|
|
}
|
|
|
|
searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{},
|
2023-03-20 11:00:14 -05:00
|
|
|
tracing.InitializeTracerForTest(), features, orgSvc, nil, nil).(*StandardSearchService)
|
2023-01-27 09:42:08 -06:00
|
|
|
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
|