Skonfiguruj śledzenie systemu

Możesz skonfigurować śledzenie systemu, aby rejestrować procesor i profil wątku aplikacji w krótkim czasie. Następnie możesz użyć raportu wyjściowego ze śledzenia systemu, aby poprawić wydajność gry.

Konfigurowanie śledzenia systemu w grze

Narzędzie Systrace jest dostępne na dwa sposoby:

Systrace to narzędzie niskiego poziomu, które:

  • Dostarcza informacje podstawowe. Systrace rejestruje dane wyjściowe bezpośrednio z jądra, więc przechwytywane wskaźniki są niemal identyczne z raportowanymi przez serię wywołań systemowych.
  • Zużywa niewiele zasobów. Systrace wprowadza bardzo małe obciążenie na urządzenie, zwykle poniżej 1%, ponieważ przesyła dane do bufora w pamięci.

Optymalne ustawienia

Ważne jest, aby dostarczyć narzędziu rozsądny zestaw argumentów:

  • Kategorie: najlepszy zestaw kategorii do włączenia śledzenia systemu opartego na grach to: {sched, freq, idle, am, wm, gfx, view, sync, binder_driver, hal, dalvik}.
  • Rozmiar bufora: ogólna zasada jest taka, że rozmiar bufora wynoszący 10 MB na rdzeń procesora zezwala na ślad o długości około 20 sekund. Jeśli na przykład urządzenie jest wyposażone w 2 procesory czterordzeniowe (łącznie 8 rdzeń), odpowiednią wartość, którą można przekazać do programu systrace,wynosi 80 000 KB (80 MB).

    Jeśli w grze często występuje przełączanie kontekstu, zwiększ bufor do 15 MB na rdzeń procesora.

  • Zdarzenia niestandardowe: jeśli zdefiniujesz zdarzenia niestandardowe, które chcesz rejestrować w grze, włącz flagę -a, co pozwoli firmie Systrace uwzględnić te zdarzenia w raporcie o wynikach.

Jeśli korzystasz z programu wiersza poleceń systrace, wpisz to polecenie, aby zarejestrować ślad systemu, który stosuje sprawdzone metody dotyczące zbioru kategorii, rozmiaru bufora i zdarzeń niestandardowych:

python systrace.py -a com.example.myapp -b 80000 -o my_systrace_report.html \
  sched freq idle am wm gfx view sync binder_driver hal dalvik

Jeśli korzystasz na urządzeniu z aplikacji systemowej Systrace, wykonaj te czynności, aby zarejestrować ślad systemu, który stosuje sprawdzone metody dotyczące zbioru kategorii, rozmiaru bufora i zdarzeń niestandardowych:

  1. Włącz opcję Śledzenie aplikacji możliwych do debugowania.

    Aby można było użyć tego ustawienia, urządzenie musi mieć 256 MB lub 512 MB (w zależności od tego, czy procesor ma 4 lub 8 rdzeń), a każdy fragment pamięci o pojemności 64 MB musi być dostępny jako ciągły fragment.

  2. Wybierz Kategorie, a potem włącz kategorie z tej listy:

    • am: Menedżer aktywności
    • binder_driver: sterownik jądra Binder
    • dalvik: maszyna wirtualna Dalvik
    • freq: częstotliwość procesora
    • gfx: grafika
    • hal: moduły sprzętowe
    • idle: procesor nieaktywny
    • sched: planowanie wykorzystania procesora
    • sync: synchronizacja
    • view: wyświetl system
    • wm: menedżer okien
  3. Włącz Rejestrowanie śledzenia.

  4. Wczytaj grę.

  5. Wykonaj w grze interakcje odpowiadające rozgrywce, której wydajność urządzenia chcesz mierzyć.

  6. Krótko po zaobserwowaniu niepożądanego zachowania w grze wyłącz śledzenie systemu.

Masz już statystyki skuteczności potrzebne do dalszej analizy problemu.

Aby zaoszczędzić miejsce na dysku, ślady systemu na urządzeniu zapisują pliki w formacie skompresowanego logu czasu (*.ctrace). Aby zdekompresować ten plik podczas generowania raportu, użyj programu wiersza poleceń i dodaj opcję --from-file:

python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \
  -o my_systrace_report.html

Popraw konkretne obszary skuteczności

