[Pascal Welsch] Heavy lift work in Flutter - get started with Isolates

[Pascal Welsch] Heavy lift work in Flutter - get started with Isolates

Presentation from GDG DevFest Ukraine 2018 - the biggest community-driven Google tech conference in the CEE.

Learn more at: https://devfest.gdg.org.ua

__

Moving work off the main thread prevents frame drops and makes users happy. Starting with Android P, apps will crash if they are not responding forcing developers to address this problem even more. Working with threads and async computations is every mobile developer daily challenge.

Flutter uses Dart, which is single threaded and has no APIs to start new threads like in Java or Swift. How does this fit in a world of phones with at least 4 CPUs?

This talk will explain the Flutter threading model, how Futures keep the app responsive on a single thread and how you can use all CPU cores for compute-heavy task via Isolates.

Pascal will demonstrate practical examples how to heavy lift tasks like:
- parsing of big json files
- deconding images
- sorting long lists

3a6de6bc902de7f75c0e753b3202ed52?s=128

Google Developers Group Lviv

October 12, 2018
Tweet

Transcript

  1. Heavy lift work in Flutter Get started with Isolates Pascal

    Welsch @passsy
  2. @passsy Disclaimer This talk might be interesting even if you

    1. Don’t plan to use Flutter in the future 2. Haven’t tried Flutter 3. Never seen Dart code
  3. @passsy

  4. @passsy Flutter is like a game engine - Like a

    high performance game framework but for apps - Draws on Canvas as efficient as possible - Cross-platform is just a side-effect
  5. @passsy High performance UIs - Smooth animation - No “jank”

    (dropping frames) - Almost-zero latency between input events and UI updates
  6. @passsy Human eye limits - 10-12 FPS minimum to detect

    motion - 24 FPS minimum for fluid motion - 60 FPS smooth motion - >60 FPS most humans can’t see the difference to 60FPS Already covered by Refael and Yonatan in the GPU talk
  7. 60FPS -> 16ms 120FPS -> 8ms 1000ms / 60 frames

    = 16.666 ms / frame
  8. 16ms 3ms

  9. @passsy Graphics pipeline GL or Vulkan UI Thread Dart GPU

    Thread Compositor Skia GPU Layer Tree Vsync GPU Throttle
  10. Problem: Sort a list of 1000 stores - by distance,

    "openingHours": [{ "day" : "2018-08-03", "hours" : [ { "open" : "2018-08-03 08:00", "close" : "2018-08-03 19:00", } ] }, { "day" : "2018-08-04", "hours" : [ { "open" : "2018-08-04 08:00", "close" : "2018-08-04 12:00", }, { "open" : "2018-08-04 13:00", "close" : "2018-08-04 19:00", } ] }] - open shops first ~50ms per store (Shop in shops my have different opening hours)
  11. Heavy lift work in Android

  12. Android solution in 2014:

  13. Android solution in 2016: RxJava

  14. Android solution in 2018: Kotlin coroutines

  15. @passsy Combined solution: Threads What we actually have to solve

    - Move work off the Main Thread - Run calculations on a different Thread
  16. @passsy Main thread responsibilities Drawing - Layout and dispatch drawing

    instructions to RenderThread Receive input events - Allow main thread to work the event loop, react to every touch event
  17. Heavy lift work in Flutter

  18. @passsy Dart deserves some love - super easy to learn

    - JIT: Just-in-time compiler for fast, sub-second development cycles - AOT: Ahead-of-time compiler for fast execution of native ARM code - Futures and Stream are part of the language (async/await)
  19. @passsy “Become the best language for client-side code” - Leaf

    Petersen Dart goal DartConf '18 But Dart is single threaded
  20. @passsy Darts single thread of execution - All code runs

    in the Main Isolate - Within an Isolate code can’t be executed concurrently - No need for synchronized blocks - The Main Isolate has an event loop (like HandlerThread) for async code
  21. @passsy Synchronous code // sync file access String versionNameSync() {

    final File file = File("version.txt"); // blocks until read file final String version = file.readAsStringSync(); return version; } MainIsolate read file draw
  22. @passsy Asynchronous code // async file access Future<String> versionNameAsync() async

    { final File file = File("version.txt"); // read file in 64k chunks final String version = await file.readAsString(); return version; }
  23. @passsy Asynchronous code MainIsolate read file draw // async file

    access Future<String> versionNameAsync() async { final File file = File("version.txt"); // read file in 64k chunks final String version = await file.readAsString(); return version; }
  24. @passsy Async doesn’t use multiple threads Splitting a long synchronous

    task into smaller ones will help to stay at 60 FPS The store opening times calculation problem can be splitted in smaller tasks
  25. @passsy Isolates MainIsolate draw Another Isolate Spawn Isolate Heavy calculation

  26. @passsy Concurrent != parallel - Code can’t be executed concurrently

    within an Isolate - One Thread per Isolate - But Isolates can run in parallel - On multiple Threads
  27. @passsy Isolate vs. Thread - No shared memory between Isolates

    - Communication via messages - Processor time is equally shared - Shared memory between all Threads (heap) - Communication via messages or shared memory - Processor time is equally shared
  28. Spawn an Isolate

  29. @passsy Spawn an Isolate static Future<Isolate> spawn<T>(void entryPoint(T message), T

    message); - Return type Isolate allows can be used to control the Isolate - entryPoint has to be a top-level or static function - with one argument T - has to return void (not Future, therefore not async) - The content of message can be: - primitive values (null, num, bool, double, String), - List and maps (cyclic allowed) - Custom object instance when Isolates share same code and process - or SendPort - Pass a SendPort as part of message to receive messages from the Isolate
  30. @passsy Create Pokemons synchronous static Pokemon createPokemon(String nickname) { var

    index = Random(nickname.hashCode).nextInt(150); final pokeData = pokemons[index]; Pokemon pokemon; for (var i = 0; i < 300; i++) { pokemon = _decodePokemon(pokeData); } pokemon.nickname = nickname; return pokemon; }
  31. @passsy

  32. @passsy Define protocol class SpawnMessage { SpawnMessage(this.sendPort, this.name); final SendPort

    sendPort; final String name; } static void spawnPokemon(SpawnMessage message) { message.sendPort.send(createPokemon(message.name)); }
  33. @passsy Spawn Isolate & connect Future<Pokemon> spawnPokemonViaIsolate(String nickname) async {

    var receivePort = ReceivePort(); var message = SpawnMessage(receivePort.sendPort, nickname); var isolate = await Isolate.spawn(spawnPokemon, message); final Pokemon pokemon = await receivePort.first; receivePort.close(); isolate.kill(); return pokemon; } // Warning! Missing error handling, do not reuse that code
  34. @passsy

  35. @passsy Isolates are different, not bad - Javascript is single

    threaded + WebWorkers - Similar to actors Erlang/Elixir - Not sharing memory is preferred in Rust and Go
  36. @passsy Is there no easier way? Future<Pokemon> spawnPokemonViaIsolate(String nickname) async

    { var receivePort = ReceivePort(); var message = SpawnMessage(receivePort.sendPort, nickname); var isolate = await Isolate.spawn(spawnPokemon, message); final Pokemon pokemon = await receivePort.first; receivePort.close(); isolate.kill(); return pokemon; } // Warning! Missing error handling, do not reuse that code
  37. @passsy Flutters compute function // import 'package:flutter/foundation.dart'; typedef ComputeCallback<Q, R>

    = R Function(Q message); Future<R> compute<Q, R>(ComputeCallback<Q, R> callback, Q message) static Pokemon createPokemon(String nickname) { ... } Future<Pokemon> spawnPokemon(String nickname) async { return await compute(createPokemon, nickname); }
  38. @passsy Flutter engine threads - Platform Task Runner (Android MainThread)

    - UI Task Runner (for MainIsolate) - GPU Task Runner (GPU access) - IO Task Runner (decompress Assets, prepare for GPU) DO NOT BLOCK
  39. @passsy Isolate hidden details - ~2mb - Depends on the

    dependencies of the isolate - Spawning an Isolate takes ~50-150ms - Messages are copied - Reaching OOM crashes the app when returning 2GB on iPhone X (3GB ram)
  40. @passsy

  41. @passsy Tip: Use LoadBalancer - from dart-lang/isolate package - Reuse

    Isolates to reduce the startup cost - Limit parallel execution to number of processors for purely computational tasks - Central registry for objects, accessible across Isolates
  42. @passsy Conclusion - Isolates are low level - The only

    way to prevent the main Isolate from blocking - You only have to use them rarely
  43. grandcentrix.net @grandcentrix Pascal Welsch @passsy