Obtener el tiempo de CPU de procesos y tareas

Introducción

Lo primero que tenemos que considerar al intentar obtener los tiempos de uso de CPU de los procesos de Windows y de sus tareas es el hecho de que windows no está, mágicamente, calculando dicho porcentaje.

En lugar de mantener un porcentaje que no le serviría para nada, Windows, para cada proceso, guarda el tiempo de CPU que este ha estado en ejecución en modo usuario, el tiempo que ha estado en ejecución en modo sistema y el instante de creación del proceso.

Dados estos datos no es posible obtener directamente el consumo de CPU instantaneo de un proceso. Podremos obtener el consumo medio de uso de CPU por parte del proceso haciendo (TiempoMedio = TiempoUsuario + TiempoSistema / TiempoActual - TiempoCreación). Podríamos decir que la medida del consumo instantaneo de CPU es como el potencial eléctrico, no existe XD.

Obteniendo información del proceso

Vale, no existe, ¿dejo de leer?. ¡No!, ahora que he conseguido que leas hasta aquí no te vayas...

Obviamente no podemos conseguir la medida instantanea de carga para un proceso en un instante dado pero si podemos calcular la carga media de la CPU entre dos instantes de tiempo, si dichos instantes de tiempo son relativamente cercanos la medición efectuada es similar a una medición instantanea, de hecho, el monitor de rendimiento de windows calcula la carga media en periodos de un segundo (y esa medida debería ser suficiente para cualquier aplicación ya que si reducimos mucho más el intervalo podemos conseguir que la llamada de consulta incremente en exceso la propia carga de la CPU, ¡una indeterminación de Heisenberg en programación!).

Obteniendo la carga de CPU del proceso en Delphi

Vale, puesto que ya tenemos claros los datos que necesitamos vamos a necesitar y como calcular nuestra carga de CPU vamos a crear una clase sencilla que nos permita calcularla. Para ello vamos a crear una clase que encapsulará la información de un proceso, en concreto y fundamentalmente necesitamos un sitio (que será un miembro privado de la clase) donde almacenar el momento en que se produjo la última llamada, de forma que podamos calcular el tiempo que ha pasado desde entonces.

type TProcessInformation = class
  private
    FLastUpdateTime : TFileTime;
    FLastWorkingTime : Int64;
    FValid : boolean;
    FHandle : THandle;
    function GetAverageCpuLoad: Cardinal;
    function GetInstantCpuLoad: Cardinal;
  protected
    FPid : Cardinal;
  public
    constructor Create(Pid : Cardinal);
    destructor Destroy; override;
    { El id del proceso }
    property Pid : Cardinal read FPid;
    { La carga de cpu media del proceso }
    property AverageCpuLoad : Cardinal read GetAverageCpuLoad;
    { La carga de cpu instantanea del proceso }
    property InstantCpuLoad : Cardinal read GetInstantCpuLoad;
    { Indica si el proceso es valido (sigue ejecutando) }
    property Valid : boolean read FValid;
end;

{ TProcessInformation }

constructor TProcessInformation.Create(Pid: Cardinal);
begin
  FPid := Pid;
  FThreadList := TThreadList.Create;
  // Abrir y obtener el handle del proceso
  FHandle := OpenProcess(PROCESS_QUERY_INFORMATION,false,Pid);
  if FHandle = 0 then
    FValid := false
  else
    FValid := true;

  Update;
end;

destructor TProcessInformation.Destroy;
var list : TList;
begin
  CloseHandle(FHandle);
  inherited;
end;

function TProcessInformation.GetAverageCpuLoad: Cardinal;
var
  lpCreationTime, lpExitTime,
  lpKernelTime, lpUserTime : TFileTime;
  workingTime,lifeTime : Int64;
  currentSystemTime : TSystemTime;
  currentFileTime : TFileTime;
  res : Double;
begin
  result := 0;
  if FValid then
  begin
    // Obtener los tiempos del proceso
    if GetProcessTimes(FHandle,lpCreationTime,lpExitTime,lpKernelTime,lpUserTime) then
    begin
      GetSystemTime(currentSystemTime);
      SystemTimeToFileTime(currentSystemTime,currentFileTime);
      lifeTime := Int64(currentFileTime) - Int64(lpCreationTime);
      workingTime := Int64(lpKernelTime) + Int64(lpUserTime);
      res := (workingTime / lifeTime);
      result := Round((res / SystemStats.SystemInfo.NumberOfProcessors) * 100);
    end
    else
      FValid := false;
  end;
