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 }