Unverified Commit b12bdd0e authored by Alexander Aprelev's avatar Alexander Aprelev Committed by GitHub

Use separate isolate for image loading. (#34188)

* Use separate isolate for image loading. Use TransferableTypedData to const-cost receive bytes from that isolate.
parent 3cf88fed
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
/// Signature for getting notified when chunks of bytes are received while
......@@ -22,11 +23,11 @@ import 'dart:typed_data';
/// returned to the client and the total size of the response may not be known
/// until the request has been fully processed).
///
/// This is used in [consolidateHttpClientResponseBytes].
/// This is used in [getHttpClientResponseBytes].
typedef BytesReceivedCallback = void Function(int cumulative, int total);
/// Efficiently converts the response body of an [HttpClientResponse] into a
/// [Uint8List].
/// [TransferableTypedData].
///
/// The future returned will forward any error emitted by `response`.
///
......@@ -43,13 +44,13 @@ typedef BytesReceivedCallback = void Function(int cumulative, int total);
/// bytes from this method (assuming the response is sending compressed bytes),
/// set both [HttpClient.autoUncompress] to false and the `autoUncompress`
/// parameter to false.
Future<Uint8List> consolidateHttpClientResponseBytes(
Future<TransferableTypedData> getHttpClientResponseBytes(
HttpClientResponse response, {
bool autoUncompress = true,
BytesReceivedCallback onBytesReceived,
}) {
assert(autoUncompress != null);
final Completer<Uint8List> completer = Completer<Uint8List>.sync();
final Completer<TransferableTypedData> completer = Completer<TransferableTypedData>.sync();
final _OutputBuffer output = _OutputBuffer();
ByteConversionSink sink = output;
......@@ -89,41 +90,54 @@ Future<Uint8List> consolidateHttpClientResponseBytes(
}
}, onDone: () {
sink.close();
completer.complete(output.bytes);
completer.complete(TransferableTypedData.fromList(output.chunks));
}, onError: completer.completeError, cancelOnError: true);
return completer.future;
}
/// Efficiently converts the response body of an [HttpClientResponse] into a
/// [Uint8List].
///
/// (This method is deprecated - use [getHttpClientResponseBytes] instead.)
///
/// The future returned will forward any error emitted by `response`.
///
/// The `onBytesReceived` callback, if specified, will be invoked for every
/// chunk of bytes that is received while consolidating the response bytes.
/// If the callback throws an error, processing of the response will halt, and
/// the returned future will complete with the error that was thrown by the
/// callback. For more information on how to interpret the parameters to the
/// callback, see the documentation on [BytesReceivedCallback].
///
/// If the `response` is gzipped and the `autoUncompress` parameter is true,
/// this will automatically un-compress the bytes in the returned list if it
/// hasn't already been done via [HttpClient.autoUncompress]. To get compressed
/// bytes from this method (assuming the response is sending compressed bytes),
/// set both [HttpClient.autoUncompress] to false and the `autoUncompress`
/// parameter to false.
@Deprecated('Use getHttpClientResponseBytes instead')
Future<Uint8List> consolidateHttpClientResponseBytes(
HttpClientResponse response, {
bool autoUncompress = true,
BytesReceivedCallback onBytesReceived,
}) async {
final TransferableTypedData bytes = await getHttpClientResponseBytes(
response,
autoUncompress: autoUncompress,
onBytesReceived: onBytesReceived,
);
return bytes.materialize().asUint8List();
}
class _OutputBuffer extends ByteConversionSinkBase {
List<List<int>> _chunks = <List<int>>[];
int _contentLength = 0;
Uint8List _bytes;
final List<Uint8List> chunks = <Uint8List>[];
@override
void add(List<int> chunk) {
assert(_bytes == null);
_chunks.add(chunk);
_contentLength += chunk.length;
chunks.add(chunk);
}
@override
void close() {
if (_bytes != null) {
// We've already been closed; this is a no-op
return;
}
_bytes = Uint8List(_contentLength);
int offset = 0;
for (List<int> chunk in _chunks) {
_bytes.setRange(offset, offset + chunk.length, chunk);
offset += chunk.length;
}
_chunks = null;
}
Uint8List get bytes {
assert(_bytes != null);
return _bytes;
}
void close() {}
}
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
......@@ -120,8 +121,8 @@ class NetworkAssetBundle extends AssetBundle {
'Unable to load asset: $key\n'
'HTTP status code: ${response.statusCode}'
);
final Uint8List bytes = await consolidateHttpClientResponseBytes(response);
return bytes.buffer.asByteData();
final TransferableTypedData transferable = await getHttpClientResponseBytes(response);
return transferable.materialize().asByteData();
}
/// Retrieve a string from the asset bundle, parse it with the given function,
......
......@@ -6,6 +6,7 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
......@@ -14,7 +15,7 @@ import 'package:mockito/mockito.dart';
import '../flutter_test_alternative.dart';
void main() {
group(consolidateHttpClientResponseBytes, () {
group(getHttpClientResponseBytes, () {
final Uint8List chunkOne = Uint8List.fromList(<int>[0, 1, 2, 3, 4, 5]);
final Uint8List chunkTwo = Uint8List.fromList(<int>[6, 7, 8, 9, 10]);
MockHttpClientResponse response;
......@@ -46,24 +47,24 @@ void main() {
test('Converts an HttpClientResponse with contentLength to bytes', () async {
when(response.contentLength)
.thenReturn(chunkOne.length + chunkTwo.length);
final List<int> bytes =
await consolidateHttpClientResponseBytes(response);
final List<int> bytes = (await getHttpClientResponseBytes(response))
.materialize().asUint8List();
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
test('Converts a compressed HttpClientResponse with contentLength to bytes', () async {
when(response.contentLength).thenReturn(chunkOne.length);
final List<int> bytes =
await consolidateHttpClientResponseBytes(response);
final List<int> bytes = (await getHttpClientResponseBytes(response))
.materialize().asUint8List();
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
test('Converts an HttpClientResponse without contentLength to bytes', () async {
when(response.contentLength).thenReturn(-1);
final List<int> bytes =
await consolidateHttpClientResponseBytes(response);
final List<int> bytes = (await getHttpClientResponseBytes(response))
.materialize().asUint8List();
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
......@@ -72,7 +73,7 @@ void main() {
final int syntheticTotal = (chunkOne.length + chunkTwo.length) * 2;
when(response.contentLength).thenReturn(syntheticTotal);
final List<int> records = <int>[];
await consolidateHttpClientResponseBytes(
await getHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]);
......@@ -110,13 +111,13 @@ void main() {
});
when(response.contentLength).thenReturn(-1);
expect(consolidateHttpClientResponseBytes(response),
expect(getHttpClientResponseBytes(response),
throwsA(isInstanceOf<Exception>()));
});
test('Propagates error to Future return value if onBytesReceived throws', () async {
when(response.contentLength).thenReturn(-1);
final Future<List<int>> result = consolidateHttpClientResponseBytes(
final Future<TransferableTypedData> result = getHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
throw 'misbehaving callback';
......@@ -157,14 +158,14 @@ void main() {
test('Uncompresses GZIP bytes if autoUncompress is true and response.compressionState is compressed', () async {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length);
final List<int> bytes = await consolidateHttpClientResponseBytes(response);
final List<int> bytes = (await getHttpClientResponseBytes(response)).materialize().asUint8List();
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
test('returns gzipped bytes if autoUncompress is false and response.compressionState is compressed', () async {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length);
final List<int> bytes = await consolidateHttpClientResponseBytes(response, autoUncompress: false);
final List<int> bytes = (await getHttpClientResponseBytes(response, autoUncompress: false)).materialize().asUint8List();
expect(bytes, gzipped);
});
......@@ -172,7 +173,7 @@ void main() {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length);
final List<int> records = <int>[];
await consolidateHttpClientResponseBytes(
await getHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]);
......@@ -192,7 +193,7 @@ void main() {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.decompressed);
when(response.contentLength).thenReturn(syntheticTotal);
final List<int> records = <int>[];
await consolidateHttpClientResponseBytes(
await getHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]);
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import '../painting/image_data.dart';
void main() {
final MockHttpClient client = MockHttpClient();
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
final MockHttpHeaders headers = MockHttpHeaders();
testWidgets('Headers', (WidgetTester tester) async {
HttpOverrides.runZoned<Future<void>>(() async {
await tester.pumpWidget(Image.network(
'https://www.example.com/images/frame.png',
headers: const <String, String>{'flutter': 'flutter'},
));
verify(headers.add('flutter', 'flutter')).called(1);
}, createHttpClient: (SecurityContext _) {
when(client.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.headers).thenReturn(headers);
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.contentLength).thenReturn(kTransparentImage.length);
when(response.statusCode).thenReturn(HttpStatus.ok);
when(response.listen(any)).thenAnswer((Invocation invocation) {
final void Function(List<int>) onData = invocation.positionalArguments[0];
final void Function() onDone = invocation.namedArguments[#onDone];
final void Function(Object, [ StackTrace ]) onError = invocation.namedArguments[#onError];
final bool cancelOnError = invocation.namedArguments[#cancelOnError];
return Stream<List<int>>.fromIterable(<List<int>>[kTransparentImage]).listen(onData, onDone: onDone, onError: onError, cancelOnError: cancelOnError);
});
return client;
});
}, skip: isBrowser);
}
class MockHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
class MockHttpHeaders extends Mock implements HttpHeaders {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment