System-Tracing konfigurieren

Sie können das System-Tracing so konfigurieren, dass ein CPU- und Thread-Profil Ihrer Anwendung über einen kurzen Zeitraum erfasst wird. Anschließend können Sie den Ausgabebericht aus einem System-Trace verwenden, um die Leistung Ihres Spiels zu verbessern.

Spielbasiertes System-Trace einrichten

Das Systrace-Tool ist auf zwei Arten verfügbar:

Systrace ist ein Low-Level-Tool, das:

  • Liefert Grundwahrheit: Systrace erfasst die Ausgabe direkt aus dem Kernel, sodass die erfassten Messwerte fast identisch mit denen sind, die von einer Reihe von Systemaufrufen erfasst werden.
  • Verbraucht nur wenige Ressourcen. Systrace führt zu einem sehr geringen Overhead auf dem Gerät, in der Regel unter 1%, da Daten in einen speicherinternen Zwischenspeicher gestreamt werden.

Optimale Einstellungen

Es ist wichtig, dem Tool eine Reihe von Argumenten zur Verfügung zu stellen:

  • Kategorien: Die besten Kategorien für ein spielbasiertes System-Trace sind: {sched, freq, idle, am, wm, gfx, view, sync, binder_driver, hal, dalvik}.
  • Puffergröße:In der Regel ist mit einer Puffergröße von 10 MB pro CPU-Kern ein Trace mit etwa 20 Sekunden möglich. Wenn ein Gerät beispielsweise zwei Quad-Core-CPUs hat (insgesamt 8 Kerne), beträgt ein geeigneter Wert, der an das systrace-Programm übergeben werden soll,80.000 KB (80 MB).

    Wenn Ihr Spiel viele Kontextwechsel ausführt, erhöhen Sie den Zwischenspeicher 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 kann Systrace diese benutzerdefinierten Ereignisse in den Ausgabebericht aufnehmen.

Wenn Sie das systrace-Befehlszeilenprogramm verwenden, erfassen Sie mit dem folgenden Befehl ein System-Trace, das Best Practices für Kategoriesatz, Puffergröße und benutzerdefinierte Ereignisse anwendet:

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 ein System-Trace zu erfassen, das Best Practices für Kategoriesatz, Puffergröße und benutzerdefinierte Ereignisse anwendet:

  1. Aktivieren Sie die Option Debug-fähige Trace-Anwendungen in Trace.

    Damit diese Einstellung verwendet werden kann, muss das Gerät über 256 MB oder 512 MB verfügen (je nachdem, ob die CPU 4 oder 8 Kerne hat) und jeder 64-MB-Arbeitsspeicher als zusammenhängender Block verfügbar sein muss.

  2. Wählen Sie Kategorien aus und aktivieren Sie die Kategorien in der folgenden Liste:

    • am: Aktivitätsmanager
    • binder_driver: Binder-Kerneltreiber
    • dalvik: Dalvik-VM
    • freq: CPU-Frequenz
    • gfx: Grafik
    • hal: Hardwaremodule
    • idle: CPU-Inaktiv
    • sched: CPU-Planung
    • sync: Synchronisierung
    • view: System aufrufen
    • wm: Fenstermanager
  3. Aktivieren Sie Datensatz-Tracing.

  4. Lade dein Spiel.

  5. Führen Sie in Ihrem Spiel die Interaktionen aus, die dem Gameplay entsprechen, dessen Geräteleistung Sie messen möchten.

  6. Sobald Sie ein unerwünschtes Verhalten in Ihrem Spiel feststellen, deaktivieren Sie die Systemverfolgung.

Sie haben die Leistungsstatistiken erfasst, die zur weiteren Analyse des Problems erforderlich sind.

Um Speicherplatz zu sparen, werden Dateien von System-Traces auf dem Gerät in einem komprimierten Trace-Format gespeichert (*.ctrace). Verwenden Sie das Befehlszeilenprogramm und fügen Sie die Option --from-file ein, um diese Datei beim Erstellen eines Berichts zu dekomprimieren:

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 Spielen für Mobilgeräte erläutert und es wird beschrieben, wie Sie diese Aspekte Ihres Spiels erkennen und verbessern können.

