Możesz skonfigurować śledzenie systemu, aby przez krótki czas rejestrować profil procesora i wątków aplikacji. Następnie możesz użyć raportu wyjściowego ze śledzenia systemu, aby poprawić wydajność gry.
Konfigurowanie śledzenia systemu opartego na grze
Narzędzie Systrace jest dostępne na 2 sposoby:
Systrace to narzędzie niskiego poziomu, które:
- Zawiera dane podstawowe. Systrace rejestruje dane wyjściowe bezpośrednio z jądra, więc rejestrowane przez niego dane są niemal identyczne z tymi, które zgłasza seria wywołań systemowych.
- Zużywa niewiele zasobów. Systrace wprowadza bardzo małe obciążenie urządzenia, zwykle poniżej 1%, ponieważ przesyła dane do bufora w pamięci.
Ustawienia optymalne
Warto podać narzędziu rozsądny zestaw argumentów:
- Kategorie: najlepszy zestaw kategorii do włączenia w przypadku systemu opartego na grach to: {
sched,freq,idle,am,wm,gfx,view,sync,binder_driver,hal,dalvik}. Rozmiar bufora: ogólna zasada mówi, że rozmiar bufora wynoszący 10 MB na rdzeń procesora umożliwia śledzenie trwające około 20 sekund. Jeśli na przykład urządzenie ma 2 czterordzeniowe procesory (łącznie 8 rdzeni), odpowiednia wartość do przekazania do programu
systraceto 80 000 KB (80 MB).Jeśli gra często przełącza kontekst, zwiększ bufor do 15 MB na rdzeń procesora.
Zdarzenia niestandardowe: jeśli definiujesz zdarzenia niestandardowe, które mają być rejestrowane w grze, włącz flagę
-a, która umożliwia uwzględnianie tych zdarzeń niestandardowych w raporcie wyjściowym Systrace.
Jeśli używasz programu wiersza poleceń systrace, użyj tego polecenia, aby zarejestrować śledzenie systemu, które stosuje sprawdzone metody w przypadku zestawu 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 używasz aplikacji systemowej Systrace na urządzeniu, wykonaj te czynności, aby zarejestrować śledzenie systemu, które stosuje sprawdzone metody w zakresie zestawu kategorii, rozmiaru bufora i zdarzeń niestandardowych:
Włącz opcję Śledź aplikacje, które można debugować.
Aby używać tego ustawienia, urządzenie musi mieć 256 MB lub 512 MB dostępnej pamięci (w zależności od tego, czy procesor ma 4 czy 8 rdzeni), a każdy 64-megabajtowy fragment pamięci musi być dostępny jako ciągły blok.
Kliknij Kategorie, a potem włącz kategorie z tej listy:
am: Menedżer aktywnościbinder_driver: sterownik jądra Binderdalvik: Dalvik VMfreq: częstotliwość procesoragfx: Grafikahal: moduły sprzętoweidle: bezczynność procesorasched: planowanie procesora,sync: Synchronizacjaview: Wyświetl systemwm: Menedżer okien
Włącz opcję Record tracing (Nagrywaj śledzenie).
Wczytaj grę.
Wykonaj w grze interakcje odpowiadające rozgrywce, której wydajność urządzenia chcesz zmierzyć.
Krótko po wystąpieniu niepożądanego zachowania w grze wyłącz śledzenie systemu.
Udało Ci się zarejestrować statystyki wydajności potrzebne do dalszej analizy problemu.
Aby zaoszczędzić miejsce na dysku, śledzenie systemu na urządzeniu zapisuje pliki w skompresowanym formacie śladu (*.ctrace). Aby rozpakować ten plik podczas generowania raportu, użyj programu wiersza poleceń i uwzględnij opcję --from-file:
python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \ -o my_systrace_report.html
Poprawianie konkretnych obszarów skuteczności
W tej sekcji znajdziesz omówienie kilku typowych problemów z wydajnością gier mobilnych oraz opis sposobów identyfikowania i ulepszania tych aspektów gry.
Szybkość wczytywania
Gracze chcą jak najszybciej rozpocząć rozgrywkę, dlatego ważne jest, aby jak najbardziej skrócić czas wczytywania gry. Zazwyczaj pomagają w tym te działania:
- Wykonywać leniwe ładowanie. Jeśli w kolejnych scenach lub poziomach gry używasz tych samych komponentów, wczytuj je tylko raz.
- Zmniejsz rozmiar komponentów. Dzięki temu możesz dołączyć nieskompresowane wersje tych zasobów do pliku APK gry.
- Użyj metody kompresji oszczędzającej miejsce na dysku. Przykładem takiej metody jest zlib.
- Używaj IL2CPP zamiast mono. (Dotyczy tylko użytkowników Unity). IL2CPP zapewnia lepszą wydajność wykonywania skryptów w języku C#.
- Umożliwiaj grze korzystanie z wielu wątków. Więcej informacji znajdziesz w sekcji stała liczba klatek na sekundę.
Spójność liczby klatek na sekundę
Jednym z najważniejszych elementów rozgrywki jest osiągnięcie stałej liczby klatek na sekundę. Aby ułatwić sobie osiągnięcie tego celu, skorzystaj z technik optymalizacji omówionych w tej sekcji.
Wielowątkowość
Podczas tworzenia gier na wiele platform naturalne jest umieszczanie całej aktywności w grze w jednym wątku. Chociaż tę metodę wykonywania można łatwo wdrożyć w wielu silnikach gier, nie jest ona optymalna w przypadku urządzeń z Androidem. W rezultacie gry jednowątkowe często wczytują się powoli i nie mają stałej liczby klatek na sekundę.
Systrace pokazany na rysunku 1 przedstawia zachowanie typowe dla gry działającej tylko na jednym procesorze naraz:
Aby zwiększyć wydajność gry, zastosuj w niej wielowątkowość. Zwykle najlepszy model to 2 wątki:
- 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 tłumaczy je na polecenia graficzne, których GPU urządzenia może używać do wyświetlania sceny.
Interfejs Vulkan API rozszerza ten model, ponieważ umożliwia równoległe przesyłanie 2 wspólnych buforów. Dzięki tej funkcji możesz rozdzielić wiele wątków renderowania na kilka procesorów, co dodatkowo skróci czas renderowania sceny.
Możesz też wprowadzić pewne zmiany w silniku, aby zwiększyć wydajność wielowątkową gry:
- Jeśli tworzysz grę za pomocą silnika Unity, włącz opcje Multithreaded Rendering (Renderowanie wielowątkowe) i GPU Skinning (Animacja na GPU).
- Jeśli używasz niestandardowego silnika renderowania, upewnij się, że potok poleceń renderowania i potok poleceń graficznych są prawidłowo dopasowane. W przeciwnym razie może to spowodować opóźnienia w wyświetlaniu scen w grze.
Po wprowadzeniu tych zmian gra powinna zajmować co najmniej 2 procesory jednocześnie, jak pokazano na rysunku 2:
Wczytywanie elementu interfejsu
Podczas tworzenia gry z wieloma funkcjami kuszące jest wyświetlanie graczowi wielu różnych opcji i działań jednocześnie. Aby jednak utrzymać stałą liczbę klatek na sekundę, warto wziąć pod uwagę stosunkowo mały rozmiar wyświetlaczy urządzeń mobilnych i uprościć interfejs użytkownika.
Raport Systrace przedstawiony na rysunku 3 to przykład klatki interfejsu, która próbuje wyrenderować zbyt wiele elementów w stosunku do możliwości urządzenia mobilnego.
Dobrym celem jest skrócenie czasu aktualizacji interfejsu do 2–3 milisekund. Takie szybkie aktualizacje 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 shadery i tekstury, które korzystają z tego samego materiału.
- Przekazywanie operacji animacji elementów do procesora graficznego.
- Wykonywanie bardziej agresywnego wycinania frustum i okluzji.
- Jeśli to możliwe, wykonuj operacje rysowania za pomocą interfejsu Vulkan API. Obciążenie wywołania rysowania jest mniejsze w przypadku interfejsu Vulkan.
Zużycie energii
Nawet po wprowadzeniu optymalizacji omówionych w poprzedniej sekcji możesz zauważyć, że liczba klatek na sekundę w Twojej grze spada w ciągu pierwszych 45–50 minut rozgrywki. Ponadto urządzenie może zacząć się nagrzewać i z czasem zużywać więcej baterii.
W wielu przypadkach ten niepożądany zestaw parametrów termicznych i zużycia energii jest związany z tym, jak obciążenie gry jest rozłożone na procesory urządzenia. Aby zwiększyć efektywność zużycia energii przez grę, zastosuj sprawdzone metody opisane w kolejnych sekcjach.
Utrzymywanie wątków wymagających dużej ilości pamięci na jednym procesorze
Na wielu urządzeniach mobilnych pamięci podręczne L1 znajdują się na określonych procesorach, a pamięci podręczne L2 – na zestawach procesorów, które mają wspólny zegar. Aby zmaksymalizować liczbę trafień w pamięci podręcznej L1, najlepiej jest zwykle uruchamiać wątek główny gry wraz z innymi wątkami zużywającymi dużo pamięci na jednym procesorze.
Przekazywanie krótkotrwałych zadań do procesorów o mniejszej mocy
Większość silników gier, w tym Unity, wie, że operacje wątków instancji roboczej należy odłożyć na inny procesor niż wątek główny gry. Silnik nie zna jednak konkretnej architektury urządzenia i nie może przewidzieć obciążenia gry tak dobrze jak Ty.
Większość układów SoC ma co najmniej 2 wspólne zegary: jeden dla szybkich procesorów urządzenia, a drugi dla wolnych procesorów. Konsekwencją tej architektury jest to, że jeśli jeden szybki procesor musi działać z maksymalną prędkością, wszystkie pozostałe szybkie procesory również działają z maksymalną prędkością.
Przykładowy raport na rysunku 4 pokazuje grę, która wykorzystuje szybkie procesory. Jednak ten wysoki poziom aktywności szybko generuje dużą moc i ciepło.
Aby zmniejszyć ogólne zużycie energii, najlepiej zasugerować harmonogramowi, aby krótsze zadania, takie jak wczytywanie dźwięku, uruchamianie wątków instancji roboczych i wykonywanie choreografii, były odkładane na zbiór wolnych procesorów na urządzeniu. Przenieś jak najwięcej tej pracy na wolniejsze procesory, zachowując przy tym pożądaną liczbę klatek na sekundę.
Większość urządzeń wyświetla najpierw wolniejsze procesory, a potem szybsze, ale nie można zakładać, że układ SOC Twojego urządzenia używa tej kolejności. Aby to sprawdzić, uruchom polecenia podobne do tych, które są pokazane w tym kodzie wykrywania topologii procesora w GitHubie.
Gdy dowiesz się, które rdzenie procesora są wolne, możesz zadeklarować powiązania dla wątków o krótkim czasie trwania, których harmonogram urządzenia będzie przestrzegać. Aby to zrobić, dodaj ten 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ężenia termiczne,
Gdy urządzenia zbytnio się nagrzewają, mogą ograniczać moc procesora lub procesora graficznego, co może mieć nieoczekiwany wpływ na gry. Problemy częściej występują w przypadku gier z zaawansowaną grafiką, wymagających dużej mocy obliczeniowej lub ciągłej aktywności sieciowej.
Używaj interfejsu Thermal API do monitorowania zmian temperatury na urządzeniu i podejmowania działań mających na celu utrzymanie niższego zużycia energii i niższej temperatury urządzenia. Gdy urządzenie zgłosi stres termiczny, wycofaj trwające aktywności, aby zmniejszyć zużycie energii. Możesz na przykład zmniejszyć liczbę klatek na sekundę lub teselację wielokątów.
Najpierw zadeklaruj obiekt PowerManager i zainicjuj go w metodzie onCreate(). Dodaj do obiektu odbiornik 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, które mają zostać podjęte, gdy odbiornik wykryje zmianę stanu. Jeśli Twoja gra korzysta z języków C/C++, dodaj kod do poziomów stanu termicznego w onThermalStatusChanged(), aby wywoływać natywny kod gry za pomocą JNI lub używać 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 od dotknięcia do wyświetlenia
Gry, które renderują klatki tak szybko, jak to możliwe, tworzą scenariusz ograniczony przez procesor graficzny, w którym bufor klatek jest przepełniony. Procesor musi czekać na procesor graficzny, co powoduje zauważalne opóźnienie między działaniem gracza a jego odzwierciedleniem na ekranie.
Aby sprawdzić, czy możesz poprawić tempo klatek w swojej grze, wykonaj te czynności:
- Wygeneruj raport Systrace, który zawiera kategorie
gfxiinput. Te kategorie obejmują szczególnie przydatne pomiary do określania opóźnienia od dotknięcia do wyświetlenia. Sprawdź sekcję
SurfaceVieww raporcie Systrace. Przepełniony bufor powoduje, że liczba oczekujących rysowań bufora oscyluje między 1 a 2, jak pokazano na rysunku 5:Rysunek 5. Raport Systrace pokazujący przepełniony bufor, który okresowo jest zbyt pełny, aby przyjmować polecenia rysowania.
Aby złagodzić tę niespójność w tempie klatek, wykonaj czynności opisane w sekcjach poniżej:
Integrowanie interfejsu Android Frame Pacing API z grą
Interfejs Android Frame Pacing API pomaga wykonywać zamiany klatek i określać interwał zamiany, dzięki czemu gra utrzymuje bardziej stabilną liczbę klatek na sekundę.
Zmniejsz rozdzielczość zasobów w grze, które nie są elementami interfejsu
Wyświetlacze nowoczesnych urządzeń mobilnych zawierają znacznie więcej pikseli, niż odtwarzacz jest w stanie przetworzyć, więc można zmniejszyć próbkowanie tak, aby ciąg 5 lub nawet 10 pikseli zawierał jeden kolor. Ze względu na strukturę większości pamięci podręcznych wyświetlaczy najlepiej jest zmniejszyć rozdzielczość tylko w jednym wymiarze.
Nie zmniejszaj jednak rozdzielczości elementów interfejsu gry. Ważne jest, aby zachować grubość linii tych elementów, aby zapewnić wystarczająco duży docelowy element dotykowy dla wszystkich graczy.
Płynność renderowania
Gdy SurfaceFlinger przyłącza się do bufora wyświetlania, aby pokazać scenę w grze, aktywność procesora na chwilę wzrasta. Jeśli te skoki aktywności procesora występują nierównomiernie, w grze mogą pojawiać się zacięcia. Diagram na rysunku 6 przedstawia przyczynę tego zjawiska:
Jeśli klatka zacznie się rysować zbyt późno, nawet o kilka milisekund, może nie zmieścić się w następnym oknie wyświetlania. Klatka musi wtedy poczekać na następną synchronizację pionową, aby zostać wyświetlona (33 milisekundy podczas grania w 30 klatkach na sekundę), co powoduje zauważalne opóźnienie z perspektywy gracza.
Aby rozwiązać ten problem, użyj interfejsu Android Frame Pacing API, który zawsze wyświetla nową klatkę na fali VSync.
Stan pamięci
Podczas długotrwałego grania na urządzeniu mogą wystąpić błędy związane z brakiem pamięci.
W takiej sytuacji sprawdź aktywność procesora w raporcie Systrace i zobacz, jak często system wywołuje demona kswapd. Jeśli podczas działania gry występuje wiele wywołań, warto przyjrzeć się bliżej temu, jak gra zarządza pamięcią i ją czyści.
Więcej informacji znajdziesz w artykule Zarządzanie pamięcią.
Stan wątku
Podczas przeglądania typowych elementów raportu Systrace możesz wyświetlić czas, jaki dany wątek spędził w każdym możliwym stanie wątku. Aby to zrobić, wybierz wątek w raporcie, jak pokazano na rysunku 7:
Jak widać na rysunku 7, wątki gry mogą nie być w stanie „running” (działanie) lub „runnable” (możliwość działania) tak często, jak powinny. 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, może to oznaczać, że występuje konflikt blokad lub oczekiwanie na aktywność GPU.
- Jeśli wątek jest stale blokowany przez operacje wejścia/wyjścia, oznacza to, że odczytujesz zbyt dużo danych z dysku naraz lub gra jest niestabilna.
Dodatkowe materiały
Więcej informacji o zwiększaniu wydajności gry znajdziesz w tych materiałach:
Filmy
- Prezentacja Systrace for Games z konferencji Android Game Developer Summit 2018