W tej sekcji omawiamy kilka częstych problemów z wydajnością gier mobilnych oraz pokazujemy, jak zidentyfikować i poprawić te aspekty gry.

Prędkość ładowania

Gracze chcą jak najszybciej rozpocząć grę, dlatego ważne jest, by skrócić czas wczytywania gry. Czasy wczytywania zazwyczaj pomagają wykonać te czynności:

  • Wykonaj leniwe ładowanie. Jeśli używasz tych samych zasobów w kolejnych scenach lub poziomach w grze, wczytaj je tylko raz.
  • Zmniejsz rozmiar komponentów. Dzięki temu możesz połączyć nieskompresowane wersje tych zasobów z plikiem APK gry.
  • Użyj metody kompresji, która oszczędza dysk. Przykładem takiej metody jest zlib.
  • Użyj IL2CPP zamiast mono. (ma zastosowanie tylko wtedy, gdy używasz Unity). IL2CPP zapewnia lepsze wykonywanie skryptów w języku C#.
  • Zadbaj o to, aby gra była wielowątkowa. Więcej informacji znajdziesz w sekcji na temat spójności liczby klatek.

Spójność liczby klatek

Jednym z najważniejszych elementów rozgrywki jest osiągnięcie stabilnej liczby klatek. Aby łatwiej osiągnąć ten cel, zastosuj techniki optymalizacji omówione w tej sekcji.

Wielowątkowość

Gdy tworzysz aplikację na wiele platform, naturalne jest, że całą aktywność w grze umieszczajesz w jednym wątku. Ta metoda jest łatwa do wdrożenia w wielu silnikach gier, ale na urządzeniach z Androidem nie jest optymalna. W efekcie gry z jednym wątkiem często ładują się powoli i nie mają stałej liczby klatek.

Systrace przedstawione na Rysunku 1 przedstawia zachowanie typowe dla gry uruchomionej w danym momencie tylko z jednym procesorem:

Schemat wątków
w ramach logu czasu systemowego

Rysunek 1. Raport Systrace dotyczący gry jednowątkowej

Aby poprawić wydajność gry, skonfiguruj ją w trybie wielowątkowym. Zwykle najlepszym modelem jest stosowanie 2 wątków:

  • Wątek gry, który zawiera główne moduły gry i wysyła polecenia renderowania.
  • Wątek renderowania, który odbiera polecenia renderowania i konwertuje je na polecenia graficzne, których GPU urządzenia może używać do wyświetlenia sceny.

Interfejs Vulkan API rozszerza ten model, ponieważ umożliwia równoległe przesyłanie 2 typowych buforów. Korzystając z tej funkcji, możesz rozłożyć wiele wątków renderowania na różne procesory, co jeszcze bardziej skraca czas renderowania sceny.

Możesz też wprowadzić zmiany związane z konkretnymi silnikami, aby zwiększyć wydajność wielowątkowości gry:

  • Jeśli tworzysz grę z użyciem silnika Unity, włącz opcje Renderowanie wielowątkowe i skórki GPU.
  • Jeśli używasz silnika renderowania niestandardowego, upewnij się, że potok poleceń renderowania i potoku poleceń grafiki są prawidłowo wyrównane. W przeciwnym razie mogą wystąpić opóźnienia w wyświetlaniu scen gry.

Po zastosowaniu tych zmian gra powinna zajmować co najmniej 2 procesory jednocześnie, jak widać na ilustracji 2:

Schemat wątków
w ramach logu czasu systemowego

Rysunek 2. Raport Systrace dotyczący gry wielowątkowej

Wczytuję element interfejsu

Schemat stosu ramek w logu czasu
Rysunek 3. Raport Systrace dotyczący gry, która renderuje jednocześnie dziesiątki elementów interfejsu

Gdy tworzysz grę z dużą liczbą funkcji, warto dać graczowi dostęp do wielu różnych opcji i działań naraz. Aby jednak utrzymać stałą liczbę klatek, weź pod uwagę stosunkowo mały rozmiar ekranów mobilnych i uprość interfejs.

Raport Systrace przedstawiony na Rysunku 3 to przykład ramki interfejsu, która próbuje wyrenderować zbyt wiele elementów w stosunku do możliwości urządzenia mobilnego.

