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

GTA Online. Ein Multiplayer-Spiel, das für langsames Laden bekannt ist. Ich bin kürzlich zurückgekommen, um einige Überfälle zu vervollständigen - und ich war schockiert, dass es genauso langsam geladen wird wie an dem Tag, an dem es vor 7 Jahren veröffentlicht wurde.



Es ist Zeit, dem auf den Grund zu gehen.



Nachrichtendienst



Zuerst wollte ich überprüfen, ob jemand das Problem bereits gelöst hat. Aber ich habe nur Geschichten über die große Komplexität des Spiels gefunden , weshalb das Laden so lange dauert , Geschichten, dass die Netzwerk-P2P-Architektur  Müll ist (obwohl dies nicht der Fall ist), einige komplexe Möglichkeiten zum Laden in den Story-Modus und dann in eine einzelne Sitzung und ein paar weitere Mods, um das R * -Logo-Video beim Booten zu überspringen. Nachdem ich die Foren ein wenig mehr gelesen hatte, stellte ich fest, dass Sie satte 10 bis 30 Sekunden sparen können, wenn Sie alle diese Methoden zusammen anwenden!



Inzwischen auf meinem Computer ...



Benchmark



Laden der Szene: ~ 1m 10s
Online-Laden: ~ 6m
Kein Boot-Menü, vom R * -Logo bis zum Gameplay (kein Social Club-Login.

Alter, aber anständiger Prozentsatz: AMD FX-8350
Günstige SSD: KINGSTON SA400S37120G
RAM muss gekauft werden: 2x Kingston 8192 MB (DDR3-1337) 99U5471
Normale GPU: NVIDIA GeForce GTX 1070


Ich weiß, dass meine Hardware veraltet ist, aber was könnte meine Downloads im Internet um das 6-fache verlangsamen? Ich konnte den Unterschied beim Laden vom Story-Modus in den Online-Modus nicht messen, wie es andere getan haben . Auch wenn es funktioniert, ist der Unterschied gering.



ich bin nicht alleine



Laut dieser Umfrage ist das Problem weit genug verbreitet, um für über 80% der Spieler leicht ärgerlich zu sein. Es ist jetzt sieben Jahre her!







Ich habe ein wenig Suche nach Informationen über die , ~ 20% Glücklichen, die Last in weniger als drei Minuten, und fand einige Benchmarks mit Top - Gaming - PCs und einer Online - Ladezeit von etwa zwei Minuten. Ich hätte jemanden getötet , der für einen solchen Computer gehackt wurde! Es sieht wirklich nach einem Hardwareproblem aus, aber etwas passt nicht zusammen ...



Warum dauert das Laden ihres Story-Modus noch etwa eine Minute? (Videos mit Logos wurden beim Booten von M.2 NVMe übrigens nicht berücksichtigt). Außerdem dauert es nur eine Minute, bis sie online aus dem Story-Modus heruntergeladen sind, während ich ungefähr fünf habe. Ich weiß, dass ihre Hardware viel besser ist, aber nicht fünfmal.



Hochpräzise Messungen



Mit einem leistungsstarken Tool wie dem Task-Manager habe ich mich auf die Suche nach dem Engpass gemacht.







Das Laden gemeinsam genutzter Ressourcen, die sowohl für den Story-Modus als auch online benötigt werden (fast auf dem Niveau von Top-End-PCs), dauert fast eine Minute. Anschließend lädt GTA einen CPU-Kern vier Minuten lang vollständig und unternimmt nichts anderes.



Festplattennutzung? Nein! Netzwerknutzung? Es gibt ein wenig, aber nach ein paar Sekunden fällt es hauptsächlich auf Null (mit Ausnahme des Ladens rotierender Informationsbanner). GPU-Nutzung? Null. Erinnerung? Gar nichts ...



Was ist das, Bitcoin-Mining oder so? Ich kann den Code hier riechen. Sehr schlechter Code.



Einzelner Stream



Mein alter AMD-Prozessor hat acht Kerne und es ist immer noch großartig, aber es ist ein altes Modell. Es wurde gemacht, als AMDs Single-Thread-Leistung viel niedriger war als die von Intel. Dies ist wahrscheinlich der Hauptgrund für solche Unterschiede in den Ladezeiten.



Was seltsam ist, ist die Art und Weise, wie die CPU verwendet wird. Ich hatte eine große Anzahl von Festplattenlesevorgängen oder eine Menge Netzwerkanforderungen erwartet, um Sitzungen in einem P2P-Netzwerk einzurichten. Aber ist es? Hier liegt wahrscheinlich ein Fehler vor.



Profilerstellung



Ein Profiler ist eine großartige Möglichkeit, CPU-Engpässe zu finden. Es gibt nur ein Problem: Die meisten von ihnen verlassen sich auf die Quellcode-Instrumentierung, um ein perfektes Bild davon zu erhalten, was dabei passiert. Und ich habe keinen Quellcode. Ich brauche auch keine perfekten Mikrosekundenwerte, ich habe einen 4-minütigen Engpass .



Willkommen zum Stapeln von Stichproben. Für Closed-Source-Anwendungen ist dies die einzige Option. Setzen Sie den laufenden Prozessstapel und die Position des aktuellen Befehlszeigers zurück, um den Aufrufbaum in den angegebenen Intervallen zu erstellen. Überlagern Sie sie dann und erhalten Sie Statistiken darüber, was los ist. Ich kenne nur einen Profiler, der dies unter Windows kann. Und es wurde seit über zehn Jahren nicht mehr aktualisiert. Es ist Luke Stackwalker ! Jemand, bitte gib Luke etwas Liebe :)







