Skip to content

Commit 4a54e79

Browse files
MagicalTuxclaude
andcommitted
Optimize idle downloader for better performance
- Optimize firstMissing() using bitmap iterator instead of O(n) linear scan Now efficiently finds first gap in downloaded blocks by iterating through set bits and detecting where the sequence breaks - Download multiple consecutive blocks per idle cycle (up to ~1 second) Previously downloaded only one 64KB block per second when idle Now streams consecutive blocks until time limit is reached - Batch savePart() calls - save once at end of idle cycle instead of after every single block, reducing disk I/O significantly - Reuse buffer across block downloads instead of allocating per block - Add ingestDataBatch() for batch operations without immediate disk save 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d4081a1 commit 4a54e79

4 files changed

Lines changed: 105 additions & 91 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,3 @@ On close:
214214
## TODO
215215

216216
- Add support for range invalidation (bad checksum causes re-download of affected area)
217-
- Refactor idle downloader for better performance

client.go

Lines changed: 67 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,9 @@ func (dl *dlClient) ReadAt(p []byte, off int64) (int, error) {
159159
}
160160

161161
// idleTaskRun is called during idle periods to download missing blocks
162-
// in the background. It runs in a separate goroutine.
162+
// in the background. It runs in a separate goroutine and downloads multiple
163+
// consecutive blocks until approximately 1 second has elapsed.
163164
func (dl *dlClient) idleTaskRun() {
164-
// this is run in a separate process
165-
166165
defer func() {
167166
atomic.AddUintptr(&dl.taskCnt, ^uintptr(0))
168167
atomic.AddUintptr(&dl.dlm.taskCnt, ^uintptr(0))
@@ -181,90 +180,89 @@ func (dl *dlClient) idleTaskRun() {
181180
// increase timer now to avoid deletion
182181
dl.expire = time.Now().Add(time.Minute)
183182

184-
if dl.reader != nil {
185-
cnt := dl.handler.wantsFollowing(dl.rPos)
186-
if cnt > 0 {
183+
startTime := time.Now()
184+
blocksDownloaded := 0
185+
blkSize := dl.handler.getBlockSize()
186+
buf := make([]byte, blkSize)
187+
188+
// Helper to save progress if we downloaded anything
189+
defer func() {
190+
if blocksDownloaded > 0 {
191+
dl.handler.savePart()
192+
dl.dlm.logf("idle: downloaded %d blocks in %v", blocksDownloaded, time.Since(startTime))
193+
}
194+
}()
195+
196+
// Download blocks until ~1 second has passed
197+
for time.Since(startTime) < time.Second {
198+
// Check if we have an existing reader at a useful position
199+
if dl.reader != nil {
200+
cnt := dl.handler.wantsFollowing(dl.rPos)
201+
if cnt <= 0 {
202+
// Current position already downloaded, close and find new position
203+
dl.reader.Body.Close()
204+
dl.reader = nil
205+
continue
206+
}
207+
208+
// Read the block
187209
rPos := dl.rPos
188-
// let's just read this from existing reader
189-
buf := make([]byte, cnt)
190-
n, err := io.ReadFull(dl.reader.Body, buf)
210+
readBuf := buf[:cnt]
211+
n, err := io.ReadFull(dl.reader.Body, readBuf)
191212
if err != nil && err != io.ErrUnexpectedEOF {
192213
dl.dlm.logf("idle read failed: %s", err)
193214
dl.reader.Body.Close()
194215
dl.reader = nil
216+
if n == 0 {
217+
continue
218+
}
195219
}
196220
dl.rPos += int64(n)
197221

198-
// feed it
199-
err = dl.handler.ingestData(buf[:n], rPos)
222+
// Ingest without saving (we'll save once at the end)
223+
err = dl.handler.ingestDataBatch(readBuf[:n], rPos)
200224
if err != nil {
201225
dl.dlm.logf("idle write failed: %s", err)
202226
dl.failure = true
227+
return
203228
}
204-
return
229+
blocksDownloaded++
230+
continue
205231
}
206232

207-
dl.reader.Body.Close()
208-
dl.reader = nil
209-
}
210-
211-
// let's just ask where to start
212-
off := dl.handler.firstMissing()
213-
if off < 0 {
214-
// do not download
215-
if dl.handler.isComplete() {
216-
dl.complete = true
233+
// No reader, find first missing block
234+
off := dl.handler.firstMissing()
235+
if off < 0 {
236+
if dl.handler.isComplete() {
237+
dl.complete = true
238+
}
239+
return
217240
}
218-
return
219-
}
220-
221-
// spawn a new reader
222-
req, err := http.NewRequest("GET", dl.url, nil)
223-
if err != nil {
224-
dl.dlm.logf("idle: failed to create request: %s", err)
225-
return
226-
}
227-
228-
if off != 0 {
229-
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", off))
230-
}
231-
232-
dl.dlm.logf("idle: initializing HTTP connection download at byte %d~", off)
233241

234-
// should respond with code 206 Partial Content
235-
resp, err := dl.dlm.Client.Do(req)
236-
if err != nil {
237-
dl.dlm.logf("idle download failed: %s", err)
238-
return
239-
}
240-
if resp.StatusCode > 299 {
241-
// that's bad
242-
resp.Body.Close()
243-
dl.dlm.logf("idle download failed due to status %s", resp.Status)
244-
return
245-
}
246-
dl.reader = resp
247-
dl.rPos = off
242+
// Create new HTTP request
243+
req, err := http.NewRequest("GET", dl.url, nil)
244+
if err != nil {
245+
dl.dlm.logf("idle: failed to create request: %s", err)
246+
return
247+
}
248248

249-
cnt := dl.handler.wantsFollowing(off)
250-
if cnt <= 0 {
251-
// why?
252-
return
253-
}
249+
if off != 0 {
250+
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", off))
251+
}
254252

255-
buf := make([]byte, cnt)
256-
n, err := io.ReadFull(dl.reader.Body, buf)
257-
if err != nil && err != io.ErrUnexpectedEOF {
258-
dl.dlm.logf("idle read failed: %s", err)
259-
dl.reader.Body.Close()
260-
dl.reader = nil
261-
}
262-
dl.rPos += int64(n)
253+
dl.dlm.logf("idle: initializing HTTP connection download at byte %d~", off)
263254

264-
// feed it (use separate thread to avoid deadlock)
265-
err = dl.handler.ingestData(buf[:n], off)
266-
if err != nil {
267-
dl.dlm.logf("idle write failed: %s", err)
268-
dl.failure = true
255+
resp, err := dl.dlm.Client.Do(req)
256+
if err != nil {
257+
dl.dlm.logf("idle download failed: %s", err)
258+
return
259+
}
260+
if resp.StatusCode > 299 {
261+
resp.Body.Close()
262+
dl.dlm.logf("idle download failed due to status %s", resp.Status)
263+
return
264+
}
265+
dl.reader = resp
266+
dl.rPos = off
269267
}
270268
}

idle.go

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,26 +67,54 @@ func (f *File) firstMissing() int64 {
6767
return -1 // can't be helped
6868
}
6969

70-
// get number of blocks
7170
blkCount := f.getBlockCount()
7271

73-
// find out first missing block
74-
// TODO this can probably be optimized, roaring api may have something
75-
for i := int64(0); i < blkCount; i++ {
76-
if !f.status.Contains(uint32(i)) {
77-
return i * f.blkSize
72+
// If bitmap is empty, first missing is block 0
73+
if f.status.IsEmpty() {
74+
return 0
75+
}
76+
77+
// Use iterator to efficiently find first gap in downloaded blocks.
78+
// The iterator returns set bits in ascending order, so we look for
79+
// the first position where the expected sequence breaks.
80+
it := f.status.Iterator()
81+
expected := uint32(0)
82+
83+
for it.HasNext() {
84+
val := it.Next()
85+
if val > expected {
86+
// Found a gap - 'expected' is the first missing block
87+
return int64(expected) * f.blkSize
7888
}
89+
expected = val + 1
90+
if int64(expected) >= blkCount {
91+
// We've covered all blocks we need
92+
break
93+
}
94+
}
95+
96+
// Check if there are blocks after the last downloaded block
97+
if int64(expected) < blkCount {
98+
return int64(expected) * f.blkSize
7999
}
80100

81-
// ?????
82-
// did we have more blocks in status than we need? did file size change? this sounds like everything is likely corrupted...
83101
return -1
84102
}
85103

86104
// ingestData writes a block of data to the local file at the specified offset.
87105
// The offset must be block-aligned and the data must be exactly one block in size
88-
// (or the correct size for the final block).
106+
// (or the correct size for the final block). This method saves progress to disk.
89107
func (f *File) ingestData(b []byte, offset int64) error {
108+
if err := f.ingestDataBatch(b, offset); err != nil {
109+
return err
110+
}
111+
f.savePart()
112+
return nil
113+
}
114+
115+
// ingestDataBatch writes a block of data without saving progress to disk.
116+
// Use this for batch operations, then call savePart() once at the end.
117+
func (f *File) ingestDataBatch(b []byte, offset int64) error {
90118
if !f.hasSize {
91119
return errors.New("invalid operation, file size unknown")
92120
}
@@ -125,11 +153,9 @@ func (f *File) ingestData(b []byte, offset int64) error {
125153
return err
126154
}
127155

128-
// mark blocks as received
156+
// mark block as received
129157
f.status.Add(uint32(block))
130158

131-
f.savePart()
132-
133159
return nil
134160
}
135161

smartremote_test.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,6 @@ func newTestServer() *httptest.Server {
4444
}))
4545
}
4646

47-
// newTestServerNoRange creates an HTTP server without Range support
48-
func newTestServerNoRange() *httptest.Server {
49-
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50-
w.Header().Set("Content-Length", itoa(int64(len(testData))))
51-
w.WriteHeader(http.StatusOK)
52-
w.Write(testData)
53-
}))
54-
}
55-
5647
// newTestServerNoHead creates an HTTP server that rejects HEAD requests
5748
func newTestServerNoHead() *httptest.Server {
5849
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)