
Dlaczego wyrzuciliśmy naszą aplikację we Flutterze i zbudowaliśmy Cliqer Remote od nowa natywnie, na rdzeniu w Rust

26 września zaczęliśmy budować Cliqer Remote, naszą aplikację na telefon dla prezenterów, we Flutterze. Pięć dni później była już w TestFlight i na wewnętrznej ścieżce testów Google Play, ze wszystkimi funkcjami webowej wersji dla prezentera: pilotem do prezentacji, podglądem ekranu na żywo, rysowaniem po slajdach, czatem, notatkami i połączeniami. Działała. Potem zmierzyliśmy ją na prawdziwych telefonach, zobaczyliśmy, jak jeden z nich osiąga temperaturę obudowy 42 °C przy samym wyświetlaniu udostępnionego ekranu, i wyrzuciliśmy ją do kosza.
Nikt spoza naszego zespołu nigdy nie widział tej aplikacji. Zamiast niej dostajesz natywną aplikację na każdej platformie: SwiftUI na urządzeniach iPhone, iPad i Apple Watch, Kotlin i Jetpack Compose na systemach Android i Wear OS, a pod spodem jeden silnik, napisany raz, w Rust. Oto historia tego, dlaczego tak zrobiliśmy, wraz z liczbami, które zmierzyliśmy po drodze.
- Pilot do prezentacji ma nietypowe zadanie jak na aplikację na telefon: leżeć przez godzinę w ciepłej dłoni, pokazywać obraz ekranu na żywo i nigdy nie zgubić żadnego dotknięcia. Czas klatki, temperatura i rozmiar liczą się tu bardziej niż w większości aplikacji.
- Nasza wersja we Flutterze powstała szybko i dobrze wyglądała, ale efekty szkła i ścieżka wideo obciążały GPU w każdej klatce. Na Galaxy S24+ skróciliśmy pracę GPU na jednym ekranie z 19,8 ms do 5,7 ms na klatkę, a mimo to wciąż walczyliśmy z silnikiem, zamiast budować produkt.
- Następnie uruchomiliśmy trzy wersje na tym samym telefonie, w tym samym pomieszczeniu, każdą przez dziesięć minut z tym samym ruchomym ekranem. Pierwsza wersja we Flutterze rysowała 47,6 klatki na sekundę, ogrzała telefon o 5,9 °C i zużyła 168 mAh. Aplikacja natywna nie narysowała ani jednej klatki interfejsu, ogrzała telefon o 0,2 °C i zużyła 86 mAh: o połowę mniej baterii.
- Aplikacje natywne są mniejsze: 10,8 MB dla aplikacji na iPhone razem z aplikacją na Apple Watch w środku (wersja we Flutterze miała 27,6 MB) i 5,8 MB do pobrania na Androida (Flutter: 15,6 MB), mierzone w ten sam sposób.
- W tym samym rzucie sprawiliśmy, że aplikacja desktopowa zamienia kliknięcie w zmianę slajdu dziesięć razy szybciej: 48 ms w PowerPoint i 42 ms w Keynote na komputerze Mac, gdzie wcześniej trwało to około pół sekundy.

