Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FileLoader: Allow HTTP Range requests #24580

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Conversation

takahirox
Copy link
Collaborator

@takahirox takahirox commented Sep 1, 2022

Fixed #24485

Update: The proposed API has been updated. See #24580 (comment)

Description

This PR introduces HTTP range requests to FileLoader.

It is helpful to save network usage by downloading only needed part of a file.

Proposed API

const loader = new FileLoader();
loader.setRange(8 /* offset in bytes */, 16 /* length in bytes */); 
loader.load(url, data => { ... });

Changes

  • Accept HTTP response status 206 Partial Content
  • Add FileLoader.setRange(offsetInBytes, lengthInBytes) method.
    • Range request header can be set even with FileLoader.setRequestHeader() but FileLoader want to know the range because of the following items so having a new clear method would be simpler (by avoiding to use regex for this.requestHeader.Range)
  • Take into account the range in the key for Cache (for fetched data cache) and loading (for duplicated requests management).
    • Otherwise, different range requests to the same url can be wrongly handled as the same request.
  • Add fallback for servers not supporing range requests.
    • They can respond with 200 and the full body content. If it happens, .slice() for arraybuffer and blob response types, and throw an error for other types because of the complexity to rescue them. Another option may be throw an error regardless of response types and ask users for fallback in their onError callback.

Example use case

glB bundle + glTF LOD extension + progressive loading. First load the lowest levels and then progressively load higher levels on demand.

Currently FileLoader and GLTFLoader don't support HTTP range requests so they load the entire content. With HTTP range request they will be able to partially load files so can save network usage.

Codes

Additional context

When previously HTTP range requests support is discussed, (if I understand correctly) there seems to be a chance that content length in onProgress callback can't be computable if servers support gzip Content-Encoding.

But the recent FileLoader already checks if the content length is computable and it's notified to the callback. So, non-computable content length may not be a big deal (?).

const contentLength = response.headers.get( 'Content-Length' );
const total = contentLength ? parseInt( contentLength ) : 0;
const lengthComputable = total !== 0;

const offset = this.offset;
const length = this.length;
const isRangeRequest = offset !== null;
const key = url + ( isRangeRequest ? `:${offset}-${length}` : '' );
Copy link
Contributor

Choose a reason for hiding this comment

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

Please fix linting (spaces in ${ something }).

Copy link
Collaborator Author

@takahirox takahirox Sep 2, 2022

Choose a reason for hiding this comment

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

@mrdoob Are the spaces needed even in ${ }? I couldn't find it in the coding style guide and the lint test passes. Also I can find some lines not having spaces in ${ }in the core

lines2.push( `${line === errorLine ? '>' : ' '} ${line}: ${lines[ i ]}` );

@donmccurdy
Copy link
Collaborator

donmccurdy commented Sep 2, 2022

Hi and thank you @takahirox!

glB bundle + glTF LOD extension + progressive loading. First load the lowest levels and then progressively load higher levels on demand.

There's a lot I'm not sure about within this workflow...

  • if you're using LODs, do you often want separate vertex streams for each LOD? it's also possible to use a single vertex stream and just have alternate indices for higher LODs, MSFT_lod supports this. Makes good sense for maintaining framerate, and uses less bandwidth and memory overall, but doesn't help with progressive loading of course.
  • for progressive loading what do you think of using range requests vs. partitioning data into multiple .bin files? e.g.
gltf-transform partition in.glb out.gltf --meshes

We've supported lazy-loading in GLTFLoader for a while, and glTF-Transform has supported partioning data into multiple .bin files for a while, but honestly I don't get the feeling that very many users are making much use of either of these features. So I am a bit worried about adding complexity to GLTFLoader for what feels like a different way of doing the same thing.

I am happy with the idea of THREE.FileLoader supporting range requests. I am still trying to decide about GLTFLoader. There are some other use cases we could think about too, like streaming mip levels (low to high res) of KTX2 textures with HTTP Range Requests, the KTX2 format is designed to allow that. It is just all a tradeoff in terms of complexity and goals of GLTFLoader. 😅

@takahirox
Copy link
Collaborator Author

