diff options
author | Ben Johnson <benbjohnson@yahoo.com> | 2014-04-18 21:37:45 -0500 |
---|---|---|
committer | Ben Johnson <benbjohnson@yahoo.com> | 2014-04-18 22:15:31 -0500 |
commit | a42d74da7e6b3162701ae17d59647a6880ccb6bf (patch) | |
tree | b32ca009bfaeae67cc0db5e388ba574b07cfa333 /cmd | |
parent | move bench package to bench/cmd/bolt/bench (diff) | |
download | dedo-a42d74da7e6b3162701ae17d59647a6880ccb6bf.tar.gz dedo-a42d74da7e6b3162701ae17d59647a6880ccb6bf.tar.xz |
Add 'bolt bench'.
This commit adds a flexible benchmarking tool to the 'bolt' CLI. It allows
the user to separately specify the write mode and read mode (e.g. sequential
random, etc). It also allows the user to isolate profiling to either the
read or the writes.
Currently the bench tool only supports "seq" read and write modes. It also
does not support streaming of Bolt counters yet.
Fixes #95.
/cc @snormore
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/bolt/bench.go | 270 | ||||
-rw-r--r-- | cmd/bolt/bench/bench.go | 126 | ||||
-rw-r--r-- | cmd/bolt/bench/config.go | 7 | ||||
-rw-r--r-- | cmd/bolt/bench/generate.go | 24 | ||||
-rw-r--r-- | cmd/bolt/generate.go | 55 | ||||
-rw-r--r-- | cmd/bolt/main.go | 46 |
6 files changed, 279 insertions, 249 deletions
diff --git a/cmd/bolt/bench.go b/cmd/bolt/bench.go index 2b6ff8a..72144b8 100644 --- a/cmd/bolt/bench.go +++ b/cmd/bolt/bench.go @@ -1,31 +1,271 @@ package main import ( - "testing" + "encoding/binary" + "errors" + "fmt" + "io/ioutil" + "os" + "runtime" + "runtime/pprof" + "time" "github.com/boltdb/bolt" - "github.com/boltdb/bolt/bench" ) -// Import converts an exported database dump into a new database. -// readWriteMode: 'read' or 'write' -// traversalPattern: 'sequentrial' or 'random' -// parallelism: integer representing number of concurrent reads/writes -func Bench(inputPath string, readWriteMode string, traversalPattern string, parallelism int) { +// File handlers for the various profiles. +var cpuprofile, memprofile, blockprofile *os.File - // Open the database. - db, err := bolt.Open(inputPath, 0600) +var benchBucketName = []byte("bench") + +// Bench executes a customizable, synthetic benchmark against Bolt. +func Bench(options *BenchOptions) { + var results BenchResults + + // Find temporary location. + path := tempfile() + defer os.Remove(path) + + // Create database. + db, err := bolt.Open(path, 0600) if err != nil { - fatalf("error: %+v", err) + fatal(err) return } defer db.Close() - b := bench.New(db, &bench.Config{ - ReadWriteMode: readWriteMode, - TraversalPattern: traversalPattern, - Parallelism: parallelism, + // Start profiling for writes. + if options.ProfileMode == "rw" || options.ProfileMode == "w" { + benchStartProfiling(options) + } + + // Write to the database. + if err := benchWrite(db, options, &results); err != nil { + fatal("bench: write: ", err) + } + + // Stop profiling for writes only. + if options.ProfileMode == "w" { + benchStopProfiling() + } + + // Start profiling for reads. + if options.ProfileMode == "r" { + benchStartProfiling(options) + } + + // Read from the database. + if err := benchRead(db, options, &results); err != nil { + fatal("bench: read: ", err) + } + + // Stop profiling for writes only. + if options.ProfileMode == "rw" || options.ProfileMode == "r" { + benchStopProfiling() + } + + // Print results. + fmt.Printf("# Write\t%v\t(%v/op)\t(%v op/sec)\n", results.WriteDuration, results.WriteOpDuration(), results.WriteOpsPerSecond()) + fmt.Printf("# Read\t%v\t(%v/op)\t(%v op/sec)\n", results.ReadDuration, results.ReadOpDuration(), results.ReadOpsPerSecond()) + fmt.Println("") +} + +// Writes to the database. +func benchWrite(db *bolt.DB, options *BenchOptions, results *BenchResults) error { + var err error + var t = time.Now() + + switch options.WriteMode { + case "seq": + err = benchWriteSequential(db, options, results) + default: + return fmt.Errorf("invalid write mode: %s", options.WriteMode) + } + + results.WriteDuration = time.Since(t) + + return err +} + +func benchWriteSequential(db *bolt.DB, options *BenchOptions, results *BenchResults) error { + results.WriteOps = options.Iterations + + return db.Update(func(tx *bolt.Tx) error { + b, _ := tx.CreateBucketIfNotExists(benchBucketName) + + for i := 0; i < options.Iterations; i++ { + var key = make([]byte, options.KeySize) + var value = make([]byte, options.ValueSize) + binary.BigEndian.PutUint32(key, uint32(i)) + if err := b.Put(key, value); err != nil { + return err + } + } + + return nil + }) +} + +// Reads from the database. +func benchRead(db *bolt.DB, options *BenchOptions, results *BenchResults) error { + var err error + var t = time.Now() + + switch options.ReadMode { + case "seq": + err = benchReadSequential(db, options, results) + default: + return fmt.Errorf("invalid read mode: %s", options.ReadMode) + } + + results.ReadDuration = time.Since(t) + + return err +} + +func benchReadSequential(db *bolt.DB, options *BenchOptions, results *BenchResults) error { + return db.View(func(tx *bolt.Tx) error { + var t = time.Now() + + for { + c := tx.Bucket(benchBucketName).Cursor() + var count int + for k, v := c.First(); k != nil; k, v = c.Next() { + if v == nil { + return errors.New("invalid value") + } + count++ + } + + if count != options.Iterations { + return fmt.Errorf("read seq: iter mismatch: expected %d, got %d", options.Iterations, count) + } + + results.ReadOps += count + + // Make sure we do this for at least a second. + if time.Since(t) >= time.Second { + break + } + } + + return nil }) +} + +// Starts all profiles set on the options. +func benchStartProfiling(options *BenchOptions) { + var err error + + // Start CPU profiling. + if options.CPUProfile != "" { + cpuprofile, err = os.Create(options.CPUProfile) + if err != nil { + fatal("bench: could not create cpu profile %q: %v", options.CPUProfile, err) + } + pprof.StartCPUProfile(cpuprofile) + } + + // Start memory profiling. + if options.MemProfile != "" { + memprofile, err = os.Create(options.MemProfile) + if err != nil { + fatal("bench: could not create memory profile %q: %v", options.MemProfile, err) + } + runtime.MemProfileRate = 4096 + } + + // Start fatal profiling. + if options.BlockProfile != "" { + blockprofile, err = os.Create(options.BlockProfile) + if err != nil { + fatal("bench: could not create block profile %q: %v", options.BlockProfile, err) + } + runtime.SetBlockProfileRate(1) + } +} + +// Stops all profiles. +func benchStopProfiling() { + if cpuprofile != nil { + pprof.StopCPUProfile() + cpuprofile.Close() + cpuprofile = nil + } + + if memprofile != nil { + pprof.Lookup("heap").WriteTo(memprofile, 0) + memprofile.Close() + memprofile = nil + } + + if blockprofile != nil { + pprof.Lookup("block").WriteTo(blockprofile, 0) + blockprofile.Close() + blockprofile = nil + runtime.SetBlockProfileRate(0) + } +} + +// BenchOptions represents the set of options that can be passed to Bench(). +type BenchOptions struct { + ProfileMode string + WriteMode string + ReadMode string + Iterations int + KeySize int + ValueSize int + CPUProfile string + MemProfile string + BlockProfile string +} + +// BenchResults represents the performance results of the benchmark. +type BenchResults struct { + WriteOps int + WriteDuration time.Duration + ReadOps int + ReadDuration time.Duration +} + +// Returns the duration for a single write operation. +func (r *BenchResults) WriteOpDuration() time.Duration { + if r.WriteOps == 0 { + return 0 + } + return r.WriteDuration / time.Duration(r.WriteOps) +} + +// Returns average number of write operations that can be performed per second. +func (r *BenchResults) WriteOpsPerSecond() int { + var op = r.WriteOpDuration() + if op == 0 { + return 0 + } + return int(time.Second) / int(op) +} + +// Returns the duration for a single read operation. +func (r *BenchResults) ReadOpDuration() time.Duration { + if r.ReadOps == 0 { + return 0 + } + return r.ReadDuration / time.Duration(r.ReadOps) +} + +// Returns average number of read operations that can be performed per second. +func (r *BenchResults) ReadOpsPerSecond() int { + var op = r.ReadOpDuration() + if op == 0 { + return 0 + } + return int(time.Second) / int(op) +} - println(testing.Benchmark(b.Run)) +// tempfile returns a temporary file path. +func tempfile() string { + f, _ := ioutil.TempFile("", "bolt-bench-") + f.Close() + os.Remove(f.Name()) + return f.Name() } diff --git a/cmd/bolt/bench/bench.go b/cmd/bolt/bench/bench.go deleted file mode 100644 index df584f2..0000000 --- a/cmd/bolt/bench/bench.go +++ /dev/null @@ -1,126 +0,0 @@ -package bench - -import ( - "errors" - "fmt" - "sync" - "testing" - - "github.com/boltdb/bolt" -) - -const ( - BenchReadMode = "read" - BenchWriteMode = "write" - BenchSequentialTraversal = "sequential" - BenchRandomTraversal = "random" -) - -type Benchmark struct { - db *bolt.DB - config *Config -} - -func New(db *bolt.DB, config *Config) *Benchmark { - b := new(Benchmark) - b.db = db - b.config = config - return b -} - -func (bm *Benchmark) Run(b *testing.B) { - - // Read buckets and keys before benchmark begins so we don't knew the - // results. - buckets, err := buckets(bm.db) - if err != nil { - b.Fatalf("error: %+v", err) - } - bucketsWithKeys := make(map[string][]string) - for _, bucket := range buckets { - keys, err := keys(bm.db, bucket) - if err != nil { - b.Fatalf("error: %+v", err) - } - bucketsWithKeys[bucket] = keys - } - - b.ResetTimer() - - // Keep running a fixed number of parallel reads until we run out of time. - for i := 0; i < b.N; i++ { - var wg sync.WaitGroup - for j := 0; j < bm.config.Parallelism; j++ { - wg.Add(1) - go func() { - defer wg.Done() - if err := bm.readBuckets(b, bm.db, bucketsWithKeys); err != nil { - b.Fatalf("error: %+v", err) - } - }() - } - wg.Wait() - } -} - -// Run benchmark(s) for each of the given buckets. -func (bm *Benchmark) readBuckets(b *testing.B, db *bolt.DB, bucketsWithKeys map[string][]string) error { - return db.View(func(tx *bolt.Tx) error { - bucketsCount := len(bucketsWithKeys) - count := 0 - for bucket, keys := range bucketsWithKeys { - bucket := tx.Bucket([]byte(bucket)) - if err := bm.readKeys(b, bucket, keys); err != nil { - return err - } - count++ - } - if count != bucketsCount { - return errors.New(fmt.Sprintf("wrong count: %d; expected: %d", count, bucketsCount)) - } - return nil - }) -} - -func (bm *Benchmark) readKeys(b *testing.B, bucket *bolt.Bucket, keys []string) error { - c := bucket.Cursor() - keysCount := len(keys) - count := 0 - for k, _ := c.First(); k != nil; k, _ = c.Next() { - count++ - } - if count != keysCount { - return errors.New(fmt.Sprintf("wrong count: %d; expected: %d", count, keysCount)) - } - return nil -} - -func buckets(db *bolt.DB) ([]string, error) { - buckets := []string{} - err := db.View(func(tx *bolt.Tx) error { - // Iterate over each bucket. - return tx.ForEach(func(name []byte, _ *bolt.Bucket) error { - buckets = append(buckets, string(name)) - return nil - }) - }) - return buckets, err -} - -func keys(db *bolt.DB, bucket string) ([]string, error) { - keys := []string{} - err := db.View(func(tx *bolt.Tx) error { - // Find bucket. - b := tx.Bucket([]byte(bucket)) - if b == nil { - return errors.New(fmt.Sprintf("bucket %+v not found", b)) - } - - // Iterate over each key. - return b.ForEach(func(key, _ []byte) error { - keys = append(keys, string(key)) - return nil - }) - }) - return keys, err -} diff --git a/cmd/bolt/bench/config.go b/cmd/bolt/bench/config.go deleted file mode 100644 index dea08fd..0000000 --- a/cmd/bolt/bench/config.go +++ /dev/null @@ -1,7 +0,0 @@ -package bench - -type Config struct { - ReadWriteMode string - TraversalPattern string - Parallelism int -} diff --git a/cmd/bolt/bench/generate.go b/cmd/bolt/bench/generate.go deleted file mode 100644 index 8c5554d..0000000 --- a/cmd/bolt/bench/generate.go +++ /dev/null @@ -1,24 +0,0 @@ -package bench - -import ( - "fmt" - "strings" - - "github.com/boltdb/bolt" -) - -// Generate and write data to specified number of buckets/items. -func GenerateDB(db *bolt.DB, numBuckets, numItemsPerBucket int) error { - return db.Update(func(tx *bolt.Tx) error { - for bucketIndex := 0; bucketIndex < numBuckets; bucketIndex++ { - bucketName := fmt.Sprintf("bucket%08d") - tx.CreateBucket([]byte(bucketName)) - bucket := tx.Bucket([]byte(bucketName)) - for i := 0; i < numItemsPerBucket; i++ { - value := []byte(strings.Repeat("0", 100)) - bucket.Put([]byte(fmt.Sprintf("key%08d", i)), value) - } - } - return nil - }) -} diff --git a/cmd/bolt/generate.go b/cmd/bolt/generate.go deleted file mode 100644 index 15edb27..0000000 --- a/cmd/bolt/generate.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/boltdb/bolt" -) - -// Generate data for benchmarks. -func Generate(destPath string, numBuckets, numItems int) { - - // Open the database. - db, err := bolt.Open(destPath, 0600) - if err != nil { - fatalf("open db:", err) - return - } - defer db.Close() - - for bucketIndex := 0; bucketIndex < numBuckets; bucketIndex++ { - bucketName := fmt.Sprintf("bucket%03d", bucketIndex) - - err = db.Update(func(tx *bolt.Tx) error { - - // Create the bucket if it doesn't exist. - if err := tx.CreateBucketIfNotExists([]byte(bucketName)); err != nil { - fatalf("create bucket: %s", err) - return nil - } - - // Find bucket. - b := tx.Bucket([]byte(bucketName)) - if b == nil { - fatalf("bucket not found: %s", bucketName) - return nil - } - - for i := 0; i < numItems; i++ { - key := fmt.Sprintf("key%03d", i) - value := fmt.Sprintf("value%03d", i) - - // Set value for a given key. - if err := b.Put([]byte(key), []byte(value)); err != nil { - return err - } - } - - return nil - }) - } - if err != nil { - fatal(err) - return - } -} diff --git a/cmd/bolt/main.go b/cmd/bolt/main.go index ac71631..719bf00 100644 --- a/cmd/bolt/main.go +++ b/cmd/bolt/main.go @@ -6,7 +6,6 @@ import ( "fmt" "log" "os" - "strconv" "github.com/codegangsta/cli" ) @@ -92,30 +91,33 @@ func NewApp() *cli.App { }, }, { - Name: "generate", - Usage: "Generate data for benchmarks", - Action: func(c *cli.Context) { - destPath := c.Args().Get(0) - numBuckets, err := strconv.Atoi(c.Args().Get(1)) - if err != nil { - fatal(err) - } - numItems, err := strconv.Atoi(c.Args().Get(2)) - if err != nil { - fatal(err) - } - Generate(destPath, numBuckets, numItems) - }, - }, - { Name: "bench", - Usage: "Run benchmarks on a given dataset", + Usage: "Performs a synthetic benchmark", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "profile-mode", Value: "rw", Usage: "Profile mode"}, + &cli.StringFlag{Name: "write-mode", Value: "seq", Usage: "Write mode"}, + &cli.StringFlag{Name: "read-mode", Value: "seq", Usage: "Read mode"}, + &cli.IntFlag{Name: "count", Value: 1000, Usage: "Item count"}, + &cli.IntFlag{Name: "key-size", Value: 8, Usage: "Key size"}, + &cli.IntFlag{Name: "value-size", Value: 32, Usage: "Value size"}, + &cli.StringFlag{Name: "cpuprofile", Usage: "CPU profile output path"}, + &cli.StringFlag{Name: "memprofile", Usage: "Memory profile output path"}, + &cli.StringFlag{Name: "blockprofile", Usage: "Block profile output path"}, + }, Action: func(c *cli.Context) { - srcPath := c.Args().Get(0) - Bench(srcPath, "read", "sequential", 1) + Bench(&BenchOptions{ + ProfileMode: c.String("profile-mode"), + WriteMode: c.String("write-mode"), + ReadMode: c.String("read-mode"), + Iterations: c.Int("count"), + KeySize: c.Int("key-size"), + ValueSize: c.Int("value-size"), + CPUProfile: c.String("cpuprofile"), + MemProfile: c.String("memprofile"), + BlockProfile: c.String("blockprofile"), + }) }, - }, - } + }} return app } |