Pilot to nietypowa aplikacja do zbudowania
Większości aplikacji używa się w krótkich seriach. Pilota do prezentacji nie. Przez całe wystąpienie spoczywa w jednej dłoni, często w świetle reflektorów, z włączonym ekranem i radiem zajętym przez cały czas. W Cliqer pokazuje też obraz ekranu prezentera na żywo, rysuje po slajdach, obsługuje czat i notatki, a gdy prowadzący zaprosi prezentera, może pokazać go na kamerze.
Taki zakres obowiązków wyznacza trzy zasady:
- Każde dotknięcie musi dotrzeć, natychmiast. Kliknięcie, które dociera z opóźnieniem, to mówca stojący w ciszy przed publicznością.
- Telefon musi pozostać chłodny. Gorący telefon dławi wydajność, a zdławiony telefon gubi klatki, dekoduje wideo z opóźnieniem i szybciej rozładowuje baterię. Do tego niewygodnie trzyma się go przez godzinę.
- Aplikacja musi być mała i szybko się uruchamiać. Ludzie instalują ją na miejscu, często przez konferencyjne Wi-Fi, kilka minut przed wyjściem na scenę.
W większości aplikacji Flutter radzi sobie ze wszystkimi trzema. Nasza okazała się takim rodzajem aplikacji, w którym kosztuje więcej, niż daje.
Co Flutter zrobił dobrze
Oddajmy sprawiedliwość. Flutter przeprowadził nas od pustego folderu do w pełni funkcjonalnej aplikacji w obu sklepach w pięć dni, z jedną bazą kodu, hot reloadem, świetnym podejściem do testów i interfejsem, który wszędzie wyglądał tak samo. Napisaliśmy do niego 266 testów. Jeśli budujesz aplikację opartą na formularzach i listach, panel lub sklep, nadal polecilibyśmy go bez wahania.
Wiele się dzięki niemu nauczyliśmy. Niemal każde odkrycie z tego wpisu pochodzi z profilowania tamtej wersji we Flutterze na prawdziwym telefonie, a część poprawek działa dziś w każdym kliencie Cliqer, łącznie z wersją webową.
Gdzie bolało: szkło kosztuje cały ekran na klatkę
Wygląd Cliqer to matowe szkło: półprzezroczyste panele, które rozmywają to, co znajduje się za nimi. Na urządzeniach iPhone i iPad było to tanie. Na naszym testowym telefonie z Androidem, Galaxy S24+ z wyświetlaczem 120 Hz, już nie. Przy 120 Hz na narysowanie całej klatki jest 8,3 ms.
Sprofilowaliśmy GPU na urządzeniu i znaleźliśmy przyczynę w sposobie, w jaki silnik renderujący odczytuje tło: każdy szklany panel kopiował cały ekran, w każdej klatce, do tego z multisamplingiem. Ekran dołączania miał dziewięć szklanych paneli, czyli dziewięć pełnoekranowych kopii na klatkę. GPU w tym telefonie ma architekturę klasy desktopowej, która nie jest tilerem: nie ma taniego sposobu na odczytanie tego, co właśnie narysował, a każdy nowy rozmiar tekstury oznaczał świeżą alokację pamięci, którą jądro najpierw zeruje. W jednym z profili 71% czasu wątku rastra pochłaniała alokacja pamięci graficznej.

Naprawialiśmy to ekran po ekranie. Przeszliśmy na przedpremierową wersję silnika, żeby rozmycie można było ograniczyć do jego panelu (z 41 ms do 17 ms). Zgrupowaliśmy dziewięć paneli na ekranie dołączania w jeden wspólny odczyt (z 19,8 ms do 5,7 ms). Przestaliśmy animować rozmiar jakiejkolwiek szklanej powierzchni, bo każdy nowy rozmiar oznaczał nową alokację, i zamiast tego udawaliśmy animacje warstwami o stałym rozmiarze. Najgorsza klatka skanera kodów QR spadła z 285 ms do 18 ms.
Każda z tych poprawek była obejściem ograniczeń silnika, a nie funkcją dla prezentera. A skoro szkło odpowiadało za 80 do 90% czasu GPU w pokoju, zawsze dzielił nas jeden nowy panel od ponownego gubienia klatek.
Gdzie bolało naprawdę: wideo i ciepło, które generuje
Udostępniony ekran to najcięższa rzecz, jaką pokazuje aplikacja. W wersji we Flutterze każda zdekodowana klatka wideo trafiała do interfejsu jako tekstura, co oznaczało, że interfejs przerysowywał się przy każdej klatce wideo: pokój, szkło, elementy sterujące, wszystko, 45 razy na sekundę, tylko po to, by przesunąć piksele wewnątrz jednego prostokąta.
Po raz pierwszy zmierzyliśmy to na Galaxy za pomocą śladów systemowych 1 października. Podczas odtwarzania wideo GPU pracował z częstotliwością około 440 MHz. Przy zmianie slajdu klatki zajmowały od 25 do 33 ms, a GPU przez ponad połowę przejścia osiągał swój pułap 800 MHz. Telefon był w systemowym stanie termicznym „umiarkowanym” przy temperaturze obudowy 42 °C i sam ograniczał swój GPU, żeby sobie poradzić. Ciepło fałszowało też nasze pomiary: ciepły telefon wypadał o około 20% wolniej niż chłodny, więc w każdym porównaniu musieliśmy naprzemiennie testować wersje w tej samej temperaturze.
Potem spróbowaliśmy oczywistego eksperymentu: umieścić wideo na natywnej warstwie, którą system operacyjny komponuje sam, poza interfejsem. Ten sam telefon, ta sama aplikacja, ten sam pokój: interfejs przestał przerysowywać się dla wideo, taktowanie GPU spadło z 440 do 121 MHz, a klatka przy zmianie slajdu zajmowała już od 3 do 7 ms zamiast od 25 do 33 ms. Tę poprawkę wypuściliśmy w aplikacji we Flutterze 1 października. Pokazała nam też, gdzie leży prawdziwa odpowiedź: w aplikacji natywnej wideo zawsze żyje na warstwie systemowej, zarówno na iPhone, jak i na Androidzie, więc cała ta klasa problemów znika z samej konstrukcji, a nie dzięki obejściom.
Ten sam telefon, to samo pomieszczenie, dziesięć minut
Deklaracje dotyczące temperatury nic nie kosztują, więc ją zmierzyliśmy. 5 października uruchomiliśmy trzy wersje na tym samym Galaxy S24+, jedną po drugiej, każdą przez dziesięć minut, bez ładowarki, w tym samym pomieszczeniu, z tym samym ruchomym udostępnionym ekranem. Przed każdym przebiegiem telefon z powrotem stygł.
- Flutter, pierwsza wersja: aplikacja w takiej postaci, w jakiej ją najpierw zbudowaliśmy, z wideo rysowanym przez interfejs.
- Flutter, zoptymalizowany: ta sama aplikacja po opisanych wyżej poprawkach szkła i wideo.
- Natywna: nowy Cliqer Remote.