Normalerweise würde Luke die gleichen Funktionen gruppieren, aber ich habe keine Debug-Symbole, also musste ich nach Adressen in der Nähe suchen, um nach gemeinsamen Orten zu suchen. Und was sehen wir? Nicht einer, sondern zwei Engpässe!



In den Kaninchenbau



Nachdem ich mir von einem Freund eine absolut legitime Kopie des Standard-Disassemblers geliehen hatte (nein, ich kann es mir wirklich nicht leisten ... ich werde die Hydra jemals beherrschen ), ging ich zum Zerlegen des GTA.







Sieht völlig falsch aus. Ja, die meisten Top-Spiele verfügen über einen integrierten Reverse Engineering-Schutz, um sie vor Piraten, Cheats und Moddern zu schützen. Nicht, dass es sie jemals aufgehalten hätte ...



Es sieht so aus, als ob hier eine Art Verschleierung / Verschlüsselung angewendet wurde, die die meisten Anweisungen durch Kauderwelsch ersetzt. Keine Sorge, Sie müssen nur den Speicher des Spiels zurücksetzen, während es den Teil ausführt, den wir sehen möchten. Anweisungen müssen vor dem Start auf die eine oder andere Weise deobfusciert werden. Ich hatte Process Dump in der Nähe , also habe ich es genommen, aber es gibt viele andere Tools für ähnliche Aufgaben.



Problem 1: Ist das ... strlen ?!



Eine weitere Analyse des Dumps ergab eine der Adressen mit einem bestimmten Etikett strlen



, das von irgendwoher kommt! Wenn Sie den Aufrufstapel durchgehen, wird die vorherige Adresse als markiert vscan_fn



, und danach werden die Beschriftungen angezeigt, obwohl ich mir ziemlich sicher bin, dass dies der Fall ist sscanf



.



Wo kann ich ohne Zeitplan auskommen?



Er analysiert etwas. Aber was? Das logische Parsen wird ewig dauern, daher habe ich beschlossen, einige Beispiele aus dem laufenden Prozess mit x64dbg zu sichern . Nach ein paar Schritten des Debuggens stellt sich heraus, dass dies ... JSON ist! Es analysiert JSON. Eine satte zehn Megabyte JSON mit 63.000 Artikel .