Warto skrócić czas aktualizowania interfejsu do 2–3 milisekund. Tak szybkie zmiany możesz uzyskać, przeprowadzając optymalizacje podobne do tych:

  • Aktualizuj tylko te elementy na ekranie, które zostały przeniesione.
  • Ogranicz liczbę tekstur i warstw interfejsu. Rozważ połączenie wywołań graficznych, takich jak cieniowanie i tekstury, używając tego samego materiału.
  • Opóźnij operacje animacji elementów na GPU.
  • Bardziej agresywnie eliminuj problemy z frustracją i okluzją.
  • Jeśli to możliwe, wykonuj operacje rysowania za pomocą interfejsu API Vulkan. Widok wywołania rysowania jest niższy na interfejsie Vulkan.

Zużycie energii

Nawet po wprowadzeniu optymalizacji omówionych w poprzedniej sekcji może się okazać, że liczba klatek na sekundę w grze spada w ciągu pierwszych 45–50 minut rozgrywki. Ponadto urządzenie może się nagrzewać i z czasem zużywać więcej baterii.

W wielu przypadkach ten niepożądany zestaw temperatur i zużycia energii jest związany ze sposobem, w którym obciążenie gry jest rozłożone na procesory urządzenia. Aby zwiększyć zużycie energii przez grę, stosuj sprawdzone metody opisane w kolejnych sekcjach.

Przechowywanie wątków z dużą ilością pamięci na jednym procesorze

W przypadku wielu urządzeń mobilnych pamięć podręczna L1 znajduje się w określonych procesorach, a pamięci podręczne L2 – w zestawie procesorów, które współużytkują zegar. Aby zmaksymalizować trafienia w pamięci podręcznej L1, najlepiej jest uruchomić wątek główny gry wraz z innymi wątkami z dużą ilością pamięci na jednym procesorze.

Odrocz krótkie czasy do procesorów o mniejszej mocy

Większość silników gier, w tym Unity, wie, że operacje w wątkach roboczych są opóźnione na innym procesorze niż w grze głównym. Mechanizm nie jest jednak świadomy architektury urządzenia i nie jest w stanie tak dobrze przewidywać obciążenia pracą gry.

Większość urządzeń z układem scalonym ma co najmniej 2 współdzielone zegary: jeden dla szybkich procesorów urządzenia i jeden dla wolnych procesorów. W konsekwencji taka architektura polega na tym, że jeśli jeden szybki procesor musi działać z maksymalną szybkością, wszystkie pozostałe również będą działać z maksymalną prędkością.

Przykładowy raport widoczny na Rysunku 4 pokazuje grę, która korzysta z szybkich procesorów. Jednak taki wysoki poziom aktywności generuje dużo energii i ciepła.

Schemat wątków
w ramach logu czasu systemowego

Rysunek 4. Raport Systrace przedstawiający nieoptymalne przypisanie wątków do procesorów urządzenia.

Aby zmniejszyć ogólne zużycie energii, najlepiej zasugerować algorytmowi szeregowania, aby najkrótsze zadania, takie jak ładowanie dźwięku, uruchamianie wątków instancji roboczych czy wykonywanie choreografa, były opóźnione do grupy wolnych procesorów na urządzeniu. Przenieś jak najwięcej tej pracy na wolne procesory, zachowując pożądaną liczbę klatek.

Większość urządzeń wymienia wolne procesory przed szybszymi procesorami, ale nie możesz zakładać, że SOC urządzenia korzysta z tej kolejności. Aby to sprawdzić, uruchom polecenia podobne do tych przedstawionych w tym kodzie wykrywania topologii procesora na GitHubie.

Gdy dowiesz się, które procesory działają wolno, możesz zadeklarować koligacje w krótkich wątkach, które są zgodne z algorytmem szeregowania urządzenia. W tym celu dodaj następujący kod w każdym wątku:

#include <sched.h>
#include <sys/types.h>
#include <unistd.h>

pid_t my_pid; // PID of the process containing your thread.

// Assumes that cpu0, cpu1, cpu2, and cpu3 are the "slow CPUs".
cpu_set_t my_cpu_set;
CPU_ZERO(&my_cpu_set);
CPU_SET(0, &my_cpu_set);
CPU_SET(1, &my_cpu_set);
CPU_SET(2, &my_cpu_set);
CPU_SET(3, &my_cpu_set);
sched_setaffinity(my_pid, sizeof(cpu_set_t), &my_cpu_set);

Naprężenie termiczne