Pierwsza wersja we Flutterze przerysowywała cały interfejs 47,6 razy na sekundę tylko po to, by pokazać wideo, utrzymywała GPU na średnim poziomie 366 MHz i zużywała 1,47 sekundy czasu CPU na każdą sekundę. Aplikacja natywna nie narysowała w ciągu dziesięciu minut ani jednej klatki interfejsu: poruszała się tylko warstwa wideo. Jej GPU pracował średnio z częstotliwością 129 MHz, CPU zużywał 0,69 sekundy na sekundę, a zużycie baterii wyniosło 86 mAh wobec 168 mAh. To połowa baterii przy tym samym zadaniu.

I temperatura. W ciągu dziesięciu minut pierwsza wersja we Flutterze podniosła temperaturę baterii z 36,2 °C do 42,1 °C, a telefon zaczął się dławić. Zoptymalizowana wersja we Flutterze podniosła ją o 1,9 °C. Aplikacja natywna o 0,2 °C. Wystąpienie trwa dłużej niż dziesięć minut, a tę różnicę czujesz w dłoni.
Gwoli uczciwości wobec zoptymalizowanej wersji we Flutterze: była blisko. Nasze własne poprawki wewnątrz Fluttera usunęły większość kosztów, a wersja natywna wygrała z mniejszą przewagą: 12% mniej CPU i 25% mniej baterii. Chodzi o to, skąd każda z nich startuje. Pierwsza wersja pokazywała, jak wygląda dobra aplikacja we Flutterze prosto z pudełka; wersja natywna osiąga chłodny wynik bez obejść.
Jak mierzyliśmy, żeby można było to powtórzyć: ślady Perfetto na urządzeniu (oś czasu klatek, częstotliwość GPU, liczniki baterii), czas CPU procesu odczytany z jądra i temperatura baterii podawana przez sam telefon, z tą samą animacją 60 fps na udostępnionym ekranie przy każdym przebiegu. Jeden telefon i jeden przebieg na wersję, więc traktuj te liczby jako staranny test terenowy, a nie benchmark laboratoryjny. Przebieg natywny zaczął się też w temperaturze o około 2 °C niższej, dlatego porównujemy przyrost.
Co zbudowaliśmy zamiast tego
Nowy Cliqer Remote jest natywny na każdej platformie i zbudowany na jednym wspólnym silniku.
Interfejs jest natywny na każdej platformie. Na urządzeniach iPhone i iPad to SwiftUI z systemowym Liquid Glass. Na Androidzie to Jetpack Compose, z prawdziwym rozmyciem tła tam, gdzie platforma na to pozwala. Aplikacje na Apple Watch i Wear OS też są natywne i na tyle małe, że przyciski Dalej i Wstecz masz na nadgarstku.
Silnik jest napisany raz, w Rust. Dołączanie do pokoju, ponowne łączenie i wznawianie po zmianie sieci, odbieranie wideo z ekranu, wysyłanie kliknięć, wskaźnika laserowego i rysunków, czat, notatki, timer, połączenia oraz parowanie telefonu z komputerem: wszystko to żyje w jednej bibliotece Rust, którą współdzielą obie aplikacje. Każda aplikacja tylko rysuje interfejs, dekoduje wideo sprzętowym dekoderem systemu i komunikuje się z kamerą oraz mikrofonem. Poprawka protokołu powstaje raz i trafia na obie platformy, a obie aplikacje nie mogą się od siebie rozjechać.
Rust był naturalnym wyborem dla tego silnika: żaden garbage collector nie wstrzymuje klatki, bezpieczeństwo pamięci bez środowiska uruchomieniowego, a całość kompiluje się do jednej małej biblioteki zarówno dla iOS, jak i dla Androida. Jest szybki tam, gdzie to ważne. Na komputerze Mac nasz silnik pakietyzuje klatkę H.264 o rozmiarze 20 KB w 31 µs, kończy bezpieczny handshake w 198 µs po każdej stronie, a w naszych testach przekazuje klatkę wideo 100 KB do interfejsu na Androidzie w 46 µs (zanim zoptymalizowaliśmy tę granicę, zajmowało to 440 µs). Przez serwer pośredniczący czas podróży w obie strony wynosił w naszych testach w medianie od 13 do 16 ms.
Do tego mniejsza