Ladegeschwindigkeit

Da die Spieler so schnell wie möglich in Ihr Spiel einsteigen möchten, ist es wichtig, die Ladezeiten Ihres Spiels so weit wie möglich zu verbessern. Die folgenden Maßnahmen helfen in der Regel, die Ladezeiten zu verbessern:

  • Führen Sie ein Lazy Loading durch. Wenn Sie dieselben Assets für aufeinanderfolgende Szenen oder Level in Ihrem Spiel verwenden, laden Sie sie nur einmal.
  • Reduzieren Sie die Größe der Assets. Auf diese Weise können Sie unkomprimierte Versionen dieser Assets mit dem APK Ihres Spiels bündeln.
  • Verwenden Sie eine datenträgereffiziente 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.
  • Multithread-Spiele für dein Spiel erstellen Weitere Informationen findest du im Abschnitt Konsistenz der Framerate.

Framerate-Konsistenz

Eines der wichtigsten Elemente des Spielerlebnisses ist eine konsistente Framerate. Um dieses Ziel leichter zu erreichen, befolgen Sie die in diesem Abschnitt beschriebenen Optimierungstechniken.

Multithreading

Wenn Sie für mehrere Plattformen entwickeln, ist es normal, alle Aktivitäten innerhalb Ihres Spiels in einem einzigen Thread zu speichern. Obwohl diese Ausführungsmethode in vielen Spiel-Engines einfach zu implementieren ist, ist sie bei einem Betrieb auf Android-Geräten alles andere als optimal. Daher werden Single-Threaded-Spiele häufig langsam geladen und haben keine konsistente Framerate.

Der in Abbildung 1 gezeigte Systrace zeigt ein typisches Verhalten für ein Spiel, das jeweils nur auf einer CPU ausgeführt wird:

Ein Diagramm der Threads
in einem System-Trace

Abbildung 1. Systrace-Bericht für ein Single-Threaded-Spiel

Sie können die Leistung Ihres Spiels verbessern, indem Sie es mit mehreren Threads verbinden. In der Regel sind zwei Threads das beste Modell:

  • Einen Spiele-Thread, der die Hauptmodule Ihres Spiels enthält und Renderingbefehle sendet
  • Ein Renderingthread, der Renderingbefehle empfängt und in Grafikbefehle übersetzt, die die GPU eines Geräts zum Anzeigen einer Szene verwenden kann.

Die Vulkan API erweitert dieses Modell aufgrund der Möglichkeit, zwei gängige Puffer parallel zu laden. Mit dieser Funktion können Sie mehrere Renderingthreads auf mehrere CPUs verteilen, um die Renderingzeit einer Szene weiter zu verbessern.

Sie können auch einige suchmaschinenspezifische Änderungen vornehmen, um die Multithreading-Leistung Ihres Spiels zu verbessern:

  • Wenn Sie Ihr Spiel mit der Unity-Spiele-Engine entwickeln, aktivieren Sie die Optionen Multithreaded Rendering und GPU Skinning.
  • Wenn Sie eine benutzerdefinierte Rendering-Engine verwenden, achten Sie darauf, dass die Pipeline für Renderingbefehl und Grafikbefehl richtig ausgerichtet sind. Andernfalls kann es bei der Darstellung Ihrer Spielszenen zu Verzögerungen kommen.

Nachdem Sie diese Änderungen übernommen haben, sollte Ihr Spiel mindestens zwei CPUs gleichzeitig beanspruchen, wie in Abbildung 2 dargestellt:

Ein Diagramm der Threads
in einem System-Trace

Abbildung 2. Systrace-Bericht für ein Multithread-Spiel

UI-Element wird geladen

Diagramm eines Frame-Stacks in einem System-Trace
Abbildung 3: Systrace-Bericht für ein Spiel, das Dutzende von UI-Elementen gleichzeitig rendert

