|
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.'; |
|
} |