Wie ich die Ladezeiten von GTA Online um 70% verkürze

Bild


GTA Online ist bekannt für seine langsame Ladegeschwindigkeit. Nachdem ich das Spiel kürzlich gestartet hatte, um neue Schlachtzugsmissionen abzuschließen, war ich schockiert , dass es genauso langsam geladen wurde wie vor sieben Jahren, als es veröffentlicht wurde.



Die Zeit ist gekommen. Finden Sie vorerst die Gründe dafür heraus.



Nachrichtendienst



Zunächst wollte ich überprüfen, ob jemand dieses Problem bereits gelöst hat. Die meisten der gefundenen Ergebnisse bestanden aus anekdotischen Daten darüber, wie schwierig das Spiel war , dass es so lange geladen werden musste, Geschichten über die Lahmheit der p2p-Netzwerkarchitektur (und das ist wahr), komplexen Möglichkeiten zum Laden in den Story-Modus und dann in eine einzelne Sitzung und paarweise Mods, mit denen Sie das Eröffnungsvideo mit dem R * -Logo überspringen können. Einige Quellen berichteten, dass Sie, wenn alle diese Methoden zusammen verwendet werden, bis zu 10-30 Sekunden sparen können!



Inzwischen auf meinem PC ...



Benchmark



: 1 10

-: 6

, R* ( social club ).



, : AMD FX-8350

SSD: KINGSTON SA400S37120G

: 2 Kingston 8192 (DDR3-1337) 99U5471

GPU: NVIDIA GeForce GTX 1070


Ich weiß, dass mein Auto veraltet ist, aber warum zum Teufel wird der Online-Modus sechsmal langsamer geladen? Ich konnte keine Unterschiede in der Upload-Technik „Geschichte zuerst, dann online“ feststellen, wie es andere vor mir getan haben . Aber selbst wenn es funktionieren würde, würden die Ergebnisse innerhalb der Fehlergrenze liegen.



ich bin nicht alleine



Laut dieser Umfrage ist das Problem so weit verbreitet, dass es über 80% der Spielerbasis leicht wütend macht. Leute von R *, tatsächlich sind sieben Jahre vergangen!





18,8% der Spieler haben die leistungsstärksten Computer oder Konsolen, 81,2% sind ziemlich traurig, 35,1% sind ziemlich traurig.



Nachdem ich nach 20% der Glücklichen gesucht hatte, deren Laden weniger als drei Minuten dauert, fand ich eine Reihe von Benchmarks mit leistungsstarken Gaming-PCs und einer Online-Ladezeit von etwa zwei Minuten. Um eine Ladezeit von 2 Minuten bekommen würde ich töten Hacking alles! Es sieht so aus, als ob die Ladezeit von der Hardware abhängt, aber die Zahlen summieren sich irgendwie nicht ...



Wie kommt es, dass die Leute, die diese Benchmarks durchführen, noch ungefähr eine Minute brauchen, um den Story-Modus zu laden? (Der Benchmark mit M.2 berücksichtigt übrigens nicht die Anzeigezeit der Logos zu Beginn.) Außerdem dauert das Laden vom Story-Modus in den Online-Modus nur eine Minute, während meiner mehr als fünf Minuten dauert. Ich weiß, dass ihre Technik viel besser ist als meine, aber definitiv nicht fünfmal.



Sehr genaue Messungen



Mit leistungsstarken Tools wie dem Task-Manager ausgestattet , begann ich zu untersuchen, welche Ressourcen der Engpass sein könnten.





Innerhalb einer Minute werden die Standardressourcen des Story-Modus geladen. Danach lädt das Spiel den Prozessor länger als vier Minuten.



Nach einer Minute des Ladens der gemeinsam genutzten Ressourcen, die sowohl im Story- als auch im Online-Modus verwendet werden (ein Indikator, der fast den Benchmarks leistungsfähiger PCs entspricht), beschließt GTA, einen Kern meines Computers vier Minuten lang so weit wie möglich zu laden und nichts anderes zu tun.



Festplattenzugriff? Er ist nicht da! Netzwerknutzung? Es gibt nicht viele, aber nach wenigen Sekunden sinkt der Verkehr auf fast Null (mit Ausnahme des Ladens rotierender Banner mit Informationen). GPU-Nutzung? Durch Nullen. Speichernutzung? Perfekt flache Grafik ...



