perf: DBF mmap — zero-copy record reads, SCAN 2-3x faster
syscall.Mmap on DBF open for zero-copy GoTo: - GoTo: copy from mmap slice instead of file.ReadAt syscall - Eliminates kernel context switch per record read - Unmap before Append (file grows), Close, Flush - Re-mmap after Flush Benchmark (ext4, home dir): 10K SCAN: 4ms → 2ms (Harbour 1ms = 2x) 10K DUPKEY: 6ms → 4ms (Harbour 4ms = 1x!) 10K DELSCAN: 6ms → 2ms (Harbour 2ms = 1x!) 50K SCAN: 26ms → 15ms (42% faster) 50K DELSCAN: 29ms → 17ms (Harbour 17ms = 1x!) 50K DUPKEY: 41ms → 29ms (Harbour 23ms = 1.3x) CDX SCAN: 13ms → 4ms (Harbour 6ms — FASTER!) CDX SCOPE: 9ms → 3ms (Harbour 4ms — FASTER!) 82/82 stress PASS. All unit tests PASS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// DBFArea implements the DBF database driver.
|
||||
@@ -46,6 +47,9 @@ type DBFArea struct {
|
||||
ghost bool // at phantom record (after APPEND)
|
||||
recLoaded bool // false = recBuf stale, need loadRecord()
|
||||
|
||||
// mmap for zero-copy record reads
|
||||
mmapData []byte
|
||||
|
||||
// Memo file (FPT)
|
||||
memoFile *FPTFile
|
||||
|
||||
@@ -211,7 +215,10 @@ func openDBF(drv *DBFDriver, params hbrdd.OpenParams) (*DBFArea, error) {
|
||||
// If FPT doesn't exist, memo reads return empty string
|
||||
}
|
||||
|
||||
// Step 9: Position at first record
|
||||
// Step 9: mmap DBF for zero-copy record reads
|
||||
area.mmapDBF()
|
||||
|
||||
// Step 10: Position at first record
|
||||
area.FEof = (area.recCount == 0)
|
||||
if area.recCount > 0 {
|
||||
area.GoTo(1)
|
||||
@@ -313,10 +320,10 @@ func createDBF(drv *DBFDriver, params hbrdd.CreateParams) (*DBFArea, error) {
|
||||
func (a *DBFArea) Driver() hbrdd.Driver { return &DBFDriver{} }
|
||||
|
||||
func (a *DBFArea) Close() error {
|
||||
a.unmapDBF() // unmap before writing
|
||||
if a.dirty {
|
||||
a.flushRecord()
|
||||
}
|
||||
// Write EOF + header once on close
|
||||
a.dataFile.WriteAt([]byte{EOFMarker}, a.header.EOFOffset())
|
||||
a.updateHeader()
|
||||
if a.memoFile != nil {
|
||||
@@ -337,9 +344,12 @@ func (a *DBFArea) Flush() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
a.unmapDBF()
|
||||
a.dataFile.WriteAt([]byte{EOFMarker}, a.header.EOFOffset())
|
||||
a.updateHeader()
|
||||
return a.dataFile.Sync()
|
||||
err := a.dataFile.Sync()
|
||||
a.mmapDBF() // re-mmap after flush
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Movement ---
|
||||
@@ -386,9 +396,14 @@ func (a *DBFArea) GoTo(recNo uint32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read record from file
|
||||
// Read record — mmap fast path or file fallback
|
||||
offset := a.header.RecordOffset(recNo)
|
||||
a.dataFile.ReadAt(a.recBuf, offset)
|
||||
recLen := int(a.header.RecordLen)
|
||||
if a.mmapData != nil && int(offset)+recLen <= len(a.mmapData) {
|
||||
copy(a.recBuf, a.mmapData[offset:offset+int64(recLen)])
|
||||
} else {
|
||||
a.dataFile.ReadAt(a.recBuf, offset)
|
||||
}
|
||||
|
||||
a.recNo = recNo
|
||||
a.recLoaded = true
|
||||
@@ -507,7 +522,29 @@ func (a *DBFArea) Skip(count int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadRecord reads the current record from disk if not already loaded.
|
||||
// mmapDBF maps the DBF file for zero-copy reads. Called after open.
|
||||
func (a *DBFArea) mmapDBF() {
|
||||
fi, err := a.dataFile.Stat()
|
||||
if err != nil || fi.Size() < int64(a.header.HeaderLen) {
|
||||
return
|
||||
}
|
||||
data, err := syscall.Mmap(int(a.dataFile.Fd()), 0, int(fi.Size()),
|
||||
syscall.PROT_READ, syscall.MAP_SHARED)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
a.mmapData = data
|
||||
}
|
||||
|
||||
// unmapDBF releases the mmap.
|
||||
func (a *DBFArea) unmapDBF() {
|
||||
if a.mmapData != nil {
|
||||
syscall.Munmap(a.mmapData)
|
||||
a.mmapData = nil
|
||||
}
|
||||
}
|
||||
|
||||
// loadRecord reads the current record — from mmap or file fallback.
|
||||
func (a *DBFArea) loadRecord() {
|
||||
if a.recLoaded || a.FEof || a.recNo == 0 || a.recNo > a.recCount {
|
||||
return
|
||||
@@ -582,11 +619,15 @@ func (a *DBFArea) Append() error {
|
||||
a.flushRecord()
|
||||
}
|
||||
|
||||
// Unmap — file will grow
|
||||
if a.mmapData != nil {
|
||||
a.unmapDBF()
|
||||
}
|
||||
|
||||
a.recCount++
|
||||
a.recNo = a.recCount
|
||||
a.header.RecCount = a.recCount
|
||||
|
||||
// Clear record buffer (all spaces)
|
||||
for i := range a.recBuf {
|
||||
a.recBuf[i] = ' '
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user