Skip to content

[Isolate.exit] Unpredictable performance. #47508

Open
@modulovalue

Description

@modulovalue

This issue is about the efficiency guarantees of the new Isolate.exit feature.

Isolate.exit (via #36097) as a possible solution to #40653 seems to work ~50% of the time.

The following example uses the new Isolate.exit functionality to encode and decode json on a different isolate to not drop any frames (Timer.tick is used as a proxy for frames).

Code
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'dart:math';

Future<void> main() async {
  for (int i = 0; i < 10; i++) {
    await run();
  }
}

Future<void> run() async {
  final frames = <int>[];
  final timer = Timer.periodic(
    const Duration(milliseconds: frameDurationIsMS),
    (final timer) => frames.add(timer.tick),
  );
  final jsonString = () {
    final rnd = Random();
    final map = <String, dynamic>{
      for (int i = 0; i < 500000; i++)
        i.toString(): [
          rnd.nextInt(1000),
          () {
            final rand = Random();
            final codeUnits = List.generate(
              10,
              (final index) => rand.nextInt(33) + 89,
            );
            return String.fromCharCodes(codeUnits);
          }()
        ],
    };
    return json.encode(map);
  }();
  final map = await runAsyncOperation<dynamic>(
    description: "json.decode",
    operation: () => asyncCompute<String, dynamic>(
      fn: json.decode,
      input: jsonString,
    ),
    getTimerTick: () => timer.tick,
  );
  await Future<void>(() {});
  final encodedMap = await runAsyncOperation<dynamic>(
    description: "json.encode",
    operation: () => asyncCompute<Map<dynamic, dynamic>, String>(
      fn: json.encode,
      input: map.result as Map<dynamic, dynamic>,
    ),
    getTimerTick: () => timer.tick,
  );
  timer.cancel();
  output(
    ops: [
      map,
      encodedMap,
    ],
    ticks: frames,
  );
}

Future<O> asyncCompute<I, O>({
  required final FutureOr<O> Function(I) fn,
  required final I input,
}) async {
  final receivePort = ReceivePort();
  final inbox = StreamIterator<dynamic>(
    receivePort,
  );
  await Isolate.spawn<_IsolateMessage<I, O>>(
    (final message) => Isolate.exit(
      message.sendPort,
      message.fn(
        message.input,
      ),
    ),
    _IsolateMessage(
      sendPort: receivePort.sendPort,
      fn: fn,
      input: input,
    ),
  );
  final movedNext = await inbox.moveNext();
  assert(
    movedNext,
    "Call to moveNext is expected to return true.",
  );
  final typedResult = inbox.current as O;
  receivePort.close();
  await inbox.cancel();
  return typedResult;
}

class _IsolateMessage<I, O> {
  final SendPort sendPort;
  final FutureOr<O> Function(I) fn;
  final I input;

  const _IsolateMessage({
    required final this.sendPort,
    required final this.fn,
    required final this.input,
  });
}

const int frameDurationIsMS = 1000 ~/ 120;

Future<Operation<T>> runAsyncOperation<T>({
  required final String description,
  required final FutureOr<T> Function() operation,
  required final int Function() getTimerTick,
}) async {
  final startTick = getTimerTick();
  final value = await operation();
  final endTick = getTimerTick();
  return Operation<T>(
    description: description,
    frameInfo: FrameInfo(
      startTick: startTick,
      endTick: endTick,
    ),
    result: value,
  );
}

void output({
  required final List<Operation<Object?>> ops,
  required final List<int> ticks,
}) {
  print("== Operation Info ==");
  print(
    "Operation".padRight(30) + "Start Frame".padRight(30) + "End Frame".padRight(30),
  );
  for (final op in ops) {
    print(
      _operationToText(
        operation: op,
      ),
    );
  }
  print("== Frame Info ==");
  print("Skipped Frames".padRight(30) + "Frame Range".padRight(30));
  for (final skippedFrame in _findSkippedFrames(
    frames: ticks,
  )) {
    print(
      ((skippedFrame.endTick - skippedFrame.startTick).toString()).padRight(30) +
          (skippedFrame.startTick.toString() + " to " + skippedFrame.endTick.toString()).padRight(30),
    );
  }
}

String _operationToText<T>({
  required final Operation<T> operation,
}) =>
    operation.description.padRight(30) +
    (operation.frameInfo.startTick.toString()).padRight(30) +
    (operation.frameInfo.endTick.toString()).padRight(30);

class Operation<T> {
  final String description;
  final FrameInfo frameInfo;
  final T result;

  const Operation({
    required final this.description,
    required final this.frameInfo,
    required final this.result,
  });
}

List<FrameInfo> _findSkippedFrames({
  required final List<int> frames,
}) {
  final skippedFrames = <FrameInfo>[];
  frames.fold<int?>(
    null,
    (final previousValue, final element) {
      if (previousValue == null) {
        return element;
      } else if (previousValue + 1 != element) {
        skippedFrames.add(
          FrameInfo(
            startTick: previousValue,
            endTick: element,
          ),
        );
      } else {
        return element;
      }
    },
  );
  return skippedFrames;
}

class FrameInfo {
  final int startTick;
  final int endTick;

  const FrameInfo({
    required final this.startTick,
    required final this.endTick,
  });
}
Output
/usr/local/opt/dart-beta/libexec/bin/dart --version
Dart SDK version: 2.15.0-178.1.beta (beta) (Tue Oct 12 11:11:28 2021 +0200) on "macos_x64"

/usr/local/opt/dart-beta/libexec/bin/dart snippet.dart
snippet.dart: Warning: Interpreting this as package URI, (...)
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             255                           
json.encode                   257                           347                           
== Frame Info ==
Skipped Frames                Frame Range                   
3                             132 to 135                    
4                             149 to 153                    
4                             159 to 163                    
2                             170 to 172                    
5                             175 to 180                    
4                             193 to 197                    
3                             202 to 205                    
2                             255 to 257                    
2                             337 to 339                    
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             228                           
json.encode                   234                           337                           
== Frame Info ==
Skipped Frames                Frame Range                   
2                             128 to 130                    
4                             141 to 145                    
3                             151 to 154                    
3                             166 to 169                    
2                             180 to 182                    
6                             228 to 234                    
2                             325 to 327                    
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             225                           
json.encode                   225                           311                           
== Frame Info ==
Skipped Frames                Frame Range                   
2                             122 to 124                    
2                             131 to 133                    
5                             142 to 147                    
4                             171 to 175                    
3                             180 to 183                    
44                            225 to 269                    
2                             300 to 302                    
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             225                           
json.encode                   226                           325                           
== Frame Info ==
Skipped Frames                Frame Range                   
2                             126 to 128                    
4                             139 to 143                    
3                             149 to 152                    
3                             163 to 166                    
2                             177 to 179                    
58                            226 to 284                    
2                             312 to 314                    
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             227                           
json.encode                   227                           315                           
== Frame Info ==
Skipped Frames                Frame Range                   
3                             122 to 125                    
2                             132 to 134                    
5                             143 to 148                    
4                             172 to 176                    
2                             181 to 183                    
46                            227 to 273                    
2                             304 to 306                    
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             224                           
json.encode                   225                           324                           
== Frame Info ==
Skipped Frames                Frame Range                   
2                             125 to 127                    
3                             138 to 141                    
4                             147 to 151                    
3                             162 to 165                    
3                             176 to 179                    
59                            225 to 284                    
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             225                           
json.encode                   226                           313                           
== Frame Info ==
Skipped Frames                Frame Range                   
2                             122 to 124                    
2                             131 to 133                    
4                             143 to 147                    
4                             172 to 176                    
2                             181 to 183                    
45                            226 to 271                    
2                             303 to 305                    
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             226                           
json.encode                   231                           329                           
== Frame Info ==
Skipped Frames                Frame Range                   
2                             126 to 128                    
4                             139 to 143                    
4                             149 to 153                    
3                             164 to 167                    
2                             178 to 180                    
5                             226 to 231                    
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             224                           
json.encode                   224                           311                           
== Frame Info ==
Skipped Frames                Frame Range                   
3                             121 to 124                    
2                             131 to 133                    
5                             142 to 147                    
4                             171 to 175                    
2                             180 to 182                    
45                            224 to 269                    
2                             301 to 303                    
== Operation Info ==
Operation                     Start Frame                   End Frame                     
json.decode                   0                             226                           
json.encode                   231                           330                           
== Frame Info ==
Skipped Frames                Frame Range                   
2                             127 to 129                    
5                             139 to 144                    
3                             150 to 153                    
3                             164 to 167                    
3                             178 to 181                    
5                             226 to 231      

The Frame Info section shows that some runs caused ~40 dropped frames and some didn't. I expected to see no large ranges of dropped frames.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-vmUse area-vm for VM related issues, including code coverage, and the AOT and JIT backends.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions