lamnhan066 / isolate_manager

Create multiple long-lived isolates for the Functions, supports Worker on the Web (with the effective generator) and WASM compilation.
https://pub.dev/packages/isolate_manager
MIT License
39 stars 3 forks source link
dart flutter isolate plugin

Isolate Manager

codecov

Pub Version Pub Points Pub Popularity Pub Likes

Features

Table Of Contents

Benchmark

Execute a recursive Fibonacci function 70 times, computing the sequence for the numbers 30, 33, and 36. The results are in microseconds (On Macbook M1 Pro 14-inch 16Gb RAM).

Fibonacci Main App One Isolate Three Isolates Isolate.run
30 751,364 771,142 274,854 769,588
33 3,189,873 3,185,798 1,152,083 3,214,685
36 13,510,136 13,540,763 4,873,100 13,766,930
Fibonacci Main App One Worker Three Workers Isolate.run (Unsupported)
30 2,125,101 547,800 195,101 0
33 9,083,800 2,286,899 803,599 0
36 38,083,500 9,575,899 3,383,299 0

See here for the test details.

Setup

A function used for the Isolate MUST BE a static or top-level function. The @pragma('vm:entry-point') annotation also should be added to this function to ensure that tree-shaking doesn't remove the code since it would be invoked on the native side.

@pragma('vm:entry-point')
int add(List<int> values) {
  return values[0] + values[1];
}

Mobile and Desktop

Web

IsolateManagerShared Method

void main() async {
  // Create 3 isolateShared to solve the problems
  final isolateShared = IsolateManager.createShared(
    concurrent: 3, 

    // Remove this line (or set it to `false`) if you don't want to use the Worker
    useWorker: true, 

    // Add this mappings so we can ignore the `workerName` parameter 
    // when using the `compute` method.
    workerMappings: {
      addFuture : 'addFuture', 
      add : 'add',
    }  
  );

  // Compute the values. The return type and parameter type will respect the type
  // of the function.
  final added = await isolateShared.compute(
    addFuture, 
    [1.1, 2.2], 
    // workerFunction: 'addFuture', // Ignored because the `workerMappings` is specified
  );
  print('addFuture: 1.1 + 2.2 = $added');

  // Compute the values. The return type and parameter type will respect the type
  // of the function.
  final added = await isolateShared.compute(
    add, 
    [1, 2], 
    // workerFunction: 'add', // Ignored because the `workerMappings` is specified
  );
  print('add: 1 + 2 = $added');
}

@isolateManagerSharedWorker
Future<double> addFuture(List<double> values) async {
  return values[0] + values[1];
}

@isolateManagerSharedWorker
int add(List<int> values) {
  return values[0] + values[1];
}

Run this command to generate a Javascript Worker (named $shared_worker.js inside the web folder):

dart run isolate_manager:generate

Add flag --shared if you want to generate only for the IsolateManagerShared.

IsolateManager Method

Basic Usage

There are multiple ways to use this package. The only thing to notice is that the function has to be a static or top-level function.

main() async {
  final isolate = IsolateManager.create(
      fibonacci, 

      // And the name of the function if you want to use the Worker.
      // Otherwise, you can ignore this parameter.
      workerName: 'fibonacci',
      concurrent: 2,
    );

  isolate.stream.listen((value) {
    print(value);
  });

  final fibo = await isolate(20);
}

@isolateManagerWorker // Remove this annotation if you don't want to use the Worker
int fibonacci(int n) {
  if (n == 0) return 0;
  if (n == 1) return 1;

  return fibonacci(n - 1) + fibonacci(n - 2);
}

Run this command to generate a Javascript Worker:

dart run isolate_manager:generate

Add flag --single if you want to generate only for the IsolateManager.

You can restart or stop the isolate using this method:

await isolateManager.restart();
await isolateManager.stop();

Custom Function Usage

You can control everything with this method when you want to create multiple isolates for a function.

Step 1: Create a function of this form

Let it automatically handles the result and the Exception:

@isolateManagerCustomWorker // Remove this line if you don't want to use the Worker
void customIsolateFunction(dynamic params) {
  IsolateManagerFunction.customFunction<int, int>(
    params,
    onEvent: (controller, message) {
      /* This event will be executed every time the `message` is received from the main isolate */
      return fibonacci(message);
    },
    onInitial: (controller, initialParams) {
       /* This event will be executed before all the other events and only one time. */
    },
    onDispose: (controller) {
       /* This event will be executed after all the other events and should NOT be a `Future` event */
    },
  );
}

Handle the result and the Exception by your self:

