The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/api/folder_bench_test.go

516 lines
16 KiB

package api
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/star/startest"
"github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
LEVEL0_FOLDER_NUM = 300
LEVEL1_FOLDER_NUM = 30
LEVEL2_FOLDER_NUM = 5
LEVEL0_DASHBOARD_NUM = 300
LEVEL1_DASHBOARD_NUM = 30
LEVEL2_DASHBOARD_NUM = 5
TEAM_NUM = 50
TEAM_MEMBER_NUM = 5
MAXIMUM_INT_POSTGRES = 2147483647
)
type benchScenario struct {
db *sqlstore.SQLStore
// signedInUser is the user that is signed in to the server
cfg *setting.Cfg
signedInUser *user.SignedInUser
teamSvc team.Service
userSvc user.Service
}
func BenchmarkFolderListAndSearch(b *testing.B) {
start := time.Now()
b.Log("setup start")
sc := setupDB(b)
b.Log("setup time:", time.Since(start))
all := LEVEL0_FOLDER_NUM*LEVEL0_DASHBOARD_NUM + LEVEL0_FOLDER_NUM*LEVEL1_FOLDER_NUM*LEVEL1_DASHBOARD_NUM + LEVEL0_FOLDER_NUM*LEVEL1_FOLDER_NUM*LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM
// the maximum number of dashboards that can be returned by the search API
// otherwise the handler fails with 422 status code
const limit = 5000
withLimit := func(res int) int {
if res > limit {
return limit
}
return res
}
benchmarks := []struct {
desc string
url string
expectedLen int
features *featuremgmt.FeatureManager
}{
{
desc: "get root folders with nested folders feature enabled",
url: "/api/folders",
expectedLen: LEVEL0_FOLDER_NUM,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "get subfolders with nested folders feature enabled",
url: "/api/folders?parentUid=folder0",
expectedLen: LEVEL1_FOLDER_NUM,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "list all inherited dashboards with nested folders feature enabled",
url: "/api/search?type=dash-db&limit=5000",
expectedLen: withLimit(all),
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "search for pattern with nested folders feature enabled",
url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "search for specific dashboard nested folders feature enabled",
url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
expectedLen: 1,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "get root folders with nested folders feature disabled",
url: "/api/folders?limit=5000",
expectedLen: withLimit(LEVEL0_FOLDER_NUM),
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "list all dashboards with nested folders feature disabled",
url: "/api/search?type=dash-db&limit=5000",
expectedLen: withLimit(LEVEL0_FOLDER_NUM * LEVEL0_DASHBOARD_NUM),
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "search specific dashboard with nested folders feature disabled",
url: "/api/search?type=dash-db&query=dashboard_0_0",
expectedLen: 1,
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
}
for _, bm := range benchmarks {
b.Run(bm.desc, func(b *testing.B) {
m := setupServer(b, sc, bm.features)
req := httptest.NewRequest(http.MethodGet, bm.url, nil)
req = webtest.RequestWithSignedInUser(req, sc.signedInUser)
b.ResetTimer()
for i := 0; i < b.N; i++ {
rec := httptest.NewRecorder()
m.ServeHTTP(rec, req)
require.Equal(b, 200, rec.Code)
var resp []dtos.FolderSearchHit
err := json.Unmarshal(rec.Body.Bytes(), &resp)
require.NoError(b, err)
assert.Len(b, resp, bm.expectedLen)
}
})
}
}
func setupDB(b testing.TB) benchScenario {
b.Helper()
db := sqlstore.InitTestDB(b)
IDs := map[int64]struct{}{}
opts := sqlstore.NativeSettingsForDialect(db.GetDialect())
quotaService := quotatest.New(false, nil)
cfg := setting.NewCfg()
teamSvc := teamimpl.ProvideService(db, cfg)
orgService, err := orgimpl.ProvideService(db, cfg, quotaService)
require.NoError(b, err)
cache := localcache.ProvideService()
userSvc, err := userimpl.ProvideService(db, orgService, cfg, teamSvc, cache, &quotatest.FakeQuotaService{}, bundleregistry.ProvideService())
require.NoError(b, err)
var orgID int64 = 1
userIDs := make([]int64, 0, TEAM_MEMBER_NUM)
for i := 0; i < TEAM_MEMBER_NUM; i++ {
u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{
OrgID: orgID,
Login: fmt.Sprintf("user%d", i),
})
require.NoError(b, err)
require.NotZero(b, u.ID)
userIDs = append(userIDs, u.ID)
}
signedInUser := user.SignedInUser{UserID: userIDs[0], OrgID: orgID, Permissions: map[int64]map[string][]string{
orgID: {dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
}}
now := time.Now()
roles := make([]accesscontrol.Role, 0, TEAM_NUM)
teams := make([]team.Team, 0, TEAM_NUM)
teamMembers := make([]team.TeamMember, 0, TEAM_MEMBER_NUM)
teamRoles := make([]accesscontrol.TeamRole, 0, TEAM_NUM)
for i := 1; i < TEAM_NUM+1; i++ {
teamID := int64(i)
teams = append(teams, team.Team{
UID: fmt.Sprintf("team%d", i),
ID: teamID,
Name: fmt.Sprintf("team%d", i),
OrgID: orgID,
Created: now,
Updated: now,
})
signedInUser.Teams = append(signedInUser.Teams, teamID)
for _, userID := range userIDs {
teamMembers = append(teamMembers, team.TeamMember{
UserID: userID,
TeamID: teamID,
OrgID: orgID,
Permission: dashboards.PERMISSION_VIEW,
Created: now,
Updated: now,
})
}
name := fmt.Sprintf("managed_team_role_%d", i)
roles = append(roles, accesscontrol.Role{
ID: int64(i),
UID: name,
OrgID: orgID,
Name: name,
Updated: now,
Created: now,
})
teamRoles = append(teamRoles, accesscontrol.TeamRole{
RoleID: int64(i),
OrgID: orgID,
TeamID: teamID,
Created: now,
})
}
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
_, err := sess.BulkInsert("team", teams, opts)
require.NoError(b, err)
_, err = sess.BulkInsert("team_member", teamMembers, opts)
require.NoError(b, err)
_, err = sess.BulkInsert("role", roles, opts)
require.NoError(b, err)
_, err = sess.BulkInsert("team_role", teamRoles, opts)
return err
})
require.NoError(b, err)
foldersCap := LEVEL0_FOLDER_NUM + LEVEL0_FOLDER_NUM*LEVEL1_FOLDER_NUM + LEVEL0_FOLDER_NUM*LEVEL1_FOLDER_NUM*LEVEL2_FOLDER_NUM
folders := make([]*f, 0, foldersCap)
dashsCap := LEVEL0_FOLDER_NUM * LEVEL1_FOLDER_NUM * LEVEL2_FOLDER_NUM * LEVEL2_DASHBOARD_NUM
dashs := make([]*dashboards.Dashboard, 0, foldersCap+dashsCap)
dashTags := make([]*dashboardTag, 0, dashsCap)
permissions := make([]accesscontrol.Permission, 0, foldersCap*2)
for i := 0; i < LEVEL0_FOLDER_NUM; i++ {
f0, d := addFolder(orgID, generateID(IDs), fmt.Sprintf("folder%d", i), nil)
folders = append(folders, f0)
dashs = append(dashs, d)
roleID := int64(i%TEAM_NUM + 1)
permissions = append(permissions, accesscontrol.Permission{
RoleID: roleID,
Action: dashboards.ActionFoldersRead,
Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(f0.UID),
Updated: now,
Created: now,
},
accesscontrol.Permission{
RoleID: roleID,
Action: dashboards.ActionDashboardsRead,
Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(f0.UID),
Updated: now,
Created: now,
},
)
signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = append(signedInUser.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f0.UID))
signedInUser.Permissions[orgID][dashboards.ActionDashboardsRead] = append(signedInUser.Permissions[orgID][dashboards.ActionDashboardsRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f0.UID))
for j := 0; j < LEVEL0_DASHBOARD_NUM; j++ {
str := fmt.Sprintf("dashboard_%d_%d", i, j)
dashID := generateID(IDs)
dashs = append(dashs, &dashboards.Dashboard{
ID: dashID,
OrgID: signedInUser.OrgID,
IsFolder: false,
UID: str,
FolderID: f0.ID,
Slug: str,
Title: str,
Data: simplejson.New(),
Created: now,
Updated: now,
})
dashTags = append(dashTags, &dashboardTag{
DashboardId: dashID,
Term: fmt.Sprintf("tag%d", j),
})
}
for j := 0; j < LEVEL1_FOLDER_NUM; j++ {
f1, d1 := addFolder(orgID, generateID(IDs), fmt.Sprintf("folder%d_%d", i, j), &f0.UID)
folders = append(folders, f1)
dashs = append(dashs, d1)
for k := 0; k < LEVEL1_DASHBOARD_NUM; k++ {
str := fmt.Sprintf("dashboard_%d_%d_%d", i, j, k)
dashID := generateID(IDs)
dashs = append(dashs, &dashboards.Dashboard{
ID: dashID,
OrgID: signedInUser.OrgID,
IsFolder: false,
UID: str,
FolderID: f1.ID,
Slug: str,
Title: str,
Data: simplejson.New(),
Created: now,
Updated: now,
})
dashTags = append(dashTags, &dashboardTag{
DashboardId: dashID,
Term: fmt.Sprintf("tag%d", k),
})
}
for k := 0; k < LEVEL2_FOLDER_NUM; k++ {
f2, d2 := addFolder(orgID, generateID(IDs), fmt.Sprintf("folder%d_%d_%d", i, j, k), &f1.UID)
folders = append(folders, f2)
dashs = append(dashs, d2)
for l := 0; l < LEVEL2_DASHBOARD_NUM; l++ {
str := fmt.Sprintf("dashboard_%d_%d_%d_%d", i, j, k, l)
dashID := generateID(IDs)
dashs = append(dashs, &dashboards.Dashboard{
ID: dashID,
OrgID: signedInUser.OrgID,
IsFolder: false,
UID: str,
FolderID: f2.ID,
Slug: str,
Title: str,
Data: simplejson.New(),
Created: now,
Updated: now,
})
dashTags = append(dashTags, &dashboardTag{
DashboardId: dashID,
Term: fmt.Sprintf("tag%d", l),
})
}
}
}
}
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
_, err := sess.BulkInsert("folder", folders, opts)
require.NoError(b, err)
_, err = sess.BulkInsert("dashboard", dashs, opts)
require.NoError(b, err)
_, err = sess.BulkInsert("permission", permissions, opts)
require.NoError(b, err)
_, err = sess.BulkInsert("dashboard_tag", dashTags, opts)
return err
})
require.NoError(b, err)
return benchScenario{
db: db,
cfg: cfg,
signedInUser: &signedInUser,
teamSvc: teamSvc,
userSvc: userSvc,
}
}
func setupServer(b testing.TB, sc benchScenario, features *featuremgmt.FeatureManager) *web.Macaron {
b.Helper()
m := web.New()
initCtx := &contextmodel.ReqContext{}
m.Use(func(c *web.Context) {
initCtx.Context = c
initCtx.Logger = log.New("api-test")
initCtx.SignedInUser = sc.signedInUser
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), initCtx))
})
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
acSvc := acimpl.ProvideOSSService(sc.cfg, acdb.ProvideService(sc.db), localcache.ProvideService(), features)
quotaSrv := quotatest.New(false, nil)
dashStore, err := database.ProvideDashboardStore(sc.db, sc.db.Cfg, features, tagimpl.ProvideService(sc.db, sc.db.Cfg), quotaSrv)
require.NoError(b, err)
folderStore := folderimpl.ProvideDashboardFolderStore(sc.db)
ac := acimpl.ProvideAccessControl(sc.cfg)
folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sc.cfg, dashStore, folderStore, sc.db, features)
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc)
require.NoError(b, err)
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc)
require.NoError(b, err)
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
sc.cfg, dashStore, folderStore, nil,
features, folderPermissions, dashboardPermissions, ac,
folderServiceWithFlagOn,
)
require.NoError(b, err)
starSvc := startest.NewStarServiceFake()
starSvc.ExpectedUserStars = &star.GetUserStarsResult{UserStars: make(map[int64]bool)}
hs := &HTTPServer{
CacheService: localcache.New(5*time.Minute, 10*time.Minute),
Cfg: sc.cfg,
SQLStore: sc.db,
Features: features,
QuotaService: quotaSrv,
SearchService: search.ProvideService(sc.cfg, sc.db, starSvc, dashboardSvc),
folderService: folderServiceWithFlagOn,
DashboardService: dashboardSvc,
}
hs.AccessControl = acimpl.ProvideAccessControl(hs.Cfg)
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService)
m.Get("/api/folders", hs.GetFolders)
m.Get("/api/search", hs.Search)
return m
}
type f struct {
ID int64 `xorm:"pk autoincr 'id'"`
OrgID int64 `xorm:"org_id"`
UID string `xorm:"uid"`
ParentUID *string `xorm:"parent_uid"`
Title string
Description string
Created time.Time
Updated time.Time
}
func (f *f) TableName() string {
return "folder"
}
// SQL bean helper to save tags
type dashboardTag struct {
Id int64
DashboardId int64
Term string
}
func addFolder(orgID int64, id int64, uid string, parentUID *string) (*f, *dashboards.Dashboard) {
now := time.Now()
title := uid
f := &f{
OrgID: orgID,
UID: uid,
Title: title,
ID: id,
Created: now,
Updated: now,
ParentUID: parentUID,
}
d := &dashboards.Dashboard{
ID: id,
OrgID: orgID,
UID: uid,
Version: 1,
Title: title,
Data: simplejson.NewFromAny(map[string]any{"schemaVersion": 17, "title": title, "uid": uid, "version": 1}),
IsFolder: true,
Created: now,
Updated: now,
}
return f, d
}
func generateID(reserved map[int64]struct{}) int64 {
n := rand.Int63n(MAXIMUM_INT_POSTGRES)
if _, existing := reserved[n]; existing {
return generateID(reserved)
}
reserved[n] = struct{}{}
return n
}