Was ist los, das Spiel baut eine Krypto ab oder so? Fängt an, nach Code zu riechen. Sehr schlechter Code .



Einen Stream begrenzen



Obwohl meine alte AMD-CPU acht Kerne hat und immer noch eine gute Leistung erbringen kann, wurde sie früher gebaut. Damals lag die Single-Threaded-Leistung von AMD-Prozessoren weit hinter der von Intel-Prozessoren. Dies erklärt möglicherweise nicht den Unterschied in den Ladezeiten, sollte aber das Wichtigste erklären.



Das Seltsame ist, dass das Spiel nur die CPU verwendet. Ich hatte eine große Menge an Ressourcen erwartet, die von der Festplatte geladen wurden, oder eine Reihe von Netzwerkanforderungen, um eine Sitzung im P2P-Netzwerk zu erstellen. Aber das? Dies ist höchstwahrscheinlich ein Fehler.



Profilerstellung



Profiler sind eine großartige Möglichkeit, CPU-Engpässe zu finden. Es gibt nur ein Problem: Die meisten von ihnen verwenden Quellcode, um ein perfektes Bild davon zu erhalten, was dabei passiert. Und ich habe es nicht. Aber ich brauche auch keine mikrosekundengenauen Messwerte - der Engpass dauert vier Minuten.



Stack-Sampling kommt auf den Plan: Nur so können Closed-Source-Anwendungen untersucht werden. Wir führen einen Stack-Dump des laufenden Prozesses und der Position des aktuellen Befehlszeigers durch, um in bestimmten Intervallen einen Aufrufbaum zu erstellen. Dann addieren wir sie, um Statistiken darüber zu erhalten, was passiert. Es gibt nur einen Profiler, den ich kenne (ich könnte mich hier irren), der dies unter Windows tun kann. Und es wurde seit über zehn Jahren nicht mehr aktualisiert. Das ist Luke Stackwalker! Lassen Sie jemanden seine Liebe zu diesem Projekt geben.





Die Schuldigen Nr. 1 und Nr. 2.



Luke gruppiert normalerweise dieselben Funktionen, aber da ich keine Debug-Symbole habe, muss ich die nächsten Adressen mit meinen Augen durchsehen, um zu verstehen, dass sie sich an derselben Stelle befinden. Und was sehen wir? Nicht einer, sondern zwei Engpässe!



In den Kaninchenbau



Nachdem ich mir von einem Freund eine absolut legitime Kopie des beliebten Disassemblers ausgeliehen hatte (nein, ich kann es mir nicht leisten ... ich muss irgendwie Ghidra lernen ), begann ich, die GTA zu zerlegen.





Es scheint alles völlig falsch. Viele High-Budget-Spiele verfügen über einen integrierten Reverse Engineering-Schutz zum Schutz vor Piraten, Betrügern und Moddern (ganz zu schweigen davon, dass er sie jemals aufhält).



Es sieht so aus, als würde hier eine Art Verschleierung / Verschlüsselung verwendet, wodurch die meisten Befehle durch Kauderwelsch ersetzt werden. Aber keine Sorge, wir müssen nur den Speicher des Spiels löschen, wenn wir den Teil ausführen, den wir lernen möchten. Vor ihrer Ausführung müssen die Befehle auf die eine oder andere Weise deobfusciert werden. Ich hatte Process Dump in der Nähe , aber es gibt viele andere Tools, die ähnliche Dinge tun können.



Problem # 1: Ist das ... strlen ?!



Das Zerlegen des jetzt weniger verschleierten Dumps zeigt, dass eine der Adressen ein Etikett hat, das aus dem Nichts stammt! Ist es strlen



? Der nächste im Aufrufstapel ist als markiert vscan_fn



, wonach die Beschriftungen ausgehen, aber ich bin mir ziemlich sicher, dass dies der Fall ist sscanf



.





Sie kratzen etwas. Aber was? Das Parsen des zerlegten Codes würde unendlich dauern, daher habe ich beschlossen, einige Beispiele aus dem laufenden Prozess mit x64dbg zu sichern . Nach einigem Debuggen stellte ich fest, dass dies ... JSON ist! Sie analysieren JSON. Satte 10 Megabyte JSON-Daten mit fast 63.000 Elementen .



