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

added onUploadProgress #728

Closed
wants to merge 0 commits into from
Closed

Conversation

Mehmetyaz
Copy link

added onUploadProgress to MultipartRequest.new and Client.send. (adjusted file movements)

@Mehmetyaz
Copy link
Author

@kevmoo - More than 80 changes appeared in the previous frok. With the new fork, your inspection will be easier.

.gitignore Outdated Show resolved Hide resolved
@Mehmetyaz Mehmetyaz requested a review from kevmoo July 1, 2022 23:49
@brianquinlan
Copy link
Collaborator

@natebosch , could you take a look at this?

@brianquinlan
Copy link
Collaborator

FYI, @natebosch is on holiday until late next week so don't expect feedback until then.

pkgs/http/lib/src/base_request.dart Outdated Show resolved Hide resolved
/// In browser, uses XMLHttpRequest's "xhr.upload.onLoad" event.
///
/// In IO, uses the yield length of the stream. The total length of the bytes
/// yielded by the stream at any given moment is "uploaded" and the total
Copy link
Member

Choose a reason for hiding this comment

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

@brianquinlan, @lrhn - how meaningful do we expect this to be? Is watching a transform step in a stream being read by dart:io HTTP handling an accurate way to understand upload progress?

Copy link
Author

@Mehmetyaz Mehmetyaz Aug 2, 2022

Choose a reason for hiding this comment

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

Not exactly. While I was writing the altogic_dart package, I duplicated the http package and I add this feature. Meanwhile, I split all yields larger than the specified chunk size according to this size. But adding this to the official package would have caused more confusion. Currently MultipartRequest splits into specific chunks and progress works correctly as the part is actually. But it needs to be divided into more accurate parts for big files.

E.g current results like this for big files : 0.1- 1-99.3-100

If large yields are divided, loading can be done with closer ratios.

Copy link
Author

Choose a reason for hiding this comment

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

There is two methods for this.

  1. Like browser client, read stream until done, split by defined chunk size. This may cause more memory consumption.

  2. Split big parts by chunk size in the ByteStream. But this splitting will be done for browser also and it unnecessary. Because browser client works different.

If there is a solution you approve, I can code it. I also need to find out if the chuck size will be a static value or a parameter. Parameters can cause confusion. if it is static how much should it be?

Copy link
Member

Choose a reason for hiding this comment

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

Or make total nullable and don't provide it when you don't have one.

If the loaded is defined as being bytes, at least when total is null, then the user can still be shown a meaningful progress, it'll just be "49 MB" instead of "37%".

I'd never read the entire stream and then chunk it afterwards. That's just useless overhead. If everything comes in one big chunk, that's just what you have to deal with.

Copy link
Author

Choose a reason for hiding this comment

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

Making total nullable does not fix the problem. Instead, the ByteStream .fromBytes consturctor simply splits the List(bytes) into parts.

If MultipartFile is created with a stream, meaningful data is now obtained. But when created with fromBytes it pipes it as one piece. This prevents the rates from becoming meaningful.

Also at the moment onUploadProgress is not called with a rate anyway. There are loaded and total. Since the callback is triggered only where the total is calculated, the developer can calculate the rate.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@brianquinlan, @lrhn - how meaningful do we expect this to be? Is watching a transform step in a stream being read by dart:io HTTP handling an accurate way to understand upload progress?

Probably not. When you write to a dart socket it doesn't mean that the data has actually left the local machine (or even been copied to OS memory).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Of the 6 implementations that I know about, you could get good numbers for:

  • browser
  • cupertino_http

and bad numbers for:

  • fetch_client (though this should improve with time as the JavaScript streaming API improves)
  • cronet_http
  • io
  • java_http (not sure about this, based on Java HttpURLConnection)

///
/// If defined, this callback will be called when the upload progress changes.
///
/// In browser, uses XMLHttpRequest's "xhr.upload.onLoad" event.
Copy link
Member

Choose a reason for hiding this comment

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

More words may make this easier to read (to a limit, as always).