@isolateManagerCustomWorker // Remove this line if you don't want to use the Worker
void customIsolateFunction(dynamic params) {
  IsolateManagerFunction.customFunction<Map<String, dynamic>, String>(
    params,
    onEvent: (controller, message) {
      // This event will be executed every time the `message` is received from the main isolate.
      try {
        final result = fibonacci(message);
        controller.sendResult(result);
      } catch (err, stack) {
        controller.sendResultError(IsolateException(err, stack));
      }

      // Just returns something that unused to complete this method.
      return 0;
    },
    onInitial: (controller, initialParams) {
       /* This event will be executed before all the other events. */
    },
    onDispose: (controller) {
       /* This event will be executed after all the other events. */
    },
    autoHandleException: false,
    autoHandleResult: false,
  );
}

Step 2: Create an IsolateManager instance for your own function

final isolateManager = IsolateManager.createCustom(
    customIsolateFunction,
    initialParams: 'This is the initialParams',

    // And the name of the function if you want to use the Worker.
    // Otherwise, you can ignore this parameter.
    workerName: 'customIsolateFunction',
    debugMode: true,
  );

Now you can use everything as the Basic Usage.

Strategy Of The Queue

Compute a priority computation

When you have a computation that you want to compute as soon as possible, you can change the priority parameter to true to promote it to the top of the Queue.

Max number of Queues

You can set the maximum number of the queued computations for an IsolateManager or IsolateManagerShared by changing the maxCount value.

If the maxCount is <= 0, the max number of the queued computations is unlimited.

How a new computation is added if the max queues is exceeded

When creating a new IsolateManager or IsolateManagerShared, you can define the queueStrategy to control how new computations are added to or retrieved from the queue. There are four fundamental strategies:

/// Unlimited queued computations (default).
QueueStrategyUnlimited()

/// Remove the newest computation if the [maxCount] is exceeded.
QueueStrategyRemoveNewest();

/// Remove the oldest computation if the [maxCount] is exceeded.
QueueStrategyRemoveOldest()

/// Discard the new incoming computation if the [maxCount] is exceeded.
QueueStrategyDiscardIncoming()

Create a custom strategy

You can extend the QueueStrategy and use the queues, maxCount and queuesCount to create your own strategy. These are how the basic strategies are created:

class QueueStrategyUnlimited<R, P> extends QueueStrategy<R, P> {
  /// Unlimited queued computations.
  QueueStrategyUnlimited();

  @override
  bool continueIfMaxCountExceeded() {
    // It means the current computation should be added to the Queue
    // without doing anything with the `queues`.
    return true; 
  }
}

class QueueStrategyRemoveNewest<R, P> extends QueueStrategy<R, P> {
  /// Remove the newest computation if the [maxCount] is exceeded.
  QueueStrategyRemoveNewest({super.maxCount = 0});

  @override
  bool continueIfMaxCountExceeded() {
    // Remove the last computation if the Queue (mean the newest one).
    queues.removeLast();
    // It means the current computation should be added to the Queue.
    return true; 
  }
}

class QueueStrategyRemoveOldest<R, P> extends QueueStrategy<R, P> {
  /// Remove the oldest computation if the [maxCount] is exceeded.
  QueueStrategyRemoveOldest({super.maxCount = 0});

  @override
  bool continueIfMaxCountExceeded() {
    // Remove the first computation if the Queue (mean the oldest one).
    queues.removeFirst();
    // It means the current computation should be added to the Queue.
    return true;
  }
}

class QueueStrategyDiscardIncoming<R, P> extends QueueStrategy<R, P> {
  /// Discard the new incoming computation if the [maxCount] is exceeded.
  QueueStrategyDiscardIncoming({super.maxCount = 0});

  @override
  bool continueIfMaxCountExceeded() {
    // It means the current computation should NOT be added to the Queue.
    return false;
  }
}

Try-Catch Block

You can use try-catch to catch exceptions:

try {
  final result = await isolate(-10);
} on SomeException catch (e1) {
  print(e1);
} catch (e2) {
  print(e2);
}

Progress Values

You can even manage the final result by using this callback, useful when you create your own function that needs to send the progress value before returning the final result:

main() {
  // Create an IsolateManager instance.
  final isolateManager = IsolateManager.createCustom(progressFunction);

  // Get the result.
  final result = await isolateManager.compute(100, callback: (value) {
    // Condition to recognize the progress value. Ex:
    final data = jsonDecode(value);

    if (data.containsKey('progress')) {
      print('This is a progress value: ${data['progress']}');

      // Return `false` to mark this value is not the final.
      return false;
    }

    print('This is a final value: ${data['result']}');

    // Return `true` to mark this value is the final.
    return true;
  });

  print(result); // 100
}

// This is a progress function
@isolateManagerCustomWorker // Add this anotation for a custom function
void progressFunction(dynamic params) {
  IsolateManagerFunction.customFunction<String, int>(
    params,
    onEvent: (controller, message) {
      // This value is sent as the progress values.
      for (int i = 0; i < message; i++) {
        final progress = jsonEncode({'progress' : messsage});
        controller.sendResult(progress);
      }

      // This is a final value.
      return jsonEncode({'result' : messsage});
    },
  );
}

Complicated List, Map Functions

The Generator Options And Flags

Additional Information

Contributions