...,
{
    "key": "WP_WCT_TINT_21_t2_v9_n2",
    "price": 45000,
    "statName": "CHAR_KIT_FM_PURCHASE20",
    "storageType": "BITFIELD",
    "bitShift": 7,
    "bitSize": 1,
    "category": ["CATEGORY_WEAPON_MOD"]
},
...
      
      





Was ist das? Laut einigen Quellen sieht dies wie ein "Online-Shop-Verzeichnis" aus. Ich gehe davon aus, dass sie eine Liste aller möglichen Artikel und Upgrades enthalten, die in GTA Online erworben werden können.



Klarstellung: Ich glaube, dies sind Gegenstände, die mit Geld im Spiel gekauft wurden und nicht direkt mit Mikrotransaktionen zusammenhängen .



Aber 10 Megabyte sind eine Kleinigkeit! Und die Nutzung sscanf



mag nicht optimal sein, aber es kann nicht so schlecht sein? Gut ...





10 Megabyte C-Strings im Speicher. 1. Bewegen Sie den Zeiger einige Bytes auf den nächsten Wert. 2. Wir rufen an sscanf(p, "%d", ...)



. 3. Wir lesen jedes Zeichen in 10 Megabyte, während wir jeden kleinen Wert (!?) Lesen. 4. Geben Sie den gescannten Wert zurück.




Ja, es wird eine lange Zeit dauern ... Um ehrlich zu sein, ich hatte keine Ahnung , was die meisten Implementierungen sscanf



aufrufen strlen



, so dass ich nicht die Entwickler, der das geschrieben hat Schuld kann. Ich würde vorschlagen, dass diese Daten einfach byteweise gescannt werden und die Verarbeitung möglicherweise bei endet NULL



.



Problem Nr. 2: Verwenden wir ein Hash ... Array?



Es stellte sich heraus, dass der zweite Täter direkt neben dem ersten angerufen wird. Sie werden beide sogar in derselben Aussage genannt if



, wie in dieser hässlichen Dekompilierung verstanden werden kann:





Beide Probleme befinden sich in einer großen Parsing-Schleife aller Elemente. Problem Nr. 1 analysiert, Problem Nr. 2 speichert.



Alle Bezeichnungen sind von mir angegeben, ich habe keine Ahnung, wie die Funktionen und Parameter wirklich heißen.



Was ist das zweite Problem? Unmittelbar nach dem Parsen des Elements wird es in einem Array (oder in einer eingebetteten C ++ - Liste? Nicht ganz klar) gespeichert. Jeder Artikel sieht ungefähr so ​​aus:



struct {
    uint64_t *hash;
    item_t   *item;
} entry;
      
      





Aber was passiert vor dem Speichern? Der Code überprüft das gesamte Array Element für Element und vergleicht den Hash des Elements, um festzustellen, ob es in der Liste enthalten ist. Wenn meine Berechnungen korrekt sind, gibt dies bei ungefähr 63.000 Elementen (n^2+n)/2 = (63000^2+63000)/2 = 1984531500



Überprüfungen. Die meisten von ihnen sind nutzlos. Wir haben einzigartige Hashes . Warum also nicht eine Hash-Map verwenden ?





Der Profiler zeigt an, dass die ersten beiden Zeilen den Prozessor laden. Die Anweisung if



wird erst ganz am Ende ausgeführt. Die vorletzte Zeile fügt das Motiv ein.




Im Reverse Engineering habe ich diese Struktur benannt hashmap



, aber es ist offensichtlich, dass dies der Fall ist not_a_hashmap



. Und dann wird alles besser. Dieser Hash / Array / Liste ist vor dem Laden von JSON leer. Und alle Artikel in JSON sind einzigartig! Der Code muss nicht einmal überprüfen, ob der Artikel auf der Liste steht! Es gibt sogar eine Funktion zum direkten Einfügen von Elementen, verwenden Sie sie einfach! Ernsthaft, was zum Teufel !?



Konzeptioneller Beweiß



Das ist natürlich alles großartig, aber niemand wird mich ernst nehmen, bis ich es teste, damit ich eine Clickbait-Überschrift für einen Beitrag schreiben kann.



Wie ist der Plan? Schreiben Sie .dll



, injizieren Sie ihr GTA, fangen Sie mehrere Funktionen ab, ???, GEWINN!



