go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/iotools/bufferingreaderat.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package iotools
    16  
    17  import (
    18  	"container/list"
    19  	"fmt"
    20  	"io"
    21  	"sync"
    22  )
    23  
    24  // block is a contiguous chunk of data from the original file.
    25  type block struct {
    26  	offset int64
    27  	data   []byte
    28  	last   bool
    29  }
    30  
    31  // blocksLRU is LRU of last read blocks, keyed by their offset in the file.
    32  type blocksLRU struct {
    33  	capacity int         // how many blocks we want to cache
    34  	evicted  func(block) // called for each evicted block
    35  
    36  	blocks map[int64]*list.Element
    37  	ll     list.List // each element's Value is block{}
    38  }
    39  
    40  // init initializes LRU guts.
    41  func (l *blocksLRU) init(capacity int) {
    42  	l.capacity = capacity
    43  	l.blocks = make(map[int64]*list.Element, capacity)
    44  	l.ll.Init()
    45  }
    46  
    47  // get returns (block, true) if it's in the cache or (block{}, false) otherwise.
    48  func (l *blocksLRU) get(offset int64) (b block, ok bool) {
    49  	elem, ok := l.blocks[offset]
    50  	if !ok {
    51  		return block{}, false
    52  	}
    53  	l.ll.MoveToFront(elem)
    54  	return elem.Value.(block), true
    55  }
    56  
    57  // prepareForAdd removes oldest item from the list if it is at the capacity.
    58  //
    59  // Does nothing if it's not yet full.
    60  func (l *blocksLRU) prepareForAdd() {
    61  	switch {
    62  	case l.ll.Len() > l.capacity:
    63  		panic("impossible")
    64  	case l.ll.Len() == l.capacity:
    65  		oldest := l.ll.Remove(l.ll.Back()).(block)
    66  		delete(l.blocks, oldest.offset)
    67  		l.evicted(oldest)
    68  	}
    69  }
    70  
    71  // add adds a block to the cache (perhaps evicting the oldest block).
    72  //
    73  // The caller must verify there's no such block in the cache already using
    74  // get(). Panics if it wasn't done.
    75  func (l *blocksLRU) add(b block) {
    76  	if _, ok := l.blocks[b.offset]; ok {
    77  		panic(fmt.Sprintf("block with offset %d is already in the LRU", b.offset))
    78  	}
    79  	l.prepareForAdd()
    80  	l.blocks[b.offset] = l.ll.PushFront(b)
    81  }
    82  
    83  type bufferingReaderAt struct {
    84  	l sync.Mutex
    85  	r io.ReaderAt
    86  
    87  	blockSize int
    88  	lru       blocksLRU
    89  
    90  	// Available buffers of blockSize length. Note that docs warn that sync.Pool
    91  	// is too heavy for this case and it's better to roll our own mini pool.
    92  	pool [][]byte
    93  }
    94  
    95  // NewBufferingReaderAt returns an io.ReaderAt that reads data in blocks of
    96  // configurable size and keeps LRU of recently read blocks.
    97  //
    98  // It is great for cases when data is read sequentially from an io.ReaderAt,
    99  // (e.g. when extracting files using zip.Reader), since by setting large block
   100  // size we can effectively do lookahead reads.
   101  //
   102  // For example, zip.Reader reads data in 4096 byte chunks by default. By setting
   103  // block size to 512Kb and LRU size to 1 we reduce the number of read operations
   104  // significantly (128x), in exchange for the modest amount of RAM.
   105  //
   106  // The reader is safe to user concurrently (just like any ReaderAt), but beware
   107  // that the LRU is shared and all reads from the underlying reader happen under
   108  // the lock, so multiple goroutines may end up slowing down each other.
   109  func NewBufferingReaderAt(r io.ReaderAt, blockSize int, lruSize int) io.ReaderAt {
   110  	if blockSize < 1 {
   111  		panic(fmt.Sprintf("block size should be >= 1, not %d", blockSize))
   112  	}
   113  	if lruSize < 1 {
   114  		panic(fmt.Sprintf("lru size should be >= 1, not %d", lruSize))
   115  	}
   116  	reader := &bufferingReaderAt{
   117  		r:         r,
   118  		blockSize: blockSize,
   119  		// We actually pool at most 1 buffer, since buffers are grabbed from the
   120  		// pool immediately after they are evicted from LRU.
   121  		pool: make([][]byte, 0, 1),
   122  	}
   123  	reader.lru.init(lruSize)
   124  	reader.lru.evicted = func(b block) { reader.recycleBuf(b.data) }
   125  	return reader
   126  }
   127  
   128  // grabBuf returns a byte slice of blockSize size.
   129  func (r *bufferingReaderAt) grabBuf() []byte {
   130  	if len(r.pool) != 0 {
   131  		b := r.pool[len(r.pool)-1]
   132  		r.pool = r.pool[:len(r.pool)-1]
   133  		return b
   134  	}
   135  	return make([]byte, r.blockSize)
   136  }
   137  
   138  // recycleBuf is called when the buffer is no longer needed to put it for reuse.
   139  func (r *bufferingReaderAt) recycleBuf(b []byte) {
   140  	if cap(b) != r.blockSize {
   141  		panic("trying to return a buffer not initially requested via grabBuf")
   142  	}
   143  	if len(r.pool)+1 > cap(r.pool) {
   144  		panic("unexpected growth of byte buffer pool beyond capacity")
   145  	}
   146  	r.pool = append(r.pool, b[:cap(b)])
   147  }
   148  
   149  // readBlock returns the block of the file (of blockSize size) at an offset.
   150  //
   151  // Assumes the caller does not retain the returned buffer (just reads from it
   152  // and forgets it right away, all under the lock).
   153  //
   154  // Returns one of:
   155  //
   156  //	(full block, nil) on success
   157  //	(partial or full block, io.EOF) when reading the final block
   158  //	(partial block, err) on read errors
   159  func (r *bufferingReaderAt) readBlock(offset int64) (data []byte, err error) {
   160  	// Have it cached already?
   161  	if b, ok := r.lru.get(offset); ok {
   162  		data = b.data
   163  		if b.last {
   164  			err = io.EOF
   165  		}
   166  		return
   167  	}
   168  
   169  	// Kick out the oldest block (if any) to move its buffer to the free buffers
   170  	// pool and then immediately grab this buffer.
   171  	r.lru.prepareForAdd()
   172  	data = r.grabBuf()
   173  
   174  	// Read the block from the underlying reader.
   175  	read, err := r.r.ReadAt(data, offset)
   176  	data = data[:read]
   177  
   178  	// ReadAt promises that it returns nil only if it read the full block. We rely
   179  	// on this later, so double check.
   180  	if err == nil && read != r.blockSize {
   181  		panic(fmt.Sprintf("broken ReaderAt: should have read %d bytes, but read only %d", r.blockSize, read))
   182  	}
   183  
   184  	// Cache fully read blocks and the partially read last block, but skip blocks
   185  	// that were read partially due to unexpected errors.
   186  	if err == nil || err == io.EOF {
   187  		r.lru.add(block{
   188  			offset: offset,
   189  			data:   data,
   190  			last:   err == io.EOF,
   191  		})
   192  	} else {
   193  		// Caller promises not to retain 'data', so we can return it right away.
   194  		r.recycleBuf(data)
   195  	}
   196  
   197  	return data, err
   198  }
   199  
   200  // ReadAt implements io.ReaderAt interface.
   201  func (r *bufferingReaderAt) ReadAt(p []byte, offset int64) (read int, err error) {
   202  	if len(p) == 0 {
   203  		return r.r.ReadAt(p, offset)
   204  	}
   205  
   206  	r.l.Lock()
   207  	defer r.l.Unlock()
   208  
   209  	bs := int64(r.blockSize)
   210  	blockOff := int64((offset / bs) * bs) // block-aligned offset
   211  
   212  	// Sequentially read blocks that intersect with the requested segment.
   213  	for {
   214  		// err here may be EOF or some other error. We consume all data first and
   215  		// deal with errors later.
   216  		data, err := r.readBlock(blockOff)
   217  
   218  		// The first block may be read from the middle, since 'min' may be less than
   219  		// 'offset'.
   220  		if offset > blockOff {
   221  			pos := offset - blockOff // position inside the block to read from
   222  			if pos < int64(len(data)) {
   223  				data = data[pos:] // grab the tail of the block
   224  			} else {
   225  				data = nil // we probably hit EOF before the requested offset
   226  			}
   227  		}
   228  
   229  		// 'copy' copies min of len(data) and whatever space is left in 'p', so this
   230  		// is always safe. The last block may be copied partially (if there's no
   231  		// space left in 'p').
   232  		read += copy(p[read:], data)
   233  
   234  		switch {
   235  		case read == len(p):
   236  			// We managed to read everything we wanted, ignore the last error, if any.
   237  			return read, nil
   238  		case err != nil:
   239  			return read, err
   240  		}
   241  
   242  		// The last read was successful. Per ReaderAt contract (that we double
   243  		// checked in readBlock) it means it read ALL requested data (and we request
   244  		// 'bs' bytes). So move on to the next block.
   245  		blockOff += bs
   246  	}
   247  }