d-markey / squadron

Multithreading and worker thread pool for Dart / Flutter, to offload CPU-bound and heavy I/O tasks to Isolate or Web Worker threads.
https://pub.dev/packages/squadron
MIT License
79 stars 0 forks source link

WorkerException: error in Web Worker #976509285: /workers/solver_worker.dart.js: error / [object Event] #4

Closed SaadArdati closed 2 years ago

SaadArdati commented 2 years ago

I got the dart js command to run this time. Turns out, the SDK (that we made) that the solver classes depend on was depending on dart:ui classes for unrelated sections of the api.

I guess the dart js command isn't smart enough to tree-shake all the code that isn't being used by our api accessors.

Point is, I made a copy of the sdk and stripped it down heavely to only bare-minimum data without relying on any dart:ui or flutter code, and it worked. I compiled the js file and moved it into the web folder, but it's crashing and the error message is not useful unfortunately.

Maybe it's because it's a js file compiled from a completely separate and incompatible copy of the sdk, but in any case, an error message would go a long way.

Here's the ouput after running the project in a chrome browser:

WorkerException: error in Web Worker #976509285: /workers/solver_worker.dart.js: error / [object Event]
dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 906:28                get current
packages/squadron/src/worker_exception.dart 7:55                                  new
packages/squadron/src/browser/channel.dart 223:11                                 <fn>
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 334:14  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 339:39  dcall
dart-sdk/lib/html/dart2js/html_dart2js.dart 37230:58                              <fn>
dart-sdk/lib/async/zone.dart 1444:13                                              _rootRunUnary
dart-sdk/lib/async/zone.dart 1335:19                                              runUnary
dart-sdk/lib/async/zone.dart 1244:7                                               runUnaryGuarded
dart-sdk/lib/async/zone.dart 1281:26                                              <fn>
SaadArdati commented 2 years ago

It's almost definitely because its from a separate project. 🤞 But better errors would go a long way

SaadArdati commented 2 years ago

I split off our API and now all the files ONLY depend on dart and the dart js commands works FLAWLESSLY.

I am still, however, getting the EXACT same error. The error message suggests creating new workers is failing for some reason. I'm assuming it's unable to locate the file?

SaadArdati commented 2 years ago

Ah, the issue was this:

image

because the generated file goes into the root of the /web folder, not /web/workers.

Despite this, now I'm getting a new error

Expected a value of type 'FutureOr<Map<String, dynamic>>?', but got one of type 'LinkedMap<dynamic, dynamic>'
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 251:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 84:3        castError
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 452:10  cast
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/types.dart 367:9        as
packages/squadron/src/browser/channel.dart 63:18                                  <fn>
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 334:14  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 339:39  dcall
dart-sdk/lib/html/dart2js/html_dart2js.dart 37230:58                              <fn>
dart-sdk/lib/async/zone.dart 1444:13                                              _rootRunUnary
dart-sdk/lib/async/zone.dart 1335:19                                              runUnary
dart-sdk/lib/async/zone.dart 1244:7                                               runUnaryGuarded
dart-sdk/lib/async/zone.dart 1281:26                                              <fn>
d-markey commented 2 years ago

Hi Saad, I'll have a look, will keep you posted. Could you post the definition of the service interface + service implementation? It looks like something (the ServiceImpl?) is returning a Map where the expected result would be a Future. Or maybe an 'async' is missing on one side or unexpected on the other?

SaadArdati commented 2 years ago

That would be very unusual since the vm (macos window) app runs perfectly, why would the worker behave with a different exception like this?

In any case, here is the ServiceImpl:

import 'dart:async';

import 'package:squadron/squadron.dart';

import '../position_manager_thread.dart';

abstract class SolverService {
  FutureOr<Map<String, dynamic>> initRoot(Map<String, dynamic> rootNodeJson);

  FutureOr<Map<String, dynamic>> performLayout(
    Set<Map<String, dynamic>> nodesReference,
    Set<String> createdNodes,
    Set<String> removedNodes,
    Set<String> adoptedNodes,
    Set<String> droppedNodes,
    Set<String> changedNodes,
  );

  static const cmdPerformLayout = 1;
  static const cmdInitRoot = 2;
}

class SolverServiceImpl implements SolverService, WorkerService {
  final PositionManagerThread positionManager =
      PositionManagerThread(notifier: (pos) => print(pos));

  @override
  FutureOr<Map<String, dynamic>> initRoot(Map<String, dynamic> rootNodeJson) =>
      positionManager.initRoot(rootNodeJson).toJson();

  @override
  FutureOr<Map<String, dynamic>> performLayout(
    Set<Map<String, dynamic>> nodesReference,
    Set<String> createdNodes,
    Set<String> removedNodes,
    Set<String> adoptedNodes,
    Set<String> droppedNodes,
    Set<String> changedNodes,
  ) async {
    return positionManager
        .performLayout(
          nodesReference,
          createdNodes,
          removedNodes,
          adoptedNodes,
          droppedNodes,
          changedNodes,
        )
        .toJson();
  }

  @override
  late final Map<int, CommandHandler> operations = {
    SolverService.cmdPerformLayout: (WorkerRequest r) => performLayout(
          rebuildSet<Map<String, dynamic>>(r.args[0]),
          rebuildSet<String>(r.args[1]),
          rebuildSet<String>(r.args[2]),
          rebuildSet<String>(r.args[3]),
          rebuildSet<String>(r.args[4]),
          rebuildSet<String>(r.args[5]),
        ),
    SolverService.cmdInitRoot: (WorkerRequest r) => initRoot(
          rebuildMap(r.args[0]),
        ),
  };

  static Set<T> rebuildSet<T>(List items) => items.cast<T>().toSet();

