-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
add Utf8JsonTransformer #2239
add Utf8JsonTransformer #2239
Conversation
… isolate, but count the response bytes if the header is not available instead
import 'package:dio/src/compute/compute.dart'; | ||
|
||
/// A [Transformer] that has a fast path for decoding utf8-encoded JSON. | ||
/// If the response is utf8-encoded JSON and no custom decoder for a Request is specified, this transformer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the Request
referencing here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the RequestOptions. If a custom decoder is set in to RequestOptions, we respect that, but in this case we cannot use the fused decoder.
I changed the comments to clarify that.
/// By default, this transformer will transform responses in the main isolate, | ||
/// but a custom threshold can be set to switch to an isolate for large responses by passing | ||
/// [contentLengthIsolateThreshold]. | ||
class Utf8JsonTransformer extends Transformer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about FusedTransformer
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fine by me, changed the name
final shouldUseIsolate = !(contentLengthIsolateThreshold < 0) && | ||
contentLength >= contentLengthIsolateThreshold; | ||
if (shouldUseIsolate) { | ||
// we can't send the stream to the isolate, so we need to decode the response bytes first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would that lead to a memory issue compare to the previous transformer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, the previous transformers always decode into a single Uint8List
first. (even in a horribly slow way, this will copy byte-by-byte, potentially even creating wrapper objects for each byte)
SyncTransformer:
final chunks = await responseBody.stream.toList();
final responseBytes = Uint8List.fromList(chunks.expand((c) => c).toList());
It's an optimization in this Transformer that it has a path where it can avoid that.
} else { | ||
if (!hasContentLengthHeader || contentLength == 0) { | ||
responseBytes ??= await _consolidateStream(responseBody.stream); | ||
// if the response is empty, return null, since the decoder would throw |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain more on this case with examples? Also comments should start with capital letter and end with dot too:
// if the response is empty, return null, since the decoder would throw | |
// Returns `null` to let the decoder throws when the response is empty. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a more detailed explanation as comment.
This branch is for backwards compatibility with the other Transformers.
The old behavior was, that if a server responds with an Content-Type: application/json
header. but the response body was empty, the transformer would return null.
But the fused decoder would throw an exception in this case.
So, if we don't have a valid Content-Length header, we first check if the body is empty before converting the bytes to json. If it's empty, we manually return null.
If we do have a Content-Length header which is >0, we can feed the response bytes directly into the decoder, which is the most performant path.
Can you add a |
That doesn't really make sense for this specific transformer as far as I can tell. It's an interesting idea though. What benefits do you get by doing it? |
We could allow to pass in a jsonDecodeCallback, but it would need to operate an on a Uint8List, not a String like the However, we should measure if that even makes sense in current versions of Dart. see https://api.dart.dev/stable/3.4.4/dart-isolate/Isolate/exit.html
So the question is, is spawning a new Isolate more expensive than copying the decoded map back into the main isolate? |
FWIW, I found that a large portion of the performance improvement actually comes from the more efficient consolidating of the The
Using a |
I'm just interested in the best performance possible. In my project, the size cutoff is 50KB. Any larger than that and it gets sent to the long-running Isolate for parsing. If we find spawning many isolates is better, then I'll concede. |
Sounds like a free win. Maybe a separate PR is in order? I think this PR should move forward as well though, I think the performance benefits of using the fused decoder is worth it. |
Yeah, when this is merged I'll prepare a new one, that extracts the _consolidateStream function and make SyncTransformer use it as well. |
The question is, for which aspect do we want to optimize? The least amount of CPU usage on the main isolate is likely achieved by spawning a new isolate each time, as the spawning of the new isolate happens on another thread, and the decoded JSON does not need to be copied back in the main isolate. With a long-running Isolate, the question is first consolidating the byte stream on the main isolate, then copying it to another isolate and then copying the decoded JSON map back into the main isolate is even less CPU intensive than just decoding it in the main isolate in the first place. But taking the https://github.com/knaeckeKami/dio_benchmark repo and adapting it to show how a long running isolate performs might be a good idea. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. This is awesome. Thank you!
Signed-off-by: Jonas Uekötter <[email protected]>
Code Coverage Report: Only Changed Files listed
Minimum allowed coverage is |
We are not doing this currently, right? IMO |
Excactly, that's not yet done |
There are no drawbacks compared to the old Transformer, as the old transformer does the same unconditionally. Yes, I'll prepare a new PR for setting the default transformer. |
New Pull Request Checklist
main
branch to avoid conflicts (via merge from master or rebase)CHANGELOG.md
in the corresponding packageAdditional context and info (if any)
fixes #2238
This PR adds the
Utf8JsonTransformer
.I made sure that it is as close to
BackgroundTransformer
as possible.The only behavior change is that is checks to Content-Length header for deciding whether to switch to a background isolate for decoding or not.
If the Content-Lenght header is not set or not valid, it will fall back to counting the bytes in the response.
This is because it's slightly faster to feed to the response stream directly into the decoder without consolidating all the response bytes into a single Uint8List first. In case the server does not send a valid Content-Length header, the Transformer will check the actual bytes from the response, as the
BackgroundTransformer
does.The remaining question is if we should set the Transformer as default.
IMO it would make sense to change the default transformer to
so all users get the improved performance by default.