add test+benchmark for filter index updates

This commit is contained in:
ionutboangiu
2025-11-04 13:52:52 +02:00
committed by Dan Christian Bogos
parent d7612eb30d
commit d5f49ba1a2

View File

@@ -0,0 +1,472 @@
//go:build performance
/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package general_tests
import (
"flag"
"fmt"
"reflect"
"strconv"
"testing"
"time"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
)
var perf = flag.Bool("perf", false, "check performance")
func TestFilterIndexUpdates(t *testing.T) {
var cfgJSON string
switch *utils.DBType {
case utils.MetaInternal:
cfgJSON = `{
"db": {
"db_conns": {
"*default": {
"db_type": "*internal",
"db_port": 0
}
},
"opts": {
"internalDBDumpInterval": "0s",
"internalDBRewriteInterval": "0s"
}
}
}`
case utils.MetaRedis:
cfgJSON = `{
"db": {
"db_conns": {
"*default": {
"db_type": "*redis",
"db_port": 6379
}
}
}
}`
case utils.MetaMongo:
cfgJSON = `{
"db": {
"db_conns": {
"*default": {
"db_type": "*mongo",
"db_port": 27017
}
}
}
}`
}
cfg, err := config.NewCGRConfigFromJSONStringWithDefaults(cfgJSON)
if err != nil {
t.Fatal(err)
}
dbConnCfg := cfg.DbCfg().DBConns[utils.MetaDefault]
dbConn, err := engine.NewDataDBConn(dbConnCfg.Type,
dbConnCfg.Host, dbConnCfg.Port,
dbConnCfg.Name, dbConnCfg.User,
dbConnCfg.Password, cfg.GeneralCfg().DBDataEncoding,
nil, nil, cfg.DbCfg().Opts, cfg.DbCfg().Items)
if err != nil {
t.Fatal(err)
}
defer dbConn.Close()
if err := dbConn.Flush(""); err != nil {
t.Fatal(err)
}
dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dbConn}, cfg.DbCfg())
dm := engine.NewDataManager(dbCM, cfg, nil)
checkIndexes := func(want map[string]utils.StringSet) {
t.Helper()
if *perf {
before := time.Now()
defer func() {
t.Helper()
dur := time.Since(before)
t.Logf("checkIndexes() took %s", dur)
}()
}
indexes, err := dm.GetIndexes(context.Background(),
utils.CacheAttributeFilterIndexes, "cgrates.org",
"", utils.NonTransactional, true, false)
if err != nil {
if len(want) == 0 && err == utils.ErrNotFound {
return
}
t.Fatalf("dm.GetIndexes unexpected error: %v", err)
}
if !reflect.DeepEqual(want, indexes) {
t.Errorf("dm.GetIndexes = %s, want %s",
utils.ToJSON(indexes), utils.ToJSON(want))
}
}
setFilter := func(id, typ string, values ...string) {
t.Helper()
if *perf {
before := time.Now()
defer func() {
t.Helper()
dur := time.Since(before)
t.Logf("setFilter(%q) took %s", id, dur)
}()
}
fltr := &engine.Filter{
Tenant: "cgrates.org",
ID: id,
Rules: []*engine.FilterRule{
{
Type: typ,
Element: "~*req.Destination",
Values: values,
},
},
}
if err := dm.SetFilter(context.Background(), fltr, true); err != nil {
t.Fatal(err)
}
}
setProfiles := func(startIdx, prflCount int, fltrIDs ...string) {
t.Helper()
if *perf {
before := time.Now()
defer func() {
t.Helper()
dur := time.Since(before)
t.Logf("setProfiles(%d, %d, %q) took %s",
startIdx, prflCount, fltrIDs, dur)
}()
}
for i := range prflCount {
ap := &utils.AttributeProfile{
Tenant: "cgrates.org",
ID: fmt.Sprintf("test%d", startIdx+i),
FilterIDs: fltrIDs,
Attributes: []*utils.Attribute{
{
Path: "*req.Password",
Type: utils.MetaConstant,
Value: utils.NewRSRParsersMustCompile("CGRateS.org", utils.InfieldSep),
},
},
}
if err := dm.SetAttributeProfile(context.Background(), ap, true); err != nil {
t.Fatal(err)
}
}
}
removeProfiles := func(startIdx, prflCount int) {
t.Helper()
if *perf {
before := time.Now()
defer func() {
t.Helper()
dur := time.Since(before)
t.Logf("removeProfiles(%d, %d) took %s", startIdx, prflCount, dur)
}()
}
for i := range prflCount {
if err := dm.RemoveAttributeProfile(context.Background(), "cgrates.org",
fmt.Sprintf("test%d", startIdx+i), true); err != nil {
t.Fatal(err)
}
}
}
setFilter("FLTR1", utils.MetaString, "1001", "1002")
setProfiles(0, 1, "FLTR1")
checkIndexes(map[string]utils.StringSet{
"*string:*req.Destination:1001": {
"test0": {},
},
"*string:*req.Destination:1002": {
"test0": {},
},
})
setProfiles(0, 2, "*string:~*req.Account:2001|2002", "FLTR1")
checkIndexes(map[string]utils.StringSet{
"*string:*req.Account:2001": {
"test0": {},
"test1": {},
},
"*string:*req.Account:2002": {
"test0": {},
"test1": {},
},
"*string:*req.Destination:1001": {
"test0": {},
"test1": {},
},
"*string:*req.Destination:1002": {
"test0": {},
"test1": {},
},
})
setProfiles(0, 2, "FLTR1")
checkIndexes(map[string]utils.StringSet{
"*string:*req.Destination:1001": {
"test0": {},
"test1": {},
},
"*string:*req.Destination:1002": {
"test0": {},
"test1": {},
},
})
setFilter("FLTR2", utils.MetaPrefix, "1003", "1004")
setProfiles(0, 2, "FLTR2")
checkIndexes(map[string]utils.StringSet{
"*prefix:*req.Destination:1003": {
"test0": {},
"test1": {},
},
"*prefix:*req.Destination:1004": {
"test0": {},
"test1": {},
},
})
setFilter("FLTR2", utils.MetaPrefix, "1004", "1005")
checkIndexes(map[string]utils.StringSet{
"*prefix:*req.Destination:1004": {
"test0": {},
"test1": {},
},
"*prefix:*req.Destination:1005": {
"test0": {},
"test1": {},
},
})
setFilter("FLTR2", utils.MetaString, "1005", "1006")
checkIndexes(map[string]utils.StringSet{
"*string:*req.Destination:1005": {
"test0": {},
"test1": {},
},
"*string:*req.Destination:1006": {
"test0": {},
"test1": {},
},
})
removeProfiles(0, 1) // Remove test0
checkIndexes(map[string]utils.StringSet{
"*string:*req.Destination:1005": {
"test1": {},
},
"*string:*req.Destination:1006": {
"test1": {},
},
})
removeProfiles(1, 1) // Remove test1
checkIndexes(map[string]utils.StringSet{})
}
func BenchmarkFilterIndexUpdates(b *testing.B) {
cases := []struct {
name string
profiles int
initialValues int
finalValues []string
}{
{"Remove1_1P_10V", 1, 10, generateValues(0, 9)}, // Keep values 0-8 (remove value 9)
{"Remove5_1P_10V", 1, 10, generateValues(0, 5)}, // Keep values 0-4 (remove values 5-9)
{"Remove9_1P_10V", 1, 10, generateValues(0, 1)}, // Keep value 0 (remove values 1-9)
{"Add1_1P_10V", 1, 10, generateValues(0, 11)}, // Keep values 0-9, add value 10
{"Add5_1P_10V", 1, 10, generateValues(0, 15)}, // Keep values 0-9, add values 10-14
{"Remove1_10P_100V", 10, 100, generateValues(0, 99)}, // Keep values 0-98 (remove value 99)
{"Remove10_10P_100V", 10, 100, generateValues(0, 90)}, // Keep values 0-89 (remove values 90-99)
{"Remove50_10P_100V", 10, 100, generateValues(0, 50)}, // Keep values 0-49 (remove values 50-99)
{"Remove90_10P_100V", 10, 100, generateValues(0, 10)}, // Keep values 0-9 (remove values 10-99)
{"Add1_10P_100V", 10, 100, generateValues(0, 101)}, // Keep values 0-99, add value 100
{"Add10_10P_100V", 10, 100, generateValues(0, 110)}, // Keep values 0-99, add values 100-109
{"Add50_10P_100V", 10, 100, generateValues(0, 150)}, // Keep values 0-99, add values 100-149
{"Remove1_100P_500V", 100, 500, generateValues(0, 499)}, // Keep values 0-498 (remove value 499)
{"Remove50_100P_500V", 100, 500, generateValues(0, 450)}, // Keep values 0-449 (remove values 450-499)
{"Remove450_100P_500V", 100, 500, generateValues(0, 50)}, // Keep values 0-49 (remove values 50-499)
{"Add1_100P_500V", 100, 500, generateValues(0, 501)}, // Keep values 0-499, add value 500
{"Add50_100P_500V", 100, 500, generateValues(0, 550)}, // Keep values 0-499, add values 500-549
{"Add100_100P_500V", 100, 500, generateValues(0, 600)}, // Keep values 0-499, add values 500-599
{"Remove990_10P_1000V", 10, 1000, generateValues(0, 10)}, // Keep values 0-9 (remove values 10-999)
{"Remove495_50P_500V", 50, 500, generateValues(0, 5)}, // Keep values 0-4 (remove values 5-499)
{"Remove198_100P_200V", 100, 200, generateValues(0, 2)}, // Keep values 0-1 (remove values 2-199)
{"Add990_10P_10V", 10, 10, generateValues(0, 1000)}, // Keep values 0-9, add values 10-999
{"Add495_50P_5V", 50, 5, generateValues(0, 500)}, // Keep values 0-4, add values 5-499
{"Replace1_1P_10V", 1, 10,
append(generateValues(0, 9), generateValues(1000, 1001)...)}, // Keep 0-8, replace 9 with 1000
{"Replace5_10P_100V", 10, 100,
append(generateValues(0, 95), generateValues(1000, 1005)...)}, // Keep 0-94, replace 95-99 with 1000-1004
{"Replace50_100P_500V", 100, 500,
append(generateValues(0, 450), generateValues(1000, 1050)...)}, // Keep 0-449, replace 450-499 with 1000-1049
{"Replace400_100P_500V", 100, 500,
append(generateValues(0, 100), generateValues(1000, 1400)...)}, // Keep 0-99, replace 100-499 with 1000-1399
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
benchmarkFilterUpdate(b, tc.profiles, tc.initialValues, tc.finalValues)
})
}
}
func benchmarkFilterUpdate(b *testing.B, profileCount, initialValueCount int, finalValues []string) {
var cfgJSON string
switch *utils.DBType {
case utils.MetaInternal:
cfgJSON = `{
"db": {
"db_conns": {
"*default": {
"db_type": "*internal",
"db_port": 0
}
},
"opts": {
"internalDBDumpInterval": "0s",
"internalDBRewriteInterval": "0s"
}
}
}`
case utils.MetaRedis:
cfgJSON = `{
"db": {
"db_conns": {
"*default": {
"db_type": "*redis",
"db_port": 6379
}
}
}
}`
case utils.MetaMongo:
cfgJSON = `{
"db": {
"db_conns": {
"*default": {
"db_type": "*mongo",
"db_port": 27017
}
}
}
}`
}
cfg, err := config.NewCGRConfigFromJSONStringWithDefaults(cfgJSON)
if err != nil {
b.Fatal(err)
}
dbConnCfg := cfg.DbCfg().DBConns[utils.MetaDefault]
dbConn, err := engine.NewDataDBConn(dbConnCfg.Type,
dbConnCfg.Host, dbConnCfg.Port,
dbConnCfg.Name, dbConnCfg.User,
dbConnCfg.Password, cfg.GeneralCfg().DBDataEncoding,
nil, nil, cfg.DbCfg().Opts, cfg.DbCfg().Items)
if err != nil {
b.Fatal(err)
}
defer dbConn.Close()
if err := dbConn.Flush(""); err != nil {
b.Fatal(err)
}
dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dbConn}, cfg.DbCfg())
dm := engine.NewDataManager(dbCM, cfg, nil)
initialValues := generateValues(0, initialValueCount)
initialFilter := &engine.Filter{
Tenant: "cgrates.org",
ID: "BENCH_FLTR",
Rules: []*engine.FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Destination",
Values: initialValues,
},
},
}
if err := dm.SetFilter(context.Background(), initialFilter, true); err != nil {
b.Fatal(err)
}
for i := range profileCount {
profile := &utils.AttributeProfile{
Tenant: "cgrates.org",
ID: fmt.Sprintf("bench_test%d", i),
FilterIDs: []string{"BENCH_FLTR"},
Attributes: []*utils.Attribute{
{
Path: "*req.Password",
Type: utils.MetaConstant,
Value: utils.NewRSRParsersMustCompile("CGRateS.org", utils.InfieldSep),
},
},
}
if err := dm.SetAttributeProfile(context.Background(), profile, true); err != nil {
b.Fatal(err)
}
}
updateFilter := &engine.Filter{
Tenant: "cgrates.org",
ID: "BENCH_FLTR",
Rules: []*engine.FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Destination",
Values: finalValues,
},
},
}
for b.Loop() {
b.StopTimer()
if err := dm.SetFilter(context.Background(), initialFilter, true); err != nil {
b.Fatal(err)
}
b.StartTimer()
if err := dm.SetFilter(context.Background(), updateFilter, true); err != nil {
b.Fatal(err)
}
}
}
func generateValues(start, end int) []string {
values := make([]string, 0, end-start)
for i := start; i < end; i++ {
values = append(values, strconv.Itoa(i))
}
return values
}