/// When run in a browser, this callback relies on the
/// browser's XMLHttpRequest's xhr.upload.onLoad events.
///
/// When run natively, the number of bytes is
/// ....

I don't understand the description of the native version.
Probably because I don't know what stream we are talking about, and how you can possibly know the total length of a stream before it's done.

Copy link
Author

Choose a reason for hiding this comment

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

MultipartRequest calculates the length value before the stream starts. If the length value is a non-calculated request (which I just added to the MultipartRequest), onUploadProgress will never be triggered.

Documentation will be extended

Copy link
Collaborator

Choose a reason for hiding this comment

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

@lrhn I actually prefer the original API (using a callback rather than a Stream) because it means that the Client implementation can easily check whether the caller is interesting in progress notification one at the beginning of their send implementation. In package:cupertino_http, I'd probably implement something like:

  @override
  Future<StreamedResponse> send(BaseRequest request) async {
    ...
    if (request.onUploadProgress != null) {
      // This `Timer` is an unnecessary expense if no one is listening for notifications.
      final timer = Timer.periodic(const Duration(milliseconds: 100), (timer) { 
      if (taskTracker.isDone) {
        timer.cancel();
      } else {
        progress = Progress(task.countOfBytesExpectedToSend,  task.countOfBytesSent);
        request.onUploadProcess(progress);
      });
    }

If the Stream/StreamController approach were used, it seems like the StreamController would have to be public and so the send implementation can do something like:

  @override
  Future<StreamedResponse> send(BaseRequest request) async {
    void setupTimer() {
    
    }

    if (request.controller.isListening) {
      setupTimer();
    } else {
      request.onListen = setupTimer;
   }

pkgs/http/lib/src/base_request.dart Outdated Show resolved Hide resolved
/// In browser, uses XMLHttpRequest's "xhr.upload.onLoad" event.
///
/// In IO, uses the yield length of the stream. The total length of the bytes
/// yielded by the stream at any given moment is "uploaded" and the total
Copy link
Member

Choose a reason for hiding this comment

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

Or make total nullable and don't provide it when you don't have one.

If the loaded is defined as being bytes, at least when total is null, then the user can still be shown a meaningful progress, it'll just be "49 MB" instead of "37%".

I'd never read the entire stream and then chunk it afterwards. That's just useless overhead. If everything comes in one big chunk, that's just what you have to deal with.

pkgs/http/lib/src/io_client.dart Outdated Show resolved Hide resolved
pkgs/http/lib/src/utils.dart Outdated Show resolved Hide resolved
pkgs/http/lib/src/utils.dart Outdated Show resolved Hide resolved
@Mehmetyaz Mehmetyaz requested a review from lrhn August 13, 2022 09:10
/// lengthComputable :
/// library.html : xhr.lengthComputable
/// library.io : content-length is provided (MultipartRequest provide)
final void Function(int uploaded, int total)? onUploadProgress;

Choose a reason for hiding this comment

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

Hi @Mehmetyaz, thanks for this PR, it is very useful! We are using your fork in our app overriding the dependency of the http package until this get deployed in a new version of the package.

Said this, we noted that this signature could be wrong and has the parameters in the wrong order.
In the rest of the code you are calling the onUploadProgress callback using the total as the first parameter and the uploaded bytes as the second one.

Just that. Thanks a lot again.

@mehcode
Copy link

mehcode commented Sep 14, 2022

Is there anything needed here before this is merged? I'd really love to see this merged and support added to cupertino_http.

@kevmoo
Copy link
Member

kevmoo commented Sep 14, 2022

@brianquinlan @lrhn ?

@natebosch
Copy link
Member

I'd still like to understand how meaningful these numbers are.
Moving the http implementation to the progress event helps - it feels like we're adding specific value in that case. If we feel like the stream watching is high enough fidelity for the IO case we can support both through the same interface. Are we confident in that fidelity @brianquinlan @lrhn ?
Is there any buffering in between this Stream that we can account for better with an API in the SDK? Will watching the Stream consumption be meaningful for the cronet and cupertino implementations?

If what we're offering is implemented as a callback that gets invoked as the Stream is consumed - does that need to be shipped as part of package:http?

static String _validateMethod(String method) {
if (!_tokenRE.hasMatch(method)) {
throw ArgumentError.value(method, 'method', 'Not a valid method');
}
return method;
}

BaseRequest(String method, this.url)
/// On upload progress callback.
Copy link
Member

Choose a reason for hiding this comment

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

Could we provide a (broadcast) stream instead, instead of putting the callback directly into the class.

That's the traditional way to provide events, and the on name would be traditional for such a stream.

/// Upload progress notification
///
/// Sends events when upload progress information is
/// available.
///
/// ....
Stream<Progress> get onUploadProgress;

with a

abstract class Progress {
  int progress;
  int? get total;
}

(Which I'd make a record when we get records, and document as something users must not implement or extend.)

Copy link
Author

Choose a reason for hiding this comment

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

If you agree, I can commit to implement this solution.

Copy link
Author

Choose a reason for hiding this comment

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

Unfortunately I found that this is not a viable solution. What will I return in the getter you defined to BaseRequest? StreamController.stream.

A StreamController is required and if the developer does not want to listen to this progress, it creates an unnecessary load. In addition, since I need to know if the user has decided to listen for progress and this controller must be nullable.

If I could access the private values of BaseRequest from within the client (e.g IOClient), maybe I could come up with suitable solutions. Otherwise, two more properties are required, such as BaseRequest.onUploadProgress and BaseRequest.uploadProgressController?. I can guess that you don't want this visible to the developer.

Copy link
Member

@lrhn lrhn Sep 15, 2022

Choose a reason for hiding this comment

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

You can always create the controller when requested, and store it in a private variable:

  StreamController<Progress>? _controller;
  Stream<Progress> get onProgress => (_controller ??= StreamController()).stream;

  void _addProgress(int bytes, int? total) => _controller?.add(Progress(bytes, total));

This code has very little overhead when it's not used (a single null field and a single null check of that field whenever there is progress).
You can check whether _controller is null anywhere you currently check whether onProgressCallback is set.

If you need to access the internals inside IOClient, then you should just make the onProgress getter abstract and implement it in all the concrete subclasses. (I admit that the lack of protected access is annoying.)

Or add a

void addProgressToRequest(Progress progress, BaseRequest request) {
  request._addProgress(progress);
}

to base_progress.dart, use it from the other libraries, but don't export it as part of the public interface of the package.
(That's a traditional way to share privates between libraries in the same package.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

AFAIK, we are waiting on a package:http major version bump to review changes like this.

Is that true @natebosch ?

Copy link
Member

Choose a reason for hiding this comment

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

AFAIK, we are waiting on a package:http major version bump to review changes like this.

I haven't evaluated how difficult it will be to roll this internally. I do think it would need to wait for a major version bump.

I'm still a little unsure how meaningful these numbers are, or why it's important that we provide this API in this package instead of having the caller watch the stream progress.

Copy link
Author

Choose a reason for hiding this comment

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

You are indeed right. In my implementation for Altogic, onUploadProgress was useful because I split the bytes into chunks even if bytes weren't streamed(e.g. multipartfile.fromBytes).

If it is not a problem for you, a solution is also possible, such as automatically increasing the number of stream parts in certain situations.

@brianquinlan
Copy link
Collaborator

I patched this change locally and did some testing.

For IOClient, the progress seemed useful for large requests (assuming that the stream is split into relatively small lists). On my Mac, ~500KiB of progress would be indicated before the first byte arrives on the server. So presumably that is approximately when the TCP write buffer is full [1].

For BrowserClient, I was not able go get any progress messages when running on Chrome - @Mehmetyaz how did you test this?

[1] I think that this can be verified by checking availableSendBuffer

@Mehmetyaz
Copy link
Author

Moved to #1071

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.

None yet

7 participants