Skip to content

Instantly share code, notes, and snippets.

@lukepighetti
Last active December 6, 2022 10:43
Show Gist options
  • Save lukepighetti/921f347d94c889bc7febf59971892f10 to your computer and use it in GitHub Desktop.
Save lukepighetti/921f347d94c889bc7febf59971892f10 to your computer and use it in GitHub Desktop.
sync_async_io

sync_async_io

This gist demonstrates jank observed during a large filesystem operation. There is a lint called avoid_slow_async_io that recommends people not use async io methods. These methods are definitely slower than the sync methods, but they provide concurrency which is critical to a Flutter application. We are exploring if the linter recommendation introduces jank in a flutter app or not.

Findings

If you use sync io methods that are of moderate to large size, it will freeze the ui.

If you use the linter recommendations, the result is nearly identical to using async methods as a default. I have seen some dropped frames on Simulator but it's barely noticable.

The pure async method is almost identical, but is completely free of dropped frames.

duration jank
sync 1949ms blocked ui 🙅
linted 3377ms a few dropped frames 🙈
async 3337ms worked as expected 👍

Results

If you blindly use avoid_slow_async_io, you should be totally fine. Personally I will default to the gauranteed concurrency of async io calls, especially since the performance gain is imperceptable in this particular case.

Video of Jank

External discussion

