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 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
/// Signature for getting notified when chunks of bytes are received while /// Signature for getting notified when chunks of bytes are received while
...@@ -22,11 +23,11 @@ import 'dart:typed_data'; ...@@ -22,11 +23,11 @@ import 'dart:typed_data';
/// returned to the client and the total size of the response may not be known /// returned to the client and the total size of the response may not be known
/// until the request has been fully processed). /// 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); typedef BytesReceivedCallback = void Function(int cumulative, int total);
/// Efficiently converts the response body of an [HttpClientResponse] into a /// Efficiently converts the response body of an [HttpClientResponse] into a
/// [Uint8List]. /// [TransferableTypedData].
/// ///
/// The future returned will forward any error emitted by `response`. /// The future returned will forward any error emitted by `response`.
/// ///
...@@ -43,13 +44,13 @@ typedef BytesReceivedCallback = void Function(int cumulative, int total); ...@@ -43,13 +44,13 @@ typedef BytesReceivedCallback = void Function(int cumulative, int total);
/// bytes from this method (assuming the response is sending compressed bytes), /// bytes from this method (assuming the response is sending compressed bytes),
/// set both [HttpClient.autoUncompress] to false and the `autoUncompress` /// set both [HttpClient.autoUncompress] to false and the `autoUncompress`
/// parameter to false. /// parameter to false.
Future<Uint8List> consolidateHttpClientResponseBytes( Future<TransferableTypedData> getHttpClientResponseBytes(
HttpClientResponse response, { HttpClientResponse response, {
bool autoUncompress = true, bool autoUncompress = true,
BytesReceivedCallback onBytesReceived, BytesReceivedCallback onBytesReceived,
}) { }) {
assert(autoUncompress != null); assert(autoUncompress != null);
final Completer<Uint8List> completer = Completer<Uint8List>.sync(); final Completer<TransferableTypedData> completer = Completer<TransferableTypedData>.sync();
final _OutputBuffer output = _OutputBuffer(); final _OutputBuffer output = _OutputBuffer();
ByteConversionSink sink = output; ByteConversionSink sink = output;
...@@ -89,41 +90,54 @@ Future<Uint8List> consolidateHttpClientResponseBytes( ...@@ -89,41 +90,54 @@ Future<Uint8List> consolidateHttpClientResponseBytes(
} }
}, onDone: () { }, onDone: () {
sink.close(); sink.close();
completer.complete(output.bytes); completer.complete(TransferableTypedData.fromList(output.chunks));
}, onError: completer.completeError, cancelOnError: true); }, onError: completer.completeError, cancelOnError: true);
return completer.future; 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 { class _OutputBuffer extends ByteConversionSinkBase {
List<List<int>> _chunks = <List<int>>[]; final List<Uint8List> chunks = <Uint8List>[];
int _contentLength = 0;
Uint8List _bytes;
@override @override
void add(List<int> chunk) { void add(List<int> chunk) {
assert(_bytes == null); chunks.add(chunk);
_chunks.add(chunk);
_contentLength += chunk.length;
} }
@override @override
void close() { 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;
}
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -120,8 +121,8 @@ class NetworkAssetBundle extends AssetBundle { ...@@ -120,8 +121,8 @@ class NetworkAssetBundle extends AssetBundle {
'Unable to load asset: $key\n' 'Unable to load asset: $key\n'
'HTTP status code: ${response.statusCode}' 'HTTP status code: ${response.statusCode}'
); );
final Uint8List bytes = await consolidateHttpClientResponseBytes(response); final TransferableTypedData transferable = await getHttpClientResponseBytes(response);
return bytes.buffer.asByteData(); return transferable.materialize().asByteData();
} }
/// Retrieve a string from the asset bundle, parse it with the given function, /// Retrieve a string from the asset bundle, parse it with the given function,
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -14,7 +15,7 @@ import 'package:mockito/mockito.dart'; ...@@ -14,7 +15,7 @@ import 'package:mockito/mockito.dart';
import '../flutter_test_alternative.dart'; import '../flutter_test_alternative.dart';
void main() { void main() {
group(consolidateHttpClientResponseBytes, () { group(getHttpClientResponseBytes, () {
final Uint8List chunkOne = Uint8List.fromList(<int>[0, 1, 2, 3, 4, 5]); final Uint8List chunkOne = Uint8List.fromList(<int>[0, 1, 2, 3, 4, 5]);
final Uint8List chunkTwo = Uint8List.fromList(<int>[6, 7, 8, 9, 10]); final Uint8List chunkTwo = Uint8List.fromList(<int>[6, 7, 8, 9, 10]);
MockHttpClientResponse response; MockHttpClientResponse response;
...@@ -46,24 +47,24 @@ void main() { ...@@ -46,24 +47,24 @@ void main() {
test('Converts an HttpClientResponse with contentLength to bytes', () async { test('Converts an HttpClientResponse with contentLength to bytes', () async {
when(response.contentLength) when(response.contentLength)
.thenReturn(chunkOne.length + chunkTwo.length); .thenReturn(chunkOne.length + chunkTwo.length);
final List<int> bytes = final List<int> bytes = (await getHttpClientResponseBytes(response))
await consolidateHttpClientResponseBytes(response); .materialize().asUint8List();
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}); });
test('Converts a compressed HttpClientResponse with contentLength to bytes', () async { test('Converts a compressed HttpClientResponse with contentLength to bytes', () async {
when(response.contentLength).thenReturn(chunkOne.length); when(response.contentLength).thenReturn(chunkOne.length);
final List<int> bytes = final List<int> bytes = (await getHttpClientResponseBytes(response))
await consolidateHttpClientResponseBytes(response); .materialize().asUint8List();
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}); });
test('Converts an HttpClientResponse without contentLength to bytes', () async { test('Converts an HttpClientResponse without contentLength to bytes', () async {
when(response.contentLength).thenReturn(-1); when(response.contentLength).thenReturn(-1);
final List<int> bytes = final List<int> bytes = (await getHttpClientResponseBytes(response))
await consolidateHttpClientResponseBytes(response); .materialize().asUint8List();
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}); });
...@@ -72,7 +73,7 @@ void main() { ...@@ -72,7 +73,7 @@ void main() {
final int syntheticTotal = (chunkOne.length + chunkTwo.length) * 2; final int syntheticTotal = (chunkOne.length + chunkTwo.length) * 2;
when(response.contentLength).thenReturn(syntheticTotal); when(response.contentLength).thenReturn(syntheticTotal);
final List<int> records = <int>[]; final List<int> records = <int>[];
await consolidateHttpClientResponseBytes( await getHttpClientResponseBytes(
response, response,
onBytesReceived: (int cumulative, int total) { onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]); records.addAll(<int>[cumulative, total]);
...@@ -110,13 +111,13 @@ void main() { ...@@ -110,13 +111,13 @@ void main() {
}); });
when(response.contentLength).thenReturn(-1); when(response.contentLength).thenReturn(-1);
expect(consolidateHttpClientResponseBytes(response), expect(getHttpClientResponseBytes(response),
throwsA(isInstanceOf<Exception>())); throwsA(isInstanceOf<Exception>()));
}); });
test('Propagates error to Future return value if onBytesReceived throws', () async { test('Propagates error to Future return value if onBytesReceived throws', () async {
when(response.contentLength).thenReturn(-1); when(response.contentLength).thenReturn(-1);
final Future<List<int>> result = consolidateHttpClientResponseBytes( final Future<TransferableTypedData> result = getHttpClientResponseBytes(
response, response,
onBytesReceived: (int cumulative, int total) { onBytesReceived: (int cumulative, int total) {
throw 'misbehaving callback'; throw 'misbehaving callback';
...@@ -157,14 +158,14 @@ void main() { ...@@ -157,14 +158,14 @@ void main() {
test('Uncompresses GZIP bytes if autoUncompress is true and response.compressionState is compressed', () async { test('Uncompresses GZIP bytes if autoUncompress is true and response.compressionState is compressed', () async {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed); when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length); 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]); 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 { test('returns gzipped bytes if autoUncompress is false and response.compressionState is compressed', () async {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed); when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length); 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); expect(bytes, gzipped);
}); });
...@@ -172,7 +173,7 @@ void main() { ...@@ -172,7 +173,7 @@ void main() {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed); when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length); when(response.contentLength).thenReturn(gzipped.length);
final List<int> records = <int>[]; final List<int> records = <int>[];
await consolidateHttpClientResponseBytes( await getHttpClientResponseBytes(
response, response,
onBytesReceived: (int cumulative, int total) { onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]); records.addAll(<int>[cumulative, total]);
...@@ -192,7 +193,7 @@ void main() { ...@@ -192,7 +193,7 @@ void main() {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.decompressed); when(response.compressionState).thenReturn(HttpClientResponseCompressionState.decompressed);
when(response.contentLength).thenReturn(syntheticTotal); when(response.contentLength).thenReturn(syntheticTotal);
final List<int> records = <int>[]; final List<int> records = <int>[];
await consolidateHttpClientResponseBytes( await getHttpClientResponseBytes(
response, response,
onBytesReceived: (int cumulative, int total) { onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, 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