lab.nexedi.com/kirr/go123@v0.0.0-20240207185015-8299741fa871/xbufio/seqbuf_ioat.go (about)

     1  // Copyright (C) 2017  Nexedi SA and Contributors.
     2  //                     Kirill Smelkov <kirr@nexedi.com>
     3  //
     4  // This program is free software: you can Use, Study, Modify and Redistribute
     5  // it under the terms of the GNU General Public License version 3, or (at your
     6  // option) any later version, as published by the Free Software Foundation.
     7  //
     8  // You can also Link and Combine this program with other software covered by
     9  // the terms of any of the Free Software licenses or any of the Open Source
    10  // Initiative approved licenses and Convey the resulting work. Corresponding
    11  // source of such a combination shall include the source code for all other
    12  // software used.
    13  //
    14  // This program is distributed WITHOUT ANY WARRANTY; without even the implied
    15  // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    16  //
    17  // See COPYING file for full licensing terms.
    18  // See https://www.nexedi.com/licensing for rationale and options.
    19  
    20  package xbufio
    21  // buffering for io.ReaderAt optimized for sequential access
    22  
    23  import (
    24  	"io"
    25  	//"log"
    26  )
    27  
    28  // SeqReaderAt implements buffering for a io.ReaderAt optimized for sequential access.
    29  //
    30  // Both forward, backward and interleaved forward/backward access patterns are supported
    31  //
    32  // NOTE SeqReaderAt is not safe to use from multiple goroutines concurrently.
    33  //
    34  //	Strictly speaking this goes against io.ReaderAt interface but sequential
    35  //	workloads usually mean sequential processing. It would be a pity to
    36  //	add mutex for nothing.
    37  type SeqReaderAt struct {
    38  	// buffer for data at pos. cap(buf) - whole buffer capacity
    39  	buf []byte
    40  	pos int64
    41  
    42  	posLastAccess   int64 // position of last access request
    43  	posLastFwdAfter int64 // position of last forward access request
    44  	posLastBackward int64 // position of last backward access request
    45  
    46  	r io.ReaderAt
    47  
    48  	// debug: for ioReadAt tracing
    49  	//posLastIO	int64
    50  }
    51  
    52  const defaultSeqBufSize = 8192
    53  
    54  // NewSeqReaderAt wraps r with SeqReaderAt with buffer of default size.
    55  func NewSeqReaderAt(r io.ReaderAt) *SeqReaderAt {
    56  	return NewSeqReaderAtSize(r, defaultSeqBufSize)
    57  }
    58  
    59  // NewSeqReaderAtSize wraps r with SeqReaderAt with buffer of specified size.
    60  func NewSeqReaderAtSize(r io.ReaderAt, size int) *SeqReaderAt {
    61  	sb := &SeqReaderAt{r: r, buf: make([]byte, 0, size)} // all positions are zero initially
    62  	return sb
    63  }
    64  
    65  // // XXX temp
    66  // func init() {
    67  // 	log.SetFlags(0)
    68  // }
    69  
    70  // debug helper for sb.r.ReadAt
    71  func (sb *SeqReaderAt) ioReadAt(p []byte, pos int64) (int, error) {
    72  	/*
    73  	verb := "read"
    74  	if len(p) > cap(sb.buf) {
    75  		verb = "READ"
    76  	}
    77  	log.Printf("%s\t[%v, %v)\t#%v\tIO%+d", verb, pos, pos + len64(p), len(p), pos - sb.posLastIO)
    78  	sb.posLastIO = pos
    79  	*/
    80  	return sb.r.ReadAt(p, pos)
    81  }
    82  
    83  func (sb *SeqReaderAt) ReadAt(p []byte, pos int64) (int, error) {
    84  	//log.Printf("access\t[%v, %v)\t#%v\t@%+d", pos, pos + len64(p), len(p), pos - sb.posLastAccess)
    85  
    86  	// read-in last access positions and update them in *sb with current ones for next read
    87  	posLastAccess := sb.posLastAccess
    88  	posLastFwdAfter := sb.posLastFwdAfter
    89  	posLastBackward := sb.posLastBackward
    90  	sb.posLastAccess = pos
    91  	if pos >= posLastAccess {
    92  		sb.posLastFwdAfter = pos + len64(p)
    93  	} else {
    94  		sb.posLastBackward = pos
    95  	}
    96  
    97  	// if request size > buffer - read data directly
    98  	if len(p) > cap(sb.buf) {
    99  		// no copying from sb.buf here at all as if e.g. we could copy from sb.buf, the
   100  		// kernel can copy the same data from pagecache as well, and it will take the same time
   101  		// because for data in sb.buf corresponding page in pagecache has high p. to be hot.
   102  		return sb.ioReadAt(p, pos)
   103  	}
   104  
   105  	var nhead int // #data read from buffer for p head
   106  	var ntail int // #data read from buffer for p tail
   107  
   108  	// try to satisfy read request via (partly) reading from buffer
   109  
   110  	// use buffered data: head(p)
   111  	if sb.pos <= pos && pos < sb.pos + len64(sb.buf) {
   112  		nhead = copy(p, sb.buf[pos - sb.pos:]) // NOTE len(p) can be < len(sb[copyPos:])
   113  
   114  		// if all was read from buffer - we are done
   115  		if nhead == len(p) {
   116  			return nhead, nil
   117  		}
   118  
   119  		p = p[nhead:]
   120  		pos += int64(nhead)
   121  
   122  	// empty request (possibly not hitting buffer - do not let it go to real IO path)
   123  	// `len(p) != 0` is also needed for backward reading from buffer, so this condition goes before
   124  	} else if len(p) == 0 {
   125  		return 0, nil
   126  
   127  	// use buffered data: tail(p)
   128  	} else if posAfter := pos + len64(p);
   129  		sb.pos < posAfter && posAfter <= sb.pos + len64(sb.buf) {
   130  		// here we know pos < sb.pos
   131  		//
   132  		// proof: consider if pos >= sb.pos.
   133  		// Then from `pos <= sb.pos + len(sb.buf) - len(p)` above it follow that:
   134  		//   `pos < sb.pos + len(sb.buf)`  (NOTE strictly < because len(p) > 0)
   135  		// and we come to condition which is used in `start + forward` if
   136  		ntail = copy(p[sb.pos - pos:], sb.buf) // NOTE ntail == len(p[sb.pos - pos:])
   137  
   138  		// NOTE no return here: full p read is impossible for backward
   139  		// p filling: it would mean `pos = sb.pos` which in turn means
   140  		// the condition for forward buf reading must have been triggered.
   141  
   142  		p = p[:sb.pos - pos]
   143  		// pos stays the same
   144  	}
   145  
   146  
   147  	// here we need to refill the buffer. determine range to read by current IO direction.
   148  	// NOTE len(p) <= cap(sb.buf)
   149  	var xpos int64 // position for new IO request
   150  
   151  	if pos >= posLastAccess {
   152  		// forward
   153  		xpos = pos
   154  
   155  		// if forward trend continues and buffering can be made adjacent to
   156  		// previous forward access - shift reading down right to after it.
   157  		xLastFwdAfter := posLastFwdAfter + int64(nhead)	// adjusted for already read from buffer
   158  		if xLastFwdAfter <= xpos && xpos + len64(p) <= xLastFwdAfter + cap64(sb.buf) {
   159  			xpos = xLastFwdAfter
   160  		}
   161  
   162  		// NOTE no symmetry handling for "alternatively" in backward case
   163  		// because symmetry would be:
   164  		//
   165  		//	← →   ← →
   166  		//	3 4   2 1
   167  		//
   168  		// but buffer for 4 already does not overlap 3 as for
   169  		// non-trendy forward reads buffer always grows forward.
   170  
   171  	} else {
   172  		// backward
   173  		xpos = pos
   174  
   175  		// if backward trend continues and buffering would overlap with
   176  		// previous backward access - shift reading up right to it.
   177  		xLastBackward := posLastBackward - int64(ntail)	// adjusted for already read from buffer
   178  		if xpos < xLastBackward && xLastBackward < xpos + cap64(sb.buf) {
   179  			xpos = max64(xLastBackward, xpos + len64(p)) - cap64(sb.buf)
   180  
   181  		// alternatively even if backward trend does not continue anymore
   182  		// but if this will overlap with last access range, probably
   183  		// it is better (we are optimizing for sequential access) to
   184  		// shift loading region down not to overlap. example:
   185  		//
   186  		//	← →   ← →
   187  		//	2 1   4 3
   188  		//
   189  		// here we do not want 4'th buffer to overlap with 3
   190  		} else if xpos + cap64(sb.buf) > posLastAccess {
   191  			xpos = max64(posLastAccess, xpos + len64(p)) - cap64(sb.buf)
   192  		}
   193  
   194  		// don't let reading go beyond start of the file
   195  		xpos = max64(xpos, 0)
   196  	}
   197  
   198  	nn, err := sb.ioReadAt(sb.buf[:cap(sb.buf)], xpos)
   199  
   200  	// even if there was an error, or data partly read, we cannot retain
   201  	// the old buf content as io.ReaderAt can use whole buf as scratch space
   202  	sb.pos = xpos
   203  	sb.buf = sb.buf[:nn]
   204  
   205  	// here we know:
   206  	// - some data was read
   207  	// - in case of successful read pos/p lies completely inside sb.pos/sb.buf
   208  
   209  	// copy loaded data from buffer to p
   210  	pBufOffset := pos - xpos // offset corresponding to p in sb.buf
   211  	if pBufOffset >= len64(sb.buf) {
   212  		// this can be only due to some IO error
   213  
   214  		// if original request was narrower than buffer try to satisfy
   215  		// it once again directly
   216  		if pos != xpos {
   217  			nn, err = sb.ioReadAt(p, pos)
   218  			if nn < len(p) {
   219  				return nhead + nn, err
   220  			}
   221  			return nhead + nn + ntail, nil	// request fully satisfied - we can ignore error
   222  		}
   223  
   224  		// Just return the error
   225  		return nhead, err
   226  	}
   227  	nn = copy(p, sb.buf[pBufOffset:])
   228  	if nn < len(p) {
   229  		// some error - do not account tail - we did not get to it
   230  		return nhead + nn, err
   231  	}
   232  
   233  	// all ok
   234  	// NOTE if there was an error - we can skip it if original read request was completely satisfied
   235  	// NOTE not preserving EOF at ends - not required per ReaderAt interface
   236  	return nhead + nn + ntail, nil
   237  }
   238  
   239  
   240  // utilities:
   241  
   242  // len and cap as int64 (we frequently need them and int64 is covering int so
   243  // the conversion is not lossy)
   244  func len64(b []byte) int64 { return int64(len(b)) }
   245  func cap64(b []byte) int64 { return int64(cap(b)) }
   246  
   247  // min/max
   248  func min64(a, b int64) int64 { if a < b { return a } else { return b} }
   249  func max64(a, b int64) int64 { if a > b { return a } else { return b} }