include: package:flutter_lints/flutter.yaml
linter:
rules:
avoid_slow_async_io: true
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'sync_async_io',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'sync_async_io'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
asyncInit();
super.initState();
}
final strategy = WriteStrategy();
late final Directory directory;
Future<void> asyncInit() async {
// 1. get app directory
final appSupportDirectory = await getApplicationSupportDirectory();
// 2. get sandbox directory
directory = Directory(join(appSupportDirectory.path, 'sandbox'));
// 3. delete sandbox if it exists, then create a fresh one
if (directory.existsSync()) directory.deleteSync(recursive: true);
directory.createSync(recursive: true);
}
Duration? lastSyncDuration;
int? lastSyncLength;
void doSync() {
final stopwatch = Stopwatch()..start();
// for each file
for (var i = 0; i < strategy.numberOfFiles; i++) {
// 1. create reference to file
final file = File(join(directory.path, '$i.foo'));
// 2. check if file exists, delete if it does
if (file.existsSync()) file.deleteSync();
// 3. create file
file.createSync();
// 4. write text to file
for (var i = 0; i < strategy.numberOfWrites; i++) {
file.writeAsStringSync(strategy.textToWrite);
}
// 5. check size
lastSyncLength = file.lengthSync();
// 6. read file
file.readAsLinesSync();
}
lastSyncDuration = stopwatch.elapsed;
stopwatch.stop();
setState(() {});
}
Duration? lastLintedDuration;
int? lastLintedLength;
void doLinted() async {
final stopwatch = Stopwatch()..start();
// for each file
for (var i = 0; i < strategy.numberOfFiles; i++) {
// 1. create reference to file
final file = File(join(directory.path, '$i.foo'));
// 2. check if file exists, delete if it does
if (file.existsSync()) await file.delete();
// 3. create file
await file.create();
// 4. write text to file
for (var i = 0; i < strategy.numberOfWrites; i++) {
await file.writeAsString(strategy.textToWrite);
}
// 5. check size
lastLintedLength = await file.length();
// 6. read file
await file.readAsLines();
}
lastLintedDuration = stopwatch.elapsed;
stopwatch.stop();
setState(() {});
}
Duration? lastAsyncDuration;
int? lastAsyncLength;
void doAsync() async {
final stopwatch = Stopwatch()..start();
// for each file
for (var i = 0; i < strategy.numberOfFiles; i++) {
// 1. create reference to file
final file = File(join(directory.path, '$i.foo'));
// 2. check if file exists, delete if it does
// ignore: avoid_slow_async_io
if (await file.exists()) await file.delete();
// 3. create file
await file.create();
// 4. write text to file
for (var i = 0; i < strategy.numberOfWrites; i++) {
await file.writeAsString(strategy.textToWrite);
}
// 5. check size
lastAsyncLength = await file.length();
// 6. read file
await file.readAsLines();
}
lastAsyncDuration = stopwatch.elapsed;
stopwatch.stop();
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const RedSpinningRectangle(),
Padding(
padding: const EdgeInsets.all(25),
child: Text(
"Let's test for jank during a large filesystem operation "
"using both sync io methods and async io methods"
"\n\n"
"${strategy.description}",
textAlign: TextAlign.center,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RunButtonCluster(
buttonText: 'sync',
lastOperationDuration: lastSyncDuration,
lastOperationLength: lastSyncLength,
onPressed: () => doSync(),
),
RunButtonCluster(
buttonText: 'linted',
lastOperationDuration: lastLintedDuration,
lastOperationLength: lastLintedLength,
onPressed: () => doLinted(),
),
RunButtonCluster(
buttonText: 'async',
lastOperationDuration: lastAsyncDuration,
lastOperationLength: lastAsyncLength,
onPressed: () => doAsync(),
),
],
)
],
),
),
);
}
}
class RunButtonCluster extends StatelessWidget {
const RunButtonCluster({
Key? key,
required this.buttonText,
required this.lastOperationDuration,
required this.lastOperationLength,
required this.onPressed,
}) : super(key: key);
final String buttonText;
final Duration? lastOperationDuration;
final int? lastOperationLength;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: onPressed,
child: Text(buttonText),
),
Text(
lastOperationDuration == null
? 'No run data'
: 'Last run: ${lastOperationDuration!.toPrettyMilliseconds()}',
style: Theme.of(context).textTheme.caption,
),
],
);
}
}
/// Spinning widget for visually detecting jank
class RedSpinningRectangle extends StatelessWidget {
const RedSpinningRectangle({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
const animationDuration = Duration(milliseconds: 2000);
const longDuration = Duration(minutes: 30);
return TweenAnimationBuilder<double>(
duration: longDuration,
tween: Tween<double>(
begin: 0,
end: longDuration.inMilliseconds / animationDuration.inMilliseconds,
),
builder: (context, t, __) {
return Transform.rotate(
angle: 2 * pi * t,
child: Container(
height: 50,
width: 25,
color: Colors.red,
),
);
},
);
}
}
extension on Duration {
String toPrettyMilliseconds() {
return (inMicroseconds / 1000).toStringAsPrecision(4) + 'ms';
}
}
/// Write 5x 1MB files of text
class WriteStrategy {
final description = 'We will be writing 5 files, each 1 megabyte in size';
final numberOfFiles = 5;
final numberOfWrites = 1024;
final textToWrite = 'this is 1kb of text, Ï. it will take me some time but I '
'swear we can do it. This is only 101 bytes but we are getting '
'there slowly. I am going to rely on Lorem ipsum dolor sit amet, '
'consetetur adipiscing elit, sed do eiumod tempor incididunt ut '
'labore et dolore magna aliqua. Ut enim ad minim veniam, quis '
'nostrud exercitation ullamco laboris nisi ut aliquip ex ea '
'commodo consequat. Duis aute irure dolor in reprehenderit in '
'voluptate velit esse cillum dolore eu fugiat nulla pariatur. '
'Excepteur sint occaecat cupidatat non proident, sunt in culpa '
'qui officia deserunt mollit anim id est laborum. Lorem ipsum '
'dolor sit amet, consectetur adipiscing elit, sed do eiusmod '
'tempor incididunt ut labore et dolore magna aliqua. Ut enim ad '
'minim veniam, quis nostrud exercitation ullamco laboris nisi ut '
'aliquip ex ea commodo consequat. Duis aute irure dolor in '
'reprehenderit in voluptate velit esse cillum dolore eu fugiat '
'nulla pariatur. Excepteur sint occaecat cupidatat non proident, '
'sunt in culpa quit to get me there.';
}
name: sync_async_io
description: A demonstration of jank during sync io calls
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
path:
path_provider:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
flutter:
uses-material-design: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment