Open
Description
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.