Gdy urządzenia za bardzo się nagrzewają, mogą ograniczać procesor lub GPU, co może w nieoczekiwany sposób wpłynąć na działanie gier. W grach, które wykorzystują skomplikowaną grafikę, intensywne obliczenia lub długotrwałą aktywność w sieci, łatwiej będzie napotkać problemy.

Za pomocą interfejsu Heat API możesz monitorować zmiany temperatury na urządzeniu i podejmować działania mające na celu zmniejszenie zużycia energii oraz obniżenie temperatury urządzenia. Gdy urządzenie zgłasza stres cieplny, cofnij bieżące działania, aby zmniejszyć zużycie energii. Na przykład zmniejsz liczbę klatek lub tessell wielokątów.

Najpierw zadeklaruj obiekt PowerManager i zainicjuj go w metodzie onCreate(). Dodaj do obiektu detektor stanu termicznego.

Kotlin

class MainActivity : AppCompatActivity() {
    lateinit var powerManager: PowerManager

    override fun onCreate(savedInstanceState: Bundle?) {
        powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
        powerManager.addThermalStatusListener(thermalListener)
    }
}

Java

public class MainActivity extends AppCompatActivity {
    PowerManager powerManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
        powerManager.addThermalStatusListener(thermalListener);
    }
}

Określ działania, jakie ma podjąć, gdy detektor wykryje zmianę stanu. Jeśli Twoja gra używa C/C++, dodaj kod do poziomów stanu termicznego w onThermalStatusChanged(), aby wywoływać kod natywnej gry za pomocą JNI lub użyć natywnego interfejsu Thermal API.

Kotlin

val thermalListener = object : PowerManager.OnThermalStatusChangedListener() {
    override fun onThermalStatusChanged(status: Int) {
        when (status) {
            PowerManager.THERMAL_STATUS_NONE -> {
                // No thermal status, so no action necessary
            }

            PowerManager.THERMAL_STATUS_LIGHT -> {
                // Add code to handle light thermal increase
            }

            PowerManager.THERMAL_STATUS_MODERATE -> {
                // Add code to handle moderate thermal increase
            }

            PowerManager.THERMAL_STATUS_SEVERE -> {
                // Add code to handle severe thermal increase
            }

            PowerManager.THERMAL_STATUS_CRITICAL -> {
                // Add code to handle critical thermal increase
            }

            PowerManager.THERMAL_STATUS_EMERGENCY -> {
                // Add code to handle emergency thermal increase
            }

            PowerManager.THERMAL_STATUS_SHUTDOWN -> {
                // Add code to handle immediate shutdown
            }
        }
    }
}

Java

PowerManager.OnThermalStatusChangedListener thermalListener =
    new PowerManager.OnThermalStatusChangedListener () {

    @Override
    public void onThermalStatusChanged(int status) {

        switch (status)
        {
            case PowerManager.THERMAL_STATUS_NONE:
                // No thermal status, so no action necessary
                break;

            case PowerManager.THERMAL_STATUS_LIGHT:
                // Add code to handle light thermal increase
                break;

            case PowerManager.THERMAL_STATUS_MODERATE:
                // Add code to handle moderate thermal increase
                break;

            case PowerManager.THERMAL_STATUS_SEVERE:
                // Add code to handle severe thermal increase
                break;

            case PowerManager.THERMAL_STATUS_CRITICAL:
                // Add code to handle critical thermal increase
                break;

            case PowerManager.THERMAL_STATUS_EMERGENCY:
                // Add code to handle emergency thermal increase
                break;

            case PowerManager.THERMAL_STATUS_SHUTDOWN:
                // Add code to handle immediate shutdown
                break;
        }
    }
};

Opóźnienie przy dotknięciu

Gry, które renderują klatki tak szybko, jak to możliwe, tworzą scenariusz powiązany z GPU, w którym bufor klatek jest przepełniony. Procesor musi czekać na GPU, co powoduje zauważalne opóźnienie między danymi wyjściowymi odtwarzacza a ich efektami na ekranie.