  static Map<String, dynamic> rebuildMap(Map dict) =>
      dict.map((key, value) => MapEntry<String, dynamic>(key, value));

  static Set<Map<String, dynamic>> rebuildMapSet(List<Map> items) => items
      .map((item) => rebuildMap(item))
      .cast<Map<String, dynamic>>()
      .toSet();
}

pool

// this is a helper file to expose Squadron workers and worker pools as a SolverService

import 'dart:async';

import 'package:squadron/squadron.dart';

import 'solver_service.dart';

// Implementation of SolverService as a Squadron worker
class SolverWorker extends Worker implements SolverService {
  SolverWorker(dynamic entryPoint, {String? id, List args = const []})
      : super(entryPoint, id: id, args: args);

  @override
  FutureOr<Map<String, dynamic>> initRoot(Map<String, dynamic> rootNodeJson) {
    return send(
      SolverService.cmdInitRoot,
      [
        rootNodeJson,
      ],
    );
  }

  @override
  FutureOr<Map<String, dynamic>> performLayout(
    Set<Map<String, dynamic>> nodes,
    Set<String> createdNodes,
    Set<String> removedNodes,
    Set<String> adoptedNodes,
    Set<String> droppedNodes,
    Set<String> changedNodes,
  ) {
    return send(
      SolverService.cmdPerformLayout,
      [
        nodes.toList(),
        createdNodes.toList(),
        removedNodes.toList(),
        adoptedNodes.toList(),
        droppedNodes.toList(),
        changedNodes.toList(),
      ],
    );
  }
}

and the browser activator + worker

import '../solver_worker_pool.dart' show SolverWorker;

SolverWorker createWorker() => SolverWorker('/solver_worker.dart.js');
import 'package:squadron/squadron_service.dart';

import '../solver_service.dart';

void main() => run((startRequest) => SolverServiceImpl());
d-markey commented 2 years ago

I see your point but maybe there's a difference in Squadron's behavior between native and browser. Typically the Channel implementation is different. The native version can maybe cope with the situation but not the browser? I'll check my code.

In the meantime I see a difference between initRoot and performLayout in SolverServiceImpl: both return a FutureOr<Map>, both results are produced by a call to toJson(), but one is marked async and not the other. FutureOr<T> is a strange beast in Dart, analogous to a TypeScript union type, meaning that it can be a T or a Future<T>. I try to handle this situation in the code but maybe something slipped through... From my experience I believe FutureOr is good to use for abstract methods where you may accept sync or async implementations. But I try to avoid it for concrete implementations and prefer to use the appropriate T or Future<T>.

What is toJson()? I suspect it's a sync method? Can you try this in SolverServiceImpl:

   @override
   Map<String, dynamic> initRoot(Map<String, dynamic> rootNodeJson) =>
      positionManager.initRoot(rootNodeJson).toJson();

   @override
   Map<String, dynamic> performLayout( ... ) /* no async */ => 
      positionManager.performLayout( ... ).toJson();

On my side I will investigate why it works on native but not on browser.

SaadArdati commented 2 years ago

You are totally correct that I'm missing an async! Incredible eye you have!

I also hate FutureOr as it is ambiguous while I prefer strongly and explicitly typed code.

In any case, I removed the async from performLayout because you are correct, it is a synchronous function adn the whole point is to make it asynchronous via isolates/workers.

Unfortunately, it still throws an error

Expected a value of type 'FutureOr<Map<String, dynamic>>?', but got one of type 'LinkedMap<dynamic, dynamic>'
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 251:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 84:3        castError
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 452:10  cast
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/types.dart 367:9        as
packages/squadron/src/browser/channel.dart 63:18                                  <fn>
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 334:14  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 339:39  dcall
dart-sdk/lib/html/dart2js/html_dart2js.dart 37277:58                              <fn>
dart-sdk/lib/async/zone.dart 1442:13                                              _rootRunUnary
dart-sdk/lib/async/zone.dart 1335:19                                              runUnary
dart-sdk/lib/async/zone.dart 1244:7                                               runUnaryGuarded
dart-sdk/lib/async/zone.dart 1281:26                                              <fn>

I'm going to do something you suggested previously in my last issue and that's to remove the generic types.

d-markey commented 2 years ago

Yes, Isolates can be forgiving as it will accept strong types when code runs in the same process. I believe JavaScript is not :-(

d-markey commented 2 years ago

Just thinking, did you recompile the Web worker after removing the async? Any change that impacts code running in a Web worker requires rebuilding it.

SaadArdati commented 2 years ago

I did, yes. Still need some time to remove generics though and I heavily suspect it's the issue because the error message seems to be a casting error

SaadArdati commented 2 years ago

Seems to be working now!! I'll report if I find any new issues :)

d-markey commented 2 years ago

Great news!

FYI the latest version of Squadron includes logging capabilities to help you debug worker code. It can be difficult with JavaScript workers because they are not connected to the Dart debugger.

Typically you would do this:

void main() => run((startRequest) {
   Squadron.logLevel = SquadronLogLevel.ALL; // optional, the log level set in your main app will be applied in workers by default
   Squadron.logger = ConsoleSquadronLogger(); // logs to browser console
   return SolverServiceImpl();
});

And in your service code you can then use Squadron.info(), Squadron.warning(), ect. to see what's going on.

In you main app, set the log level as appropriate, it will be passed on to the workers when they start up. In you main app and in Isolate workers, you can use DevSquadronLogger instead of ConsoleSquadronLogger.

SaadArdati commented 2 years ago

Excellent to know!! Add this to the main pub page :) I'll be using this for sure.

You should add everything we've learned in the best issues into the pub page actually!

d-markey commented 2 years ago

Will sure do! And don't hesitate to like the package on pub.dev ;-) or github... or both!!