diff --git a/benchmarks/gluon_bench/flags/store_benchmarks.go b/benchmarks/gluon_bench/flags/store_benchmarks.go new file mode 100644 index 00000000..2fb062c1 --- /dev/null +++ b/benchmarks/gluon_bench/flags/store_benchmarks.go @@ -0,0 +1,8 @@ +package flags + +import "flag" + +var Store = flag.String("store", "default", "Name of the storage implementation to benchmark. Defaults to regular on disk storage by default.") +var StoreWorkers = flag.Uint("store-workers", 1, "Number of concurrent workers for store operations.") +var StoreItemCount = flag.Uint("store-item-count", 1000, "Number of items to generate in the store benchmarks.") +var StoreItemSize = flag.Uint("store-item-size", 15*1024*1024, "Number of items to generate in the store benchmarks.") diff --git a/benchmarks/gluon_bench/imap_benchmarks/imap_benchmark.go b/benchmarks/gluon_bench/imap_benchmarks/imap_benchmark.go index f830a9d5..57f26186 100644 --- a/benchmarks/gluon_bench/imap_benchmarks/imap_benchmark.go +++ b/benchmarks/gluon_bench/imap_benchmarks/imap_benchmark.go @@ -3,11 +3,12 @@ package imap_benchmarks import ( "context" "fmt" - "github.com/ProtonMail/gluon/benchmarks/gluon_bench/reporter" - "github.com/ProtonMail/gluon/profiling" "net" "strings" "time" + + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/reporter" + "github.com/ProtonMail/gluon/profiling" ) // IMAPBenchmark is intended to be used to build benchmarks which bench IMAP commands on a given server. diff --git a/benchmarks/gluon_bench/main.go b/benchmarks/gluon_bench/main.go index bfa2f71a..aabffb70 100644 --- a/benchmarks/gluon_bench/main.go +++ b/benchmarks/gluon_bench/main.go @@ -4,6 +4,7 @@ import ( "github.com/ProtonMail/gluon/benchmarks/gluon_bench/benchmark" _ "github.com/ProtonMail/gluon/benchmarks/gluon_bench/gluon_benchmarks" _ "github.com/ProtonMail/gluon/benchmarks/gluon_bench/imap_benchmarks" + _ "github.com/ProtonMail/gluon/benchmarks/gluon_bench/store_benchmarks" ) func main() { diff --git a/benchmarks/gluon_bench/store_benchmarks/create.go b/benchmarks/gluon_bench/store_benchmarks/create.go new file mode 100644 index 00000000..03ab8dbe --- /dev/null +++ b/benchmarks/gluon_bench/store_benchmarks/create.go @@ -0,0 +1,51 @@ +package store_benchmarks + +import ( + "context" + "math/rand" + + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/benchmark" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/flags" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/reporter" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/utils" + "github.com/ProtonMail/gluon/store" + "github.com/google/uuid" +) + +type Create struct{} + +func (*Create) Name() string { + return "store-create" +} + +func (*Create) Setup(ctx context.Context, store store.Store) error { + return nil +} + +func (*Create) TearDown(ctx context.Context, store store.Store) error { + return nil +} + +func (*Create) Run(ctx context.Context, st store.Store) (*reporter.BenchmarkRun, error) { + return RunStoreWorkers(ctx, st, func(ctx context.Context, s store.Store, dc *utils.DurationCollector, u uint) error { + messages := []string{utils.MessageAfterNoonMeeting, utils.MessageMultiPartMixed, utils.MessageEmbedded} + messagesLen := len(messages) + + for i := uint(0); i < *flags.StoreItemCount; i++ { + dc.Start() + err := s.Set(uuid.NewString(), []byte(messages[rand.Intn(messagesLen)])) + dc.Stop() + + if err != nil { + return err + } + + } + + return nil + }), nil +} + +func init() { + benchmark.RegisterBenchmark(NewStoreBenchmarkRunner(&Create{})) +} diff --git a/benchmarks/gluon_bench/store_benchmarks/default_store.go b/benchmarks/gluon_bench/store_benchmarks/default_store.go new file mode 100644 index 00000000..b630d561 --- /dev/null +++ b/benchmarks/gluon_bench/store_benchmarks/default_store.go @@ -0,0 +1,16 @@ +package store_benchmarks + +import ( + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/flags" + "github.com/ProtonMail/gluon/store" +) + +type DefaultStoreBuilder struct{} + +func (*DefaultStoreBuilder) New(path string) (store.Store, error) { + return store.NewOnDiskStore(path, []byte(*flags.UserPassword)) +} + +func init() { + RegisterStoreBuilder("default", &DefaultStoreBuilder{}) +} diff --git a/benchmarks/gluon_bench/store_benchmarks/delete.go b/benchmarks/gluon_bench/store_benchmarks/delete.go new file mode 100644 index 00000000..233f30ae --- /dev/null +++ b/benchmarks/gluon_bench/store_benchmarks/delete.go @@ -0,0 +1,54 @@ +package store_benchmarks + +import ( + "context" + + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/benchmark" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/flags" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/reporter" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/utils" + "github.com/ProtonMail/gluon/store" +) + +type Delete struct { + uuids []string +} + +func (*Delete) Name() string { + return "store-delete" +} + +func (d *Delete) Setup(ctx context.Context, s store.Store) error { + uuids, err := CreateRandomState(s, *flags.StoreItemCount) + if err != nil { + return err + } + + d.uuids = uuids + + return nil +} + +func (*Delete) TearDown(ctx context.Context, store store.Store) error { + return nil +} + +func (d *Delete) Run(ctx context.Context, st store.Store) (*reporter.BenchmarkRun, error) { + return RunStoreWorkersSplitRange(ctx, st, uint(len(d.uuids)), func(ctx context.Context, s store.Store, dc *utils.DurationCollector, start, end uint) error { + for i := start; i < end; i++ { + dc.Start() + err := s.Delete(d.uuids[i]) + dc.Stop() + + if err != nil { + panic(err) + } + } + + return nil + }), nil +} + +func init() { + benchmark.RegisterBenchmark(NewStoreBenchmarkRunner(&Delete{})) +} diff --git a/benchmarks/gluon_bench/store_benchmarks/get.go b/benchmarks/gluon_bench/store_benchmarks/get.go new file mode 100644 index 00000000..2fe86951 --- /dev/null +++ b/benchmarks/gluon_bench/store_benchmarks/get.go @@ -0,0 +1,60 @@ +package store_benchmarks + +import ( + "context" + "math/rand" + + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/benchmark" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/flags" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/reporter" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/utils" + "github.com/ProtonMail/gluon/store" +) + +type Get struct { + uuids []string +} + +func (*Get) Name() string { + return "store-get" +} + +func (g *Get) Setup(ctx context.Context, s store.Store) error { + uuids, err := CreateRandomState(s, *flags.StoreItemCount) + if err != nil { + return err + } + + g.uuids = uuids + + return nil +} + +func (*Get) TearDown(ctx context.Context, store store.Store) error { + return nil +} + +func (g *Get) Run(ctx context.Context, st store.Store) (*reporter.BenchmarkRun, error) { + uuidLen := len(g.uuids) + + return RunStoreWorkers(ctx, st, func(ctx context.Context, s store.Store, dc *utils.DurationCollector, u uint) error { + for i := 0; i < uuidLen; i++ { + index := rand.Intn(uuidLen) + + dc.Start() + _, err := s.Get(g.uuids[index]) + dc.Stop() + + if err != nil { + panic(err) + } + + } + + return nil + }), nil +} + +func init() { + benchmark.RegisterBenchmark(NewStoreBenchmarkRunner(&Get{})) +} diff --git a/benchmarks/gluon_bench/store_benchmarks/rename.go b/benchmarks/gluon_bench/store_benchmarks/rename.go new file mode 100644 index 00000000..347b5150 --- /dev/null +++ b/benchmarks/gluon_bench/store_benchmarks/rename.go @@ -0,0 +1,61 @@ +package store_benchmarks + +import ( + "context" + + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/benchmark" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/flags" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/reporter" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/utils" + "github.com/ProtonMail/gluon/store" + "github.com/google/uuid" +) + +type Rename struct { + uuids []string + uuidsRenamed []string +} + +func (*Rename) Name() string { + return "store-rename" +} + +func (r *Rename) Setup(ctx context.Context, s store.Store) error { + uuids, err := CreateRandomState(s, *flags.StoreItemCount) + if err != nil { + return err + } + + r.uuids = uuids + r.uuidsRenamed = make([]string, len(r.uuids)) + + for i := 0; i < len(r.uuids); i++ { + r.uuidsRenamed[i] = uuid.NewString() + } + + return nil +} + +func (*Rename) TearDown(ctx context.Context, store store.Store) error { + return nil +} + +func (r *Rename) Run(ctx context.Context, st store.Store) (*reporter.BenchmarkRun, error) { + return RunStoreWorkersSplitRange(ctx, st, uint(len(r.uuids)), func(ctx context.Context, s store.Store, dc *utils.DurationCollector, start, end uint) error { + for i := start; i < end; i++ { + dc.Start() + err := s.Update(r.uuids[i], r.uuidsRenamed[i]) + dc.Stop() + + if err != nil { + panic(err) + } + } + + return nil + }), nil +} + +func init() { + benchmark.RegisterBenchmark(NewStoreBenchmarkRunner(&Rename{})) +} diff --git a/benchmarks/gluon_bench/store_benchmarks/store_benchmark.go b/benchmarks/gluon_bench/store_benchmarks/store_benchmark.go new file mode 100644 index 00000000..54f738d0 --- /dev/null +++ b/benchmarks/gluon_bench/store_benchmarks/store_benchmark.go @@ -0,0 +1,81 @@ +package store_benchmarks + +import ( + "context" + "os" + + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/benchmark" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/flags" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/reporter" + "github.com/ProtonMail/gluon/store" +) + +type StoreBenchmark interface { + // Name returns benchmark's name. + Name() string + + // Setup should prepare the benchmark. + Setup(ctx context.Context, store store.Store) error + + // TearDown should clean the benchmark. + TearDown(ctx context.Context, store store.Store) error + + // Run the benchmark. + Run(ctx context.Context, store store.Store) (*reporter.BenchmarkRun, error) +} + +type StoreBenchmarkRunner struct { + benchmark StoreBenchmark + benchmarkDir string + store store.Store +} + +func (s *StoreBenchmarkRunner) Name() string { + return s.benchmark.Name() +} + +func (s *StoreBenchmarkRunner) Setup(ctx context.Context, benchmarkDir string) error { + store, err := NewStore(*flags.Store, benchmarkDir) + if err != nil { + return err + } + + s.store = store + s.benchmarkDir = benchmarkDir + + if err := s.benchmark.Setup(ctx, s.store); err != nil { + return err + } + + return nil +} + +func (s *StoreBenchmarkRunner) Run(ctx context.Context) (*reporter.BenchmarkRun, error) { + benchRuns, err := s.benchmark.Run(ctx, s.store) + + if err != nil { + return nil, err + } + + return benchRuns, nil +} + +func (s *StoreBenchmarkRunner) TearDown(ctx context.Context) error { + if err := s.benchmark.TearDown(ctx, s.store); err != nil { + return err + } + + if err := s.store.Close(); err != nil { + return err + } + + if err := os.RemoveAll(s.benchmarkDir); err != nil { + return err + } + + return nil +} + +func NewStoreBenchmarkRunner(bench StoreBenchmark) benchmark.Benchmark { + return &StoreBenchmarkRunner{benchmark: bench} +} diff --git a/benchmarks/gluon_bench/store_benchmarks/store_factory.go b/benchmarks/gluon_bench/store_benchmarks/store_factory.go new file mode 100644 index 00000000..96765942 --- /dev/null +++ b/benchmarks/gluon_bench/store_benchmarks/store_factory.go @@ -0,0 +1,51 @@ +package store_benchmarks + +import ( + "fmt" + + "github.com/ProtonMail/gluon/store" +) + +type StoreBuilder interface { + New(path string) (store.Store, error) +} + +type storeFactory struct { + builders map[string]StoreBuilder +} + +func newStoreFactory() *storeFactory { + return &storeFactory{builders: make(map[string]StoreBuilder)} +} + +func (sf *storeFactory) Register(name string, builder StoreBuilder) error { + if _, ok := sf.builders[name]; ok { + return fmt.Errorf("builder already exists") + } + + sf.builders[name] = builder + + return nil +} + +func (sf *storeFactory) New(name, path string) (store.Store, error) { + builder, ok := sf.builders[name] + + if !ok { + return nil, fmt.Errorf("no such builder exists") + } + + return builder.New(path) +} + +var storeFactoryInstance = newStoreFactory() + +func RegisterStoreBuilder(name string, storeBuilder StoreBuilder) { + if err := storeFactoryInstance.Register(name, storeBuilder); err != nil { + panic(err) + } +} + +func NewStore(name, path string) (store.Store, error) { + return storeFactoryInstance.New(name, path) +} diff --git a/benchmarks/gluon_bench/store_benchmarks/utils.go b/benchmarks/gluon_bench/store_benchmarks/utils.go new file mode 100644 index 00000000..c4e2cdd9 --- /dev/null +++ b/benchmarks/gluon_bench/store_benchmarks/utils.go @@ -0,0 +1,73 @@ +package store_benchmarks + +import ( + "context" + "sync" + "time" + + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/flags" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/reporter" + "github.com/ProtonMail/gluon/benchmarks/gluon_bench/utils" + "github.com/ProtonMail/gluon/store" + "github.com/google/uuid" +) + +func CreateRandomState(store store.Store, count uint) ([]string, error) { + uuids := make([]string, 0, count) + data := make([]byte, *flags.StoreItemSize) + + for i := uint(0); i < count; i++ { + uuid := uuid.NewString() + if err := store.Set(uuid, data); err != nil { + return nil, nil + } + + uuids = append(uuids, uuid) + } + + return uuids, nil +} + +func RunStoreWorkers(ctx context.Context, st store.Store, fn func(context.Context, store.Store, *utils.DurationCollector, uint) error) *reporter.BenchmarkRun { + wg := sync.WaitGroup{} + + durations := make([]time.Duration, 0, *flags.StoreWorkers**flags.StoreItemCount) + collectors := make([]*utils.DurationCollector, *flags.StoreWorkers) + + for i := uint(0); i < *flags.StoreWorkers; i++ { + wg.Add(1) + + go func(index uint) { + defer wg.Done() + + collector := utils.NewDurationCollector(int(*flags.StoreItemCount)) + + if err := fn(ctx, st, collector, index); err != nil { + panic(err) + } + + collectors[index] = collector + }(i) + } + + wg.Wait() + + for _, v := range collectors { + durations = append(durations, v.Durations()...) + } + + return reporter.NewBenchmarkRun(durations, nil) +} + +func RunStoreWorkersSplitRange(ctx context.Context, st store.Store, length uint, fn func(context.Context, store.Store, *utils.DurationCollector, uint, uint) error) *reporter.BenchmarkRun { + workDivision := length / *flags.StoreWorkers + + return RunStoreWorkers(ctx, st, func(ctx context.Context, s store.Store, collector *utils.DurationCollector, u uint) error { + end := workDivision * (u + 1) + if end > length { + end = length + } + + return fn(ctx, st, collector, u*workDivision, end) + }) +} diff --git a/benchmarks/gluon_bench/utils/utils.go b/benchmarks/gluon_bench/utils/utils.go index 4da3857a..929f05c5 100644 --- a/benchmarks/gluon_bench/utils/utils.go +++ b/benchmarks/gluon_bench/utils/utils.go @@ -43,3 +43,27 @@ func ReadLinesFromFile(path string) ([]string, error) { return lines, nil } + +type DurationCollector struct { + durations []time.Duration + timer ScopedTimer +} + +func NewDurationCollector(capacity int) *DurationCollector { + return &DurationCollector{ + durations: make([]time.Duration, 0, capacity), + } +} + +func (d *DurationCollector) Start() { + d.timer.Start() +} + +func (d *DurationCollector) Stop() { + d.timer.Stop() + d.durations = append(d.durations, d.timer.Elapsed()) +} + +func (d *DurationCollector) Durations() []time.Duration { + return d.durations +}