Sie können System-Tracing konfigurieren, um ein CPU- und Thread-Profil Ihrer App über einen kurzen Zeitraum zu erfassen. Anschließend können Sie den Ausgabereport aus einem System-Trace verwenden, um die Leistung Ihres Spiels zu verbessern.
System-Trace für Spiele einrichten
Das Systrace-Tool ist auf zwei Arten verfügbar:
Systrace ist ein Low-Level-Tool, das folgende Funktionen bietet:
- Liefert Ground Truth: Systrace erfasst die Ausgabe direkt vom Kernel. Die erfassten Messwerte sind daher nahezu identisch mit denen, die von einer Reihe von Systemaufrufen gemeldet würden.
- Verbraucht nur wenige Ressourcen: Systrace verursacht auf dem Gerät einen sehr geringen Overhead, in der Regel weniger als 1%, da Daten in einen In-Memory-Puffer gestreamt werden.
Optimale Einstellungen
Es ist wichtig, dem Tool eine angemessene Anzahl von Argumenten zu geben:
- Kategorien:Die besten Kategorien für einen auf Spielen basierenden System-Trace sind: {
sched,freq,idle,am,wm,gfx,view,sync,binder_driver,hal,dalvik}. Puffergröße:Als Faustregel gilt, dass eine Puffergröße von 10 MB pro CPU-Kern einen etwa 20 Sekunden langen Trace ermöglicht. Wenn ein Gerät beispielsweise zwei Quad-Core-CPUs (insgesamt 8 Kerne) hat, ist ein geeigneter Wert für das
systrace-Programm 80.000 KB (80 MB).Wenn Ihr Spiel viele Kontextwechsel durchführt, erhöhen Sie den Puffer auf 15 MB pro CPU-Kern.
Benutzerdefinierte Ereignisse:Wenn Sie benutzerdefinierte Ereignisse definieren, die in Ihrem Spiel erfasst werden sollen, aktivieren Sie das Flag
-a. Dadurch können diese benutzerdefinierten Ereignisse in den Ausgabereport von Systrace aufgenommen werden.
Wenn Sie das systrace-Befehlszeilenprogramm verwenden, können Sie mit dem folgenden Befehl einen System-Trace erfassen, der Best Practices für Kategoriegruppe, Puffergröße und benutzerdefinierte Ereignisse berücksichtigt:
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
Wenn Sie die Systrace-System-App auf einem Gerät verwenden, führen Sie die folgenden Schritte aus, um einen System-Trace zu erfassen, der Best Practices für die Kategoriegruppe, die Puffergröße und benutzerdefinierte Ereignisse entspricht:
Aktivieren Sie die Option Debugfähige Anwendungen verfolgen.
Damit diese Einstellung verwendet werden kann, muss das Gerät 256 MB oder 512 MB verfügbar haben (je nachdem, ob die CPU 4 oder 8 Kerne hat). Außerdem muss jeder 64-MB-Arbeitsspeicherblock als zusammenhängender Block verfügbar sein.
Wählen Sie Kategorien aus und aktivieren Sie die Kategorien in der folgenden Liste:
am: Activity Managerbinder_driver: Binder-Kernel-Treiberdalvik: Dalvik VMfreq: CPU-Frequenzgfx: Grafikenhal: Hardware-Moduleidle: CPU Idlesched: CPU-Planungsync: Synchronisierungview: System ansehenwm: Window Manager
Aktivieren Sie Tracing aufzeichnen.
Laden Sie Ihr Spiel.
Führen Sie die Interaktionen in Ihrem Spiel aus, die dem Gameplay entsprechen, dessen Geräteleistung Sie messen möchten.
Deaktiviere das System-Tracing kurz nachdem du unerwünschtes Verhalten in deinem Spiel festgestellt hast.
Sie haben die Leistungsstatistiken erfasst, die für die weitere Analyse des Problems erforderlich sind.
Um Speicherplatz zu sparen, werden Dateien bei System-Traces auf dem Gerät in einem komprimierten Trace-Format (*.ctrace) gespeichert. Wenn Sie diese Datei beim Generieren eines Berichts dekomprimieren möchten, verwenden Sie das Befehlszeilenprogramm und fügen Sie die Option --from-file ein:
python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \ -o my_systrace_report.html
Bestimmte Leistungsbereiche verbessern
In diesem Abschnitt werden einige häufige Leistungsprobleme bei mobilen Spielen behandelt und es wird beschrieben, wie Sie diese Aspekte Ihres Spiels identifizieren und verbessern können.
Ladegeschwindigkeit
Spieler möchten so schnell wie möglich mit dem Spielen beginnen. Daher ist es wichtig, die Ladezeiten Ihres Spiels so weit wie möglich zu verkürzen. Die folgenden Maßnahmen können die Ladezeiten in der Regel verbessern:
- Lazy Loading durchführen: Wenn Sie dieselben Assets in aufeinanderfolgenden Szenen oder Levels Ihres Spiels verwenden, laden Sie sie nur einmal.
- Reduzieren Sie die Größe Ihrer Assets. So können Sie unkomprimierte Versionen dieser Assets mit der APK Ihres Spiels bündeln.
- Verwenden Sie eine speichereffiziente Komprimierungsmethode. Ein Beispiel für eine solche Methode ist zlib.
- Verwenden Sie IL2CPP anstelle von Mono. (Gilt nur, wenn Sie Unity verwenden.) IL2CPP bietet eine bessere Ausführungsleistung für Ihre C#-Skripts.
- Multithreading für Ihr Spiel aktivieren Weitere Informationen finden Sie im Abschnitt Framerate-Konsistenz.
Gleichbleibende Framerate
Eines der wichtigsten Elemente für ein gutes Spielerlebnis ist eine konsistente Framerate. Um dieses Ziel leichter zu erreichen, sollten Sie die in diesem Abschnitt beschriebenen Optimierungstechniken anwenden.
Multithreading
Bei der Entwicklung für mehrere Plattformen ist es naheliegend, alle Aktivitäten in Ihrem Spiel in einem einzigen Thread zu platzieren. Diese Ausführungsmethode ist zwar in vielen Spiele-Engines einfach zu implementieren, aber bei der Ausführung auf Android-Geräten ist sie alles andere als optimal. Daher werden Single-Thread-Spiele oft langsam geladen und haben keine gleichmäßige Framerate.
Das in Abbildung 1 dargestellte Systrace zeigt ein Verhalten, das typisch für ein Spiel ist, das jeweils nur auf einer CPU ausgeführt wird:
Um die Leistung Ihres Spiels zu verbessern, sollten Sie es für die Verwendung mehrerer Threads optimieren. In der Regel ist es am besten, zwei Threads zu verwenden:
- Ein Game-Thread, der die Hauptmodule des Spiels enthält und Rendering-Befehle sendet.
- Ein Render-Thread, der Render-Befehle empfängt und in Grafikbefehle übersetzt, die die GPU eines Geräts zum Darstellen einer Szene verwenden kann.
Die Vulkan API erweitert dieses Modell, da sie zwei gemeinsame Puffer parallel übertragen kann. Mit dieser Funktion können Sie mehrere Rendering-Threads auf mehrere CPUs verteilen und so die Rendering-Zeit einer Szene weiter verkürzen.
Sie können auch einige enginespezifische Änderungen vornehmen, um die Multithreading-Leistung Ihres Spiels zu verbessern:
- Wenn Sie Ihr Spiel mit der Unity-Spiel-Engine entwickeln, aktivieren Sie die Optionen Multithreaded Rendering und GPU Skinning.
- Wenn Sie eine benutzerdefinierte Rendering-Engine verwenden, müssen Sie darauf achten, dass die Pipeline für Renderbefehle und die Pipeline für Grafikbefehle richtig ausgerichtet sind. Andernfalls kann es zu Verzögerungen bei der Darstellung der Szenen Ihres Spiels kommen.
Nachdem Sie diese Änderungen vorgenommen haben, sollte Ihr Spiel mindestens zwei CPUs gleichzeitig belegen, wie in Abbildung 2 dargestellt:
Laden von UI-Elementen
Wenn Sie ein funktionsreiches Spiel entwickeln, ist es verlockend, dem Spieler viele verschiedene Optionen und Aktionen gleichzeitig zu präsentieren. Um eine gleichbleibende Framerate zu erzielen, ist es jedoch wichtig, die relativ geringe Größe von mobilen Displays zu berücksichtigen und die Benutzeroberfläche so einfach wie möglich zu halten.
Der in Abbildung 3 dargestellte Systrace-Bericht ist ein Beispiel für einen UI-Frame, in dem versucht wird, zu viele Elemente im Verhältnis zu den Möglichkeiten eines Mobilgeräts zu rendern.
Ein gutes Ziel ist es, die Zeit für UI-Updates auf 2 bis 3 Millisekunden zu reduzieren. Sie können solche schnellen Updates durch Optimierungen wie die folgenden erreichen:
- Nur die Elemente auf dem Bildschirm aktualisieren, die sich bewegt haben.
- Begrenzen Sie die Anzahl der UI-Texturen und ‑Ebenen. Erwägen Sie, Grafikanrufe wie Shader und Texturen, die dasselbe Material verwenden, zu kombinieren.
- Verschiebt Elementanimationsvorgänge auf die GPU.
- Aggressiveres Frustum- und Okklusions-Culling durchführen
- Führen Sie Zeichenvorgänge nach Möglichkeit mit der Vulkan API aus. Der Draw-Call-Overhead ist bei Vulkan geringer.
Stromverbrauch
Auch nach den im vorherigen Abschnitt beschriebenen Optimierungen kann es sein, dass die Framerate Ihres Spiels innerhalb der ersten 45 bis 50 Minuten des Spiels abnimmt. Außerdem kann es mit der Zeit zu einer Erwärmung des Geräts kommen und der Akku kann schneller entladen werden.
In vielen Fällen hängt diese unerwünschte Kombination aus Wärmeentwicklung und Stromverbrauch damit zusammen, wie die Arbeitslast Ihres Spiels auf die CPUs eines Geräts verteilt wird. Wenn du den Stromverbrauch deines Spiels optimieren möchtest, solltest du die Best Practices in den folgenden Abschnitten anwenden.
Arbeitsspeicherintensive Threads auf einer CPU ausführen
Auf vielen Mobilgeräten befinden sich die L1-Caches auf bestimmten CPUs und die L2-Caches auf den CPUs, die sich eine Taktung teilen. Um die Anzahl der L1-Cache-Treffer zu maximieren, ist es in der Regel am besten, den Hauptthread Ihres Spiels sowie alle anderen speicherintensiven Threads auf einer einzelnen CPU auszuführen.
Kurze Aufgaben auf CPUs mit geringerer Leistung verschieben
Die meisten Spiele-Engines, einschließlich Unity, wissen, dass Arbeitsthread-Vorgänge auf eine andere CPU als den Hauptthread Ihres Spiels verschoben werden müssen. Die Engine kennt jedoch die spezifische Architektur eines Geräts nicht und kann die Arbeitslast Ihres Spiels nicht so gut vorhersagen wie Sie.
Die meisten System-on-a-Chip-Geräte haben mindestens zwei gemeinsame Taktgeber, einen für die schnellen CPUs des Geräts und einen für die langsamen CPUs des Geräts. Eine Folge dieser Architektur ist, dass, wenn eine schnelle CPU mit maximaler Geschwindigkeit arbeiten muss, alle anderen schnellen CPUs ebenfalls mit maximaler Geschwindigkeit arbeiten.
Der Beispielbericht in Abbildung 4 zeigt ein Spiel, das schnelle CPUs nutzt. Diese hohe Aktivität erzeugt jedoch schnell viel Strom und Wärme.
Um den Gesamtstromverbrauch zu senken, ist es am besten, dem Scheduler vorzuschlagen, dass Arbeiten mit kürzerer Dauer, z. B. das Laden von Audio, das Ausführen von Arbeitsthreads und die Ausführung des Choreografen, auf die langsamen CPUs eines Geräts verschoben werden. Lagern Sie so viel Arbeit wie möglich auf die langsamen CPUs aus, ohne die gewünschte Framerate zu beeinträchtigen.
Auf den meisten Geräten werden die langsamen CPUs vor den schnellen CPUs aufgeführt. Sie können jedoch nicht davon ausgehen, dass der SOC Ihres Geräts diese Reihenfolge verwendet. Führen Sie dazu Befehle aus, die denen im Code zur Ermittlung der CPU-Topologie auf GitHub ähneln.
Nachdem Sie herausgefunden haben, welche CPUs die langsamen CPUs auf Ihrem Gerät sind, können Sie Affinitäten für Ihre Threads mit kurzer Ausführungszeit deklarieren, die vom Scheduler des Geräts berücksichtigt werden. Fügen Sie dazu den folgenden Code in jeden Thread ein:
#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);
Thermischer Stress
Wenn Geräte zu warm werden, kann es sein, dass die CPU und/oder GPU gedrosselt werden. Das kann sich auf unerwartete Weise auf Spiele auswirken. Bei Spielen mit komplexen Grafiken, rechenintensiven Vorgängen oder anhaltenden Netzwerkaktivitäten treten eher Probleme auf.
Mit der Thermal API können Sie Temperaturänderungen auf dem Gerät überwachen und Maßnahmen ergreifen, um den Stromverbrauch und die Gerätetemperatur niedrig zu halten. Wenn das Gerät eine thermische Belastung meldet, sollten Sie laufende Aktivitäten beenden, um den Stromverbrauch zu senken. Sie können beispielsweise die Framerate oder die Polygon-Tessellierung verringern.
Deklarieren Sie zuerst das PowerManager-Objekt und initialisieren Sie es in der Methode onCreate(). Fügen Sie dem Objekt einen Listener für den thermischen Status hinzu.
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); } }
Definieren Sie die Aktionen, die ausgeführt werden sollen, wenn der Listener eine Statusänderung erkennt. Wenn Ihr Spiel C/C++ verwendet, fügen Sie den thermischen Statusstufen in onThermalStatusChanged() Code hinzu, um mit JNI in Ihren nativen Spielcode aufzurufen oder die native Thermal API zu verwenden.
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; } } };
Latenz zwischen Berührung und Anzeige
Bei Spielen, bei denen Frames so schnell wie möglich gerendert werden, entsteht ein GPU-gebundenes Szenario, in dem der Framebuffer überlastet wird. Die CPU muss auf die GPU warten, was zu einer spürbaren Verzögerung zwischen der Eingabe eines Spielers und der Ausführung der Eingabe auf dem Bildschirm führt.
So ermitteln Sie, ob Sie das Frame-Pacing Ihres Spiels verbessern können:
- Generieren Sie einen Systrace-Bericht, der die Kategorien
gfxundinputenthält. Diese Kategorien umfassen besonders nützliche Messungen zur Bestimmung der Touch-to-Display-Latenz. Sehen Sie sich den Bereich
SurfaceVieweines Systrace-Berichts an. Bei einem überfüllten Puffer schwankt die Anzahl der ausstehenden Pufferzeichnungen zwischen 1 und 2, wie in Abbildung 5 dargestellt:Abbildung 5: Systrace-Bericht mit einem überfüllten Puffer, der regelmäßig zu voll ist, um Zeichenbefehle anzunehmen
Um diese Inkonsistenz beim Frame-Pacing zu beheben, führen Sie die Aktionen aus, die in den folgenden Abschnitten beschrieben werden:
Android Frame Pacing API in Ihr Spiel einbinden
Mit der Android Frame Pacing API können Sie Frame-Swaps durchführen und ein Swap-Intervall definieren, damit Ihr Spiel eine gleichmäßigere Framerate beibehält.
Auflösung der Nicht-UI-Assets Ihres Spiels reduzieren
Die Displays moderner Mobilgeräte enthalten viel mehr Pixel, als ein Player verarbeiten kann. Daher ist es in Ordnung, die Auflösung so zu verringern, dass eine Gruppe von 5 oder sogar 10 Pixeln jeweils eine Farbe enthält. Angesichts der Struktur der meisten Display-Caches ist es am besten, die Auflösung nur entlang einer Dimension zu verringern.
Reduzieren Sie jedoch nicht die Auflösung der UI-Elemente Ihres Spiels. Es ist wichtig, die Linienstärke dieser Elemente beizubehalten, um einen ausreichend großen Berührungszielbereich für alle Spieler zu gewährleisten.
Rendering-Glätte
Wenn SurfaceFlinger einen Displaypuffer erfasst, um eine Szene in Ihrem Spiel anzuzeigen, steigt die CPU-Aktivität vorübergehend an. Wenn diese Spitzen in der CPU-Aktivität ungleichmäßig auftreten, kann es zu Rucklern im Spiel kommen. Das Diagramm in Abbildung 6 veranschaulicht, warum dies geschieht:
Wenn ein Frame auch nur wenige Millisekunden zu spät gerendert wird, kann es sein, dass er das nächste Display-Fenster verpasst. Das Frame muss dann bis zum nächsten Vsync warten, um angezeigt zu werden (33 Millisekunden bei einem Spiel mit 30 FPS). Dies führt aus Sicht des Spielers zu einer spürbaren Verzögerung.
Um dieses Problem zu beheben, verwenden Sie die Android Frame Pacing API, die immer einen neuen Frame auf einer VSync-Wellenfront präsentiert.
Speicherstatus
Wenn Ihr Spiel über einen längeren Zeitraum ausgeführt wird, kann es auf dem Gerät zu Fehlern aufgrund von Arbeitsspeichermangel kommen.
Prüfen Sie in diesem Fall die CPU-Aktivität in einem Systrace-Bericht und sehen Sie nach, wie oft das System Aufrufe an den kswapd-Daemon sendet. Wenn während der Ausführung Ihres Spiels viele Aufrufe erfolgen, sollten Sie sich genauer ansehen, wie Ihr Spiel den Speicher verwaltet und bereinigt.
Weitere Informationen finden Sie unter Speicherverwaltung.
Thread-Status
Wenn Sie durch die typischen Elemente eines Systrace-Berichts navigieren, können Sie die Zeit sehen, die ein bestimmter Thread in jedem möglichen Threadstatus verbracht hat. Wählen Sie dazu den Thread im Bericht aus, wie in Abbildung 7 dargestellt:
Wie in Abbildung 7 zu sehen ist, befinden sich die Threads Ihres Spiels möglicherweise nicht so oft im Status „running“ (wird ausgeführt) oder „runnable“ (kann ausgeführt werden) wie sie sollten. In der folgenden Liste sind einige häufige Gründe dafür aufgeführt, warum ein bestimmter Thread möglicherweise regelmäßig in einen ungewöhnlichen Zustand wechselt:
- Wenn ein Thread über einen längeren Zeitraum inaktiv ist, kann dies entweder an einer Sperrenkonkurrenz oder an einem Wartezustand für GPU-Aktivitäten liegen.
- Wenn ein Thread ständig durch E/A blockiert wird, lesen Sie entweder zu viele Daten gleichzeitig von der Festplatte oder Ihr Spiel ist überlastet.
Zusätzliche Ressourcen
Weitere Informationen zur Leistungsoptimierung Ihres Spiels finden Sie in den folgenden zusätzlichen Ressourcen:
Videos
- Systrace for Games-Präsentation vom Android Game Developer Summit 2018