GTA Online est connu pour sa vitesse de chargement lente. Ayant récemment lancé le jeu pour terminer de nouvelles missions de raid, j'ai été choqué de le voir se charger aussi lentement que lors de sa sortie il y a sept ans.
Le temps est venu. Pour l'instant, déterminez les raisons de cela.
Service de renseignements
Pour commencer, je voulais vérifier si quelqu'un avait déjà résolu ce problème. La plupart des résultats trouvés consistaient en des données anecdotiques sur la difficulté du jeu , qu'il a dû se charger pendant si longtemps, des histoires sur la boiterie de l'architecture réseau p2p (et c'est vrai), des moyens complexes de chargement en mode histoire, et puis en une seule session et des paires de mods qui vous ont permis de sauter la vidéo d'ouverture avec le logo R *. Certaines sources ont rapporté que lorsque toutes ces méthodes sont utilisées ensemble, vous pouvez gagner jusqu'à 10-30 secondes!
En attendant, sur mon PC ...
Référence
: 1 10
-: 6
, R* ( social club ).
, : AMD FX-8350
SSD: KINGSTON SA400S37120G
: 2 Kingston 8192 (DDR3-1337) 99U5471
GPU: NVIDIA GeForce GTX 1070
Je sais que ma voiture est obsolète, mais pourquoi diable le mode en ligne se charge-t-il six fois plus lentement? Je n'ai trouvé aucune différence dans la technique de téléchargement «histoire d'abord, puis en ligne», comme d'autres l'ont fait avant moi . Mais même si cela fonctionnait, les résultats seraient dans la marge d'erreur.
je ne suis pas seul
Selon ce sondage , le problème est si répandu qu'il exaspère légèrement plus de 80% de la base de joueurs. Les gars de R *, en fait sept ans se sont écoulés!
18,8% des joueurs ont les ordinateurs ou consoles les plus puissants, 81,2% sont assez tristes, 35,1% sont assez tristes.
Après avoir recherché 20% de ces chanceux dont le chargement prend moins de trois minutes, j'ai trouvé un certain nombre de repères avec des PC de jeu puissants et un temps de chargement en ligne d'environ deux minutes. Pour obtenir un temps de chargement de deux minutes, je
Comment se fait-il que les personnes effectuant ces tests prennent encore environ une minute pour charger le mode histoire? (D'ailleurs, le benchmark avec M.2 ne prend pas en compte le temps d'affichage des logos au début.) De plus, le chargement du mode histoire au mode en ligne ne leur prend qu'une minute, tandis que le mien en prend plus de cinq. Je sais que leur technique est bien meilleure que la mienne, mais certainement pas cinq fois.
Mesures très précises
Armé d'outils puissants comme le gestionnaire de tâches , j'ai commencé à enquêter pour savoir quelles ressources pourraient être le goulot d'étranglement.
En une minute, les ressources standard du mode histoire sont chargées, après quoi le jeu charge le processeur pendant plus de quatre minutes.
Après une minute de chargement des ressources partagées utilisées à la fois en mode histoire et en ligne (un indicateur presque égal aux benchmarks des PC puissants), GTA décide de charger un cœur de ma machine autant que possible pendant quatre minutes et ne rien faire d'autre.
Accès au disque? Il n'est pas là! Utilisation du réseau? Il n'y en a pas beaucoup, mais au bout de quelques secondes, le trafic tombe presque à zéro (sauf pour le chargement de bannières tournantes avec des informations). Utilisation du GPU? Par des zéros. Utilisation de la mémoire? Graphique parfaitement plat ...
Que se passe-t-il, le jeu extrait une crypto ou quelque chose du genre? Commence à sentir le code. Très mauvais code .
Limiter un flux
Bien que mon ancien processeur AMD ait huit cœurs et puisse toujours fonctionner correctement, il a été construit dans l'ancien temps. À l'époque, les performances mono-thread des processeurs AMD étaient loin derrière celles des processeurs Intel. Cela n'explique peut-être pas toute la différence des temps de chargement, mais cela devrait expliquer le plus important.
La chose étrange est que le jeu n'utilise le CPU. Je m'attendais à une énorme quantité de ressources chargées à partir du disque ou à un tas de demandes réseau pour créer une session sur le réseau p2p. Mais ça? Il s'agit probablement d'un bug.
Profilage
Les profileurs sont un excellent moyen de trouver les goulots d'étranglement du processeur. Il n'y a qu'un seul problème - la plupart d'entre eux utilisent le code source pour obtenir une image parfaite de ce qui se passe dans le processus. Et je ne l'ai pas. Mais je n'ai pas non plus besoin de lectures précises à la microseconde - le goulot d'étranglement dure quatre minutes.
L'échantillonnage en pile entre en scène: c'est le seul moyen d'explorer des applications source fermées. Nous effectuons un vidage de la pile du processus en cours et de l'emplacement du pointeur de commande actuel pour créer un arbre d'appels à des intervalles spécifiés. Ensuite, nous les additionnons pour obtenir des statistiques sur ce qui se passe. Il n'y a qu'un seul profileur que je connais (je peux me tromper ici) qui peut le faire sur Windows. Et il n'a pas été mis à jour depuis plus de dix ans. C'est Luke Stackwalker! Laissez quelqu'un donner son amour à ce projet.
Les coupables n ° 1 et n ° 2.
Luke regroupe généralement les mêmes fonctions, mais comme je n'ai pas de symboles de débogage, j'ai besoin de regarder les adresses les plus proches avec mes yeux pour comprendre qu'elles sont au même endroit. Et que voyons-nous? Pas un, mais deux goulots d'étranglement!
Dans le trou de lapin
Ayant emprunté une copie parfaitement légitime du désassembleur populaire à un ami (non, je ne peux pas me le permettre ... je vais devoir apprendre ghidra d'une manière ou d'une autre ), j'ai commencé à démonter la GTA.
Tout cela semble complètement faux. De nombreux jeux à gros budget ont une protection intégrée contre l'ingénierie inverse pour se prémunir contre les pirates, les tricheurs et les moddeurs (pour ne pas dire que cela les arrête jamais).
Il semble qu'une sorte d'obfuscation / cryptage soit utilisée ici, en raison de laquelle la plupart des commandes sont remplacées par du charabia. Mais ne vous inquiétez pas, nous avons juste besoin de vider la mémoire du jeu lorsque nous exécutons la partie que nous voulons apprendre. Avant leur exécution, les commandes doivent être désobfusquées d'une manière ou d'une autre. J'avais Process Dump à portée de main , mais il existe de nombreux autres outils qui peuvent faire des choses similaires.
Problème n ° 1: est-ce ... strlen?!
Le démontage de la décharge maintenant moins obscurcie révèle que l'une des adresses a une étiquette prise de nulle part! Vraiment
strlen
? Le suivant dans la pile d'appels est marqué comme
vscan_fn
, après quoi les étiquettes sont épuisées, mais je suis presque sûr que c'est le cas
sscanf
.
Ils grattent quelque chose. Mais quoi? L'analyse du code désassemblé prendrait l'infini, j'ai donc décidé de vider certains échantillons du processus en cours en utilisant x64dbg . Après un peu de débogage, j'ai compris que c'était ... JSON! Ils analysent JSON. Un énorme 10 mégaoctets de données JSON avec près de 63000 éléments .
...,
{
"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"]
},
...
Qu'est-ce que c'est? Selon certaines sources, cela ressemble à des données "d'annuaire de boutique en ligne". Je suppose qu'ils contiennent une liste de tous les éléments et mises à niveau possibles qui peuvent être achetés dans GTA Online.
Clarification: je pense que ce sont des objets achetés avec de l'argent dans le jeu et qui ne sont pas directement liés aux microtransactions .
Mais 10 mégaoctets, c'est une bagatelle! Et l'utilisation
sscanf
n'est peut-être pas optimale, mais elle ne peut pas être si mauvaise? Bien ...
10 mégaoctets de chaînes C en mémoire. 1. Déplacez le pointeur de quelques octets vers la valeur suivante. 2. Nous appelons
sscanf(p, "%d", ...)
. 3. Nous lisons chaque caractère dans 10 mégaoctets en lisant chaque petite valeur (!?). 4. Renvoyez la valeur numérisée.
Oui, cela prendra du temps ... Pour être honnête, je n'avais aucune idée de ce que la plupart des implémentations
sscanf
appellent
strlen
, donc je ne peux pas blâmer le développeur qui a écrit ceci. Je suggérerais que ces données sont simplement analysées octet par octet et que le traitement peut s'arrêter à
NULL
.
Problème n ° 2: Utilisons un tableau de hachage ...?
Il s'est avéré que le deuxième coupable est appelé directement à côté du premier. Ils sont même appelés tous les deux dans la même déclaration
if
, comme on peut le comprendre dans cette vilaine décompilation:
Les deux problèmes sont à l'intérieur d'une grande boucle d'analyse de tous les éléments. Le problème n ° 1 est l'analyse, le problème n ° 2 enregistre.
Toutes les étiquettes sont spécifiées par moi, je n'ai aucune idée de ce que les fonctions et paramètres sont vraiment appelés.
Quel est le deuxième problème? Immédiatement après l'analyse de l'élément, il est enregistré dans un tableau (ou dans une liste intégrée C ++? Pas tout à fait clair) Chaque élément ressemble à ceci:
struct {
uint64_t *hash;
item_t *item;
} entry;
Mais que se passe-t-il avant d'économiser? Le code vérifie le tableau entier , élément par élément, en comparant le hachage de l'élément pour voir s'il est dans la liste. Si mes calculs sont corrects, alors avec environ 63 mille éléments, cela donne des
(n^2+n)/2 = (63000^2+63000)/2 = 1984531500
vérifications. La plupart d'entre eux sont inutiles. Nous avons des hachages uniques , alors pourquoi ne pas utiliser une carte de hachage ?
Le profileur montre que les deux premières lignes chargent le processeur. L'instruction
if
n'est exécutée qu'à la toute fin. L'avant-dernière ligne insère le sujet.
En reverse engineering, j'ai nommé cette structure
hashmap
, mais il est évident que c'est le cas
not_a_hashmap
. Et puis tout va mieux. Ce hachage / tableau / liste est vide avant le chargement de JSON. Et tous les éléments en JSON sont uniques! Le code n'a même pas besoin de vérifier si l'élément est sur la liste! Il existe même une fonction pour insérer directement des éléments, il suffit de l'utiliser! Sérieusement, c'est quoi ce bordel!?
Preuve de concept
Tout cela est génial, bien sûr, mais personne ne me prendra au sérieux jusqu'à ce que je le teste afin que je puisse écrire un titre clickbait pour un article.
Quel est le plan? Écrivez
.dll
, injectez son GTA, interceptez plusieurs fonctions, ???, PROFIT!
Le problème JSON est déroutant et le remplacement de l'analyseur prendrait beaucoup de temps. Il est beaucoup plus réaliste d'essayer de le remplacer
sscanf
par une fonction qui ne dépend pas de
strlen
. Mais il existe un moyen encore plus simple.
- intercepter strlen
- attendre une longue file
- "Cache" son début et sa durée
- s'il est à nouveau appelé dans la chaîne, renvoie la valeur mise en cache
Quelque chose comme ça:
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;
}
En ce qui concerne le problème du tableau de hachage, il est plus facile à gérer - vous pouvez simplement ignorer complètement les vérifications en double et insérer des éléments directement, car nous savons que les valeurs sont uniques.
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;
}
Les sources complètes de preuve de concept peuvent être trouvées ici .
résultats
Alors, comment ça a fonctionné?
Temps de chargement initial pour le mode en ligne: environ 6 minutes
Temps avec uniquement les vérifications en double corrigées: 4 minutes 30 secondes
Temps avec le correctif de l'analyseur JSON uniquement: 2 minutes 50 secondes
Temps avec les correctifs des deux problèmes: 1 minute 50 secondes
(6 * 60 - (1 * 60 + 50)) / (6 * 60) = temps de chargement réduit de 69,4% (super!)
Oh oui, comment ça a fonctionné!
Très probablement, cela ne réduira pas le temps de chargement pour tous les joueurs - il peut y avoir d'autres goulots d'étranglement sur d'autres systèmes, mais c'est un problème tellement évident que je ne comprends pas comment R * ne l'a pas remarqué toutes ces années.
tl; dr
- Il y a un goulot d'étranglement du processeur lors du lancement de GTA Online en raison d'une exécution à thread unique
- Il s'avère que GTA se bat actuellement pour analyser un fichier JSON de 10 Mo.
- L'analyseur JSON lui-même est mal écrit / implémenté naïvement et
- Après l'analyse, une procédure lente est effectuée pour vérifier qu'il n'y a pas d'éléments en double
R * veuillez résoudre le problème
S'il vous plaît, si cet article parvient d'une manière ou d'une autre à Rockstar, il ne faudra pas plus d'un jour à un développeur pour résoudre ces problèmes. Veuillez faire quelque chose.
Vous pouvez passer à hashmap pour éliminer les doublons, ou ignorer complètement cette vérification, ce qui sera plus rapide. Dans l'analyseur JSON, remplacez la bibliothèque par une bibliothèque plus efficace. Je ne pense pas qu'il y ait une solution plus simple ici.
Remercier.