Obie wersje zmierzyliśmy w ten sam sposób. Aplikacja na iPhone w postaci przesłanej do Apple zmniejszyła się z 27,6 MB do 10,8 MB, i to razem z aplikacją na Apple Watch (1,5 MB) w środku. Rozmiar pobierania na telefon z Androidem, zmierzony własnym narzędziem Google do pakietów aplikacji dla tego samego 64-bitowego telefonu, spadł z 15,6 MB do 5,8 MB. Mniej więcej jedna trzecia dawnego rozmiaru ma znaczenie, gdy ktoś instaluje aplikację przez Wi-Fi na miejscu wydarzenia na pięć minut przed wystąpieniem.
Polowanie na milisekundy nie skończyło się na telefonie
Szybsza aplikacja na telefon to tylko połowa kliknięcia. Druga połowa to komputer, na którym działa prezentacja, i podczas gdy aplikacje powstawały od nowa, zajęliśmy się także tą stroną.
Kliknięcia: z pół sekundy do jednej dziesiątej
Gdy kliknięcie dociera do aplikacji desktopowej Cliqer, musi ona przesunąć prezentację, a potem poinformować wszystkich w pokoju, który slajd jest wyświetlany. Ta druga część polegała wcześniej na pytaniu aplikacji prezentacyjnej o numer slajdu po każdym kliknięciu, a to pytanie było wolne.

Cliqer 2.3 utrzymuje jedno otwarte połączenie z PowerPoint lub Keynote i odczytuje pozycję bezpośrednio z kroku, który właśnie wykonał. Na komputerze Mac, w 20 przebiegach, mediana czasu od kliknięcia do slajdu spadła z 509 ms do 48 ms w PowerPoint i z 408 ms do 42 ms w Keynote. Dziesięć szybkich naciśnięć, takich, jakie prezenter wykonuje, żeby pominąć fragment, potrzebowało wcześniej 5,3 sekundy, żeby się ustabilizować. Teraz łączą się w jeden ruch i docierają w 254 ms.
Wideo, które nadąża przy słabym Wi-Fi
Na początku tego roku odkryliśmy, dlaczego udostępniony ekran mógł zamarzać na kilka sekund przy Wi-Fi. Wideo z ekranu w Cliqer przesyłane jest niezawodnym kanałem danych, a standard, na którym ten kanał się opiera, czeka co najmniej jedną pełną sekundę, zanim ponownie wyśle utracony pakiet, i podwaja ten czas przy każdym powtórzeniu. Chrome używa 400 ms. Każda klatka za utraconą czekała w kolejce, host wymuszał następnie pełne odświeżenie do tej samej kolejki, a na wolnym łączu robił to raz za razem.

