Cómo reduzco los tiempos de carga de GTA Online en un 70%

GTA Online. Un juego multijugador conocido por su carga lenta. Recientemente regresé para completar algunos atracos, y me sorprendió que se cargue tan lentamente como lo hizo el día en que se lanzó hace 7 años.



Es hora de llegar al fondo.



Servicio de inteligencia



Primero quería comprobar si alguien ya había resuelto el problema. Pero solo encontré historias sobre la gran complejidad del juego , razón por la cual tarda tanto en cargarse, historias de que la arquitectura p2p de la red  es basura (aunque no lo es), algunas formas complejas de cargar en modo historia, y luego en una sola sesión y un par de modificaciones más para omitir el video del logotipo R * en el momento del arranque. Después de leer un poco más los foros, descubrí que puedes ahorrar la friolera de 10 a 30 segundos si usas todos estos métodos juntos.



Mientras tanto, en mi computadora ...



Punto de referencia



Carga de escena: ~ 1 m 10 s
Carga en línea: ~ 6 m
Sin menú de inicio, desde el logotipo R * hasta el juego (sin inicio de sesión en el Social Club.

Porcentaje antiguo, pero decente: AMD FX-8350
SSD barato: KINGSTON SA400S37120G
Necesita comprar RAM: 2x Kingston 8192 MB (DDR3-1337) 99U5471
GPU normal: NVIDIA GeForce GTX 1070


Sé que mi hardware está desactualizado, pero diablos, ¿qué podría ralentizar mis descargas 6 veces cuando estoy en línea? No pude medir la diferencia al cargar del modo historia a en línea como lo han hecho otros . Incluso si funciona, la diferencia es pequeña.



no estoy solo



Según esta encuesta , el problema está lo suficientemente extendido como para ser un poco molesto para más del 80% de los jugadores. ¡Han pasado siete años!







Hice una pequeña búsqueda de información sobre esos ~ 20% afortunados que cargan en menos de tres minutos, y encontré varios puntos de referencia con las mejores PC para juegos y un tiempo de carga en línea de aproximadamente dos minutos. ¡Hubiera matado a alguien pirateado para una computadora así! Realmente parece un problema de hardware, pero algo no cuadra ...



¿Por qué su modo historia todavía tarda aproximadamente un minuto en cargarse? (por cierto, los videos con logotipos no se tuvieron en cuenta al arrancar desde M.2 NVMe). Además, solo tardan un minuto en descargarse del modo Historia en línea, mientras que yo tengo unos cinco. Sé que su hardware es mucho mejor, pero no cinco veces.



Medidas de alta precisión



Armado con una herramienta poderosa como el Administrador de tareas , me propuse encontrar el cuello de botella.







Se tarda casi un minuto en cargar los recursos compartidos, que son necesarios tanto para el modo historia como en línea (casi a la par con las PC de gama alta), luego GTA carga completamente un núcleo de CPU durante cuatro minutos, sin hacer nada más.



¿Uso del disco? ¡No! ¿Uso de la red? Hay un poco, pero después de unos segundos cae principalmente a cero (a excepción de la carga de pancartas de información giratorias). ¿Uso de GPU? Cero. ¿Memoria? Nada en absoluto ...



¿Qué es, minería de Bitcoin o algo así? Puedo oler el código aquí. Código muy malo.



Flujo único



Mi antiguo procesador AMD tiene ocho núcleos y sigue siendo genial, pero es un modelo antiguo. Se hizo cuando el rendimiento de un solo hilo de AMD era mucho menor que el de Intel. Esta es probablemente la razón principal de tales diferencias en los tiempos de carga.



Lo extraño es la forma en que se usa la CPU. Esperaba una gran cantidad de lecturas de disco o un montón de solicitudes de red para configurar sesiones en una red p2p. ¿Pero es? Probablemente haya algún error aquí.



Perfilado



Un generador de perfiles es una excelente manera de encontrar cuellos de botella en la CPU. Solo hay un problema: la mayoría de ellos se basan en la instrumentación del código fuente para obtener una imagen perfecta de lo que está sucediendo en el proceso. Y no tengo el código fuente. Tampoco necesito lecturas perfectas de microsegundos, tengo un cuello de botella de 4 minutos .



Entonces, bienvenido al muestreo de pila. Para aplicaciones de código cerrado, esta es la única opción. Restablezca la pila de procesos en ejecución y la ubicación del puntero de instrucción actual para construir el árbol de llamadas en los intervalos especificados. Luego, superpóngalos y obtenga estadísticas sobre lo que está sucediendo. Solo conozco un generador de perfiles que puede hacer esto en Windows. Y no se ha actualizado en más de diez años. ¡Es Luke Stackwalker ! Alguien, por favor, dale un poco de amor a Luke :)







Normalmente, Luke agruparía las mismas funciones, pero no tengo símbolos de depuración, así que tuve que buscar direcciones cercanas para buscar lugares comunes. Y ¿qué vemos? ¡No uno, sino dos cuellos de botella!



Por la madriguera del conejo



Después de pedir prestada a un amigo mío una copia perfectamente legítima del desensamblador estándar (no, realmente no puedo pagarlo ... nunca dominaré la hidra ), fui a desensamblar el GTA.







Parece completamente mal. Sí, la mayoría de los mejores juegos tienen protección de ingeniería inversa incorporada para mantenerlos a salvo de piratas, trampas y modders. No es que alguna vez los detuviera ...



