Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use concurrency with data passing without blocking the main isolate. #40653

Closed
modulovalue opened this issue Feb 15, 2020 · 4 comments
Closed
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-question A question about expected behavior or functionality

Comments

@modulovalue
Copy link
Contributor

modulovalue commented Feb 15, 2020

Please take a look at the following example:

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:isolate/isolate.dart';

String str = _generateString();

Future<void> main(List<String> args) async {
  final IsolateRunner r = await IsolateRunner.spawn();
  final List<Operation> operationAndDuration = [];
  const int frameLengthIsMS = 1000 ~/ 120;
  final List<int> frames = [];
  final Timer timer =
      Timer.periodic(const Duration(milliseconds: frameLengthIsMS), (timer) => frames.add(timer.tick));
  Stopwatch s;

  Future<T> runAsyncOperation<T>(String description, Future<T> Function() operation) async {
    s = Stopwatch()..start();
    final value = await operation();
    operationAndDuration.add(Operation(description, s.elapsedMilliseconds ~/ frameLengthIsMS, timer.tick));
    return value;
  }

  T runSyncOperation<T>(String description, T Function() operation) {
    s = Stopwatch()..start();
    final value = operation();
    operationAndDuration.add(Operation(description, s.elapsedMilliseconds ~/ frameLengthIsMS, timer.tick));
    return value;
  }

  print("${str.length} bytes, please wait ~10 seconds.");

  final map = await runAsyncOperation("Isolate Decode", () => r.run(decode, str));
  final encodedMap = await runAsyncOperation("Isolate Encode", () => r.run(encode, map));

  await Future(() {});

  final map2 = runSyncOperation("Decode", () => decode(str));
  await Future(() {});

  final encodedMap2 = runSyncOperation("Encode", () => encode(map2));
  await Future(() {});

  assert(encodedMap == encodedMap2);

  timer.cancel();

  print(" • ${_zip(operationAndDuration, findSkippedFrames(frames)).map((v) {
    assert(v.value.key == v.key.at);
    final dif = v.value.value - v.value.key;
    return [
      v.key.description,
      "Took          : ${v.key.frames}",
      "SkippedFrames : ${dif}     (Frame ${v.value.key} to ${v.value.value})",
    ].join("\n   - ");
  }).join("\n\n • ")}");
}

class Operation {
  final String description;
  final int frames;
  final int at;

  const Operation(this.description, this.frames, this.at);

  @override
  String toString() => "Operation '$description' took '$frames' frames at '$at'";
}

List<MapEntry<A, B>> _zip<A, B>(List<A> a, List<B> b) {
  assert(a.length == b.length);
  return a.asMap().entries.map((entry) => MapEntry(entry.value, b[entry.key])).toList();
}

List<MapEntry<int, int>> findSkippedFrames(List<int> frames) {
  final List<MapEntry<int, int>> skippedFrames = [];
  frames.fold<int>(null, (previousValue, element) {
    if (previousValue == null) return element;
    if (previousValue + 1 != element) {
      skippedFrames.add(MapEntry(previousValue, element));
    }
    return element;
  });
  return skippedFrames;
}

Map<dynamic, dynamic> decode(String str) => json.decode(str) as Map<dynamic, dynamic>;

String encode(Map<dynamic, dynamic> str) => json.encode(str);

String _generateString() {
  final Random rnd = Random();

  final Map<dynamic, dynamic> map = <dynamic, dynamic>{};
  for (int i = 0; i < 500000; i++) {
    map[i.toString()] = [
      rnd.nextInt(1000),
      (int length) {
        final rand = Random();
        final codeUnits = List.generate(length, (index) {
          return rand.nextInt(33) + 89;
        });

        return String.fromCharCodes(codeUnits);
      }(10)
    ];
  }

  return json.encode(map);
}

Output:

13985982 bytes, please wait ~10 seconds.
 • Isolate Decode
   - Took          : 196
   - SkippedFrames : 152     (Frame 241 to 393)

 • Isolate Encode
   - Took          : 187
   - SkippedFrames : 6     (Frame 482 to 488)

 • Decode
   - Took          : 47
   - SkippedFrames : 47     (Frame 488 to 535)

 • Encode
   - Took          : 29
   - SkippedFrames : 30     (Frame 535 to 565)

I'd like to do expensive operations outside of the main isolate and receive a lot of data (e.g. large decoded json maps) back.

This example is supposed to show that using an isolate to decode a large json would result in 152 skipped frames for this particular run while doing the same work on the main isolate would only cost 47.

How do I decode large JSON strings where

  • all of the decoded data is needed in the main isolate (only passing back parts of it is not an option)
  • the main isolate is supposed to be able to run smoothly at 120FPS during the whole process.

Related issues: #29480 dart-lang/language#124

@mraleph
Copy link
Member

mraleph commented Feb 17, 2020

The work on #37835 (which is under active development) should address this particular use-case.

As a stop-gap solution you could try using chunked JSON decoding on the main thread.

all of the decoded data is needed in the main isolate (only passing back parts of it is not an option)

I would be curious to hear more about this requirement. If you have so much data that it takes a long time to copy it into the main isolate - then it is unlikely you really are going to display all of it in the UI. Then what is the point of passing it all back into the main isolate? If your UI is going to display only slices of it - then you should be able to just pass over those slices back to the UI isolate.

Can you elaborate where the requirement to have the whole gigantic JSON-like data structure in the main isolate comes from?

@modulovalue
Copy link
Contributor Author

I would be curious to hear more about this requirement.

I would like to be able to visualize vast amounts of data on a virtual canvas. without having to worry about blocking the UI and without adding unecessary complexity due to limitations by dart.

Since I need that canvas to be scalable, all of the data should optimally be available on the first frame. If that is not possible then at least the UI should not skip any frames so that a progress indicator can be displayed.

Bildschirmfoto 2020-02-09 um 20 28 51

I will try a chunked approach, but I should have mentioned that encoding and decoding json is just a placeholder for many other problems. Working with audio/video/images, calculating technical indicators, any other form of data visualization with big data sets.

Yes, there are be workarounds for most of those problems with which one might be able to get a smooth UI, but i should not have to worry about that, just to get a smooth UI.

@devoncarew devoncarew added area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-question A question about expected behavior or functionality labels Feb 18, 2020
@mkustermann
Copy link
Member

@modulovalue #36097 and #37835 has been mainly addressed (on master). Both a SendPort.send() and a Isolate.exit() should now be able to send messages to the UI isolate without causing any meaningful pause times on the UI isolate (i.e. the receiver of the message).

@modulovalue
Copy link
Contributor Author

@mkustermann Thank you, I've tried to solve the issues that I've experienced here with Isolate.exit here: #47508

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-question A question about expected behavior or functionality
Projects
None yet
Development

No branches or pull requests

4 participants