Bei der Entwicklung eines Spiels mit vielen Funktionen ist es verlockend, dem Spieler viele verschiedene Optionen und Aktionen gleichzeitig zu zeigen. Um eine konsistente Framerate beizubehalten, solltest du jedoch die relativ kleine Größe mobiler Displays berücksichtigen und die Benutzeroberfläche so einfach wie möglich halten.

Der in Abbildung 3 gezeigte Systrace-Bericht ist ein Beispiel für einen UI-Frame, der versucht, zu viele Elemente im Verhältnis zu den Funktionen eines Mobilgeräts zu rendern.

Ein gutes Ziel ist es, die Aktualisierungszeit der Benutzeroberfläche auf zwei bis drei Millisekunden zu reduzieren. Derartige schnelle Aktualisierungen lassen sich durch ähnliche Optimierungen wie die folgenden erreichen:

  • Nur die Elemente auf dem Bildschirm aktualisieren, die verschoben wurden.
  • Begrenzen Sie die Anzahl der UI-Texturen und -Ebenen. Ziehen Sie in Betracht, Grafikaufrufe wie Shader und Texturen zu kombinieren, die dasselbe Material verwenden.
  • Elementanimationsvorgänge auf die GPU übertragen.
  • Führen Sie eine aggressivere Filterung des Sichtbereichs und der Verdeckung durch.
  • Führen Sie nach Möglichkeit Zeichenvorgänge mit der Vulkan API durch. Der Aufwand für den Zeichenaufruf bei Vulkan ist geringer.

Energieverbrauch

Auch nachdem Sie die im vorherigen Abschnitt beschriebenen Optimierungen vorgenommen haben, kann es vorkommen, dass sich die Framerate Ihres Spiels innerhalb der ersten 45 bis 50 Minuten des Spiels verschlechtert. Außerdem kann sich das Gerät mit der Zeit erwärmen und den Akkuverbrauch erhöhen.

In vielen Fällen hängen diese unerwünschte Überhitzung und der Stromverbrauch damit zusammen, wie die Arbeitslast deines Spiels auf die CPUs eines Geräts verteilt wird. Wenden Sie die Best Practices in den folgenden Abschnitten an, um die Stromverbrauchseffizienz Ihres Spiels zu steigern.

Speicherlastige Threads auf einer CPU behalten

Auf vielen Mobilgeräten befinden sich die L1-Caches auf bestimmten CPUs und die L2-Caches auf einer Gruppe von CPUs, die eine gemeinsame Uhr nutzen. Zur Maximierung der L1-Cache-Treffer ist es im Allgemeinen am besten, den Hauptthread des Spiels zusammen mit allen anderen speicherlastigen Threads auf einer einzelnen CPU laufen zu lassen.

Kurzfristige Arbeit auf CPUs mit geringerer Leistung auf später verschieben

Die meisten Spiele-Engines, einschließlich Unity, wissen, dass sie Worker-Thread-Vorgänge relativ zum Hauptthread Ihres Spiels auf eine andere CPU verschieben. Die Engine kennt jedoch die spezifische Architektur eines Geräts nicht und kann die Arbeitslast deines Spiels nicht so gut vorhersehen, wie du es kannst.

Die meisten System-on-a-Chip-Geräte haben mindestens zwei gemeinsame Takte, einen für die schnellen CPUs und einen für die langsamen CPUs des Geräts. Eine Konsequenz dieser Architektur besteht darin, dass, wenn eine schnelle CPU mit Höchstgeschwindigkeit arbeiten muss, alle anderen schnellen CPUs ebenfalls mit Höchstgeschwindigkeit arbeiten.

Der Beispielbericht in Abbildung 4 zeigt ein Spiel, das schnelle CPUs nutzt. Dieses hohe Aktivitätsniveau erzeugt jedoch schnell viel Energie und Wärme.

Ein Diagramm der Threads
in einem System-Trace

Abbildung 4: Systrace-Bericht mit einer suboptimalen Zuweisung von Threads zu den CPUs des Geräts

