Introducción

Hace poco me he encontrado en el trabajo con un programa que estaba haciendo estaba teniendo pérdidas de memoria, no demasiada cada vez pero si lo suficiente como para convertirse en un problema a medio plazo.

Cuando te encuentras un problema de consumo de memoria lo más habitual es mirar el código de la zona en la que piensas que puede estar el problema intentando encontrar el lugar donde consumes memoria que luego no vas a liberar, o bien recurrir a algún programa de monitorización de memoria que te ayude a encontrar dichos problemas (yo no he tenido suerte con ellos).

Puesto que el programa tiene demasiadas lineas de código para hacer una revisión al azar sin saber concretamente donde se producían las perdidas de memoria se me ocurrió que la forma más sencilla de acotar el problema sería ir monitorizando el uso de memoria del programa (esto así dicho suena muy facil pero en realidad no lo es tanto).

Uso de memoria

Obtener el uso de memoria real de un proceso es algo que dista de ser sencillo, en primer lugar por que no hay funciones especificas que determinen la cantidad exacta de memoria que usa un proceso y en segundo lugar porque es dificil contabilizar que memoria pertenece al proceso y que memoria no (por ejemplo una dll es una biblioteca de carga dinámica que se carga tan solo una vez en memoria de forma que la memoria consumida por la dll está o no incluida dentro de lo que consideramos espacio de memoria del proceso).

GetProcessMemoryInfo

Para conocer el uso de memoria de un programa utilizaremos dos funciones del API de windows, la primera de ellas es la función OpenProcess que nos permitirá obtener el handle (un handle es un manejador interno que usa windows para saber que y quien es que) del proceso sobre el que queremos obtener la información y la función GetProcessMemoryInfo q nos devuelve una estructura con la información sobre la memoria usada por el proceso.

function GetMemoryUsage(pid : cardinal) : DWORD;
var
  hdl : cardinal;
  pcb : PROCESS_MEMORY_COUNTERS;
begin
  result := 0;
  // Abrir el proceso
  hdl := OpenProcess(PROCESS_QUERY_INFORMATION,false,pid);
  if hdl > 0 then
  begin
    // Obtener la información de memoria
    GetProcessMemoryInfo(hdl,@pcb,sizeof(pcb));
    result := pcb.WorkingSetSize;
  end;
end;
  • Lo primero que hacemos es abrir el proceso con la directiva de seguridad PROCESS_QUERY_INFORMATION que indica que queremos consultar información sobre el proceso.
  • Una vez hecho esto llamamos a la función GetProcessMemoryInfo con el handle del proceso obtenido y la referencia de memoria de una estructura de tipo PROCESS_MEMORY_COUNTERS que la función se encargará de rellenar, el último parametro es el tamaño de la estructura.
  • Por último devolvemos el uso total de memoria del proceso que es la suma de su conjunto residente en memoria y el total de memoria virtual que está usando

PROCESS_MEMORY_COUNTERS

La estructura que rellena la función GetProcessMemoryInformation tiene varios campos, en el paso anterior hemos usado dos de ellos para obtener el total de memoria usada por el proceso pero de hecho hay mucha más información ahí que puede ser util.

typedef struct _PROCESS_MEMORY_COUNTERS {
  DWORD cb;
  DWORD PageFaultCount;
  SIZE_T PeakWorkingSetSize;
  SIZE_T WorkingSetSize;
  SIZE_T QuotaPeakPagedPoolUsage;
  SIZE_T QuotaPagedPoolUsage;
  SIZE_T QuotaPeakNonPagedPoolUsage;
  SIZE_T QuotaNonPagedPoolUsage;
  SIZE_T PagefileUsage;
  SIZE_T PeakPagefileUsage;
} PROCESS_MEMORY_COUNTERS
  • cb: Es el tamaño de la estructura.
  • PageFaultCount: Indica el numero de fallos de página que ha producido el proceso. Un fallo de página es un acceso a una parte de la memoria del proceso que no estaba en ese momento ubicada en la memoria RAM y que ha provocado que el proceso se detenga, traiga la página de donde corresponda (disco duro o swap) a la memoria principal y continue su ejecución.
  • PeakWorkingSetSize: Indica el valor máximo en bytes que ha tenido alguna vez el working set del proceso. Los valores precedidos con Peak indican siempre el valor máximo que ha alcanzado el valor al que se refieren
  • WorkingSetSize: Indica el tamaño actual del conjunto residente del proceso en bytes. El conjunto residente de un proceso es aquella parte del proceso que está actualmente en memoria RAM.
  • PageFileUsage: Indica el tamaño actual en bytes del conjunto de memoria virtual del proceso. Algunas de estas páginas pueden estar memoria.
  • PagedPool and NonPagedPool: Estos dos valores representan el tamaño ocupado por el proceso de la memoria pool paginada y no paginada (que son conceptos que darían para escribir su propio artículo y que probablemente ponga más adelante en su propio artículo)

El problema de usar este metodo estriba en que estamos interpretando que el WorkingSetSize del proceso es la memoria total que usa lo cual es falso ya que dicho dato se refierte exclusivamente a la cantidad de memoria del proceso que está actualmente ubicada en la memoria principal (RAM) del proceso pero para medir perdidas de memoria o para hacerse una idea del consumo que está haciendo un programa nos puede servir perfectamente.

Nota final

Podríamos integrar perfectamente y de forma sencilla está función en la clase TProcessInformation que ya vimos en este otro articulo sobre como [Obtener el tiempo de CPU de procesos y tareas](Obtener el tiempo de CPU de procesos y tareas «wikilink»)