Hi Don @donmccurdy , thanks for the comments.

I am happy with the idea of THREE.FileLoader supporting range requests. I am still trying to decide about GLTFLoader.

I think it would be better to separate FileLoader HTTP range requests support and GLTFLoader HTTP range requests support discussions. The concerns about GLTFLoader HTTP range requests support don't need to block FileLoader HTTP range requests support. Let's discuss GLTFLoader stuffs in #24506.

Most of your comments look GLTFLoader related. I will reply in #24506.

@mrdoob mrdoob added this to the r145 milestone Sep 7, 2022
@mrdoob
Copy link
Owner

mrdoob commented Sep 7, 2022

@donmccurdy

I am happy with the idea of THREE.FileLoader supporting range requests. I am still trying to decide about GLTFLoader.

I think @elalish has a few opinions about this.

@sunag
Copy link
Collaborator

sunag commented Sep 7, 2022

It is possible too creating an streaming linear LOD for progressive loader. I made this in sea3d for texture and geometry, it is more efficient once it not needed multiples request.

Example:
https://sunag.github.io/sea3d/
using one single file, one request, progressive loader:

@elalish
Copy link
Contributor

elalish commented Sep 7, 2022

This looks great, but my interests are in GLTF, so I'll comment in #24506.

@donmccurdy
Copy link
Collaborator

Add fallback for servers not supporing range requests. They can respond with 200 and the full body content. If it happens, .slice() for arraybuffer and blob response types, and throw an error for other types because of the complexity to rescue them. Another option may be throw an error regardless of response types and ask users for fallback in their onError callback.

The fallback feels dicey to me. We're creating a new FileLoader for each request, progressive loading will make multiple requests, and caching is disabled by default. This may lead to downloading a full asset multiple times if the server doesn't support range requests.

I'd suggest that HTTP Range requests should be opt-in for downstream classes like GLTFLoader and KTX2Loader, and should fail in FileLoader if the server does not support them, or at least log warnings.


I'm also wondering if HTTP Range requests might be better routed through a new Promise-based method, and just not be supported through the load(onLoad, onProgress, onError) signature?

const loader = new FileLoader();

loader.onProgress(() => { ... });

const result = await loader.loadAsync('path/to/file.glb', options);

The options argument above could extend any headers already configured on the loader.

@takahirox
Copy link
Collaborator Author

takahirox commented Sep 7, 2022

This may lead to downloading a full asset multiple times if the server doesn't support range requests.

Curious to know if we can expect the browser's cache. Or are the response data distinguished because they are responses to different range headers requests? If browser cache works, might be acceptable. I will check Chromium so far.

should fail in FileLoader if the server does not support them, or at least log warnings.

I'm ok that FileLoader fails for 200 response against range request as I raised as another option because it can avoid additional complexity in FileLoader.

Another option may be throw an error regardless of response types and ask users for fallback in their onError callback.

I'm also wondering if HTTP Range requests might be better routed through a new Promise-based method, and just not be supported through the load(onLoad, onProgress, onError) signature?

I may like this idea. Honestly I don't prefer .setRange() but didn't want to change the .load() API. This idea doesn't break the API.

@takahirox takahirox force-pushed the FileLoaderRangeRequests branch from c8c86c1 to c378d59 Compare September 14, 2022 18:56
@takahirox
Copy link
Collaborator Author

takahirox commented Sep 14, 2022

I have updated the PR to simplify. New proposed API (no change from the current API).

const loader = new FileLoader();
loader.setRequestHeader({Range: 'bytes=8-23'});
loader.load(url, data => { ... });

I have omitted .setRange() method. I started to think that it may be confusing to users if there are two wayt to set up range, .setRange() and .setRequestHeader(). Providing only one way may be less confusing.

Don's overriding request header idea is interesting. But I want to focus on minimal change in this PR. I hope we can think of better API in another PR if needed.

And I changed to always call onError callback if servers respond with 200 + full body content against range requests because the implementation can be simpler.

Changes

@takahirox takahirox changed the title FileLoader: HTTP Range requests support FileLoader: Allow HTTP Range requests Sep 14, 2022
@LeviPesin
Copy link
Contributor

