From 06b01dbb67dde581977db43d0b36c19251f83af4 Mon Sep 17 00:00:00 2001
From: Ben Johnson <benbjohnson@yahoo.com>
Date: Fri, 23 May 2014 10:36:23 -0600
Subject: [PATCH] Remove allocations from read-only buckets.

---
 Makefile          |  2 ++
 bucket.go         | 23 +++++++++----
 cmd/bolt/bench.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 101 insertions(+), 7 deletions(-)

diff --git a/Makefile b/Makefile
index 67635ae..3fec2c7 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,8 @@ BRANCH=`git rev-parse --abbrev-ref HEAD`
 COMMIT=`git rev-parse --short HEAD`
 GOLDFLAGS="-X main.branch $(BRANCH) -X main.commit $(COMMIT)"
 
+default: build
+
 bench:
 	go test -v -test.run=NOTHINCONTAINSTHIS -test.bench=$(BENCH)
 
diff --git a/bucket.go b/bucket.go
index 877c310..2338a8b 100644
--- a/bucket.go
+++ b/bucket.go
@@ -76,8 +76,8 @@ type bucket struct {
 // newBucket returns a new bucket associated with a transaction.
 func newBucket(tx *Tx) Bucket {
 	var b = Bucket{tx: tx}
-	b.buckets = make(map[string]*Bucket)
 	if tx.writable {
+		b.buckets = make(map[string]*Bucket)
 		b.nodes = make(map[pgid]*node)
 	}
 	return b
@@ -115,8 +115,10 @@ func (b *Bucket) Cursor() *Cursor {
 // Bucket retrieves a nested bucket by name.
 // Returns nil if the bucket does not exist.
 func (b *Bucket) Bucket(name []byte) *Bucket {
-	if child := b.buckets[string(name)]; child != nil {
-		return child
+	if b.buckets != nil {
+		if child := b.buckets[string(name)]; child != nil {
+			return child
+		}
 	}
 
 	// Move cursor to key.
@@ -130,7 +132,9 @@ func (b *Bucket) Bucket(name []byte) *Bucket {
 
 	// Otherwise create a bucket and cache it.
 	var child = b.openBucket(v)
-	b.buckets[string(name)] = child
+	if b.buckets != nil {
+		b.buckets[string(name)] = child
+	}
 
 	return child
 }
@@ -139,8 +143,15 @@ func (b *Bucket) Bucket(name []byte) *Bucket {
 // from a parent into a Bucket
 func (b *Bucket) openBucket(value []byte) *Bucket {
 	var child = newBucket(b.tx)
-	child.bucket = &bucket{}
-	*child.bucket = *(*bucket)(unsafe.Pointer(&value[0]))
+
+	// If this is a writable transaction then we need to copy the bucket entry.
+	// Read-only transactions can point directly at the mmap entry.
+	if b.tx.writable {
+		child.bucket = &bucket{}
+		*child.bucket = *(*bucket)(unsafe.Pointer(&value[0]))
+	} else {
+		child.bucket = (*bucket)(unsafe.Pointer(&value[0]))
+	}
 
 	// Save a reference to the inline page if the bucket is inline.
 	if child.root == 0 {
diff --git a/cmd/bolt/bench.go b/cmd/bolt/bench.go
index 6379144..5d0dbec 100644
--- a/cmd/bolt/bench.go
+++ b/cmd/bolt/bench.go
@@ -100,6 +100,10 @@ func benchWrite(db *bolt.DB, options *BenchOptions, results *BenchResults) error
 		err = benchWriteSequential(db, options, results)
 	case "rnd":
 		err = benchWriteRandom(db, options, results)
+	case "seq-nest":
+		err = benchWriteSequentialNested(db, options, results)
+	case "rnd-nest":
+		err = benchWriteRandomNested(db, options, results)
 	default:
 		return fmt.Errorf("invalid write mode: %s", options.WriteMode)
 	}
@@ -119,6 +123,16 @@ func benchWriteRandom(db *bolt.DB, options *BenchOptions, results *BenchResults)
 	return benchWriteWithSource(db, options, results, func() uint32 { return r.Uint32() })
 }
 
+func benchWriteSequentialNested(db *bolt.DB, options *BenchOptions, results *BenchResults) error {
+	var i = uint32(0)
+	return benchWriteNestedWithSource(db, options, results, func() uint32 { i++; return i })
+}
+
+func benchWriteRandomNested(db *bolt.DB, options *BenchOptions, results *BenchResults) error {
+	r := rand.New(rand.NewSource(time.Now().UnixNano()))
+	return benchWriteNestedWithSource(db, options, results, func() uint32 { return r.Uint32() })
+}
+
 func benchWriteWithSource(db *bolt.DB, options *BenchOptions, results *BenchResults, keySource func() uint32) error {
 	results.WriteOps = options.Iterations
 
@@ -144,6 +158,35 @@ func benchWriteWithSource(db *bolt.DB, options *BenchOptions, results *BenchResu
 	return nil
 }
 
+func benchWriteNestedWithSource(db *bolt.DB, options *BenchOptions, results *BenchResults, keySource func() uint32) error {
+	results.WriteOps = options.Iterations
+
+	for i := 0; i < options.Iterations; i += options.BatchSize {
+		err := db.Update(func(tx *bolt.Tx) error {
+			top, _ := tx.CreateBucketIfNotExists(benchBucketName)
+
+			var name = make([]byte, options.KeySize)
+			binary.BigEndian.PutUint32(name, keySource())
+			b, _ := top.CreateBucketIfNotExists(name)
+
+			for j := 0; j < options.BatchSize; j++ {
+				var key = make([]byte, options.KeySize)
+				var value = make([]byte, options.ValueSize)
+				binary.BigEndian.PutUint32(key, keySource())
+				if err := b.Put(key, value); err != nil {
+					return err
+				}
+			}
+
+			return nil
+		})
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 // Reads from the database.
 func benchRead(db *bolt.DB, options *BenchOptions, results *BenchResults) error {
 	var err error
@@ -151,7 +194,11 @@ func benchRead(db *bolt.DB, options *BenchOptions, results *BenchResults) error
 
 	switch options.ReadMode {
 	case "seq":
-		err = benchReadSequential(db, options, results)
+		if options.WriteMode == "seq-nest" || options.WriteMode == "rnd-nest" {
+			err = benchReadSequentialNested(db, options, results)
+		} else {
+			err = benchReadSequential(db, options, results)
+		}
 	default:
 		return fmt.Errorf("invalid read mode: %s", options.ReadMode)
 	}
@@ -191,6 +238,40 @@ func benchReadSequential(db *bolt.DB, options *BenchOptions, results *BenchResul
 	})
 }
 
+func benchReadSequentialNested(db *bolt.DB, options *BenchOptions, results *BenchResults) error {
+	return db.View(func(tx *bolt.Tx) error {
+		var t = time.Now()
+
+		for {
+			var count int
+			var top = tx.Bucket(benchBucketName)
+			top.ForEach(func(name, _ []byte) error {
+				c := top.Bucket(name).Cursor()
+				for k, v := c.First(); k != nil; k, v = c.Next() {
+					if v == nil {
+						return errors.New("invalid value")
+					}
+					count++
+				}
+				return nil
+			})
+
+			if options.WriteMode == "seq-nest" && count != options.Iterations {
+				return fmt.Errorf("read seq-nest: 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