sql >> Base de Datos >  >> RDS >> Database

Manejo de una fuga de recursos de GDI

La fuga de GDI (o simplemente el uso de demasiados objetos GDI) es uno de los problemas más comunes. Eventualmente causa problemas de renderizado, errores y/o problemas de rendimiento. El artículo describe cómo solucionamos este problema.

En 2016, cuando la mayoría de los programas se ejecutan en sandboxes desde donde incluso el desarrollador más incompetente no puede dañar el sistema, me sorprende enfrentar el problema del que hablaré en este artículo. Hablando con franqueza, esperaba que este problema se hubiera ido para siempre junto con Win32Api. Sin embargo, lo enfrenté. Antes de eso, solo escuché historias de terror de desarrolladores antiguos más experimentados.

El problema

Fuga o uso de la enorme cantidad de objetos GDI.

Síntomas

  1. La columna de objetos GDI en la pestaña Detalles del Administrador de tareas muestra 10000 críticos (si esta columna no está, puede agregarla haciendo clic con el botón derecho en el encabezado de la tabla y seleccionando Seleccionar columnas).
  2. Al desarrollar en C# o en otros lenguajes que son ejecutados por CLR, ocurre el siguiente error poco informativo:
    Mensaje:Ocurrió un error genérico en GDI+.
    Fuente:System.Drawing
    TargetSite:IntPtr GetHbitmap(System.Drawing.Color)
    Tipo:System.Runtime.InteropServices.ExternalException
    Es posible que el error no ocurra con ciertas configuraciones o en ciertas versiones del sistema, pero su aplicación no podrá representar un solo objeto:
  3. Durante el desarrollo en С/С++, todos los métodos GDI, como Create%SOME_GDI_OBJECT%, comenzaron a devolver NULL.

¿Por qué?

Los sistemas Windows no permiten crear más de 65535 objetos GDI. Este número, de hecho, es impresionante y apenas puedo imaginar un escenario normal que requiera una cantidad tan grande de objetos. Existe una limitación para los procesos:10000 por proceso que se puede modificar (cambiando HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota valor en el rango de 256 a 65535), pero Microsoft no recomienda aumentar esta limitación. Si aún lo hace, un proceso podrá congelar el sistema para que no pueda mostrar ni siquiera el mensaje de error. En este caso, el sistema puede reactivarse solo después de reiniciar.

¿Cómo solucionarlo?

Si vive en un mundo CLR cómodo y administrado, existe una alta probabilidad de que tenga una pérdida de memoria habitual en su aplicación. El problema es desagradable, pero es un caso bastante común. Hay al menos una docena de excelentes herramientas para detectar esto. Deberá usar cualquier generador de perfiles para ver si aumenta la cantidad de objetos que envuelven los recursos GDI (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics). Si es el caso, puedes dejar de leer este artículo. Si no se detectó la fuga de objetos de contenedor, su código usa la API de GDI directamente y hay un escenario en el que no se eliminan

¿Qué recomiendan los demás?

La guía oficial de Microsoft u otros artículos sobre este tema le recomendarán algo como esto:

Buscar todo Crear %SOME_GDI_OBJECT% y detectar si el DeleteObject correspondiente (o ReleaseDC para objetos HDC) existe. Si tal DeleteObject existe, puede haber un escenario que no lo llame.

Hay una versión ligeramente mejorada de este método que contiene un paso adicional:

Descargue la utilidad GDIView. Puede mostrar el número exacto de objetos GDI por tipo. Tenga en cuenta que el número total de objetos no se corresponde con el valor de la última columna. Pero podemos cerrar los ojos ante esto si ayuda a reducir el campo de búsqueda.

El proyecto en el que estoy trabajando tiene una base de código de 9 millones de registros, aproximadamente la misma cantidad de registros se encuentra en las bibliotecas de terceros, cientos de llamadas de la función GDI que se distribuyen en docenas de archivos. Había desperdiciado mucho tiempo y energía antes de comprender que el análisis manual sin fallas es imposible.

¿Qué puedo ofrecer?

Si este método te parece demasiado largo y tedioso, no has superado todas las etapas de desesperación con el anterior. Puede intentar seguir los pasos anteriores, pero si no ayuda, no se olvide de esta solución.

En la búsqueda de la fuga, me cuestioné:¿Dónde se crean los objetos que se filtran? Era imposible establecer puntos de interrupción en todos los lugares donde se llama a la función API. Además, no estaba seguro de que no suceda en .NET Framework o en una de las bibliotecas de terceros que usamos. Unos minutos de búsqueda en Google me llevaron a la utilidad API Monitor que permitía registrar y rastrear llamadas a todas las funciones del sistema. Encontré fácilmente la lista de todas las funciones que generan objetos GDI, las ubiqué y seleccioné en API Monitor. Luego, establezco puntos de interrupción.

Después de eso, ejecuté el proceso de depuración Visual Studio y lo seleccionó en el árbol Procesos. El quinto punto de interrupción funcionó de inmediato:

