mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 17:30:42 +01:00
A ranged GET whose first read lands within SeqTolerance of offset 0 is judged sequential immediately, and even a far-starting range could flip back mid-request; either way readChunkSliceAt downloads each covered chunk in full, multiplying disk reads for small ranged reads (measured ~7x). Pin random mode for ranged requests no larger than SeqTolerance so all of the request's buffer reads stay range fetches. Larger ranges keep the dynamic pattern, where whole-chunk fetches amortize. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
78 lines
2.7 KiB
Go
78 lines
2.7 KiB
Go
package filer
|
|
|
|
import (
|
|
"sync/atomic"
|
|
)
|
|
|
|
type ReaderPattern struct {
|
|
isSequentialCounter int64
|
|
readFrontier int64 // highest (offset+size) observed across reads
|
|
pinnedRandom int64 // set when the caller already knows the reads are a partial range
|
|
}
|
|
|
|
const ModeChangeLimit = 3
|
|
|
|
// SeqTolerance: a read whose start is within this many bytes of the current read
|
|
// frontier still counts as sequential. Using a tolerance window rather than
|
|
// strict contiguity absorbs reordered/concurrent readahead (multiple ReadAt can
|
|
// be in flight at once) while still rejecting far random jumps.
|
|
const SeqTolerance = 8 << 20 // 8 MiB
|
|
|
|
// For streaming read: only cache the first chunk
|
|
// For random read: only fetch the requested range, instead of the whole chunk
|
|
|
|
func NewReaderPattern() *ReaderPattern {
|
|
return &ReaderPattern{
|
|
isSequentialCounter: 0,
|
|
readFrontier: 0,
|
|
}
|
|
}
|
|
|
|
func (rp *ReaderPattern) MonitorReadAt(offset int64, size int) {
|
|
// Advance the frontier to max(frontier, offset+size) and capture, in the same
|
|
// CAS loop, the pre-image this read is judged against. Reading the frontier
|
|
// inside the loop (rather than once up front) keeps `diff` below comparing
|
|
// against the freshest value even if a concurrent readahead advances the
|
|
// frontier while we loop. Lock-free, consistent with the rest of this type.
|
|
end := offset + int64(size)
|
|
var frontier int64
|
|
for {
|
|
frontier = atomic.LoadInt64(&rp.readFrontier)
|
|
if end <= frontier || atomic.CompareAndSwapInt64(&rp.readFrontier, frontier, end) {
|
|
break
|
|
}
|
|
}
|
|
|
|
// near = this read starts within SeqTolerance of where reads had reached.
|
|
// Hysteresis (the ±ModeChangeLimit counter) keeps a single outlier read from
|
|
// flipping the mode.
|
|
diff := offset - frontier
|
|
if diff < 0 {
|
|
diff = -diff
|
|
}
|
|
counter := atomic.LoadInt64(&rp.isSequentialCounter)
|
|
if diff <= SeqTolerance {
|
|
if counter < ModeChangeLimit {
|
|
atomic.AddInt64(&rp.isSequentialCounter, 1)
|
|
}
|
|
} else if counter <= 0 {
|
|
// Entering random mode is a strong verdict: drop to the bottom of
|
|
// the window so the contiguous tail of one ranged request cannot
|
|
// flip it back on the next buffer read and pay a whole-chunk fetch.
|
|
atomic.StoreInt64(&rp.isSequentialCounter, -ModeChangeLimit)
|
|
} else {
|
|
atomic.AddInt64(&rp.isSequentialCounter, -1)
|
|
}
|
|
}
|
|
|
|
func (rp *ReaderPattern) IsRandomMode() bool {
|
|
return atomic.LoadInt64(&rp.pinnedRandom) != 0 || atomic.LoadInt64(&rp.isSequentialCounter) < 0
|
|
}
|
|
|
|
// PinRandomMode pins the pattern to random for readers that already know
|
|
// they serve a partial range, e.g. a small ranged GET: contiguous buffer
|
|
// reads within the request must not escalate into whole-chunk fetches.
|
|
func (rp *ReaderPattern) PinRandomMode() {
|
|
atomic.StoreInt64(&rp.pinnedRandom, 1)
|
|
}
|