Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions packages/imitation_game/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Imitation Game

## Description

`imitation_game` is a platform for performing automated tests to compare the
performance of different UI frameworks. For example, how much memory does the
same app written in Flutter and UIKit take?

## Running all the tests

You need a mobile device plugged into your computer and setup for development.
The mobile device and the computer need to be on the same network, one that
allows communication between computers since that's how the mobile phone will
report its results to the computer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A local mode with a mobile device and a host computer is definitely needed for development. Additionally, I think it's nice to also have a cloud mode where the tests can run from something like Firebase test lab.


```sh
dart imitation_game.dart
```

## Dependencies

In order to run the tests you will need the union of all the platforms being
tested. As new tests are added please add to this list:

### iOS

- Flutter
- Xcode
- [ios_deploy](https://github.com/ios-control/ios-deploy) - used to launch apps
on the attached iOS device.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd list out the currently existing scenarios tested and a brief description of the scenarios

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That information should exist in the readme file for each test. I don't want it duplicated here.

## Example File Layout

```text
./
├─ imitation_game.dart
└─ tests/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There may need to be another OS subdirectory under tests like

tests/ios/smiley/...
tests/android/smiley/...

This may introduce some duplicate code if a UI framework is cross-platform (e.g., Flutter), or maybe we can symlink to make tests/ios/smiley/flutter and tests/android/smiley/flutter the same. But some UI framework only works for a single OS (e.g., uikit doesn't make sense for Android). It's also very likely that run_ios.sh and the measurement tool/code would be OS specific.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think there will be an opportunity for sharing more between tests. I think those things will become apparent when we have more implementations and they should come organically from that.

├─ smiley/
│ ├─ README.md
│ ├─ flutter/
│ │ ├─ run_ios.sh
│ │ └─ <flutter project files>
│ └─ uikit/
│ ├─ run_ios.sh
│ └─ <uikit project files>
└─ memory/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this directory isn't in this PR. Is this readme right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just an example of the layout

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explicitly called it an example

├─ README.md
├─ flutter/
│ ├─ run_ios.sh
│ └─ <flutter project files>
└─ uikit/
├─ run_ios.sh
└─ <uikit project files>
```

Here there are 2 different tests with 2 different platform implementations. The
tests are named `smiley` and `memory`, they are both implemented on the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memory is self explanatory. Maybe smiley needs a more descriptive name? :D

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Smiley" is just referencing the name in the example above. It could have been anything. What it is testing is ill-relevant to the point. =)

platforms `flutter` and `uikit`.

### Adding a test

Tests should comprise of implementations on one or more platform. The directory
for the test should be added to `./tests`. Inside that directory there should

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your tree above didn't list out the various platform implementations. Is it a partial tree for illustration? Maybe add that to the description.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added <flutter project files> and '` as placeholders for those concepts.

be a directory of implementations and a `README.md` file that explains the test.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice for README.md to designate an implementation as the imitation goal so all other implementations should be compared to it for validation. When differences are found, other implementations should always try to match the imitation goal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing I just recalled today from checking this doc: the test README.md should also specify

  • the ideal device D*. D* is usually very fast/powerful that eliminates any user-perceivable performance differences. D* is used for imitation test: test if different UI implementations are distinguishable by users on D*. This ensures different implementations are implementing the same UIs.
  • the target device D. D is usually limited in power that will surface performance differences.

For example, we can probably use Pixel 4 as D* and Moto G4 as D for Android tests.


### Adding an implementation to a test

An implementation has to follow these rules:

- It needs to perform the same operations as the other implementations and
follow the description in the test's `README.md`.
- It needs to contain a `run_ios.sh` script that will build and launch the test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Let rename run_ios.sh to run_test.sh as we can put ios in the upper level directory name. Similarly, there could also be .../android/.../run_test.sh, .../web/.../run_test.sh.
  2. We should have both run_test.sh and install_app.sh. The install_app.sh would install the app on the target OS so users can freely interact with the UI to see if they look and function the same on a powerful device. The run_test.sh is for automated testing often on a slow device where no human interaction would be needed or allowed.
    • If there's only run_test.sh that does the automated test, then one could potentially just submit an app that replays a recorded video of the test as the implementation.
    • The install_app.sh can also be used for setup and other costly operations that we don't want to measure the performance. For example, regarding the first frame performance, install_app.sh could be used to build, install, and set up everything, while run_test.sh would simply launch the app and see when the first frame is presented on screen.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, how do you think about changing run_test.sh to run_test.dart so the script can be run from Linux/Windows/Mac? See the discussion here.

on the connected device.
- It should contain a file named `ip.txt` which will be overwritten by

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still feel that it's better to put the device-to-host reporting logic inside the test (e.g., smiley) so different target OSes and tests could choose reporting mechanisms differently. For example, for Android memory reporting, it's probably easier to just use adb on the host side so no HTTP communication is required from the device side. If multiple tests do want to use the HTTP reporting, then they could all import the same HTTP reporting module?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is value in having the reporting mechanism the same across all implementations. HTTP is the most universal mechanism that should be easy for users. Once we have a few tests we can probably just wrap it up in a library so when you implement a test you just call reportResult(result); and you don't even care how it's getting reported.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like just calling reportResult without having to worry about HTTP. It feels better to me if we don't put this HTTP communication as a contract or API of the imitation game so we'll have the freedom to change.

`imitation_game.dart` with the ip address and port that should be used to
report results to.
- It needs to report its results to the ip and port in the `ip.txt` via an HTTP
POST of JSON data.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that the measurement code/tool should not live inside the implementation directory. There should only be a single measurement code/tool that's under the test (e.g., smiley) directory, and all implementations must share it to measure the performance. Otherwise, it's hard to prove that different implementations are measuring the same thing.

This might be critical when a UI framework that no one is familiar enters the arena. Say if someone invented a new framework called Glutter, we probably don't have the expertise and time to vet its measuring code and tools.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just have to rely on humans to read the code. Someone reading the code can say if it is measuring the same thing. Certain things are only measurable on device, like time since the process was launched. From the host computer we can just measure the time between when we asked it to launch which isn't as accurate.

If we find there is a way to share measurement code that can be factored out. Nothing about the current design disallows it.


## Data format for results

```json
{
"test": "name_of_test",
"platform": "name_of_platform",
"results": {
"some_result_name": 1.23,
"some_result_name2": 4.56,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do multiple results correspond to? Are they for different implementations such as some_result_name for smiley/flutter/liyuqian and some_result_name2 for smiley/flutter/gaaclarke?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's a way to measure 2 different things for the same execution. For example, the startup test could measure memory as well as startup time.

}
}
```

A single test run can report multiple numbers.
170 changes: 170 additions & 0 deletions packages/imitation_game/imitation_game.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

const int _port = 4040;

Future<String> _findIpAddress() async {
String result;
final List<NetworkInterface> interfaces = await NetworkInterface.list();
for (NetworkInterface interface in interfaces) {
for (InternetAddress address in interface.addresses) {
if (address.type == InternetAddressType.IPv4) {
// TODO(gaaclarke): Implment having multiple addresses.
assert(result == null);
result = address.address;
}
}
}
return result;
}

typedef FileFilter = bool Function(FileSystemEntity);
Future<List<FileSystemEntity>> findFiles(Directory dir, {FileFilter where}) {
final List<FileSystemEntity> files = <FileSystemEntity>[];
final Completer<List<FileSystemEntity>> completer =
Completer<List<FileSystemEntity>>();
final Stream<FileSystemEntity> lister = dir.list(recursive: true);
lister.listen((FileSystemEntity file) {
if (where == null || where(file)) {
files.add(file);
}
}, onDone: () => completer.complete(files));
return completer.future;
}

class _Script {
_Script({this.path});
String path;
}

class _ScriptRunner {
_ScriptRunner(this._scriptPaths);

final List<String> _scriptPaths;
Process _currentProcess;
StreamSubscription<String> _stdoutSubscription;
StreamSubscription<String> _stderrSubscription;

Future<_Script> runNext() async {
if (_currentProcess != null) {
_stdoutSubscription.cancel();
_stderrSubscription.cancel();
_currentProcess.kill();
_currentProcess = null;
}

if (_scriptPaths.isEmpty) {
return null;
} else {
final String path = _scriptPaths.last;
print('running: $path');
_scriptPaths.removeLast();
_currentProcess = await Process.start('sh', <String>[path]);
// TODO(gaaclarke): Implement a timeout.
_stdoutSubscription =
_currentProcess.stdout.transform(utf8.decoder).listen((String data) {
print(data);
});
_stderrSubscription =
_currentProcess.stderr.transform(utf8.decoder).listen((String data) {
print(data);
});
return _Script(path: path);
}
}
}

class _ImitationGame {
final Map<String, dynamic> results = <String, dynamic>{};
_ScriptRunner _scriptRunner;
_Script _currentScript;

Future<bool> start(List<String> iosScripts) {
_scriptRunner = _ScriptRunner(iosScripts);
return _runNext();
}

Future<bool> handleResult(Map<String, dynamic> data) {
final String test = data['test'];
final String platform = data['platform'];
if (!results.containsKey(test)) {
results[test] = <String, dynamic>{};
}
if (!results[test].containsKey(platform)) {
results[test][platform] = <String, dynamic>{};
}
data['results'].forEach((String k, dynamic v) {
results[test][platform][k] = v as double;
});
return _runNext();
}

Future<bool> handleTimeout() {
return _runNext();
}

Future<bool> _runNext() async {
_currentScript = await _scriptRunner.runNext();
return _currentScript != null;
}
}

Future<void> main() async {
final HttpServer server = await HttpServer.bind(
InternetAddress.anyIPv4,
_port,
);
final String ipaddress = await _findIpAddress();
print('Listening on $ipaddress:${server.port}');

for (FileSystemEntity entity in await findFiles(Directory.current,
where: (FileSystemEntity f) => f.path.endsWith('ip.txt'))) {
final File file = File(entity.path);
file.writeAsStringSync('$ipaddress:${server.port}');
}

final List<String> iosScripts = (await findFiles(Directory.current,
where: (FileSystemEntity f) => f.path.endsWith('run_ios.sh')))
.map((FileSystemEntity e) => e.path)
.toList();

if (iosScripts.isEmpty) {
return;
}

final _ImitationGame game = _ImitationGame();
bool keepRunning = await game.start(iosScripts);

while (keepRunning) {
try {
final Stream<HttpRequest> timeoutServer = server.timeout(
const Duration(minutes: 5), onTimeout: (EventSink<HttpRequest> sink) {
print('TIMEOUT!');
throw TimeoutException('timeout');
});
await for (HttpRequest request in timeoutServer) {
print('got request: ${request.method}');
if (request.method == 'POST') {
final String content = await utf8.decoder.bind(request).join();
final Map<String, dynamic> data =
jsonDecode(content) as Map<String, dynamic>;
print('$data');
keepRunning = await game.handleResult(data);
if (!keepRunning) {
break;
}
} else {
request.response.write('use post');
}
await request.response.close();
}
} on TimeoutException catch (_) {
keepRunning = await game.handleTimeout();
}
}
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
final String jsonResults = encoder.convert(game.results);
print('$jsonResults');
await server.close(force: true);
}
11 changes: 11 additions & 0 deletions packages/imitation_game/tests/smiley/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Smiley

This test is an app that just draws one large image to the screen `smiley.png`,
stretched to fit inside the screen.

The following things are measured:

- 'startupTime' - The time between the start of the process and the rendering of
the image to the screen.
- 'memory' - The amount of system memory the app is using after having rendered
the image to the screen.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while we're here, should we add a cumulative cpu+gpu cost test? (in a different pr as needed)

4 changes: 4 additions & 0 deletions packages/imitation_game/tests/smiley/flutter/run_ios.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
cd $( dirname "${BASH_SOURCE[0]}" )
cd smiley
flutter run --release
44 changes: 44 additions & 0 deletions packages/imitation_game/tests/smiley/flutter/smiley/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/

# Web related
lib/generated_plugin_registrant.dart

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
10 changes: 10 additions & 0 deletions packages/imitation_game/tests/smiley/flutter/smiley/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: 227990ab308574184e06944710bae000330412d0
channel: unknown

project_type: app
16 changes: 16 additions & 0 deletions packages/imitation_game/tests/smiley/flutter/smiley/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# smiley

A new Flutter project.

## Getting Started

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)

For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java

# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
Loading