Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve page free/alloc performance #76

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 85 additions & 42 deletions pkg/btree/pagefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type ReadWriteSeekPager interface {
io.ReadWriteSeeker

Page(int) (int64, error)
NewPage() (int64, error)
NewPage([]byte) (int64, error)
FreePage(int64) error

PageSize() int
Expand All @@ -21,7 +21,8 @@ type PageFile struct {
pageSize int

// local cache of free pages to avoid reading from disk too often.
freePageIndexes [512]int64
freePageIndexes [512]int64
freePageHead, freePageCount int
}

var _ ReadWriteSeekPager = &PageFile{}
Expand All @@ -46,12 +47,19 @@ func NewPageFile(rws io.ReadWriteSeeker) (*PageFile, error) {
}
if err == io.EOF {
// allocate one page for the free page indexes
if _, err := rws.Write(make([]byte, pageSizeBytes)); err != nil {
if _, err := rws.Write(buf); err != nil {
return nil, err
}
} else {
for i := 0; i < len(pf.freePageIndexes); i++ {
pf.freePageIndexes[i] = int64(binary.BigEndian.Uint64(buf[i*8 : (i+1)*8]))
offset := int64(binary.BigEndian.Uint64(buf[i*8 : (i+1)*8]))
if offset != 0 {
pf.freePageIndexes[pf.freePageHead] = offset
pf.freePageHead = (pf.freePageHead + 1) % len(pf.freePageIndexes)
pf.freePageCount++
} else {
break
}
}
}
return pf, nil
Expand All @@ -65,65 +73,100 @@ func (pf *PageFile) Page(i int) (int64, error) {
return int64(i+1) * int64(pf.pageSize), nil
}

func (pf *PageFile) NewPage() (int64, error) {
// if there are free pages, return the first one
for i := 0; i < len(pf.freePageIndexes); i++ {
if pf.freePageIndexes[i] != 0 {
offset := pf.freePageIndexes[i]
// zero out this free page index on disk
if _, err := pf.ReadWriteSeeker.Seek(int64(i*8), io.SeekStart); err != nil {
return 0, err
}
if _, err := pf.ReadWriteSeeker.Write(make([]byte, 8)); err != nil {
return 0, err
}
// seek to the free page
if _, err := pf.ReadWriteSeeker.Seek(offset, io.SeekStart); err != nil {
return 0, err
}
return offset, nil
}
func (pf *PageFile) writeFreePageIndices() error {
buf := make([]byte, len(pf.freePageIndexes)*8)
tail := (pf.freePageHead - pf.freePageCount + len(pf.freePageIndexes)) % len(pf.freePageIndexes)
for i := 0; i < pf.freePageCount; i++ {
offset := pf.freePageIndexes[tail+i]
binary.BigEndian.PutUint64(buf[i*8:(i+1)*8], uint64(offset))
}
if _, err := pf.ReadWriteSeeker.Seek(0, io.SeekStart); err != nil {
return err
}
if _, err := pf.ReadWriteSeeker.Write(buf); err != nil {
return err
}
return nil
}

func (pf *PageFile) FreePageIndex() (int64, error) {
// find the first free page index and return it
if pf.freePageCount == 0 {
return -1, nil
}
// pop from the tail
tail := (pf.freePageHead - pf.freePageCount + len(pf.freePageIndexes)) % len(pf.freePageIndexes)
offset := pf.freePageIndexes[tail]
pf.freePageIndexes[tail] = 0
pf.freePageCount--

if err := pf.writeFreePageIndices(); err != nil {
return 0, err
}

return offset, nil
}

func (pf *PageFile) NewPage(buf []byte) (int64, error) {
if buf != nil && len(buf) > pf.pageSize {
return 0, errors.New("buffer is too large")
}

// seek to the end of the file
offset, err := pf.ReadWriteSeeker.Seek(0, io.SeekEnd)
// if there are free pages, return the first one
offset, err := pf.FreePageIndex()
if err != nil {
return 0, err
}
if offset != -1 {
// seek to the free page
if _, err := pf.ReadWriteSeeker.Seek(offset, io.SeekStart); err != nil {
return 0, err
}
} else {
n, err := pf.ReadWriteSeeker.Seek(0, io.SeekEnd)
if err != nil {
return 0, err
}
offset = n
}

// if the offset is not a multiple of the page size, we need to pad the file
// with zeros to the next page boundary.
var pad int64
if pf.pageSize > 0 && offset%int64(pf.pageSize) != 0 {
// Calculate the number of bytes to pad
pad := int64(pf.pageSize) - (offset % int64(pf.pageSize))
pad = int64(pf.pageSize) - (offset % int64(pf.pageSize))
// Write the padding
if _, err := pf.Write(make([]byte, pad)); err != nil {
return 0, err
}
return offset + pad, nil
}
return offset, nil
page := make([]byte, pf.pageSize)
if buf != nil {
copy(page, buf)
}
if _, err := pf.ReadWriteSeeker.Write(page); err != nil {
return 0, err
}
if _, err := pf.ReadWriteSeeker.Seek(offset, io.SeekStart); err != nil {
return 0, err
}
return offset + pad, nil
}

func (pf *PageFile) FreePage(offset int64) error {
if offset%int64(pf.pageSize) != 0 {
return errors.New("offset is not a multiple of the page size")
}
// find the last nonzero free page index and insert it after that
for i := len(pf.freePageIndexes) - 1; i >= 0; i-- {
if pf.freePageIndexes[i] == 0 {
j := (i + 1) % len(pf.freePageIndexes)
pf.freePageIndexes[j] = offset

// write the free page index to the last page
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(offset))
if _, err := pf.ReadWriteSeeker.Seek(int64(j*8), io.SeekStart); err != nil {
return err
}
return nil
}
if pf.freePageCount == len(pf.freePageIndexes) {
return errors.New("free page index is full")
}
return errors.New("too many free pages")
// push to the head
pf.freePageIndexes[pf.freePageHead] = offset
pf.freePageHead = (pf.freePageHead + 1) % len(pf.freePageIndexes)
pf.freePageCount++

return pf.writeFreePageIndices()
}

func (pf *PageFile) PageSize() int {
Expand Down
103 changes: 84 additions & 19 deletions pkg/btree/pagefile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestPageFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
offset, err := pf.NewPage()
offset, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
Expand All @@ -23,52 +23,66 @@ func TestPageFile(t *testing.T) {
}
})

t.Run("page size reuses page without allocation", func(t *testing.T) {
t.Run("page size allocates pages", func(t *testing.T) {
buf := buftest.NewSeekableBuffer()
pf, err := NewPageFile(buf)
if err != nil {
t.Fatal(err)
}
offset1, err := pf.NewPage()
offset1, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
if offset1 != pageSizeBytes {
t.Fatalf("expected offset %d, got %d", pageSizeBytes, offset1)
}
// since no data has been written, this page should be reused.
offset2, err := pf.NewPage()
// check the seek location
n, err := buf.Seek(0, io.SeekCurrent)
if err != nil {
t.Fatal(err)
}
if offset2 != pageSizeBytes {
if n != pageSizeBytes {
t.Fatalf("expected offset %d, got %d", pageSizeBytes, n)
}
offset2, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
if offset2 != pageSizeBytes*2 {
t.Fatalf("expected offset %d, got %d", pageSizeBytes*2, offset2)
}
m, err := buf.Seek(0, io.SeekCurrent)
if err != nil {
t.Fatal(err)
}
if m != pageSizeBytes*2 {
t.Fatalf("expected offset %d, got %d", pageSizeBytes*2, m)
}
})

t.Run("page size allocates second page", func(t *testing.T) {
t.Run("page size allocates page with data", func(t *testing.T) {
buf := buftest.NewSeekableBuffer()
pf, err := NewPageFile(buf)
if err != nil {
t.Fatal(err)
}
offset1, err := pf.NewPage()
data := []byte("hello")
offset1, err := pf.NewPage(data)
if err != nil {
t.Fatal(err)
}
if offset1 != pageSizeBytes {
t.Fatalf("expected offset %d, got %d", pageSizeBytes, offset1)
}
// need to write at least one byte to trigger a new page.
if _, err := pf.Write(make([]byte, 1)); err != nil {
if _, err := pf.Seek(offset1, io.SeekStart); err != nil {
t.Fatal(err)
}
offset2, err := pf.NewPage()
if err != nil {
buf2 := make([]byte, len(data))
if _, err := pf.Read(buf2); err != nil {
t.Fatal(err)
}
if offset2 != pageSizeBytes*2 {
t.Fatalf("expected offset %d, got %d", pageSizeBytes*2, offset2)
if string(buf2) != string(data) {
t.Fatalf("expected %s, got %s", string(data), string(buf2))
}
})

Expand All @@ -78,7 +92,7 @@ func TestPageFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
offset1, err := pf.NewPage()
offset1, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
Expand All @@ -97,7 +111,7 @@ func TestPageFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
offset1, err := pf.NewPage()
offset1, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
Expand All @@ -108,23 +122,74 @@ func TestPageFile(t *testing.T) {
if _, err := pf.Write(make([]byte, 1)); err != nil {
t.Fatal(err)
}
offset2, err := pf.NewPage()
offset2, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
if offset2 != pageSizeBytes*2 {
t.Fatalf("expected offset %d, got %d", pageSizeBytes, offset2)
t.Fatalf("expected offset %d, got %d", 2*pageSizeBytes, offset2)
}

if err := pf.FreePage(offset1); err != nil {
t.Fatal(err)
}
offset3, err := pf.NewPage()
offset3, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
if offset3 != offset1 {
t.Fatalf("expected offset %d, got %d", offset2, offset3)
}
})

t.Run("free page behaves like a circular buffer", func(t *testing.T) {
buf := buftest.NewSeekableBuffer()
pf, err := NewPageFile(buf)
if err != nil {
t.Fatal(err)
}
offsets := make([]int64, 0, 10)
for i := 0; i < 10; i++ {
offset, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
if i > 0 && offset != offsets[i-1]+pageSizeBytes {
t.Fatalf("expected offset %d, got %d", offsets[i-1]+pageSizeBytes, offset)
}
offsets = append(offsets, offset)
}
for i := 0; i < 10; i++ {
if err := pf.FreePage(offsets[i]); err != nil {
t.Fatal(err)
}
}
for i := 0; i < 10; i++ {
offset, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
if offset != offsets[i] {
t.Fatalf("expected offset %d, got %d", offsets[i], offset)
}
}
})

t.Run("cannot double free a page", func(t *testing.T) {
buf := buftest.NewSeekableBuffer()
pf, err := NewPageFile(buf)
if err != nil {
t.Fatal(err)
}
offset, err := pf.NewPage(nil)
if err != nil {
t.Fatal(err)
}
if err := pf.FreePage(offset); err != nil {
t.Fatal(err)
}
if err := pf.FreePage(offset); err == nil {
t.Fatal("expected error")
}
})
}
Loading