Um den gesamten Stromverbrauch zu reduzieren, empfiehlt es sich, dem Planer vorzuschlagen, die Arbeit mit kürzerer Zeit – wie z. B. das Laden von Audiodaten, das Ausführen von Worker-Threads und das Ausführen des Choreografen – auf die Gruppe langsamer CPUs auf einem Gerät zu übertragen. Übertrage so viel dieser Arbeit wie möglich auf die langsamen CPUs und behalte dabei die gewünschte Framerate bei.

Bei den meisten Geräten werden die langsamen CPUs vor den schnellen CPUs aufgeführt. Sie können jedoch nicht davon ausgehen, dass das SOC Ihres Geräts diese Reihenfolge verwendet. Führen Sie zur Überprüfung ähnliche Befehle wie in diesem Erkennungscode für CPU-Topologie auf GitHub aus.

Sobald Sie wissen, welche CPUs die langsamen CPUs auf Ihrem Gerät sind, können Sie Affinitäten für Ihre kurzen Threads deklarieren, die vom Planer des Geräts befolgt werden. Fügen Sie dazu in jedem Thread den folgenden Code 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);

Thermische Belastung

Wenn Geräte zu warm werden, können CPU und/oder GPU gedrosselt werden, was sich auf unerwartete Weise auf Spiele auswirken kann. Bei Spielen mit komplexer Grafik, intensiver Rechenleistung oder kontinuierlicher Netzwerkaktivität ist die Wahrscheinlichkeit höher, dass Probleme auftreten.

Verwenden Sie die Thermal API, um Temperaturänderungen des Geräts zu überwachen und Maßnahmen zu ergreifen, um den Stromverbrauch zu senken und die Gerätetemperatur zu senken. Wenn das Gerät eine thermische Belastung meldet, brechen Sie laufende Aktivitäten ab, um den Stromverbrauch zu reduzieren. Reduzieren Sie beispielsweise die Framerate oder die Polygon-Tesellation.

Deklarieren Sie zuerst das Objekt PowerManager und initialisieren Sie es in der Methode onCreate(). Fügen Sie dem Objekt einen Listener für den Temperaturstatus 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 Code zu den Temperaturstatusstufen in onThermalStatusChanged() hinzu, um Ihren nativen Spielcode mit JNI 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;
        }
    }
};

Touch-to-Display-Latenz

Spiele, die Frames so schnell wie möglich rendern, erzeugen ein GPU-gebundenes Szenario, bei dem der Frame-Zwischenspeicher überfüllt wird. Die CPU muss auf die GPU warten. Dadurch entsteht eine deutliche Verzögerung zwischen der Eingabe eines Spielers und der Eingabe, die auf dem Bildschirm wirksam wird.

So ermitteln Sie, ob Sie das Frame-Taktung Ihres Spiels verbessern können:

  1. Erstellen Sie einen Systrace-Bericht, der die Kategorien gfx und input enthält. Diese Kategorien umfassen besonders nützliche Messwerte zur Bestimmung der Touch-to-Display-Latenz.
  2. Prüfen Sie den Abschnitt SurfaceView eines Systrace-Berichts. Ein überfüllter Zwischenspeicher führt dazu, dass die Anzahl der ausstehenden Puffer zwischen 1 und 2 schwankt, wie in Abbildung 5 dargestellt:

    Diagramm einer Pufferwarteschlange in einem System-Trace

    Abbildung 5: Systrace-Bericht mit einem übermäßig gefüllten Zwischenspeicher, der regelmäßig zu voll ist, um Zeichenbefehle zu akzeptieren

Führen Sie die in den folgenden Abschnitten beschriebenen Aktionen aus, um diese Inkonsistenz bei der Frame-Taktung zu minimieren:

Android Frame Pacing API in Ihr Spiel integrieren

Mit der Android Frame Pacing API können Sie Frames austauschen und ein Auslagerungsintervall so definieren, dass Ihr Spiel eine konsistente Framerate beibehält.

Auflösung der Nicht-UI-Assets des Spiels verringern