Aby sprawdzić, czy można poprawić tempo klatek w grze, wykonaj te czynności:

  1. Wygeneruj raport Systrace obejmujący kategorie gfx i input. Kategorie te są szczególnie przydatne do określania czasu oczekiwania na wyświetlenie reklamy.
  2. Sprawdź sekcję SurfaceView raportu Systrace. Przeciążony bufor powoduje, że liczba oczekujących operacji wczytywania bufora przekracza między 1 a 2, jak widać na Rysunku 5:

    Schemat kolejki bufora w ramach logu czasu systemowego

    Rysunek 5. Raport Systrace przedstawiający przeciążony bufor, który okresowo jest zapełniony, by akceptować polecenia rysowania

Aby wyeliminować tę niespójność w tempie klatek, wykonaj czynności opisane w tych sekcjach:

Zintegruj interfejs API Android Frame Pacing ze swoją grą

Interfejs Android Frame Pacing API ułatwia zastępowanie klatek i określa odstęp czasu, który zapewni Ci stałą liczbę klatek.

Zmniejsz rozdzielczość zasobów gry niebędących interfejsem użytkownika

Wyświetlacze na nowoczesnych urządzeniach mobilnych zawierają znacznie więcej pikseli, niż może przetworzyć odtwarzacz, dlatego można zmniejszać próbkowanie tak, aby 5- lub nawet 10-pikselowe obszary zawierały jeden kolor. Ze względu na strukturę większości pamięci podręcznych dla reklam displayowych najlepiej zmniejszyć rozdzielczość tylko do jednego wymiaru.

Nie zmniejszaj jednak rozdzielczości elementów interfejsu gry. Ważne jest, by zachować grubość linii tych elementów, by zachować odpowiednio duży rozmiar docelowego elementu dotykowego dla wszystkich graczy.

Płynność renderowania

Gdy SurfaceFlinger dotyka bufora wyświetlacza, aby wyświetlić scenę z gry, aktywność procesora na chwilę rośnie. Jeśli te skoki aktywności procesora następują nierównomiernie, może się zdarzyć, że w grze występuje zacinanie. Diagram na Rysunku 6 przedstawia przyczynę zaistniałej sytuacji:

Diagram klatek, w których brakuje okna Vsync, ponieważ rysowanie rozpoczęło się za późno

Rysunek 6. Raport Systrace pokazujący, jak w klatce może brakować synchronizacji Vsync.

Jeśli ramka zacznie rysować zbyt późno, nawet o kilka milisekund, może nie wyświetlić się następnego okna. Ramka musi następnie czekać na wyświetlenie kolejnej synchronizacji Vsync (33 milisekundy w przypadku gry z szybkością 30 kl./s), co powoduje zauważalne opóźnienie z perspektywy gracza.

Aby rozwiązać ten problem, użyj interfejsu API Android Frame Pacing, który zawsze przedstawia nową klatkę na fali VSync.

Stan pamięci

Jeśli grasz przez dłuższy czas, na urządzeniu mogą wystąpić błędy „brak pamięci”.

W takiej sytuacji sprawdź aktywność CPU w raporcie Systrace i zobacz, jak często system wywołuje demona kswapd. Jeśli podczas uruchamiania gry następuje wiele wywołań, warto przyjrzeć się dokładniej, w jaki sposób gra zarządza pamięcią i oczyszcza ją.

Więcej informacji znajdziesz w artykule Efektywne zarządzanie pamięcią w grach.

Stan wątku

Nawigując po typowych elementach raportu Systrace, możesz wyświetlić czas spędzony w danym wątku w każdym możliwym stanie wątku, wybierając go w raporcie, tak jak to widać na Rysunku 7:

Schemat raportu
Systrace

Rysunek 7. Raport Systrace pokazujący, jak wybór wątku powoduje wyświetlenie w raporcie podsumowania stanu tego wątku.

Jak widać na Rysunku 7, może się okazać, że wątki dotyczące gry nie są „uruchomione” lub „uruchamiane” tak często, jak powinny. Na liście poniżej znajdziesz kilka typowych przyczyn, dla których dany wątek może okresowo przechodzić w nietypowy stan:

  • Jeśli wątek jest uśpiony przez dłuższy czas, być może występuje w nim rywalizacja o blokadę lub oczekiwanie na aktywność GPU.
  • Jeśli wątek jest stale blokowany podczas wejścia-wyjścia, oznacza to, że odczytuje się za dużo danych z dysku naraz lub gra w pełni ekscytuje.

Dodatkowe materiały

Więcej informacji o zwiększaniu wydajności gry znajdziesz w tych dodatkowych materiałach:

Filmy