diff --git a/bufferpool/page.go b/bufferpool/page.go index 2b06fb9ee..38271b8a4 100644 --- a/bufferpool/page.go +++ b/bufferpool/page.go @@ -2,7 +2,6 @@ package bufferpool import ( "encoding/binary" - "errors" "fmt" "sync" ) @@ -39,8 +38,8 @@ const PAGE_TYPE_HASH_TABLE = 12 // | | // |----------------------------------------------------| // | 52 | slotcount | slot entry is int16 | -// | | * slotwidth | values (payloadOffset, | -// | | * #slots | payloadLength) | +// | | * slotwidth | values (payloadOffset) | +// | | * #slots | | // |----------------------------------------------------| // | | // |----------------------------------------------------| @@ -53,6 +52,12 @@ const PAGE_TYPE_HASH_TABLE = 12 // keyBytes // ptrValue (int64) (page number) +// == hash table payload chunk == +// keyLength (int16) +// keyBytes +// rowPayloadChunkLen (int16) +// rowPayloadChunkBytes + // == leaf payload chunk == // keyLength (int16) // keyBytes @@ -87,15 +92,16 @@ const PAGE_SLOTS_START_OFFSET = 52 // offset 52 // PAGE_SLOT_LENGTH is the size of the page slot offset value. const PAGE_SLOT_LENGTH = 2 -// 1k is the max key size for now +// MAX_KEY_ON_PAGE_SIZE is 1k; the max key size for now // we can make this bigger later with overflow const MAX_KEY_ON_PAGE_SIZE = 1024 -// 768 bytes is the max payload on a page before we overflow -// this gives us 1792 bytes for key and payload -// available space on a page after header is 8144 ish -// this gives us room for 4 key/value @ 2036 bytes per page, so -// there is a little bit of wiggle room +// MAX_PAYLOAD_CHUNK_SIZE_ON_PAGE bytes is the max payload on a +// page before we overflow. The default of 768 (assumming max key size) +// gives us 1792 bytes for key and payload. +// Thus available space on a page after header is 8144 (ish) +// This gives us room for 4 key/value @ 2036 bytes per page, plus +// a little bit of wiggle room. const MAX_PAYLOAD_CHUNK_SIZE_ON_PAGE = 768 type PageLatchState int @@ -239,49 +245,6 @@ func (p *Page) WritePageSlot(slot int16, value PageSlot) { binary.BigEndian.PutUint16(p.data[offset:], uint16(value.PayloadOffset)) } -// TODO(pok) deprecate this once we get b+tree working, this -// is just used in hash table right now -func (p *Page) PutKeyValueInPageSlot(slotNumber int16, keyBytes []byte, payloadBytes []byte) error { - freeSpaceOffset := p.ReadFreeSpaceOffset() - - keyLength := len(keyBytes) - payloadChunkLength := len(payloadBytes) - - // check for overflow - if payloadChunkLength > MAX_PAYLOAD_CHUNK_SIZE_ON_PAGE { - return errors.New("overflow") - } - - totalPayloadSize := p.ComputeLeafPayloadTotalLength(keyLength, payloadChunkLength) - - // check to make sure we don't blow free space - if p.FreeSpaceOnPage() < int16(totalPayloadSize) { - return errors.New("page is full") - } - - // compute the new free space offset - freeSpaceOffset -= int16(totalPayloadSize) - offset := freeSpaceOffset - - // write the header - offset = p.WriteLeafPagePayloadHeader(offset, int16(keyLength), keyBytes, 0, 0, int32(payloadChunkLength)) - - // write the payload - p.WriteLeafPagePayloadBytes(offset, int16(payloadChunkLength), payloadBytes) - - // update the free space offset - p.WriteFreeSpaceOffset(int16(freeSpaceOffset)) - - // make a slot - slot := PageSlot{ - PayloadOffset: freeSpaceOffset, - } - // write the slot - p.WritePageSlot(slotNumber, slot) - - return nil -} - func (p *Page) ComputeInternalPayloadTotalLength(keyLength int) int32 { l := /*keyLength*/ 2 + keyLength + /*ptrValue*/ 8 return int32(l) @@ -304,6 +267,11 @@ func (p *Page) ComputeLeafPayloadTotalLength(keyLength int, payloadLength int) i return int32(l) } +func (p *Page) ComputeHashPayloadTotalLength(keyLength int, payloadLength int) int32 { + l := /*keyLength*/ 2 + keyLength + /*payLoadLength*/ 2 + payloadLength + return int32(l) +} + func (p *Page) WriteLeafPagePayloadHeader(offset int16, keyLen int16, keyBytes []byte, flags int8, overflowPtr int64, payloadTotalLen int32) int16 { binary.BigEndian.PutUint16(p.data[offset:], uint16(keyLen)) offset += 2 @@ -340,6 +308,21 @@ func (p *Page) ReadLeafPagePayloadBytes(offset int16) (int16, []byte) { return chunkLen, chunkBytes } +func (p *Page) WriteHashPagePayloadBytes(offset int16, keyLen int16, keyBytes []byte, payloadChunkLength int16, payloadChunkBytes []byte) { + // write key len + binary.BigEndian.PutUint16(p.data[offset:], uint16(keyLen)) + offset += 2 + // write key + copy(p.data[offset:], keyBytes) + offset += keyLen + // chunk length + binary.BigEndian.PutUint16(p.data[offset:], uint16(payloadChunkLength)) + offset += 2 + // now copy the payload bytes + copy(p.data[offset:], payloadChunkBytes) + p.isDirty = true +} + func (p *Page) WriteInternalPageChunk(offset int16, chunk InternalPageChunk) { binary.BigEndian.PutUint16(p.data[offset:], uint16(chunk.KeyLength)) offset += 2 @@ -500,16 +483,6 @@ func (l *LeafPayload) ValueLength(page *Page) int32 { return valueLen } -// only call this if you are sure this there is no overflow -func (l *LeafPayload) ValueAsBytes(page *Page) []byte { - offset := l.valueOffset(page) - valueLen := int32(binary.BigEndian.Uint32(page.data[offset:])) - offset += 4 - result := make([]byte, valueLen) - copy(result, page.data[offset:int32(offset)+valueLen]) - return result -} - func (l *LeafPayload) GetPayloadReader(page *Page) LeafPagePayLoadReader { offset := l.BaseOffset keyLen := int16(binary.BigEndian.Uint16(page.data[offset:])) @@ -549,6 +522,42 @@ func (l *LeafPayload) IsVisibleToTID(page *Page, tid int64) bool { return true } +type HashPayload struct { + BaseOffset int16 +} + +func (l *HashPayload) valueOffset(page *Page) int16 { + offset := l.BaseOffset + keyLen := int16(binary.BigEndian.Uint16(page.data[offset:])) + offset += 2 + keyLen + return offset +} + +func (l *HashPayload) ValueLength(page *Page) int16 { + offset := l.valueOffset(page) + valueLen := int16(binary.BigEndian.Uint16(page.data[offset:])) + return valueLen +} + +func (l *HashPayload) GetPayloadReader(page *Page) HashPagePayLoadReader { + offset := l.BaseOffset + keyLen := int16(binary.BigEndian.Uint16(page.data[offset:])) + offset += 2 + keyBytes := make([]byte, keyLen) + copy(keyBytes, page.data[offset:offset+keyLen]) + offset += keyLen + valueLen := int16(binary.BigEndian.Uint16(page.data[offset:])) + offset += 2 + valueBytes := make([]byte, valueLen) + copy(valueBytes, page.data[offset:int16(offset)+valueLen]) + return HashPagePayLoadReader{ + KeyLength: keyLen, + KeyBytes: keyBytes, + PayloadChunkLength: valueLen, + PayloadChunkBytes: valueBytes, + } +} + type PageSlot struct { PayloadOffset int16 } @@ -565,6 +574,22 @@ func (s *PageSlot) LeafPayload(page *Page) LeafPayload { return LeafPayload{BaseOffset: s.PayloadOffset} } +func (s *PageSlot) HashPayload(page *Page) HashPayload { + return HashPayload{BaseOffset: s.PayloadOffset} +} + +type HashPagePayLoadReader struct { + KeyLength int16 + KeyBytes []byte + PayloadChunkLength int16 + PayloadChunkBytes []byte +} + +func (pc *HashPagePayLoadReader) Length() int32 { + l := /*KeyLength*/ 2 + pc.KeyLength + /*PayLoadChunkLength*/ 2 + pc.PayloadChunkLength + return int32(l) +} + type LeafPagePayLoadReader struct { KeyLength int16 KeyBytes []byte diff --git a/extendiblehash/extendiblehash.go b/extendiblehash/extendiblehash.go index 8b354c782..510983e42 100644 --- a/extendiblehash/extendiblehash.go +++ b/extendiblehash/extendiblehash.go @@ -2,6 +2,7 @@ package extendiblehash import ( "bytes" + "errors" "fmt" "github.com/featurebasedb/featurebase/v3/bufferpool" @@ -18,7 +19,7 @@ type ExtendibleHashTable struct { // NewExtendibleHashTable creates a new ExtendibleHashTable func NewExtendibleHashTable(keyLength int, valueLength int, bufferPool *bufferpool.BufferPool) (*ExtendibleHashTable, error) { - bytesPerKV := keyLength + valueLength + bufferpool.PAGE_SLOT_LENGTH + bytesPerKV := /* keyLength */ 2 + keyLength + /* valueLength */ 2 + valueLength + bufferpool.PAGE_SLOT_LENGTH keysPerPage := int(bufferpool.PAGE_SIZE-bufferpool.PAGE_SLOTS_START_OFFSET) / bytesPerKV //create the root page @@ -54,8 +55,9 @@ func (e *ExtendibleHashTable) Get(key []byte) ([]byte, bool, error) { index, found := e.findKey(page, key) if found { slot := page.ReadPageSlot(int16(index)) - lpl := slot.LeafPayload(page) - return lpl.ValueAsBytes(page), true, nil + lpl := slot.HashPayload(page) + rdr := lpl.GetPayloadReader(page) + return rdr.PayloadChunkBytes, true, nil } return []byte{}, false, nil } @@ -162,21 +164,21 @@ func (e *ExtendibleHashTable) splitOnKey(page *bufferpool.Page, key []byte) erro if slot == nil { break } - pl := slot.KeyPayload(page) - lpl := slot.LeafPayload(page) - keyBytes := pl.KeyBytes(page) + lpl := slot.HashPayload(page) + rdr := lpl.GetPayloadReader(page) + keyBytes := rdr.KeyBytes k := string(keyBytes) h := Key(k).Hash() if h&hiBit > 0 { sc := p1.ReadSlotCount() - p1.PutKeyValueInPageSlot(sc, keyBytes, lpl.ValueAsBytes(page)) + e.putKeyValueInPageSlot(p1, sc, keyBytes, rdr.PayloadChunkBytes) // update the slot count p1.WriteSlotCount(int16(sc + 1)) } else { sc := p0.ReadSlotCount() - p0.PutKeyValueInPageSlot(sc, keyBytes, lpl.ValueAsBytes(page)) + e.putKeyValueInPageSlot(p0, sc, keyBytes, rdr.PayloadChunkBytes) // update the slot count p0.WriteSlotCount(int16(sc + 1)) } @@ -196,44 +198,54 @@ func (e *ExtendibleHashTable) splitOnKey(page *bufferpool.Page, key []byte) erro } func (e *ExtendibleHashTable) cleanPage(page *bufferpool.Page) error { - scratch := e.bufferPool.ScratchPage() - // copy page number - scratch.WritePageNumber(page.ID().Page) - // set the page type - scratch.WritePageType(bufferpool.PAGE_TYPE_HASH_TABLE) - // copy local depth - scratch.WriteLocalDepth(page.ReadLocalDepth()) + // 1. make a scratch page + // 2. iterate the slots on this page and copy data + // 3. put the scratch page back over this one - // copy slots from page to scratch - si := bufferpool.NewPageSlotIterator(page, 0) - for { - slot := si.Next() - if slot == nil { - break - } - pl := slot.KeyPayload(page) - lpl := slot.LeafPayload(page) - scratch.PutKeyValueInPageSlot(si.Cursor(), pl.KeyBytes(page), lpl.ValueAsBytes(page)) + scratchPage := e.bufferPool.ScratchPage() + scratchPage.WritePageType(bufferpool.PAGE_TYPE_BTREE_LEAF) + scratchPage.WritePageNumber(page.ID().Page) + freeSpaceOffset := scratchPage.ReadFreeSpaceOffset() + + slotCount := int(page.ReadSlotCount()) + + for i := 0; i < slotCount; i++ { + + // read the slot from the page + s := page.ReadPageSlot(int16(i)) + // read the chunk from the page + lbl := s.HashPayload(page) + c := lbl.GetPayloadReader(page) + + // mod the freespace offset based on the size of the chunk + freeSpaceOffset -= int16(c.Length()) + // update the values in the slot + s.PayloadOffset = freeSpaceOffset + + // write the chunk + scratchPage.WriteHashPagePayloadBytes(freeSpaceOffset, c.KeyLength, c.KeyBytes, c.PayloadChunkLength, c.PayloadChunkBytes) + //write the slot + scratchPage.WritePageSlot(int16(i), s) + + // update the free space offset + scratchPage.WriteFreeSpaceOffset(int16(freeSpaceOffset)) } + // set the new slotcounts + scratchPage.WriteSlotCount(int16(slotCount)) - // update the slot count - scratch.WriteSlotCount(page.ReadSlotCount()) + scratchPage.CopyPageTo(page) - // write scratch back to page - scratch.CopyPageTo(page) return nil } func (e *ExtendibleHashTable) keyValueWillFit(page *bufferpool.Page, key, value []byte) bool { // will this k/v fit on the page? - slotLen := 4 // we need 2 len words for the slot - chunkLen := 6 + len(key) + len(value) // int16 len + int32 len + len of respective []byte + payloadLen := page.ComputeHashPayloadTotalLength(len(key), len(value)) fs := page.FreeSpaceOnPage() - return fs > (int16(slotLen) + int16(chunkLen)) + return fs > int16(payloadLen) } func (e *ExtendibleHashTable) putKeyValue(page *bufferpool.Page, key, value []byte) error { - if !e.keyValueWillFit(page, key, value) { // try to garbage collect the page first err := e.cleanPage(page) @@ -248,20 +260,17 @@ func (e *ExtendibleHashTable) putKeyValue(page *bufferpool.Page, key, value []by slotCount := int(page.ReadSlotCount()) if found { // we found the key, so we will update the value - err := page.PutKeyValueInPageSlot(int16(newIndex), []byte(key), []byte(value)) + err := e.putKeyValueInPageSlot(page, int16(newIndex), []byte(key), []byte(value)) if err != nil { return err } } else { - // TODO(pok) should check in WriteSlot() to see if we are out space too... - - // TODO(pok) we should move all the slots in one fell swoop, because,... performance - // move all the slots after where we are going to insert + // move the slots over if needed for j := slotCount; j > newIndex; j-- { sl := page.ReadPageSlot(int16(j - 1)) page.WritePageSlot(int16(j), sl) } - err := page.PutKeyValueInPageSlot(int16(newIndex), []byte(key), []byte(value)) + err := e.putKeyValueInPageSlot(page, int16(newIndex), []byte(key), []byte(value)) if err != nil { return err } @@ -270,3 +279,40 @@ func (e *ExtendibleHashTable) putKeyValue(page *bufferpool.Page, key, value []by } return nil } + +func (e *ExtendibleHashTable) putKeyValueInPageSlot(page *bufferpool.Page, slotNumber int16, keyBytes []byte, payloadBytes []byte) error { + freeSpaceOffset := page.ReadFreeSpaceOffset() + + keyLength := len(keyBytes) + payloadChunkLength := len(payloadBytes) + + // check for overflow + if payloadChunkLength > bufferpool.MAX_PAYLOAD_CHUNK_SIZE_ON_PAGE { + return errors.New("overflow") + } + + totalPayloadSize := page.ComputeHashPayloadTotalLength(keyLength, payloadChunkLength) + + // check to make sure we don't blow free space + if page.FreeSpaceOnPage() < int16(totalPayloadSize) { + return errors.New("page is full") + } + + // compute the new free space offset + freeSpaceOffset -= int16(totalPayloadSize) + + // write the payload + page.WriteHashPagePayloadBytes(freeSpaceOffset, int16(keyLength), keyBytes, int16(payloadChunkLength), payloadBytes) + + // update the free space offset + page.WriteFreeSpaceOffset(int16(freeSpaceOffset)) + + // make a slot + slot := bufferpool.PageSlot{ + PayloadOffset: freeSpaceOffset, + } + // write the slot + page.WritePageSlot(slotNumber, slot) + + return nil +} diff --git a/extendiblehash/extendiblehash_test.go b/extendiblehash/extendiblehash_test.go index d1535a4a4..bfe04a44c 100644 --- a/extendiblehash/extendiblehash_test.go +++ b/extendiblehash/extendiblehash_test.go @@ -114,7 +114,7 @@ func TestHashTable_Get(t *testing.T) { key := "478" value := "Hi" - page.PutKeyValueInPageSlot(0, []byte(key), []byte(value)) + d.putKeyValueInPageSlot(page, 0, []byte(key), []byte(value)) page.WriteSlotCount(int16(1)) result, _, err := d.Get([]byte(key)) @@ -150,7 +150,7 @@ func TestHashTable_Put(t *testing.T) { t.Fatal(err) } defer d.bufferPool.UnpinPage(page.ID()) - err = addToPage(page, 5) + err = addToPage(d, page, 5) if err != nil { t.Fatal(err) } @@ -176,7 +176,7 @@ func TestHashTable_Put_ShouldIncreaseSize_WhenTableIsFull(t *testing.T) { t.Fatal(err) } defer d.bufferPool.UnpinPage(page.ID()) - err = addToPage(page, 227) // keys per page with key 12, value 20 + err = addToPage(d, page, 227) // keys per page with key 12, value 20 if err != nil { t.Fatal(err) } @@ -203,7 +203,7 @@ func TestHashTable_PutShouldIncrementLD_WhenPageIsFull(t *testing.T) { t.Fatal(err) } defer d.bufferPool.UnpinPage(page.ID()) - err = addToPage(page, 227) // keys per page with key 12, value 20 + err = addToPage(d, page, 227) // keys per page with key 12, value 20 if err != nil { t.Fatal(err) } @@ -241,6 +241,10 @@ func TestHashTable_Put_INT(t *testing.T) { } } + // for _, x := range d.directory { + // fmt.Printf("{ObjectID: 0, Shard: 0, Page: %d},\n", x.Page) + // } + assert.Equal(t, []bufferpool.PageID{ {ObjectID: 0, Shard: 0, Page: 0}, {ObjectID: 0, Shard: 0, Page: 1}, @@ -250,26 +254,26 @@ func TestHashTable_Put_INT(t *testing.T) { {ObjectID: 0, Shard: 0, Page: 7}, {ObjectID: 0, Shard: 0, Page: 6}, {ObjectID: 0, Shard: 0, Page: 5}, - {ObjectID: 0, Shard: 0, Page: 13}, + {ObjectID: 0, Shard: 0, Page: 12}, {ObjectID: 0, Shard: 0, Page: 15}, - {ObjectID: 0, Shard: 0, Page: 11}, + {ObjectID: 0, Shard: 0, Page: 13}, {ObjectID: 0, Shard: 0, Page: 9}, {ObjectID: 0, Shard: 0, Page: 8}, {ObjectID: 0, Shard: 0, Page: 14}, + {ObjectID: 0, Shard: 0, Page: 11}, {ObjectID: 0, Shard: 0, Page: 10}, - {ObjectID: 0, Shard: 0, Page: 12}, {ObjectID: 0, Shard: 0, Page: 28}, - {ObjectID: 0, Shard: 0, Page: 25}, - {ObjectID: 0, Shard: 0, Page: 21}, - {ObjectID: 0, Shard: 0, Page: 18}, - {ObjectID: 0, Shard: 0, Page: 4}, + {ObjectID: 0, Shard: 0, Page: 24}, + {ObjectID: 0, Shard: 0, Page: 22}, + {ObjectID: 0, Shard: 0, Page: 19}, + {ObjectID: 0, Shard: 0, Page: 30}, {ObjectID: 0, Shard: 0, Page: 20}, {ObjectID: 0, Shard: 0, Page: 29}, - {ObjectID: 0, Shard: 0, Page: 19}, + {ObjectID: 0, Shard: 0, Page: 18}, {ObjectID: 0, Shard: 0, Page: 27}, - {ObjectID: 0, Shard: 0, Page: 24}, + {ObjectID: 0, Shard: 0, Page: 25}, + {ObjectID: 0, Shard: 0, Page: 21}, {ObjectID: 0, Shard: 0, Page: 23}, - {ObjectID: 0, Shard: 0, Page: 22}, {ObjectID: 0, Shard: 0, Page: 17}, {ObjectID: 0, Shard: 0, Page: 14}, {ObjectID: 0, Shard: 0, Page: 16}, @@ -347,11 +351,11 @@ func BenchmarkHashTable_Put_Many_Keys(b *testing.B) { } } -func addToPage(page *bufferpool.Page, numberOfRecords int) error { +func addToPage(table *ExtendibleHashTable, page *bufferpool.Page, numberOfRecords int) error { for i := 0; i < numberOfRecords; i++ { //fmt.Printf("writing record %d\n", i+1) itoa := strconv.Itoa(i) - err := page.PutKeyValueInPageSlot(int16(i), []byte("key"+itoa), []byte("value foo bar")) + err := table.putKeyValueInPageSlot(page, int16(i), []byte("key"+itoa), []byte("value foo bar")) if err != nil { return err } diff --git a/tstore/btree.go b/tstore/btree.go index ec6e6008f..6ac501422 100644 --- a/tstore/btree.go +++ b/tstore/btree.go @@ -561,6 +561,7 @@ func (b *BTree) compactLeafPage(node *BTreeNode) error { scratchPage := b.bufferpool.ScratchPage() scratchPage.WritePageType(bufferpool.PAGE_TYPE_BTREE_LEAF) + scratchPage.WritePageNumber(node.page.ID().Page) freeSpaceOffset := scratchPage.ReadFreeSpaceOffset() slotCount := int(node.page.ReadSlotCount()) @@ -608,6 +609,7 @@ func (b *BTree) compactInternalPage(node *BTreeNode) error { scratchPage := b.bufferpool.ScratchPage() scratchPage.WritePageType(bufferpool.PAGE_TYPE_BTREE_INTERNAL) + scratchPage.WritePageNumber(node.page.ID().Page) freeSpaceOffset := scratchPage.ReadFreeSpaceOffset() slotCount := int(node.page.ReadSlotCount()) @@ -742,16 +744,16 @@ func (b *BTree) writeLeafEntryInSlot(node *BTreeNode, slotNumber int16, keyBytes lowWater = hiWater // set the payload chunk length to the free space on the page // less the 2 byte chunk length - payloadChunkLength := overflowFreeSpace - 2 - if payloadChunkLength > bytesRemaining { - payloadChunkLength = bytesRemaining + overflowChunkLength := overflowFreeSpace - 2 + if overflowChunkLength > bytesRemaining { + overflowChunkLength = bytesRemaining } - hiWater += payloadChunkLength + hiWater += overflowChunkLength overflowPage.page.WriteNextPointer(bufferpool.PageID{ObjectID: b.objectID, Shard: b.shard, Page: nextOverflowPtr}) overflowPage.page.WriteLeafPagePayloadBytes(bufferpool.PAGE_SLOTS_START_OFFSET, int16(payloadChunkLength), payloadBytes[lowWater:hiWater]) - bytesRemaining -= payloadChunkLength + bytesRemaining -= overflowChunkLength overflowPage = nextOverflowPage } diff --git a/tstore/btree_test.go b/tstore/btree_test.go index 86c7f7d6b..d61e56d0f 100644 --- a/tstore/btree_test.go +++ b/tstore/btree_test.go @@ -137,8 +137,8 @@ func TestAddItemsToBTreeAndValidate_VeryWide(t *testing.T) { for j, i := range inserts { rr[0] = int64(i) - for j := 0; j < numCols; j++ { - rr[j+1] = fmt.Sprintf("%04d", j) + for jn := 0; jn < numCols; jn++ { + rr[jn+1] = fmt.Sprintf("%04d", jn) } tup := &BTreeTuple{