Parece que aquí se ha aplicado algún tipo de ofuscación / cifrado, reemplazando la mayoría de las instrucciones con galimatías. No te preocupes, solo necesitas restablecer la memoria del juego mientras está haciendo la parte que queremos ver. Las instrucciones deben desofuscarse antes del lanzamiento de una forma u otra. Tenía Process Dump cerca , así que lo tomé, pero hay muchas otras herramientas para tareas similares.



Problema 1: ¿es eso ... strlen?



Un análisis más detallado del vertedero reveló una de las direcciones con una etiqueta determinada strlen



, que se tomó de algún lugar. Al bajar por la pila de llamadas, la dirección anterior se marca como vscan_fn



, y luego las etiquetas terminan, aunque estoy bastante seguro de que lo es sscanf



.



¿Dónde puedo hacer sin un horario?



Analiza algo. ¿Pero que? El análisis lógico llevará años, así que decidí volcar algunas muestras del proceso en ejecución usando x64dbg . Después de algunos pasos de depuración, resulta que esto es ... ¡JSON! Analiza JSON. La friolera de diez megabytes de JSON con 63.000 elementos .



...,
{
    "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é es? A juzgar por algunos de los enlaces, estos son los datos del "directorio de comercio en línea". Supongo que contiene una lista de todos los posibles elementos y actualizaciones que puedes comprar en GTA Online.



Para aclarar algo de confusión, creo que estos son elementos de dinero del juego que no están directamente relacionados con las microtransacciones .



10 megabytes? En principio, no tanto. Aunque sscanf



no se utiliza de la forma más óptima, ¿pero por supuesto que no es tan malo? Bueno ...







Sí, tal procedimiento llevará algún tiempo ... Para ser honesto, no tenía idea de que la mayoría de las implementaciones sscanf



llaman strlen



así que no puedo culpar al desarrollador que escribió esto. Supongo que solo estaba escaneando byte a byte y podría detenerse en NULL



.



Problema 2: usemos un hash ... ¿matriz?



Resulta que el segundo criminal se llama inmediatamente después del primero. Incluso en la misma construcción if



, como puede ver en esta desagradable descompilación:







todas las etiquetas son mías y no tengo idea de cómo se llaman realmente las funciones / parámetros.



Segundo problema? Inmediatamente después de analizar el elemento, se almacena en una matriz (¿o en una lista en línea de C ++? No estoy seguro). Cada entrada se parece a esto:



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





¿Y antes de ahorrar? Verifica toda la matriz comparando el hash de cada elemento, ya sea que esté en la lista o no. Con 63 mil entradas, esto es aproximadamente (n^2+n)/2 = (63000^2+63000)/2 = 1984531500



, si no me equivoco en mis cálculos. Y estos son en su mayoría cheques inútiles. Tienes hashes únicos, ¿por qué no usar una tabla hash?







Durante la ingeniería inversa, lo nombré hashmap



, pero es obvio _hashmap



. Y luego se vuelve aún más interesante. Esta lista de matrices hash está vacía antes de cargar el JSON. ¡Y todos los elementos en JSON son únicos! ¡Ni siquiera necesitan verificar si están en la lista o no! ¡Incluso tienen una función de inserción directa de elementos! ¡Úsalo! En serio, chicos, ¿¡qué carajo !?



Prueba de concepto



Todo esto es genial, pero nadie me tomará en serio hasta que escriba el código real para acelerar la carga y hacer un título de clickbait para una publicación.



El plan es el siguiente. 1. Escriba .dll, 2. impleméntelo en GTA, 3. enganche algunas funciones, 4. ???, 5. beneficio. Todo es sumamente sencillo.



El problema con JSON no es trivial, realmente no puedo reemplazar su analizador. Parece más realista reemplazar sscanf con uno que no dependa de strlen. Pero hay una forma aún más sencilla.



  • gancho strlen

  • espera una larga cola

  • Inicio y duración de la "caché"

  • si otra llamada entra dentro del rango de la cadena, devuelve el valor en caché


Algo como esto:



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;
}
      
      







En cuanto al problema de la matriz hash, simplemente saltamos todas las comprobaciones por completo e insertamos los elementos directamente, ya que sabemos que los valores son únicos.



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;
}
      
      





El código fuente completo de PoC está aquí .



resultados



¿Entonces, cómo funciona?



Tiempo de carga anterior en línea: aproximadamente 6 m
Tiempo con revisión de parches para duplicados: 4m 30s
Tiempo con analizador JSON: 2 min 50 s
Tiempo con dos parches juntos: 1m 50s

(6 * 60 - (1 * 60 + 50)) / (6 * 60) = 69,4% de mejora en el tiempo (¡clase!)


Sí, maldita sea, ¡funcionó! :))



Esto probablemente no resolverá todos los problemas de arranque; puede haber otros cuellos de botella en diferentes sistemas, pero es un agujero tan enorme que no tengo idea de cómo R * lo pasó por alto a lo largo de los años.



Resumen



  • Hay un cuello de botella de un solo hilo al iniciar GTA Online

  • Resulta que GTA está luchando para analizar un archivo JSON de 1 MB

  • El analizador JSON en sí está mal hecho / es ingenuo y

  • Después del análisis, hay un procedimiento lento para eliminar duplicados.


R * por favor corrija



Si la información llega de alguna manera a los ingenieros de Rockstar, entonces el problema puede resolverse en unas pocas horas con los esfuerzos de un desarrollador. Por favor, chicos, hagan algo al respecto: <



Pueden ir a una tabla hash para eliminar los duplicados u omitir la deduplicación al inicio por completo como una solución rápida. Para un analizador JSON, simplemente reemplace la biblioteca por una de mayor rendimiento. No creo que haya una opción más sencilla.



ty <3



All Articles