Call onError callback if servers respond with 200 and the full body content against range requests because they don't support them.

I'm not sure this is the best.
I think for such servers the entire response should be cached with key-without-range and then for each range just a portion of response should be returned, like it was before.

@takahirox
Copy link
Collaborator Author

takahirox commented Sep 17, 2022

Adding workarounds adds complexity. I don't think HTTP range requests is main stream use case so I don't think FileLoader needs to be complex for non-mainstream use case. It calls onError callback with response so fallback can be done on user end.

@mrdoob mrdoob modified the milestones: r145, r146 Sep 29, 2022
@takahirox
Copy link
Collaborator Author

What blocks this PR? Do we need more discussion about fallback?

Copy link
Collaborator

@Mugen87 Mugen87 left a comment

Choose a reason for hiding this comment

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

The new simplified version of the PR looks good to me.

@takahirox
Copy link
Collaborator Author

takahirox commented Oct 25, 2022

I want to add one comment for fallback. FileLoader will call onError callback with response if server responds with 200 for range requests. response has full range content. If users want to avoid duplicated full range request, they can write fallback on their ends, for example caching the full content and return sliced buffer for range requests coming next. This can avoid to make FileLoader more complex than it really needs to be. (Remember that probably range requests is a minor use case. FileLoader doesn't need to be complex for minor use cases.)

loader.setRequestHeader({Range: 'bytes=...'});
loader.load(url, onLoad, onProgress, (error) => {
  const response = error.response;
  if (response && response.status === 200) {
    // fallback on user end.
    // response has full content
  }
});

And I speculate even duplicated full range requests won't be a big deal thanks to browser's disk cache.

If you folks think the name "HttpError" is confusing because 200 is not an error, we may add "RangeRequestError".

@mrdoob mrdoob modified the milestones: r146, r147 Oct 27, 2022
@ycw
Copy link
Contributor

ycw commented Nov 18, 2022

But the recent FileLoader already checks if the content length is computable and it's notified to the callback. So, non-computable content length may not be a big deal (?)

@takahirox lengthcomputable maybe false positive because fileloader doesn't handle content-encoding header, if server responds with compressed content, or #24962 (comment), loaded/total will >1,
it can be verified by servers that support compression, ex. githack: http://raw.githack.com/mrdoob/three.js/dev/examples/index.html#webgl_renderer_pathtracer "loading...599%"

@takahirox
Copy link
Collaborator Author

Thanks for the comment. If I understand correctly, the problem is not range requests specific so it won't block this PR, and we can work on resolving it in another PR if possible and needed, isn't it?

@ycw
Copy link
Contributor

ycw commented Nov 18, 2022

@takahirox yes.

@mrdoob mrdoob removed this from the r147 milestone Nov 30, 2022
@mrdoob mrdoob modified the milestones: r160, r161 Dec 22, 2023
@mrdoob mrdoob modified the milestones: r161, r162 Jan 31, 2024
@mrdoob mrdoob modified the milestones: r162, r163 Feb 29, 2024
@mrdoob mrdoob modified the milestones: r163, r164 Mar 29, 2024
@mrdoob mrdoob modified the milestones: r164, r165 Apr 25, 2024
@mrdoob mrdoob modified the milestones: r165, r166 May 31, 2024
@mrdoob mrdoob modified the milestones: r166, r167 Jun 28, 2024
@mrdoob mrdoob modified the milestones: r167, r168 Jul 25, 2024
@mrdoob mrdoob modified the milestones: r168, r169 Aug 30, 2024
@mrdoob mrdoob modified the milestones: r169, r170 Sep 26, 2024
@mrdoob mrdoob modified the milestones: r170, r171 Oct 31, 2024
@mrdoob mrdoob modified the milestones: r171, r172 Nov 29, 2024
@mrdoob mrdoob modified the milestones: r172, r173 Dec 31, 2024
@mrdoob mrdoob modified the milestones: r173, r174 Jan 31, 2025
@mrdoob mrdoob modified the milestones: r174, r175 Feb 27, 2025
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.

FileLoader: Range request support
9 participants