Me di cuenta de que me ahogaría en este torrente y que necesitaba algo más. Eliminé los puntos de interrupción de las funciones y decidí ver el registro. Mostró miles de llamadas. Quedó claro que no podré analizarlos manualmente.

La tarea es Encontrar las llamadas de las funciones GDI que no provocan el borrado . El registro presentaba todo lo que necesitaba:la lista de llamadas a funciones en orden cronológico, sus valores devueltos y parámetros. Por lo tanto, necesitaba obtener un valor devuelto de la función Create%SOME_GDI_OBJECT% y encontrar la llamada de DeleteObject con este valor como argumento. Seleccioné todos los registros en API Monitor, los inserté en un archivo de texto y obtuve algo como CSV con el delimitador TAB. Ejecuté VS, donde tenía la intención de escribir un pequeño programa para analizar, pero antes de que pudiera cargarse, se me ocurrió una idea mejor:exportar datos a una base de datos y escribir una consulta para encontrar lo que necesitaba. Fue la elección correcta ya que me permitió hacer preguntas y obtener respuestas rápidamente.

Hay muchas herramientas para importar datos de CSV a una base de datos, por lo que no me detendré en este tema (mysql, mssql, sqlite).

Tengo la siguiente tabla:

CREATE TABLE apicalls (
id int(11) DEFAULT NULL,
`Time of Day` datetime DEFAULT NULL,
Thread int(11) DEFAULT NULL,
Module varchar(50) DEFAULT NULL,
API varchar(200) DEFAULT NULL,
`Return Value` varchar(50) DEFAULT NULL,
Error varchar(100) DEFAULT NULL,
Duration varchar(50) DEFAULT NULL
)

Escribí la siguiente función de MySQL para obtener el descriptor del objeto eliminado de la llamada a la API:

CREATE FUNCTION getHandle(api varchar(1000))
RETURNS varchar(100) CHARSET utf8
BEGIN
DECLARE start int(11);
DECLARE result varchar(100);
SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )'
IF start = 0 THEN
SET start := INSTR(api, '(');
END IF;
SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1);
RETURN TRIM(result);
END

Y finalmente, escribí una consulta para ubicar todos los objetos actuales:

SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi
FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates
LEFT JOIN (SELECT
d.id,
d.API,
getHandle(d.API) handle
FROM apicalls d
WHERE API LIKE 'DeleteObject%'
OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels
ON dels.handle = creates.handle
WHERE creates.API LIKE 'Create%';

(Básicamente, simplemente encontrará todas las llamadas Eliminar para todas las llamadas Crear).

Como puede ver en la imagen de arriba, todas las llamadas sin una sola eliminación se han encontrado a la vez.

Entonces, queda la última pregunta:¿Cómo determinar de dónde se llaman estos métodos en el contexto de mi código? Y aquí me ayudó un truco elegante:

  1. Ejecute la aplicación en VS para la depuración
  2. Encuéntralo en Api Monitor y selecciónalo.
  3. Seleccione una función requerida en la API y coloque un punto de interrupción.
  4. Siga haciendo clic en "Siguiente" hasta que se llame con los parámetros en cuestión (realmente me perdí los puntos de interrupción condicionales de VS)
  5. Cuando llegue a la llamada requerida, cambie a CS y haga clic en Dividir todo .
  6. VS Debugger se detendrá justo donde se crea el objeto filtrado y todo lo que debe hacer es averiguar por qué no se eliminó.

Nota:El código está escrito con fines ilustrativos.

Resumen:

El algoritmo descrito es complicado y requiere muchas herramientas, pero dio el resultado mucho más rápido en comparación con una búsqueda tonta a través de la enorme base de código.

Aquí hay un resumen de todos los pasos:

  1. Buscar fugas de memoria de objetos contenedor GDI.
  2. Si existen, elimínelos y repita el paso 1.
  3. Si no hay fugas, busque las llamadas a las funciones de la API explícitamente.
  4. Si su cantidad no es grande, busque una secuencia de comandos donde no se elimine un objeto.
  5. Si su cantidad es grande o difícilmente se pueden rastrear, descargue API Monitor y configúrelo para registrar llamadas de las funciones GDI.
  6. Ejecute la aplicación para la depuración en VS.
  7. Reproducir la fuga (inicializará el programa para ocultar los objetos cobrados).
  8. Conéctese con API Monitor.
  9. Reproduce la fuga.
  10. Copie el registro en un archivo de texto, impórtelo a cualquier base de datos disponible (los scripts que aparecen en este artículo son para MySQL, pero se pueden adoptar fácilmente para cualquier sistema de administración de bases de datos relacionales).
  11. Compare los métodos Create y Delete (puede encontrar el script SQL en este artículo anterior) y busque los métodos sin las llamadas Delete.
  12. Establezca un punto de interrupción en API Monitor en la llamada del método requerido.
  13. Siga haciendo clic en Continuar hasta que se llame al método con los parámetros readquiridos.
  14. Cuando se llame al método con los parámetros requeridos, haga clic en Romper todo en VS.
  15. Descubra por qué este objeto no se elimina.

Espero que este artículo te sea útil y te ayude a ahorrar tiempo.