...,
{
    "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? Nach einigen Links zu urteilen, sind dies die Daten für das "Online-Handelsverzeichnis". Ich gehe davon aus, dass es eine Liste aller möglichen Artikel und Upgrades enthält, die Sie in GTA Online kaufen können.



Um einige Verwirrung zu beseitigen, glaube ich, dass dies Geldgegenstände im Spiel sind, die nicht direkt mit Mikrotransaktionen zusammenhängen .



10 Megabyte? Im Prinzip nicht so sehr. Obwohl sscanf



nicht optimal genutzt, aber natürlich nicht so schlimm? Nun ...







Ja, ein solches Verfahren wird einige Zeit dauern ... Um ehrlich zu sein, hatte ich keine Ahnung, dass die meisten Implementierungen dies sscanf



erfordern strlen



Daher kann ich dem Entwickler, der das geschrieben hat, nicht wirklich die Schuld geben. Ich würde vermuten, dass es nur Byte für Byte scannte und bei anhalten konnte NULL



.



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



Es stellt sich heraus, dass der zweite Verbrecher direkt nach dem ersten gerufen wird. Selbst im selben Konstrukt if



, wie Sie aus dieser hässlichen Dekompilierung sehen können:







Alle Bezeichnungen gehören mir und ich habe keine Ahnung, wie die Funktionen / Parameter tatsächlich heißen.



Zweites Problem? Unmittelbar nach dem Parsen des Elements wird es in einem Array (oder einer C ++ - Inline-Liste? Nicht sicher) gespeichert. Jeder Eintrag sieht ungefähr so ​​aus:



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





Und vor dem Speichern? Es überprüft das gesamte Array, indem es den Hash jedes Elements vergleicht, ob es in der Liste enthalten ist oder nicht. Mit 63.000 Einträgen ist dies ungefähr (n^2+n)/2 = (63000^2+63000)/2 = 1984531500



, wenn ich mich in meinen Berechnungen nicht irre. Und das sind meistens nutzlose Schecks. Sie haben eindeutige Hashes. Verwenden Sie eine Hash-Tabelle.







Während des Reverse Engineering habe ich es benannt hashmap



, aber es ist offensichtlich _hashmap



. Und dann wird es noch interessanter. Diese Hash-Array-Liste ist vor dem Laden des JSON leer. Und alle Elemente in JSON sind einzigartig! Sie müssen nicht einmal überprüfen, ob sie auf der Liste stehen oder nicht! Sie haben sogar eine direkte Elementeinfügefunktion! Benutze es einfach! Ernsthaft, Leute, was zum Teufel !?



Konzeptioneller Beweiß



All dies ist großartig, aber niemand wird mich ernst nehmen, bis ich den eigentlichen Code schreibe, um das Laden zu beschleunigen und einen Clickbait-Titel für einen Beitrag zu erstellen.



Der Plan ist wie folgt. 1. Schreiben Sie .dll, 2. implementieren Sie es in GTA, 3. haken Sie einige Funktionen ein, 4. ???, 5. profit. Alles ist sehr einfach.



Das Problem mit JSON ist nicht trivial, ich kann ihren Parser nicht wirklich ersetzen. Es erscheint realistischer, sscanf durch eine zu ersetzen, die nicht von strlen abhängt. Aber es gibt noch einen einfacheren Weg.



  • Haken strlen

  • warte auf eine lange Schlange

  • "Cache" Start und Länge

  • Wenn ein anderer Aufruf in den Bereich der Zeichenfolge fällt, 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 (start && str >= start && str <= end) {
    // calculate the new strlen
    len = end - str;

    //    , 
    //        
    if (len < cap / 2)
      MH_DisableHook((LPVOID)strlen_addr);

    //  !
    return len;
  }

  //   
  //      JSON
  //   strlen   
  len = builtin_strlen(str);

  //     
  //     
  if (len > cap) {
    start = str;
    end = str + len;
  }

  // ,  
  return len;
}
      
      







Was das Hash-Array-Problem betrifft, überspringen wir einfach alle Prüfungen vollständig und fügen die Elemente direkt ein, da wir wissen, dass die Werte eindeutig sind.



char __fastcall netcat_insert_dedupe_hooked(uint64_t catalog, uint64_t* key, uint64_t* item)
{
  //   
  uint64_t not_a_hashmap = catalog + 88;

  //  ,   ,   
  if (!(*(uint8_t(__fastcall**)(uint64_t*))(*item + 48))(item))
    return 0;

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

  //      
  //   .dll,   :)
  if (*key == 0x7FFFD6BE) {
    MH_DisableHook((LPVOID)netcat_insert_dedupe_addr);
    unload();
  }

  return 1;
}
      
      





Der vollständige PoC-Quellcode ist hier .



Ergebnisse



Wie funktioniert es?



Vorherige Ladezeit online: ca. 6m
Zeit mit Patch-Prüfung auf Duplikate: 4m 30s
Zeit mit JSON-Parser: 2m 50s
Zeit mit zwei Patches zusammen: 1m 50s

(6 * 60 - (1 * 60 + 50)) / (6 * 60) = 69,4% Zeitverbesserung (Klasse!)


Ja, verdammt, es hat funktioniert! :))



Dies wird wahrscheinlich nicht alle Bootprobleme lösen - es kann andere Engpässe auf verschiedenen Systemen geben, aber es ist ein so klaffendes Loch, dass ich keine Ahnung habe, wie R * es im Laufe der Jahre verpasst hat.



Zusammenfassung



  • Beim Starten von GTA Online tritt ein Engpass mit einem Thread auf

  • Es stellt sich heraus, dass GTA Probleme hat, eine 1-MB-JSON-Datei zu analysieren

  • Der JSON-Parser selbst ist schlecht gemacht / naiv und

  • Nach dem Parsen gibt es ein langsames Verfahren zum Entfernen von Duplikaten


R * bitte korrigieren



Wenn die Informationen die Rockstar-Ingenieure irgendwie erreichen, kann das Problem innerhalb weniger Stunden durch die Bemühungen eines Entwicklers gelöst werden. Bitte machen Sie etwas dagegen: <



Sie können entweder zu einer Hash-Tabelle gehen, um Duplikate zu entfernen, oder die Deduplizierung beim Start als schnelle Lösung ganz überspringen. Ersetzen Sie für einen JSON-Parser einfach die Bibliothek durch eine leistungsfähigere. Ich glaube nicht, dass es eine einfachere Option gibt.



ty <3



All Articles