Die Displays auf modernen Mobilgeräten enthalten weitaus mehr Pixel, als ein Player verarbeiten kann. Daher ist ein Downsampling in Ordnung, wenn ein Lauf von 5 oder sogar 10 Pixeln alle eine Farbe enthält. Angesichts der Struktur der meisten Anzeige-Caches empfiehlt es sich, die Auflösung nur auf eine Dimension zu reduzieren.

Verringern Sie jedoch nicht die Auflösung der UI-Elemente Ihres Spiels. Es ist wichtig, die Linienstärke dieser Elemente beizubehalten, damit das Berührungsziel für alle Spieler groß genug ist.

Flüssiges Rendering

Wenn sich SurfaceFlinger an einen Anzeigepuffer anschließt, um eine Szene in Ihrem Spiel anzuzeigen, erhöht sich die CPU-Aktivität kurzzeitig. Wenn diese Spitzen in der CPU-Aktivität ungleichmäßig auftreten, kann es sein, dass Ihr Spiel ruckelt. Das Diagramm in Abbildung 6 zeigt den Grund für dieses Problem:

Diagramm mit Frames, für die ein
VSync-Fenster fehlt, weil zu spät begonnen hat,

Abbildung 6: Systrace-Bericht, der zeigt, wie ein Frame eine VSync-Verbindung übersehen kann

Wenn ein Frame zu spät zu zeichnen beginnt, selbst um einige Millisekunden, wird möglicherweise das nächste Anzeigefenster verpasst. Der Frame muss dann warten, bis die nächste VSync-Version angezeigt wird (33 Millisekunden, wenn ein Spiel mit 30 fps ausgeführt wird). Dies führt aus Sicht des Spielers zu einer deutlichen Verzögerung.

Verwenden Sie zur Behebung dieses Problems die Android Frame Pacing API, die immer einen neuen Frame auf einer VSync-Wavefront darstellt.

Speicherstatus

Wenn Sie Ihr Spiel über einen längeren Zeitraum hinweg ausführen, kann es zu Fehlern aufgrund unzureichenden Arbeitsspeichers auf dem Gerät kommen.

Prüfen Sie in diesem Fall die CPU-Aktivität in einem Systrace-Bericht, um festzustellen, wie oft das System Aufrufe an den Daemon kswapd sendet. Wenn es während der Ausführung Ihres Spiels viele Aufrufe gibt, sollten Sie sich genauer ansehen, wie Ihr Spiel Arbeitsspeicher verwaltet und bereinigt.

Weitere Informationen finden Sie unter Arbeitsspeicher in Spielen effektiv verwalten.

Threadstatus

Wenn Sie durch die typischen Elemente eines Systrace-Berichts navigieren, können Sie sehen, wie lange ein bestimmter Thread in jedem möglichen Thread-Status verbracht hat, indem Sie den Thread im Bericht auswählen, wie in Abbildung 7 dargestellt:

Das Diagramm eines
Systrace-Berichts

Abbildung 7: Systrace-Bericht, der zeigt, wie die Auswahl eines Threads dazu führt, dass im Bericht eine Statuszusammenfassung für diesen Thread angezeigt wird

Wie Abbildung 7 zeigt, können Sie feststellen, dass sich die Threads Ihres Spiels möglicherweise nicht so oft im Status „Aktiv“ oder „Laufbar“ befinden. Die folgende Liste enthält einige häufige Gründe dafür, dass ein bestimmter Thread regelmäßig in einen ungewöhnlichen Zustand übergeht:

  • Wenn ein Thread für einen längeren Zeitraum im Ruhezustand ist, liegt möglicherweise ein Sperrkonflikt vor oder das Warten auf GPU-Aktivität.
  • Wenn ein Thread ständig auf E/A blockiert ist, lesen Sie entweder zu viele Daten von der Festplatte aus oder Ihr Spiel ist durcheinander.

Weitere Informationen

Weitere Informationen zur Verbesserung der Leistung Ihres Spiels finden Sie in den folgenden zusätzlichen Ressourcen:

Videos