Cronet can't handle modest number of JSON file downloads in my app
Note: Assuming the below issues can be fixed (that means, cronet = faster than http), is there someone I can send a monetary donation to in advance for their time?
I had high hopes of using cronet instead of http, but so far, only http will work right for Android.
Two sections of the app attempt to use cronet: A) Normal API requests (JSON files) and B) via flutter_map / dio / NativeAdapter (many hundreds of map tiles).
Case "A"
This issue has less moving parts, so hopefully we can figure this one out:
Background: When app starts, it pulls in API data, about 36 files for each of the 8 data types = 288 JSON files. A timer downloads new data (just the latest of each data type) every 1-2 minutes.
- No issues when using http.Client();
- No issues when using CupertinoClient on iOS (required for me or else app runs into file descriptor issues).
void main() async {
...
await ApiClient.instance.initializeNativeHttpClient();
...
Then I have:
class BaseApiClient {
late http.Client client;
Future<void> initializeNativeHttpClient() async {
if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) {
final config = URLSessionConfiguration.defaultSessionConfiguration();
client = CupertinoClient.fromSessionConfiguration(config);
} else if (!kIsWeb && Platform.isAndroid) {
client = CronetClient.defaultCronetEngine();
} else {
client = http.Client();
}
}
Future<http.Response> get(Uri url, {Map<String, String>? headers}) {
return client.get(url, headers: headers);
}
Future<http.Response> post(Uri uri, {Map<String, String>? headers, Object? body}) {
return client.post(uri, headers: headers, body: body);
}
}
class ApiClient extends BaseApiClient {
ApiClient._privateConstructor();
static final ApiClient _instance = ApiClient._privateConstructor();
static ApiClient get instance => _instance;
}
In the above code, I had many errors on app startup:
Error fetching WWA: ClientException: Cronet exception: m.mb: Exception in CronetUrlRequest: net::ERR_HTTP2_PROTOCOL_ERROR, ErrorCode=11, InternalErrorCode=-337, Retryable=false, uri=https://site.com/DATA/20240924_2336.json
So some downloaded, but many did not.
So then I attempted to spread out the requests among my 8 data types (2 each) like this:
class ApiClient extends BaseApiClient {
ApiClient._privateConstructor();
static final ApiClient _instance = ApiClient._privateConstructor();
static ApiClient get instance => _instance;
}
...Apiclient2...ApiClient3...
class ApiClient4 extends BaseApiClient {
ApiClient4._privateConstructor();
static final ApiClient4 _instance = ApiClient4._privateConstructor();
static ApiClient4 get instance => _instance;
}
That actually helped, but I still have one data type that always has "Error Fetching" errors, if I go more than about 24 files. These files are the largest, at 4.2 mb each, so ~ 100 mb is where it fails. So for instance, it may download 24 of them, with the remaining 12 erroring.
It seems strange to me that cronet could not download 36 files @ 4 mb each. I'm sure I have something configured wrong? For that matter, I would assume it should be able to handle 300 files at once, being that MOST of them are SMALL JSON files (only WWA is large).
Case "B":
As for the flutter_map tiles, THAT "sorta" works. Meaning, I can download 1000 small 16 kb webp tiles using this code:
class DioSingleton {
static Dio? _dio;
static CronetEngine? _cronetEngine; // Single CronetEngine instance for Android
static Dio get dioInstance {
if (_dio == null) {
print('Creating Dio instance');
_dio = Dio();
}
// Reassign the httpClientAdapter on each request (new NativeAdapter for each request)
if (!(GetPlatform.isWeb || GetPlatform.isWindows)) {
if (GetPlatform.isIOS) {
_dio!.httpClientAdapter = NativeAdapter(); // New NativeAdapter for iOS
} else if (GetPlatform.isAndroid) {
_dio!.httpClientAdapter = NativeAdapter(
createCronetEngine: _getCronetEngine, // Reuse the CronetEngine
);
}
}
return _dio!;
}
// Initialize and reuse a single CronetEngine for Android
static CronetEngine _getCronetEngine() {
_cronetEngine ??= CronetEngine.build();
return _cronetEngine!;
}
}
The above code is called for each tile, so many hundreds of times, so I try to re-use cronet engine, and single Dio instance. Then that leaves each tile with it's own _dio.httpClientAdapter.
However, the above ends up sluggish for Android. Again, if I use normal http, everything works FAST.
- As far as I can tell, our servers are configured to the max, unless I did something wrong there:
mpm_worker.conf:
<IfModule mpm_worker_module>
ServerLimit 24
StartServers 12
MinSpareThreads 256
MaxSpareThreads 256
ThreadLimit 512
ThreadsPerChild 512
MaxRequestWorkers 9216
MaxConnectionsPerChild 0
</IfModule>
And inside apache2.conf:
<IfModule http2_module>
Protocols h2 h2c http/1.1
H2MaxSessionStreams 3000
H2MinWorkers 512
H2MaxWorkers 512
H2Push on
H2Direct on
H2PushDiarySize 256M
MaxKeepAliveRequests 9000
</IfModule>
Another error I previously came across was: Too many Broadcast Receivers > 1000, which is why I am trying this singleton-type approach. Seems I am hitting limits attempting to download 2000 map tiles at once.
I probably just don't understand how to best set up cronet, but surely it should be faster than http?
I can always fall back and just use http for Android devices, but I really feel cronet would be best, if only we could get it to work.
Do you have many concurrent requests in flight? Do your server logs indicate anything? Because that error usually indicates that the server responded with something that Cronet could not validate.
Thank you for your reply!
Interesting, I watched my server logs and all requests came through, even for the items that cronet claimed an error for.
There's a lot going on during app startup, I wonder if CPU could be the culprit for this particular issue.
The strange thing is, using http client NEVER fails. Everything processes. Why would that be?
- Could it be that cronet is more cpu intensive?
- Are there limits to concurrent connections with cronet? For example, I read http max connections is set to null , meaning unlimited, for http client. What is cronet set to?
Well good to know it's not my server (I think). During app startup, there are a couple intensive operations going on at the same time, perhaps it causes cronet to freeze or timeout or something.
As far as 'engine' configuration, I am using CronetClient.defaultCronetEngine(); Do you recommend anything different? I assumed things like cache were for re-serving already-downloaded data, which I would not typically need once it's gotten once.
At any rate, this app is very network (and cpu) intense and I tried to configure my server to the max, and it seems that end of it is working.
Here's an example of what is downloaded at app startup:
36 X
4 mb 50 kb 60 kb 150 kb 450 kb 20 kb 8 kb 5 kb
So under 300 files, and total of about 180 mb.
I tried making a single cronet engine at app start and then 4 separate CronetClients to divide up the work. That performed much worse than making 4 separate cronet engines, which seems strange to me because I thought you were optimally supposed to have 1.
In the end (so far), a single client = http.Client(); works perfectly every time.
from my tests it is best just to use one cronet client for your whole app
@brianquinlan do we have something to do here? Maybe run cronet engine as a singleton always, etc?
@mraleph CronetEngines are configurable so I don't think that we can offer a singleton. And @corepuncher 's tests indicated better performance using >1 CronetEngine.
The error ERR_HTTP2_PROTOCOL_ERROR suggests that that Cronet did not like the server's reply (it may fail when IOClient succeeds because IOClient will always use HTTP 1.1).
Could it be that cronet is more cpu intensive?
It could be, I haven't tested this. I would expect that enabling Brotli compression would also result in more CPU use.
Are there limits to concurrent connections with cronet? For example, I read http max connections is set to null , meaning unlimited, for http client. What is cronet set to?
I don't know how Cronet manages concurrency internally.
In package:cronet_http, we use a cached thread pool to manage network callbacks. You could try using a different Executor configuration (see https://github.com/dart-lang/http/blob/e1661810be1afd89462f1cd752a32fb530d42165/pkgs/cronet_http/lib/src/cronet_client.dart#L289) to see if it improves your use case (and we can make that configurable if it does).
package:jnigen also bridges UrlRequest.Callback into Dart, which may result in some performance loss.