Skip to content

feat: enhance file download and caching mechanisms#404

Open
egalvis27 wants to merge 2 commits into
mainfrom
feat/improve-download-speed
Open

feat: enhance file download and caching mechanisms#404
egalvis27 wants to merge 2 commits into
mainfrom
feat/improve-download-speed

Conversation

@egalvis27

Copy link
Copy Markdown

What is Changed / Added


  • Fixed invalid EOF and out-of-bounds range handling in virtual drive hydration and ranged downloads.
  • Added guards to skip zero-length and invalid block prefetch/download work.
  • Extended block prefetching from thumbnail reads to normal file reads used for open/copy flows.
  • Added environment-based tuning for normal read prefetch size.
  • Unified duplicated prefetch constants in the read callback path.
  • Removed noisy success metrics from ranged download logs and kept failure warnings only.
  • Added and updated regression tests for hydration bounds, prefetch behavior, and download range handling.

Why

  • Prevent crashes caused by negative range lengths and invalid buffer allocations near end-of-file.
  • Stop wasted work and memory pressure from scheduling blocks outside real file bounds.
  • Improve perceived file open and copy performance, not only thumbnail loading.
  • Make prefetch behavior easier to tune without editing code.
  • Reduce small code duplication and keep prefetch defaults consistent.
  • Keep logs useful during failures without adding per-block noise during normal operation.
  • Lock behavior down with focused tests so performance changes do not reintroduce correctness bugs.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
75.4% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

const ALLOWED_BLOCK_SIZE_MB = new Set([1, 2, 4, 8]);

function getConfiguredBlockSizeInMb() {
const configuredValue = process.env.INTERNXT_DRIVE_DOWNLOAD_BLOCK_SIZE_MB;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this needs to be a enviroment variable

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea is that these values should be easy to change. Right now, the optimal settings are 4 MB per download and 3 prefetch blocks. These values can be increased if changes are made to the backend to reduce the latency of download requests, which would greatly improve performance.

Looking back, we can adjust these values to achieve better performance if the conditions are right; that’s why I set them as environment variables so they can be easily modified.

blockLength,
}: Props): Promise<Result<void, Error>> {
if (isAborted(state)) return { data: undefined };
if (blockLength <= 0 || blockStart >= virtualFile.size) return { data: undefined };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something that can happen? meaning: Fuse can ask for a block that is negative?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not just to validate negative ranges; it's also to prevent readings in the 0 range—which fuse does sometimes—and to ensure that invalid ranges aren't attempted, which is rare but does happen.

return { blockStart: Math.max(0, Math.min(range.position, fileSize)), blockLength: 0 };
}

const clampedStart = Math.max(0, range.position);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? i assume that range will always be 0 or more

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might assume that's the case, but there are very rare instances where it might not be; this change is a simple safeguard that definitively prevents that from happening.

import { type HandleReadDeps, type ReadRange } from './types';
import { isThumbnailProcess } from './thumbnail-processes';

const PREFETCH_DEFAULT_BLOCKS_AHEAD = 5;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn this be on constants?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I've moved it.

Comment on lines +30 to +40
function getThumbnailPrefetchBlocksAhead() {
return getPrefetchBlocksAhead({
configuredValue: process.env.INTERNXT_DRIVE_THUMBNAIL_PREFETCH_BLOCKS_AHEAD,
});
}

function getReadPrefetchBlocksAhead() {
return getPrefetchBlocksAhead({
configuredValue: process.env.INTERNXT_DRIVE_READ_PREFETCH_BLOCKS_AHEAD,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned earlier, the idea is to be able to easily change the values.

return Math.max(0, Math.min(parsed, PREFETCH_MAX_BLOCKS_AHEAD));
}

function getThumbnailPrefetchBlocksAhead() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why, for a thumbnail, we need to do a prefetch? would not this slow the initial load of the file explorer?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been running tests focused on thumbnails, and you're right—the improvement is very small, and it slows down the network for other processes.

}
}

function schedulePrefetchBlocksAhead({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldnt we test this separtedly since it has a lot of logic?

}): Promise<Result<void, Error>> {
const { blockStart, blockLength } = expandToBlockBoundaries({ range, fileSize: virtualFile.size });

if (blockLength <= 0 || blockStart >= virtualFile.size) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, if this is a realistic scenario, why not prevent from calling this whole functionality?

const downloads = missingBlocks.map((block) => {
const start = block * BLOCK_SIZE;
const end = Math.min(start + BLOCK_SIZE, virtualFile.size);
const boundedBlockLength = end - start;

@AlexisMora AlexisMora Jul 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, i feel like there has to be a way to prevent this whole "negative block request" instead of just adding everywhere the check, this just adds unnecessary complexity dont you think

Comment on lines +63 to +73
if (
!shouldEmitProgress({
now: Date.now(),
bytesDownloaded,
fileSize,
state: progressReporterState,
})
) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?

Comment on lines +16 to +43
const PROGRESS_UPDATE_INTERVAL_MS = 250;

type ProgressReporterState = {
lastUpdateAt: number;
};

function shouldEmitProgress({
now,
bytesDownloaded,
fileSize,
state,
}: {
now: number;
bytesDownloaded: number;
fileSize: number;
state: ProgressReporterState;
}) {
const reachedEnd = bytesDownloaded >= fileSize;
const elapsedSinceLastUpdate = now - state.lastUpdateAt;

if (!reachedEnd && elapsedSinceLastUpdate < PROGRESS_UPDATE_INTERVAL_MS) {
return false;
}

state.lastUpdateAt = now;
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IF this is necessary this should be in its own file, where we should test it separatedly

Comment on lines +108 to +123
let bytesRead = 0;
let buffer = Buffer.alloc(length);

response.data.on('data', (chunk: Uint8Array) => {
const source = Buffer.from(chunk);
const requiredLength = bytesRead + source.length;

if (requiredLength > buffer.length) {
const next = Buffer.alloc(Math.max(buffer.length * 2, requiredLength));
buffer.copy(next, 0, 0, bytesRead);
buffer = next;
}

source.copy(buffer, bytesRead);
bytesRead += source.length;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants