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

Cronet can't handle modest number of JSON file downloads in my app #1308

Open
corepuncher opened this issue Sep 27, 2024 · 6 comments
Open
Assignees
Labels
package:cronet_http type-bug Incorrect behavior (everything from a crash to more subtle misbehavior)

Comments

@corepuncher
Copy link

corepuncher commented Sep 27, 2024

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.

@corepuncher corepuncher added package:cronet_http type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) labels Sep 27, 2024
@brianquinlan
Copy link
Collaborator

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.

@brianquinlan brianquinlan added the needs-info Additional information needed from the issue author label Sep 28, 2024
@corepuncher
Copy link
Author

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.

@corepuncher
Copy link
Author

corepuncher commented Sep 28, 2024

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.

@github-actions github-actions bot removed the needs-info Additional information needed from the issue author label Sep 28, 2024
@escamoteur
Copy link

from my tests it is best just to use one cronet client for your whole app

@mraleph
Copy link
Member

mraleph commented Nov 15, 2024

@brianquinlan do we have something to do here? Maybe run cronet engine as a singleton always, etc?

@brianquinlan
Copy link
Collaborator

brianquinlan commented Dec 9, 2024

@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

static final _executor = jb.Executors.newCachedThreadPool();
) 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package:cronet_http type-bug Incorrect behavior (everything from a crash to more subtle misbehavior)
Projects
None yet
Development

No branches or pull requests

4 participants