Skip to content

Commit

Permalink
Run response mapper in Isolate
Browse files Browse the repository at this point in the history
  • Loading branch information
scrfrk committed Mar 6, 2023
1 parent a3433c9 commit 9dd02b7
Show file tree
Hide file tree
Showing 15 changed files with 1,360 additions and 402 deletions.
43 changes: 43 additions & 0 deletions ISOLATE_PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Isolate parsing performance

Parsing JSON on an isolate allows you to perform the operation in the background without blocking the main thread, which can be useful for large JSON files or when you need to perform other operations while parsing.

**Parsing on main thread took 20-60 microseconds.**

**Parsing used IsolateManager took 2400-2500 microseconds.**

**Parsing used Isolate.run took 110000-130000 microseconds.**

```dart
Future<T> executeInIsolate<T>({
required T Function(Response) mapper,
required Response response,
}) {
return Isolate.run(() => mapper.call(response));
}
```

These results were obtained :

```dart
final performRequest = (tokenPair) async {
final response = await _createRequest(params, tokenPair);
final timer = Stopwatch()..start();
params.responseMapper.call(response);
log('Sync mapper: ${timer.elapsedMicroseconds}');
timer.stop();
final timer2 = Stopwatch()..start();
final result = await isolateManager.send(response, params.responseMapper);
log('isolateManager.send: ${timer2.elapsedMicroseconds}');
timer2.stop();
final timer3 = Stopwatch()..start();
await executeInIsolate(mapper: params.responseMapper, response: response);
log('execute in Isolate.run: ${timer3.elapsedMicroseconds}');
timer3.stop();
return result;
};
```
12 changes: 10 additions & 2 deletions example/lib/api/application_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class ApplicationApi extends ApiClient {
dio: dio,
);

Future<UsersResponseModel> getUserList() => get(
path: 'users',
Future<UsersResponseModel> getUserList({int? page}) => get(
path: 'users${page != null ? '?page=$page' : ''}',
responseMapper: response_mappers.users,
validate: false,
receiveTimeout: Duration(seconds: 30),
Expand All @@ -30,4 +30,12 @@ class ApplicationApi extends ApiClient {
return Future.error(error!);
});
}

Future<void> getErrorRequest() => get(
path: 'users/23',
responseMapper: response_mappers.users,
validate: false,
receiveTimeout: Duration(seconds: 30),
sendTimeout: Duration(seconds: 30),
);
}
1 change: 1 addition & 0 deletions example/lib/api/models/errors/error_response_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class ResponseErrorModel extends Error {
factory ResponseErrorModel.fromJson(Map<String, dynamic> json) =>
_$ResponseErrorModelFromJson(json);

@JsonKey(defaultValue: {})
final Map<String, List<String>> errors;

Map<String, dynamic> toJson() => _$ResponseErrorModelToJson(this);
Expand Down
16 changes: 8 additions & 8 deletions example/lib/api/models/errors/error_response_model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 7 additions & 8 deletions example/lib/api/models/login_response_model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 8 additions & 9 deletions example/lib/api/models/user_response_model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 6 additions & 7 deletions example/lib/api/models/users_response_model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 61 additions & 3 deletions example/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class _MyHomePageState extends State<MyHomePage> {
bool isLoading = false;
StreamSubscription? subscription;
List<UserResponseModel> users = [];
int currentPage = 1;

@override
void initState() {
Expand All @@ -61,9 +62,36 @@ class _MyHomePageState extends State<MyHomePage> {
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
child: isLoading ? _getProgressWidget() : getUsersWidget(users),
),
body: LayoutBuilder(builder: (context, constraints) {
return isLoading
? _getProgressWidget()
: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: constraints.maxHeight * 0.8,
child: getUsersWidget(users),
),
Spacer(),
MaterialButton(
child: Text("Get next users"),
padding: EdgeInsets.all(10),
color: Colors.blueAccent,
onPressed: () => _loadNextPage(),
minWidth: constraints.maxWidth * 0.9,
),
SizedBox(height: 8),
MaterialButton(
child: Text("Error request"),
padding: EdgeInsets.all(10),
color: Colors.red,
onPressed: _loadErrorPage,
minWidth: constraints.maxWidth * 0.9,
),
Spacer(),
],
);
}),
floatingActionButton: FloatingActionButton(
onPressed: _loadUserList,
tooltip: 'Load a user list',
Expand Down Expand Up @@ -152,4 +180,34 @@ class _MyHomePageState extends State<MyHomePage> {

setState(() => isLoading = false);
}

Future<void> _loadNextPage() async {
await subscription?.cancel();

setState(() => isLoading = true);

try {
currentPage++;
final response = await widget.apiClient.getUserList(page: currentPage);
users.addAll(response.data);
} catch (e) {
showErrorDialog();
}

setState(() => isLoading = false);
}

Future<void> _loadErrorPage() async {
await subscription?.cancel();

setState(() => isLoading = true);

try {
await widget.apiClient.getErrorRequest();
} catch (e) {
showErrorDialog();
}

setState(() => isLoading = false);
}
}
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ packages:
path: ".."
relative: true
source: path
version: "3.5.2"
version: "3.6.0"
dio:
dependency: transitive
description:
Expand Down
14 changes: 10 additions & 4 deletions lib/src/api_client.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'dart:async';
import 'dart:io';

import 'package:dash_kit_network/src/error_handler_delegate.dart';
import 'package:dash_kit_network/src/exceptions/network_connection_exception.dart';
import 'package:dash_kit_network/src/exceptions/refresh_tokens_delegate_missing_exception.dart';
import 'package:dash_kit_network/src/exceptions/request_error_exception.dart';
import 'package:dash_kit_network/src/isolate_manager/isolate_manager_io/isolate_manager.dart'
if (dart.library.html) 'package:dash_kit_network/src/isolate_manager/isolate_manager_web/isolate_manager.dart';
import 'package:dash_kit_network/src/models/api_environment.dart';
import 'package:dash_kit_network/src/models/http_header.dart';
import 'package:dash_kit_network/src/models/request_params.dart';
Expand All @@ -14,8 +15,9 @@ import 'package:dash_kit_network/src/refresh_tokens_delegate.dart';
import 'package:dash_kit_network/src/token_manager.dart';
import 'package:dio/dio.dart';

/// Componet for communication with an API. Includes functionality
/// for updating tokens if they expired.
/// Component for communication with an API.
///
/// Includes functionality for updating tokens if they expired.
// ignore_for_file: long-parameter-list
abstract class ApiClient {
ApiClient({
Expand All @@ -39,6 +41,7 @@ abstract class ApiClient {
dio.options.baseUrl = environment.baseUrl;
}

final isolateManager = IsolateManager()..start();
final ApiEnvironment environment;
final Dio dio;
final List<HttpHeader> commonHeaders;
Expand Down Expand Up @@ -227,7 +230,10 @@ abstract class ApiClient {
final performRequest = (tokenPair) async {
final response = await _createRequest(params, tokenPair);

return params.responseMapper.call(response);
return await isolateManager.send(
response: response,
mapper: params.responseMapper,
);
};

if (params.isAuthorisedRequest) {
Expand Down
Loading

0 comments on commit 9dd02b7

Please sign in to comment.