squadron_builder 6.1.1 squadron_builder: ^6.1.1 copied to clipboard
Dart code generator for Squadron workers. Implement your worker service and let squadron_builder bridge the gap with Web Workers and Isolates!
Example - Hello, World #
See full code in /example/hello_world.
The classic Hello, World!
example code with a HelloWorld
service:
@SquadronService(web: false)
class HelloWorld {
@squadronMethod
Future<String> hello([String? name]) async {
name = name?.trim() ?? '';
return name.isEmpty
? 'Hello, World! from Isolate $threadId'
: 'Hello, $name! from Isolate $threadId';
}
}
Generate code for HelloWorldWorker
and HelloWorldWorkerPool
with dart run build_runner build
.
Now you're ready to go:
logger.i('main() running in Isolate $threadId');
final names = [null, 'Mary', 'John', 'Joe', 'Rick', 'Bill', 'Henry'];
final worker = HelloWorldWorker();
for (var name in names) {
logger.i(await worker.hello(name));
}
worker.stop();
Sample output:
[I] main() running in Isolate 26199826
[I] Hello, World! from Isolate 572755021
[I] Hello, Mary! from Isolate 572755021
[I] Hello, John! from Isolate 572755021
[I] Hello, Joe! from Isolate 572755021
[I] Hello, Rick! from Isolate 572755021
[I] Hello, Bill! from Isolate 572755021
[I] Hello, Henry! from Isolate 572755021
Example - Fibonacci sequence #
See full code in /example/fibonacci.
The example computes Fibonacci numbers recursively, simply applying the definition of the Fibonacci sequence. It is very inefficient, but illustrates the effect of multithreading.
@SquadronService()
class FibService {
@squadronMethod
Future<int> fibonacci(int i) async => _fib(i);
// naive & inefficient implementation of the Fibonacci sequence
static int _fib(int i) => (i < 2) ? i : (_fib(i - 2) + _fib(i - 1));
}
To have squadron_builder
generate the code for the worker and the worker pool, run:
dart run build_runner build
The main program runs the same computations:
- first with a plain instance of
FibService
(single-threaded, running in the main program's Isolate), - then with an instance of
FibServiceWorker
(single-threaded, running in a dedicated Isolate), - finally with an instance of
FibServiceWorkerPool
(multi-threaded, running in specific Isolates managed by the worker pool).
The worker and worker pool generated by squadron_builder
both wrap the original service and implement it: as a result, they are interchangeable with the original service.
// compute 9 fibonnaci numbers (starting from 37)
int count = 9, start = 37;
print('''
Computing with FibService (single-threaded in the main Isolate)
The main Isolate is busy computing the numbers.
The timer won't trigger.
''');
final service = FibService();
await computeWith(service, start, count);
print('''
Computing with FibServiceWorker (single-threaded in 1 dedicated Isolate)
The main Isolate is available while the worker Isolate is computing numbers.
The computation time should be roughly the same as with FibService.
The timer triggers periodically.
''');
final worker = FibServiceWorker();
await worker.start();
await computeWith(worker, start, count);
print(' * Stats for worker ${worker.workerId}: ${worker.stats.dump()}');
worker.stop();
final maxWorkers = count ~/ 2;
print('''
Computing with FibServiceWorkerPool (multi-threaded in $maxWorkers dedicated Isolates)
The main Isolate is available while worker pool Isolates are computing numbers.
The computation time should be significantly less compared to FibService and FibServiceWorker.
The timer triggers periodically.
''');
final concurrency = ConcurrencySettings(minWorkers: 1, maxWorkers: maxWorkers, maxParallel: 1);
final pool = FibServiceWorkerPool(concurrencySettings: concurrency);
await pool.start();
await computeWith(pool, start, count);
print(pool.fullStats.map((s) => ' * Stats for pool worker ${s.id}: ${s.dump()}').join('\n'));
pool.stop();
Sample output:
tick #1...
Timer started
Computing with FibService (single-threaded in the main Isolate)
The main Isolate is busy computing the numbers.
The timer won't trigger.
* Results = [24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170]
* Total elapsed time: 0:00:11.830972
Computing with FibServiceWorker (single-threaded in 1 dedicated Isolate)
The main Isolate is available while the worker Isolate is computing numbers.
The computation time should be roughly the same as with FibService.
The timer triggers periodically.
tick #12 - skipped 11 ticks!
tick #13...
tick #14...
tick #15...
tick #16...
tick #17...
tick #18...
tick #19...
tick #20...
tick #21...
tick #22...
tick #23...
tick #24...
* Results = [24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170]
* Total elapsed time: 0:00:11.629611
* Stats for worker 494853282: totalWorkload=9 (max 9) - upTime=0:00:11.630165 - idleTime=0:00:00.002101 - status=IDLE (workerHashCode=494853282)
Computing with FibServiceWorkerPool (multi-threaded in 4 dedicated Isolates)
The main Isolate is available while worker pool Isolates are computing numbers.
The computation time should be significantly less compared to FibService and FibServiceWorker.
The timer triggers periodically.
tick #25...
tick #26...
tick #27...
tick #28...
tick #29...
tick #30...
* Results = [24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170]
* Total elapsed time: 0:00:05.781353
* Stats for pool worker 255048060: totalWorkload=3 (max 1) - upTime=0:00:05.784815 - idleTime=0:00:00.002451 - status=IDLE (workerHashCode=255048060)
* Stats for pool worker 876978540: totalWorkload=2 (max 1) - upTime=0:00:05.776815 - idleTime=0:00:04.311114 - status=IDLE (workerHashCode=876978540)
* Stats for pool worker 127911517: totalWorkload=2 (max 1) - upTime=0:00:05.776815 - idleTime=0:00:03.401190 - status=IDLE (workerHashCode=127911517)
* Stats for pool worker 543026067: totalWorkload=2 (max 1) - upTime=0:00:05.776815 - idleTime=0:00:02.049951 - status=IDLE (workerHashCode=543026067)
tick #31...
Timer stopped
Example - Performance benchmark #
See full code in /example/perf.
Sample summary output:
==== SUMMARY ====
MAX TIMER DELAY (resolution = 0:00:00.020000 aka 50.0 frames/sec)
* main thread: 0:00:05.081533 (resolution x 254.08) - max skipped = 254
* worker: 0:00:00.056210 (resolution x 2.81) - max skipped = 2
* worker pool: 0:00:00.126533 (resolution x 6.33) - max skipped = 5
MAIN THREAD (baseline): executed in the main event loop.
* Fib : 0:00:05.619219 - skipped 97.52 % (275 / 282, max = 69) - max delay = 0:00:01.403367
* Echo: 0:00:00.566552 - skipped 89.66 % (26 / 29, max = 26) - max delay = 0:00:00.534657
* Perf: 0:00:05.116553 - skipped 98.83 % (254 / 257, max = 254) - max delay = 0:00:05.081533
SINGLE WORKERS vs MAIN THREAD: worker counters should be slightly worse because
of serialization/deserialization. The main advantage in this scenario is to
free the main event loop and avoid skipping frames, eg in user-facing apps to
avoid glitches in the UI.
* Fib : 0:00:05.711924 (1.65 %) - skipped 0.00 % (0 / 286, max = 0) - max delay = 0:00:00.040161 (-97.14 %)
* Echo: 0:00:00.608711 (7.44 %) - skipped 3.23 % (1 / 31, max = 1) - max delay = 0:00:00.032436 (-93.93 %)
* Perf: 0:00:05.365830 (4.87 %) - skipped 1.86 % (5 / 269, max = 2) - max delay = 0:00:00.056210 (-98.89 %)
WORKER POOL vs MAIN THREAD: worker pool counters should be much better even
considering the overhead of serialization/deserialization and worker scheduling.
Perf improvement depends on method execution time: the heavier the workload,
the more performance will be improved.
* Fib : 0:00:02.543199 (-54.74 %) - skipped 0.78 % (1 / 128, max = 1) - max delay = 0:00:00.032507 (-97.68 %)
* Echo: 0:00:00.194108 (-65.74 %) - skipped 10.00 % (1 / 10, max = 1) - max delay = 0:00:00.035975 (-93.27 %)
* Perf: 0:00:01.511884 (-70.45 %) - skipped 15.79 % (12 / 76, max = 5) - max delay = 0:00:00.126533 (-97.51 %)