diff --git a/hbrdd/dbf/dbf.go b/hbrdd/dbf/dbf.go index 01b3ebf..84eb615 100644 --- a/hbrdd/dbf/dbf.go +++ b/hbrdd/dbf/dbf.go @@ -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] = ' ' }