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 0519b54
Show file tree
Hide file tree
Showing 15 changed files with 1,266 additions and 401 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
8 changes: 5 additions & 3 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.dart'
if (dart.library.html) 'package:dash_kit_network/src/isolate_manager/isolate_manager_web.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,7 +15,7 @@ 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
/// Component for communication with an API. Includes functionality
/// for updating tokens if they expired.
// ignore_for_file: long-parameter-list
abstract class ApiClient {
Expand All @@ -39,6 +40,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 +229,7 @@ abstract class ApiClient {
final performRequest = (tokenPair) async {
final response = await _createRequest(params, tokenPair);

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

if (params.isAuthorisedRequest) {
Expand Down
69 changes: 69 additions & 0 deletions lib/src/isolate_manager/isolate_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'dart:async';

import 'package:dash_kit_network/dash_kit_network.dart';

// This is an abstract class for managing isolates It defines the basic methods
// needed to start, stop and send messages to an isolate. Flutter Web doesn't
// support isolates so the `IsolateManager` class has a different implementation
// for web.
// ignore: prefer-match-file-name
abstract class IsolateManagerInterface {
// Method for starting the isolate. Should be called before sending messages.
Future<void> start();

// Method for stopping the isolate. Should be called when the isolate is no
//longer needed.
void stop();

// Method for sending messages to the isolate. The `mapper` function is
// executed in the isolate and the result is returned.
Future<FutureOr<T>> send<T>(
Response<dynamic> response,
FutureOr<T> Function(Response<dynamic>) mapper,
);
}

// Class Task is used to store the data needed to send a message to the isolate.
class Task<T> {
const Task(
this.id,
this.response,
this.mapper,
);

final String id;
final Response<dynamic> response;
final T Function(Response<dynamic>) mapper;

@override
String toString() {
return 'Task{id: $id, response: $response, mapper: $mapper}';
}
}

// Class Result is used to store the result of the mapper function executed in
// the isolate.
class Result<T> {
const Result({
required this.id,
this.result,
this.error,
});

final String id;
final T? result;
final dynamic error;

@override
String toString() {
return 'Result{id: $id, result: $result, error: $error}';
}
}

// The IsolateStatus enum is used to track the status of the isolate.
enum IsolateStatus {
created,
initializing,
initialized,
stopped,
}
Loading

0 comments on commit 0519b54

Please sign in to comment.