Dostroiliśmy liczniki czasu retransmisji do wartości z Chrome i sprawiliśmy, że odświeżenia nie ustawiają się już w kolejce za zaległościami. W symulacji sieci z modelem strat Wi-Fi udostępnianie w 1080p zamiast od 15 do 22 zamrożeń trwających sekundę lub dłużej co dwie minuty miało ich od 0 do 2, a mediana opóźnienia względem obrazu na żywo spadła z nawet 2,3 sekundy do 12 ms. Ograniczyliśmy też pełne odświeżenia do jednego na strumień co pół sekundy, bo każde z nich trafia do każdego widza: przed wprowadzeniem tego limitu widz mógł otrzymywać 19 pełnych odświeżeń na sekundę, czyli około 40 Mbit/s.
Po stronie telefonu sprzętowy dekoder wideo w Galaxy wstrzymywał jedną zwykłą klatkę na około 60 ms, mniej więcej raz na sekundę. Polecenie dekoderowi, by działał z pełną prędkością, sprowadziło to do zera opóźnionych klatek w ciągu 50 sekund. Ta poprawka trafia do natywnej aplikacji na Androida.
Co powiedzielibyśmy innemu zespołowi
Jeśli budujesz cokolwiek działającego w czasie rzeczywistym na telefonach, oto lekcje, których nauka kosztowała nas dni, a których wdrożenie zajmuje minuty:
- Mierz na swoim najgorszym prawdziwym urządzeniu, nie w emulatorze. Nasz emulator działał w 60 Hz na innym backendzie graficznym i nie był w stanie pokazać ani jednego z problemów ze szkłem.
- Testuj wersje naprzemiennie w tej samej temperaturze. Ciepły telefon wypadał o około 20% wolniej. Porównanie zimnego przebiegu z gorącym Cię okłamie.
- Licz odczyty rozmycia. Każde rozmycie tła może kosztować pełnoekranową kopię na klatkę. Grupuj je, ograniczaj ich obszar i nigdy nie animuj rozmiaru rozmytej powierzchni.
- Nigdy nie przerysowuj interfejsu dla wideo. Umieść wideo na warstwie, którą komponuje system. To była największa pojedyncza wygrana pod względem GPU, baterii i temperatury, jaką zmierzyliśmy.
- Poproś sprzętowy dekoder o prędkość czasu rzeczywistego. Na Androidzie ustaw operating rate dekodera. Jedna linijka usunęła 60-milisekundowe przycięcie co sekundę.
- Sprawdź liczniki czasu w warstwie transportowej. Niezawodne kanały danych dziedziczą liczniki retransmisji ze starszych standardów. Minimum jednej sekundy to wieczność dla wideo na żywo.
- Traktuj pełne odświeżenia jak kosztowne klatki, którymi są. Ograniczaj je na strumień, bo każde trafia do każdego widza.
- Łącz serie danych wejściowych. Dziesięć naciśnięć powinno być jednym ruchem o dziesięć, a nie dziesięcioma podróżami w obie strony.
- Czytaj, co Twoje narzędzia wpisują do Twojej kompilacji. Przedpremierowy zestaw narzędzi bez pytania włączył flagę renderowania w naszym manifeście Androida.
Co to oznacza dla Ciebie
10x
szybsza zmiana slajdu po kliknięciu w PowerPoint i Keynote na komputerze Mac
5.8 MB
do pobrania na Androida, około jednej trzeciej dawnego rozmiaru
10.8 MB
aplikacja na iPhone razem z aplikacją na Apple Watch w środku
Half
baterii zużywanej przez naszą pierwszą wersję we Flutterze, ten sam telefon, te same 10 minut
Cliqer Remote jest natywny na urządzeniach iPhone, iPad, Android, Apple Watch i Wear OS. Kliknięcia są szybsze, telefon pozostaje chłodny i zużywa połowę baterii, którą zużywała nasza pierwsza wersja, wideo jest rysowane tak, jak telefon został do tego zaprojektowany, a aplikacja ma około jednej trzeciej dawnego rozmiaru. Webowa wersja dla prezentera nadal działa w każdej przeglądarce bez żadnej instalacji, więc nikt w Twoim pokoju nigdy nie zostanie pominięty.
Opóźnienie pilota do prezentacji: dlaczego liczą się milisekundy
Co naprawdę dzieje się między dotknięciem a zmianą slajdu.
Często zadawane pytania
Cliqer Remote pokazuje wideo na żywo, matowe szkło i pilota do prezentacji na telefonie, który przez godzinę spoczywa w dłoni prezentera. W wersji we Flutterze interfejs przerysowywał się przy każdej klatce wideo, a każdy szklany panel kopiował cały ekran w każdej klatce, co na Androidzie kosztowało czas GPU i podnosiło temperaturę. Aplikacje natywne z samej konstrukcji rysują wideo na warstwie systemowej, a do pobrania mają około jednej trzeciej dawnego rozmiaru.
W naszym teście na Galaxy S24+ dziesięć minut wideo z ekranu na żywo podniosło temperaturę telefonu o 0,2 °C w aplikacji natywnej, wobec 5,9 °C w naszej pierwszej wersji we Flutterze. Aplikacja natywna nie narysowała żadnej klatki interfejsu podczas odtwarzania wideo i zużyła 86 mAh baterii wobec 168 mAh.
Nie. Flutter pozwolił nam zbudować pełną aplikację w obu sklepach w pięć dni, a po naszej optymalizacji aplikacja natywna zużywała tylko o 25% mniej baterii niż on. W większości aplikacji to doskonały wybór. Nasza aplikacja łączy ciągłe wideo na żywo, wiele rozmytych szklanych powierzchni i długie sesje na ciepłym telefonie, a to właśnie przypadek, w którym jego model renderowania kosztował nas najwięcej.
Obsługuje wszystko, co nie jest rysowaniem: dołączanie do pokoju i ponowne łączenie się z nim, odbieranie udostępnionego ekranu, wysyłanie kliknięć, wskaźnika laserowego i rysunków, czat, notatki, timer, połączenia oraz parowanie telefonu z komputerem. Współdzielą go aplikacje na iPhone i Androida, więc poprawka trafia do obu jednocześnie.
Na komputerze Mac z Cliqer 2.3 aplikacja desktopowa zamienia kliknięcie w zmianę slajdu w medianie 48 ms w PowerPoint i 42 ms w Keynote, mierzone w 20 przebiegach. Sieć dodaje do tego własny czas podróży w obie strony, który w naszych testach z serwerem pośredniczącym wynosił w medianie od 13 do 16 ms.
Nie. Webowa wersja dla prezentera działa w każdej nowoczesnej przeglądarce bez instalacji. Aplikacje dodają natywny skaner kodów QR, Face ID lub biometrię do sterowania hostem, ekran, który się nie wygasza, oraz piloty do prezentacji na Apple Watch i Wear OS.
Dlaczego przenieśliśmy warstwę realtime Cliqer z Cloudflare (i dlaczego wciąż jesteśmy ich fanami)
Cztery miesiące na edge'u Cloudflare: co się psuło, co zbudowaliśmy, żeby sobie z tym poradzić, i dlaczego Cliqer działa teraz na naszych własnych dedykowanych serwerach.
Sterowanie prezentacją z telefonu: porównanie wszystkich aplikacji
PowerPoint, Keynote, Canva, Unified Remote, PPTControl, Cliqer i inne: które aplikacje do sterowania z telefonu działają przez internet, pokazują notatki i pasują do Ciebie.
Poprowadź swoje następne show z Cliqer
Czytaj dalej
Więcej: nowości produktowe na Twoje następne show


Dlaczego przenieśliśmy warstwę realtime Cliqer z Cloudflare (i dlaczego wciąż jesteśmy ich fanami)

Jak przyspieszyliśmy udostępnianie ekranu czterokrotnie
Otrzymaj kolejny poradnik przed swoim następnym show
Praktyczne poradniki dla prezenterów, ekip AV i zespołów eventowych oraz wydania Cliqer, które mają znaczenie. Bez spamu, rezygnacja jednym kliknięciem.