Das JSON-Problem ist verwirrend und das Ersetzen des Parsers wäre äußerst zeitaufwändig. Es ist viel realistischer zu versuchen, es sscanf



durch eine Funktion zu ersetzen , die nicht davon abhängt strlen



. Aber es gibt noch einen einfacheren Weg.



  • abfangen strlen
  • warte auf eine lange Schlange
  • "Cache" seinen Start und seine Länge
  • Wenn es innerhalb der Zeichenfolge erneut aufgerufen wird, geben Sie den zwischengespeicherten Wert zurück


Etwas wie das:



size_t strlen_cacher(char* str)
{
  static char* start;
  static char* end;
  size_t len;
  const size_t cap = 20000;

  // if we have a "cached" string and current pointer is within it
  if (start && str >= start && str <= end) {
    // calculate the new strlen
    len = end - str;

    // if we're near the end, unload self
    // we don't want to mess something else up
    if (len < cap / 2)
      MH_DisableHook((LPVOID)strlen_addr);

    // super-fast return!
    return len;
  }

  // count the actual length
  // we need at least one measurement of the large JSON
  // or normal strlen for other strings
  len = builtin_strlen(str);

  // if it was the really long string
  // save it's start and end addresses
  if (len > cap) {
    start = str;
    end = str + len;
  }

  // slow, boring return
  return len;
}
      
      





Das Problem mit dem Hash-Array ist einfacher zu lösen. Sie können doppelte Überprüfungen einfach vollständig überspringen und Elemente direkt einfügen, da wir wissen, dass die Werte eindeutig sind.



char __fastcall netcat_insert_dedupe_hooked(uint64_t catalog, uint64_t* key, uint64_t* item)
{
  // didn't bother reversing the structure
  uint64_t not_a_hashmap = catalog + 88;

  // no idea what this does, but repeat what the original did
  if (!(*(uint8_t(__fastcall**)(uint64_t*))(*item + 48))(item))
    return 0;

  // insert directly
  netcat_insert_direct(not_a_hashmap, key, &item);

  // remove hooks when the last item's hash is hit
  // and unload the .dll, we are done here :)
  if (*key == 0x7FFFD6BE) {
    MH_DisableHook((LPVOID)netcat_insert_dedupe_addr);
    unload();
  }

  return 1;
}
      
      





Vollständige Quellen für Proof of Concept finden Sie hier .



Ergebnisse



Wie hat es funktioniert?



Anfängliche Ladezeit für den Online-Modus: ca. 6 Minuten

Zeit mit nur gepatchten doppelten Überprüfungen: 4 Minuten 30 Sekunden

Zeit nur mit JSON-Parser-Patch: 2 Minuten 50 Sekunden

Zeit mit Patches beider Probleme: 1 Minute 50 Sekunden



(6 * 60 - (1) * 60 + 50)) / (6 * 60) = Ladezeit um 69,4% verringert (großartig!)


Oh ja, wie es funktioniert hat!



Dies wird höchstwahrscheinlich nicht die Ladezeit für alle Spieler verkürzen - es kann andere Engpässe auf anderen Systemen geben, aber dies ist ein so offensichtliches Problem, dass ich nicht verstehe, wie R * es all die Jahre nicht bemerkt hat.



tl; dr



  • Beim Starten von GTA Online tritt aufgrund der Ausführung mit einem Thread ein CPU-Engpass auf
  • Es stellt sich heraus, dass GTA derzeit mit dem Parsen einer 10-MB-JSON-Datei zu kämpfen hat.
  • Der JSON-Parser selbst ist schlecht geschrieben / naiv implementiert und
  • Nach dem Parsen wird ein langsamer Vorgang ausgeführt, um zu überprüfen, ob keine doppelten Elemente vorhanden sind


R * Bitte lösen Sie das Problem



Wenn dieser Artikel es irgendwie zu Rockstar schafft, dauert es nicht länger als einen Tag, bis ein Entwickler diese Probleme behoben hat. Bitte tun Sie etwas dagegen.



Sie können zu Hashmap wechseln, um Duplikate zu entfernen, oder diese Prüfung vollständig überspringen, was schneller sein wird. Ersetzen Sie im JSON-Parser die Bibliothek durch eine effizientere. Ich glaube nicht, dass es hier eine einfachere Lösung gibt.



Danke.



All Articles