isolate_manager 6.0.1
isolate_manager: ^6.0.1 copied to clipboard
Create long-lived isolates for single or multiple functions, with support for Web Workers (via an efficient generator) and WASM compilation.
Isolate Manager #
Isolate Manager is a powerful Flutter/Dart package designed to simplify concurrent programming using isolates. It offers robust cross-platform support, including native, web (via JavaScript Workers), and WebAssembly (WASM).
Why Isolate Manager? #
- Effortless Concurrency: Takes the complexity out of Dart's isolates for smooth background processing.
- Truly Cross-Platform: Write your concurrent code once and run it seamlessly on Dart VM, Web (auto-compiles to JS Workers), and WASM.
- Robust & Safe: Provides built-in type and exception safety mechanisms, especially crucial for web worker communication.
- Optimized Performance: Features smart task queuing, priority handling, and customizable strategies to fine-tune execution.
- Flexible Isolate Types: Choose from one-off, multi-function, or single-function isolates to best suit your task's lifecycle.
Features #
- Versatile Isolate Management:
- One-off Isolates: Perfect for single, intensive computations. Supports web workers.
- Multi-Function Isolates: Reuse a single isolate for various functions, minimizing overhead.
- Single-Function Isolates: Dedicate an isolate to a specific, continuous task or data stream.
- Cross-Platform by Design:
- Web & WASM Ready: Automatically compiles isolate functions into JavaScript Workers for web deployment.
- Graceful Fallback: Defaults to
Future
/Stream
if Web Workers are unavailable.
- Intelligent Queue System:
- Automatic task queuing.
- Support for priority tasks.
- Customizable queue overflow strategies.
- Type & Exception Safety:
- Utilize specialized
ImType
wrappers (ImNum
,ImString
, etc.) for reliable data transfer with web workers. - Propagate custom exceptions across isolate boundaries, even on the web.
- Utilize specialized
- Developer-Friendly Extras:
- Custom Isolate Functions: Gain full control over the isolate's lifecycle and communication.
- Progress Reporting: Send intermediate updates from long-running tasks.
- Codeless Web Workers: Generate necessary JavaScript worker files without
build_runner
. - Built-in Benchmark: Compare performance of different concurrency models.
Getting Started #
1. Add Dependencies #
Add the following to your pubspec.yaml
:
dependencies:
isolate_manager: ^latest_version # Use the latest version
dev_dependencies:
isolate_manager_generator: ^latest_version # Required for web worker generation
Then, run dart pub get
or flutter pub get
.
2. Important Prerequisite for Isolate Functions #
Functions intended to run in an isolate must be:
- A
static
method within a class, OR - A top-level function (defined outside any class).
Additionally, annotate these functions with @pragma('vm:entry-point')
to prevent them from being removed by tree-shaking during compilation.
Note: Worker annotations like
@isolateManagerWorker
,@isolateManagerSharedWorker
, and@isolateManagerCustomWorker
are provided by theisolate_manager
package. Make sure to import them if you use them in your code.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
int sum(List<int> numbers) {
return numbers.reduce((a, b) => a + b);
}
3. Platform-Specific Setup #
Mobile & Desktop (VM)
No additional setup is required!
Web (JavaScript Workers)
To use isolates on the web, your Dart functions need to be compiled into JavaScript Workers.
-
Annotate Your Functions: Use specific annotations on the functions you want to be available as web workers. Run the generator after adding or modifying these.
@isolateManagerWorker
: For one-off or single-function isolates.@isolateManagerSharedWorker
: For shared, multi-function isolates.@isolateManagerCustomWorker
: For custom isolate functions where you manage communication manually.
-
Data Transfer Limitations:
- Functions for web workers must not depend on Flutter-specific libraries (e.g.,
dart:ui
). - Only Dart primitives (
num
,String
,bool
,null
), andMap
orList
collections containing these primitives, are directly transferable. - For other data types, use the provided
ImType
wrappers or serialize/deserialize your data manually.
- Functions for web workers must not depend on Flutter-specific libraries (e.g.,
-
Generate JS Workers: Run the following command in your terminal:
dart run isolate_manager:generate
(See Web Worker Generator for more options.)
WebAssembly (WASM) Notes
-
Type Handling: When using WASM, all
int
types (including those in collections) are treated asdouble
. Isolate Manager provides a built-in converter to handle this automatically; you can disable it by settingenableWasmConverter: false
if needed. -
Development Server Headers: If your app hangs when running with
flutter run -d chrome --wasm
, you might need to set specific headers. Try:flutter run -d chrome --wasm --web-header=Cross-Origin-Opener-Policy=same-origin --web-header=Cross-Origin-Embedder-Policy=require-corp
Usage Examples #
One-off Isolate (Simple Task) #
Ideal for fire-and-forget computations.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerWorker // For web worker generation
int fibonacciRecursive(int n) {
if (n < 2) return n;
return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}
void main() async {
// Option 1: Explicit worker parameters (useful if function name differs or for clarity)
final result1 = await IsolateManager.run(
() => fibonacciRecursive(40), // The actual function to execute
workerName: 'fibonacciRecursive', // Name used for JS worker mapping
workerParameter: 40, // Parameter for the JS worker
);
print('Fibonacci(40) - Option 1: $result1');
// Option 2: Automatic worker mapping (concise)
final result2 = await IsolateManager.runFunction(fibonacciRecursive, 40);
print('Fibonacci(40) - Option 2: $result2');
}
Long-Lived Multi-Function Isolate #
Reuse a single isolate for multiple different computations.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerSharedWorker
Future<double> addNumbersFuture(List<double> values) async {
return values[0] + values[1];
}
@pragma('vm:entry-point')
@isolateManagerSharedWorker
int multiplyNumbers(List<int> values) {
return values[0] * values[1];
}
void main() async {
final sharedIsolate = IsolateManager.createShared(
concurrent: 2, // Number of tasks this isolate can handle concurrently
useWorker: true, // Enable web worker usage if on web
workerMappings: {
// Map Dart function references to their JS worker names
addNumbersFuture: 'addNumbersFuture',
multiplyNumbers: 'multiplyNumbers',
},
);
// Optional: Listen for intermediate values (if any function sends them)
sharedIsolate.stream.listen((value) {
print('Intermediate value from shared isolate: $value');
});
final sumResult = await sharedIsolate.compute(addNumbersFuture, [10.5, 20.3]);
print('Sum (shared): 10.5 + 20.3 = $sumResult');
final productResult = await sharedIsolate.compute(multiplyNumbers, [7, 6]);
print('Product (shared): 7 * 6 = $productResult');
await sharedIsolate.stop(); // Important to release resources
// Or use `sharedIsolate.restart()` to restart the isolate
}
Long-Lived Single-Function Isolate #
Dedicate an isolate to a single, potentially continuous, function.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerWorker
int complexCalculation(int initialValue) {
// Simulate a task that might send progress updates or run for a while
int result = initialValue;
for (int i = 0; i < 5; i++) {
result += i * 2;
// If this were a custom function, you could send progress here
}
return result;
}
void main() async {
final singleFuncIsolate = IsolateManager.create(
complexCalculation, // The function this isolate is dedicated to
workerName: 'complexCalculation', // For JS worker
concurrent: 1, // Typically 1 for a single dedicated function
);
// Optional: Listen for intermediate values
singleFuncIsolate.stream.listen((value) {
print('Intermediate value from single-function isolate: $value');
});
final calculationResult = await singleFuncIsolate.compute(10); // `compute` is callable
print('Complex Calculation Result: $calculationResult');
await singleFuncIsolate.stop(); // Release resources
// Or use `singleFuncIsolate.restart()` to restart the isolate
}
Custom Function & Error Handling #
For fine-grained control over the isolate's behavior, including sending multiple results or handling initialization/disposal.
import 'dart:convert';
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerWorker // For web
int fibonacci(int n) {
if (n < 0) throw ArgumentError('Input cannot be negative.');
if (n < 2) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
@pragma('vm:entry-point')
@isolateManagerCustomWorker
void customFibonacciWorker(dynamic params) {
IsolateManagerFunction.customFunction<int, int>(
params,
onEvent: (controller, message) {
try {
final result = fibonacci(message);
controller.sendResult(result); // Send the final result
} catch (err, stack) {
// Send an IsolateException for structured error handling
controller.sendResultError(IsolateException(err.toString(), stack.toString()));
}
return 0; // Indicates completion for this event
},
onInit: (controller) {
print('Custom Fibonacci Worker: Initialized');
// Perform any setup logic here
},
onDispose: (controller) {
print('Custom Fibonacci Worker: Disposed');
// Perform any cleanup logic here
},
autoHandleException: false, // Set to true to let IsolateManager handle basic errors
autoHandleResult: false, // Set to true to let IsolateManager handle basic result sending
);
}
void main() async {
final customIsolate = IsolateManager.createCustom(
customFibonacciWorker,
workerName: 'customFibonacciWorker', // For JS Worker
debugMode: true, // Enable more logging
);
try {
final result = await customIsolate.compute(10);
print('Custom Fibonacci(10): $result');
final resultNegative = await customIsolate.compute(-5); // This will throw
print('Custom Fibonacci(-5): $resultNegative');
} on IsolateException catch (e) {
print('Caught IsolateException: ${e.error}');
// print('Stack trace: ${e.stacktrace}');
} catch (e) {
print('Caught other error: $e');
} finally {
await customIsolate.stop();
}
}
Advanced Capabilities #
Smart Queue Management #
Control how tasks are queued and processed when multiple requests are made to an isolate.
-
Priority Tasks: Add
priority: true
when callingcompute
orrun
to move a task to the front of its queue. -
Queue Size Limit: Set
maxCount
inIsolateManager.create
,createShared
, orcreateCustom
constructors to limit the number of pending tasks. -
Queue Strategies: Define behavior when
maxCount
is reached:UnlimitedStrategy()
: (Default) No limit on queued tasks.DropNewestStrategy()
: Removes the most recently added task.DropOldestStrategy()
: Removes the oldest task in the queue.RejectIncomingStrategy()
: Rejects any new tasks if the queue is full.
-
Custom Queue Strategy: For custom logic, extend
QueueStrategy<R, P>
:class MyCustomStrategy<R, P> extends QueueStrategy<R, P> { @override bool continueIfMaxCountExceeded() { // Access `queues`, `queuesCount`, `maxCount` if (queuesCount >= maxCount && queues.isNotEmpty) { // Example: Allow if the oldest task is low priority (pseudo-code) // if (queues.first.priority == false) { // print('Custom strategy: Dropping oldest to make space.'); // queues.removeFirst(); // You'd need to manage this carefully // return true; // Allow the new task // } return false; // Reject by default if full } return true; // Allow if not full } }
Note: Modifying
queues
directly incontinueIfMaxCountExceeded
requires careful implementation to ensure consistency.
Progress Updates #
Receive intermediate results from a task before it completes. This is typically used with IsolateManager.createCustom
.
In this example, progress updates are sent as JSON strings for demonstration purposes.
// In your main Dart code:
void main() async {
final isolate = IsolateManager.createCustom(
longRunningTaskWithProgress,
workerName: 'longRunningTaskWithProgress', // For JS Worker
);
print('Starting task with progress updates...');
final result = await isolate.compute(
100, // Example parameter for the task
callback: (dynamic value) {
// `value` is what `controller.sendResult()` sends from the isolate
try {
final data = jsonDecode(value as String); // Assuming JSON string for progress
if (data.containsKey('progress')) {
print('Progress: ${data['progress']}%');
return false; // Indicates this is a progress update, not the final result
} else if (data.containsKey('finalResult')) {
print('Final result package received: ${data['finalResult']}');
return true; // Indicates this is the final result
}
} catch (e) {
print('Error decoding progress/result: $e');
return true; // Treat as error, stop listening
}
return true; // Default to stop if format is unexpected
},
);
print('Task completed with final processed value: $result');
await isolate.stop();
}
// In your isolate function (e.g., custom worker):
@pragma('vm:entry-point')
@isolateManagerCustomWorker
void longRunningTaskWithProgress(dynamic params) {
IsolateManagerFunction.customFunction<String, int>(
params,
onEvent: (controller, totalSteps) {
for (int i = 0; i <= totalSteps; i += 10) {
final progressReport = jsonEncode({'progress': i});
controller.sendResult(progressReport); // Send progress update
}
// Send the final result
return jsonEncode({'finalResult': totalSteps});
},
);
}
Ensuring Type Safety (Web) #
When working with Web Workers, data transfer is restricted. IsolateManager
provides ImType
wrappers for common types to ensure they are correctly (de)serialized.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerWorker
ImMap processDataWeb(ImList numbers) {
// 1. Unwrap to get standard Dart types (List<dynamic> in this case)
final nativeList = numbers.unwrap as List<dynamic>;
// 2. Process the data
final processedMap = <ImType, ImType>{};
for (var i = 0; i < nativeList.length; i++) {
final numVal = nativeList[i] as num; // Ensure type
processedMap[ImString('item_$i')] = ImNum(numVal * 2);
}
// 3. Wrap the result in an ImType
return ImMap(processedMap);
}
void main() async {
// On web, this will use the JS worker if generated.
final isolate = IsolateManager.create(
processDataWeb,
workerName: 'processDataWeb',
);
try {
// 1. Wrap your input data
final inputData = ImList.wrap([1, 2.5, 3]);
// 2. Compute
final ImMap result = await isolate.compute(inputData) as ImMap;
// 3. Unwrap the result
final nativeResult = result.unwrap as Map<dynamic, dynamic>;
nativeResult.forEach((key, value) {
print('Web Processed: ${key} -> ${value}');
});
} on UnsupportedImTypeException catch (e) {
print('Error: Unsupported type used with ImType. ${e.message}');
} catch (e) {
print('An error occurred: $e');
} finally {
await isolate.stop();
}
}
Available ImType
wrappers (for non-nullable types only):
ImNum(double value)
/ImNum(int value)
ImString(String value)
ImBool(bool value)
ImList(List<ImType> value)
orImList.wrap(List<dynamic> nativeList)
ImMap(Map<ImType, ImType> value)
orImMap.wrap(Map<dynamic, dynamic> nativeMap)
An UnsupportedImTypeException
is thrown if ImList.wrap
or ImMap.wrap
encounters a type that cannot be converted.
Handling Exceptions (Web) #
To ensure custom exceptions are correctly propagated from Web Workers:
- Define a Custom IsolateException: Extend
IsolateException
. - Register the Exception: Use
IsolateManager.registerException()
in your main application. - Throw the Custom Exception: In your isolate function.
import 'package:isolate_manager/isolate_manager.dart';
// 1. Define your custom exception
class MyCustomWebException extends IsolateException {
const MyCustomWebException(super.error, [super.stacktrace]);
@override
String get name => 'MyCustomWebException'; // Crucial for deserialization
}
@pragma('vm:entry-point')
@isolateManagerWorker
ImNum taskThatThrowsCustom(ImNum input) {
if (input.unwrap == 0) {
throw const MyCustomWebException('Input cannot be zero!');
}
return ImNum(100 / (input.unwrap as num));
}
void main() async {
// 2. Register the exception type (typically in your app's initialization)
IsolateManager.registerException(
(message, stackTrace) => MyCustomWebException(message, stackTrace),
);
final isolate = IsolateManager.create(
taskThatThrowsCustom,
workerName: 'taskThatThrowsCustom', // For JS Worker
);
try {
print('Trying with valid input...');
final result = await isolate.compute(ImNum(10));
print('Result: ${(result as ImNum).unwrap}'); // Should be 10
print('\nTrying with input that causes custom exception...');
await isolate.compute(ImNum(0)); // This will throw
} on MyCustomWebException catch (e, s) {
print('Caught MyCustomWebException!');
print('Error: ${e.error}');
// print('Stack: $s'); // s is the stacktrace from the main isolate
// print('Original Isolate Stack: ${e.stacktrace}'); // stacktrace from the worker
} catch (e) {
print('Caught an unexpected error: $e');
} finally {
await isolate.stop();
}
}
Add a Worker Mapping Once #
Typically done during app initialization:
IsolateManager.addWorkerMapping(
fibonacciRecursive, // Dart function
'fibonacciRecursive', // Generated JS-worker file name
);
IsolateManager.addWorkerMapping(addNumbersFuture, 'addNumbersFuture');
Use the mapped function:
// No boilerplate — the manager already knows which worker to spin up.
await IsolateManager.runFunction(fibonacciRecursive, 40);
await sharedIsolate.compute(addNumbersFuture, [10.5, 20.3]);
await singleFuncIsolate.compute(10);
If you’re wondering what “magic” was removed:
// Long form (now unnecessary):
// final result = await IsolateManager.run(
// () => fibonacciRecursive(40),
// workerName: 'fibonacciRecursive',
// workerParameter: 40,
// );
Web Worker Generator #
Use the isolate_manager:generate
command to compile annotated Dart functions into JavaScript workers for web deployment.
See Web (JavaScript Workers) for web platform setup.
Command:
dart run isolate_manager:generate
Flags & Options:
--single
: Generate only for functions annotated with@isolateManagerWorker
.--shared
: Generate only for functions annotated with@isolateManagerSharedWorker
.--in <path>
(or-i <path>
): Specify the input directory to scan for annotated functions (default:lib
).--out <path>
(or-o <path>
): Specify the output directory for generated JS files (default:web
).--obfuscate <level>
: Set JavaScript obfuscation level (0-4, default is 4 for smallest size). 0 means no obfuscation.--debug
: Retain temporary files created during generation for debugging purposes.--worker-mappings-experiment=lib/main.dart
(Experimental): Attempt to auto-generateworkerMappings
forIsolateManager.createShared
by scanning the specified Dart file.
Additional Tips #
- Queue Length: Check
isolateManagerInstance.queuesLength
to get the current number of tasks in the queue. - Manual Start Control: Use
isolateManagerInstance.ensureStarted()
to await explicit initialization if needed. CheckisolateManagerInstance.isStarted
to see if the isolate is ready. - Data Flow with Converters (WASM): When WASM type converters are active, the data flow is: Main Isolate → Worker → Main Isolate → Converter → Final Result.
Performance Benchmark #
The following benchmarks demonstrate the performance of recursive Fibonacci calculations across different concurrency approaches and environments. Measurements are in microseconds (µs) on a MacBook M1 Pro 14" with 16GB RAM.
- VM (Native)
Fibonacci | Main App | One Isolate | Three Isolates | IsolateManager.runFunction | IsolateManager.run | Isolate.run |
---|---|---|---|---|---|---|
30 | 551,928 | 541,882 | 195,646 | 553,949 | 547,982 | 538,820 |
33 | 2,273,956 | 2,268,299 | 816,148 | 2,288,071 | 2,282,269 | 2,271,376 |
36 | 9,761,067 | 9,669,422 | 3,453,328 | 9,643,678 | 9,606,443 | 9,648,076 |
- Chrome (with Worker support, JS compiler)
Fibonacci | Main App | One Isolate | Three Isolates | IsolateManager.runFunction | IsolateManager.run | Isolate.run (Unsupported) |
---|---|---|---|---|---|---|
30 | 2,274,100 | 573,900 | 211,700 | 1,160,800 | 1,181,800 | 0 |
33 | 9,493,100 | 2,330,900 | 821,400 | 2,860,800 | 2,866,300 | 0 |
36 | 40,051,000 | 9,756,200 | 3,452,100 | 10,281,200 | 10,270,300 | 0 |
- Chrome (with Worker support, WASM compiler)
Fibonacci | Main App | One Isolate | Three Isolates | IsolateManager.runFunction | IsolateManager.run | Isolate.run (Unsupported) |
---|---|---|---|---|---|---|
30 | 242,701 | 552,800 | 200,300 | 1,099,100 | 1,081,800 | 0 |
33 | 1,027,300 | 2,315,700 | 819,800 | 2,863,700 | 2,852,600 | 0 |
36 | 4,396,300 | 9,709,700 | 3,446,300 | 10,284,000 | 10,375,800 | 0 |
For more details, see the full benchmark information.
Contributing #
Contributions, issues, and feature requests are welcome! Feel free to check the issues page.
If you find this package helpful, consider supporting the developer: