Plezy carried three native HTTP clients on the assumption that they beat Dart's own. Benchmarked against real Plex and Jellyfin servers on macOS, Windows, Linux and three Android devices, two of them do not. Cronet loses on every request shape the app issues: 8.1 vs 83.8 MiB/s on a LAN body read, 84 vs 150 req/s on an artwork fan-out, 14.3 vs 10.7 ms on sequential API calls. It also fails a 60-way fan-out outright with net::ERR_CACHE_WRITE_FAILURE under the 2 MiB memory cache we configured, and cost 70-705 ms of CronetEngine.build on first use. Paying that build off the critical path is the only reason AndroidPlatformHttpClient, warmUpPlatformHttpClient and the per-request delegate swap existed; all three go away with it. CupertinoClient had no measured advantage either, losing the TLS fan-out 44 vs 60 req/s, and no reported issue ever justified it. tvOS already shipped the dart:io client, so Apple platforms now agree with it. WinHttpClient stays. WINHTTP_OPTION_IPV6_FAST_FALLBACK (#1128) has no dart:io equivalent, and it brings the system proxy and the Schannel trust store. The pool tuning becomes unconditional. It was opt-in behind usePlexApiClient so generic tracker and auth clients stayed disposable, but every dart:io client has carried connectionTimeout and forceCloseOnDrainTimeout since #1972, so tuned and untuned already share shutdown semantics and the flag only cost throughput: 12 connections per host with a 90s idle measured ~4x the dart:io default on a 60-way fan-out on Linux and ~2x on Android. media3-datasource-cronet and cronet-embedded stay. They back ExoPlayer's CronetDataSource independently of package:cronet_http. Refs #2140.
65 lines
2.9 KiB
Dart
65 lines
2.9 KiB
Dart
import 'dart:io' show HttpClient, Platform;
|
|
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/io_client.dart';
|
|
import 'package:win_http/win_http.dart';
|
|
|
|
import 'app_logger.dart';
|
|
import 'managed_http_client.dart';
|
|
import 'media_server_timeouts.dart';
|
|
|
|
final Set<String> _loggedPlatformClients = <String>{};
|
|
|
|
void _logPlatformClient(String platform, String client) {
|
|
if (!_loggedPlatformClients.add(client)) return;
|
|
appLogger.i('Platform HTTP client', error: {'platform': platform, 'client': client});
|
|
}
|
|
|
|
/// dart:io leaves TCP connects unbounded (Darwin retries SYNs for ~75 s) and
|
|
/// `package:http` cannot abort a request whose connection is still being
|
|
/// established, so every IOClient gets an explicit connect bound and
|
|
/// permission to force-close after a failed drain (#1972).
|
|
///
|
|
/// The pool is always tuned. Every surface that matters fans out — Plex and
|
|
/// Jellyfin home loads, artwork rails, tracker and Seerr traffic all issue
|
|
/// several concurrent requests per pass — and the dart:io defaults of 6
|
|
/// connections per host and a 15 s idle timeout cost a fresh TLS handshake per
|
|
/// request on a high-RTT or CDN link. On a 60-way artwork fan-out, 12/90 s
|
|
/// measured ~4x the default's throughput on Linux and ~2x on Android, so there
|
|
/// is no case left for the opt-in the tuning used to be.
|
|
ManagedHttpClient _createIoClient(String debugLabel) {
|
|
final httpClient = HttpClient()
|
|
..connectionTimeout = MediaServerTimeouts.connect
|
|
..maxConnectionsPerHost = 12
|
|
..idleTimeout = const Duration(seconds: 90);
|
|
return ManagedHttpClient(IOClient(httpClient), debugLabel: debugLabel, forceCloseOnDrainTimeout: true);
|
|
}
|
|
|
|
/// Every platform except Windows runs on the tuned dart:io client.
|
|
///
|
|
/// Windows keeps WinHTTP for `WINHTTP_OPTION_IPV6_FAST_FALLBACK` (Happy
|
|
/// Eyeballs, #1128), which dart:io has no equivalent for: it tries a
|
|
/// dual-stack host's addresses sequentially, so one unreachable IPv6 address
|
|
/// stalls the connection past the endpoint probe budget. WinHTTP also brings
|
|
/// the system proxy and the Schannel trust store.
|
|
///
|
|
/// Cronet and NSURLSession used to serve Android and Apple here. Both were
|
|
/// measured slower than this client on the request shapes Plezy actually
|
|
/// issues — Cronet by ~10x on LAN body throughput — so neither survived; see
|
|
/// issue #2140.
|
|
http.Client createPlatformClient() {
|
|
if (Platform.isWindows) {
|
|
try {
|
|
final client = WinHttpClient.defaultConfiguration();
|
|
_logPlatformClient('windows', 'WinHttpClient');
|
|
return ManagedHttpClient(client, debugLabel: 'WinHttpClient');
|
|
} catch (e, st) {
|
|
appLogger.w('WinHttpClient init failed, falling back to IOClient', error: e, stackTrace: st);
|
|
_logPlatformClient('windows', 'IOClient (fallback)');
|
|
return _createIoClient('IOClient (fallback)');
|
|
}
|
|
}
|
|
_logPlatformClient(Platform.operatingSystem, 'IOClient');
|
|
return _createIoClient('IOClient');
|
|
}
|