end;

function TProcessInformation.GetInstantCpuLoad: Cardinal;
var
  lpCreationTime, lpExitTime,
  lpKernelTime, lpUserTime : TFileTime;
  workingTime, workingInterval,lifeInterval : Int64;
  currentSystemTime : TSystemTime;
  currentFileTime : TFileTime;
  res : Double;
begin
  result := 0;
  if FValid then
  begin
    // Obtener los tiempos del proceso
    if GetProcessTimes(FHandle,lpCreationTime,lpExitTime,lpKernelTime,lpUserTime) then
    begin
      GetSystemTime(currentSystemTime);
      SystemTimeToFileTime(currentSystemTime,currentFileTime);
      lifeInterval := Int64(currentFileTime) - Int64(FLastUpdateTime);
      workingTime := Int64(lpKernelTime) + Int64(lpUserTime);
      workingInterval := workingTime - FLastWorkingTime;
      FLastWorkingTime := workingTime;
      FLastUpdateTime := currentFileTime;
      res := (workingInterval / lifeInterval);
      result := Round((res * 100);
    end
    else
      FValid := false;
  end;
end;

Aunque el código anterior es bastante claro vamos a darle un repaso rápido. Estamos usando varias llamadas a la API de windows, a funciones situadas en el kernel.dll. La primera de ellas es GetProcessesTimes que devuelve diversa información sobre el proceso (momento de creación, de finalización así como el tiempo de uso de CPU en modo usuario y el tiempo de uso en modo kernel), por otro lado utilizamos la llamada a la funcion GetSystemTime para obtener el tiempo actual del sistema. De esta forma tenemos el tiempo de uso de CPU del proceso (tiempo de usuario + tiempo de sistema) así como el periodo en que queremos calcular la media (que será el tiempo de vida del proceso para la media de uso de cpu o el intervalo desde la última llamada para el calculo). Además hay que convertir todas las llamadas a Int64 para poder realizar los cálculos correctamente.

Hay otra llamada a la API de windows llamada GetThreadTimes que obtiene los mismos parametros pero para los distintos hilos de un proceso. El código sería practicamente el mismo pero cambiando la llamada.

Obteniendo la carga de CPU del proceso en .NET

En .Net es mucho más sencillo obtener la carga de CPU ya que el framework nos provee de una clase Process que, entre otras cosas, nos va a servir para calcular la carga de CPU produciad por el proceso.

using System.Diagnostics;

void GetAverageCPULoad()
{
  // Para el proceso actual
  Process proceso = Process.GetCurrentProcess();
  // O si lo quisieramos para cualqueir otro proceso
  // Process proceso = Process.GetProcessById(345);
  /* Obtener el intervalo de tiempo desde que
     se inicio el proceso */

  System.TimeSpan lifeInterval = (DateTime.Now - proceso.StartTime);
  // Calcular el uso de CPU
  float CPULoad = (proceso.TotalProcessorTime.TotalMilliseconds /
                  lifeInterval.TotalMilliseconds) * 100;
}

Apuntes finales

Hay dos apuntes finales que son importantes:

  • Esto solo funciona en la familia NT y posterior ya que la función GetProcessTimes solo funciona en Win NT 3.5 --> Win 2003 Server pero no en Windows 95, Windows 98 o Windows Me
  • Por otro la llamada a la función GetProcessTimes nos devuelve el tiempo que ha pasado un proceso en ejecución en una CPU, si el sistema es monoprocesador esto se corresponde con la carga total del sistema producido por dicha tarea pero si el sistema tiene más de una CPU (ojo, los nuevos Intel con HT tienen dos procesadores, lógicos si, pero dos) entonces, para obtener la carga producida por la tarea en en el total del sistema habría que dividirla por el numero de CPUs.

Hallando el numero de CPUs

function GetNumberOfProcessors : Integer;
var
  Info : TSystemInfo;
begin
  GetSystemInfo(Info);
  result := Info.dwNumberOfProcessors;
end;

Nota: La estructura TSystemInfo se que está predefinida en Delphi 2006, no he podido comprobarlo en Delphi 7, si no lo estuviera habría que definirla para que coincidiera con la estructura de windows SYSTEM_INFO

7.5
Average: 7.5 (6 votes)
Your rating: None