aboutsummaryrefslogtreecommitdiff
path: root/bucket.go
diff options
context:
space:
mode:
Diffstat (limited to 'bucket.go')
-rw-r--r--bucket.go161
1 files changed, 137 insertions, 24 deletions
diff --git a/bucket.go b/bucket.go
index 5dd79dd..6672356 100644
--- a/bucket.go
+++ b/bucket.go
@@ -52,11 +52,14 @@ const (
minInt = -maxInt - 1
)
+const bucketHeaderSize = int(unsafe.Sizeof(bucket{}))
+
// Bucket represents a collection of key/value pairs inside the database.
type Bucket struct {
*bucket
tx *Tx
buckets map[string]*Bucket
+ page *page
rootNode *node
nodes map[pgid]*node
}
@@ -128,6 +131,11 @@ func (b *Bucket) Bucket(name []byte) *Bucket {
*child.bucket = *(*bucket)(unsafe.Pointer(&v[0]))
b.buckets[string(name)] = &child
+ // Save a reference to the inline page if the bucket is inline.
+ if child.root == 0 {
+ child.page = (*page)(unsafe.Pointer(&v[bucketHeaderSize]))
+ }
+
return &child
}
@@ -155,22 +163,19 @@ func (b *Bucket) CreateBucket(key []byte) (*Bucket, error) {
}
}
- // Create a blank root leaf page.
- p, err := b.tx.allocate(1)
- if err != nil {
- return nil, err
- }
- p.flags = leafPageFlag
-
- // Insert key/value.
- value := make([]byte, unsafe.Sizeof(bucket{}))
- bucket := (*bucket)(unsafe.Pointer(&value[0]))
- bucket.root = p.id
+ // Create empty, inline bucket.
+ var bucket = Bucket{bucket: &bucket{}, rootNode: &node{isLeaf: true}}
+ var value = bucket.write()
// Insert into node.
key = cloneBytes(key)
c.node().put(key, key, value, 0, bucketLeafFlag)
+ // De-inline the parent bucket, if necessary.
+ if b.root == 0 {
+ b.page = nil
+ }
+
return b.Bucket(key), nil
}
@@ -224,9 +229,9 @@ func (b *Bucket) DeleteBucket(key []byte) error {
delete(b.buckets, string(key))
// Release all bucket pages to freelist.
- b.tx.forEachPage(child.root, 0, func(p *page, _ int) {
- b.tx.db.freelist.free(b.tx.id(), p)
- })
+ child.nodes = nil
+ child.rootNode = nil
+ child.free()
// Delete the node if we have a matching key.
c.node().del(key)
@@ -348,7 +353,7 @@ func (b *Bucket) ForEach(fn func(k, v []byte) error) error {
func (b *Bucket) Stats() BucketStats {
var s BucketStats
pageSize := b.tx.db.pageSize
- b.tx.forEachPage(b.root, 0, func(p *page, depth int) {
+ b.forEachPage(func(p *page, depth int) {
if (p.flags & leafPageFlag) != 0 {
s.LeafPageN++
s.KeyN += int(p.count)
@@ -375,21 +380,47 @@ func (b *Bucket) Stats() BucketStats {
return s
}
+// forEachPage iterates over every page in a bucket, including inline pages.
+func (b *Bucket) forEachPage(fn func(*page, int)) {
+ // If we have an inline page then just use that.
+ if b.page != nil {
+ fn(b.page, 0)
+ return
+ }
+
+ // Otherwise traverse the page hierarchy.
+ b.tx.forEachPage(b.root, 0, fn)
+}
+
// spill writes all the nodes for this bucket to dirty pages.
func (b *Bucket) spill() error {
// Spill all child buckets first.
for name, child := range b.buckets {
- if err := child.spill(); err != nil {
- return err
+ // If the child bucket is small enough and it has no child buckets then
+ // write it inline into the parent bucket's page. Otherwise spill it
+ // like a normal bucket and make the parent value a pointer to the page.
+ var value []byte
+ if child.inlineable() {
+ child.free()
+ value = child.write()
+ } else {
+ if err := child.spill(); err != nil {
+ return err
+ }
+
+ // Update the child bucket header in this bucket.
+ value = make([]byte, unsafe.Sizeof(bucket{}))
+ bucket := (*bucket)(unsafe.Pointer(&value[0]))
+ *bucket = *child.bucket
}
- // Update the child bucket header in this bucket.
- value := make([]byte, unsafe.Sizeof(bucket{}))
- bucket := (*bucket)(unsafe.Pointer(&value[0]))
- *bucket = *child.bucket
+ // Skip writing the bucket if there are no materialized nodes.
+ if child.rootNode == nil {
+ continue
+ }
// Update parent node.
- c := b.Cursor()
+ var c = b.Cursor()
k, _, flags := c.seek([]byte(name))
_assert(bytes.Equal([]byte(name), k), "misplaced bucket header: %x -> %x", []byte(name), k)
_assert(flags&bucketLeafFlag != 0, "unexpected bucket header flag: %x", flags)
@@ -413,6 +444,54 @@ func (b *Bucket) spill() error {
return nil
}
+// inlineable returns true if a bucket is small enough to be written inline
+// and if it contains no subbuckets. Otherwise returns false.
+func (b *Bucket) inlineable() bool {
+ var n = b.rootNode
+
+ // Bucket must only contain a single leaf node.
+ if n == nil || !n.isLeaf {
+ return false
+ }
+
+ // Bucket is not inlineable if it contains subbuckets or if it goes beyond
+ // our threshold for inline bucket size.
+ var size = pageHeaderSize
+ for _, inode := range n.inodes {
+ size += leafPageElementSize + len(inode.key) + len(inode.value)
+
+ if inode.flags&bucketLeafFlag != 0 {
+ return false
+ } else if size > b.maxInlineBucketSize() {
+ return false
+ }
+ }
+
+ return true
+}
+
+// Returns the maximum total size of a bucket to make it a candidate for inlining.
+func (b *Bucket) maxInlineBucketSize() int {
+ return b.tx.db.pageSize / 4
+}
+
+// write allocates and writes a bucket to a byte slice.
+func (b *Bucket) write() []byte {
+ // Allocate the appropriate size.
+ var n = b.rootNode
+ var value = make([]byte, bucketHeaderSize+pageHeaderSize+n.size())
+
+ // Write a bucket header.
+ var bucket = (*bucket)(unsafe.Pointer(&value[0]))
+ *bucket = *b.bucket
+
+ // Convert byte slice to a fake page and write the root node.
+ var p = (*page)(unsafe.Pointer(&value[bucketHeaderSize]))
+ n.write(p)
+
+ return value
+}
+
// rebalance attempts to balance all nodes.
func (b *Bucket) rebalance() {
for _, n := range b.nodes {
@@ -431,14 +510,22 @@ func (b *Bucket) node(pgid pgid, parent *node) *node {
return n
}
- // Otherwise create a branch node and cache it.
+ // Otherwise create a node and cache it.
n := &node{bucket: b, parent: parent}
if parent == nil {
b.rootNode = n
} else {
parent.children = append(parent.children, n)
}
- n.read(b.tx.page(pgid))
+
+ // Use the inline page if this is an inline bucket.
+ var p = b.page
+ if p == nil {
+ p = b.tx.page(pgid)
+ }
+
+ // Read the page into the node and cache it.
+ n.read(p)
b.nodes[pgid] = n
// Update statistics.
@@ -447,6 +534,19 @@ func (b *Bucket) node(pgid pgid, parent *node) *node {
return n
}
+// free recursively frees all pages in the bucket.
+func (b *Bucket) free() {
+ if b.root == 0 {
+ return
+ }
+
+ var tx = b.tx
+ tx.forEachPage(b.root, 0, func(p *page, _ int) {
+ tx.db.freelist.free(tx.id(), p)
+ })
+ b.root = 0
+}
+
// dereference removes all references to the old mmap.
func (b *Bucket) dereference() {
for _, n := range b.nodes {
@@ -464,11 +564,24 @@ func (b *Bucket) dereference() {
// pageNode returns the in-memory node, if it exists.
// Otherwise returns the underlying page.
func (b *Bucket) pageNode(id pgid) (*page, *node) {
+ // Inline buckets have a fake page embedded in their value so treat them
+ // differently. We'll return the rootNode (if available) or the fake page.
+ if b.root == 0 {
+ _assert(id == 0, "inline bucket non-zero page access(2): %d != 0", id)
+ if b.rootNode != nil {
+ return nil, b.rootNode
+ }
+ return b.page, nil
+ }
+
+ // Check the node cache for non-inline buckets.
if b.nodes != nil {
if n := b.nodes[id]; n != nil {
return nil, n
}
}
+
+ // Finally lookup the page from the transaction if no node is materialized.
return b.tx.page(id), nil
}