Windows 7: Coalescing timers

Bajo este título tan raro se encuentra escondida una de las características más interesantes de Windows 7 para desarrollar aplicaciones energéticamente eficientes.

Conforme la potencia de cálculo de los procesadores aumenta, también aumenta su consumo energético, y por supuesto el software juega un papel importante en este consumo. Así que lo que veremos en este post es algunas de las características más interesantes de Windows 7 para hacer aplicaciones energéticamente eficientes.

Los procesadores actuales incluyen funcionalidad que les permite establecerse en un estado de Power Ilde, en el que el procesador entra en un estado de bajo consumo de energía hasta que es interrumpido por alguna razón. La velocidad de los procesadores actuales hace que la medida de su tiempo comparada con la nuestra sea completamente abismal, ya que para nosotros un simple segundo es más que la eternidad para un procesador. Así que teniendo en cuenta este escenario y el consumo de CPU de la mayoría de los programas y del kernel en sí, el escenario más común a optimizar por la mayoría de las aplicaciones es el Idle. Es decir tenemos que hacer aplicaciones que respeten al máximo el estado de bajo consumo energético del procesador haciendo así que el procesador no esté constantemente saliendo y entrando de ese modo de bajo consumo energético, esto es así porque las constantes interrupciones hacen que se consuma más energía en esos cambios de estado que la energía que se ahora por estar el procesador en el estado de bajo consumo energético.

Sabiendo esto como podemos, los desarrolladores, hacer aplicaciones que no “molesten” tanto al procesador. Pongo molesten al procesador porque no se trata de no hacer funcionalidad por ahorrar energía, más quisiéramos, sino de intentar aprovechar al máximo las características de consumo de energía de Windows 7. Aquí tenemos algunas opciones.

· Evitar el uso de poolling, usar eventos, es decir en vez de están constantemente consultando una API para saber si un valor se ha establecido o se ha cambiado usar un evento de Windows para que se nos notifique ese cambio.

· No hacer poolling para conocer el estado energético del sistema, usar RegisterPowerSettingNotification

HPOWERNOTIFY WINAPI RegisterPowerSettingNotification(  
  __in  HANDLE hRecipient, 
  __in  LPCGUID PowerSettingGuid, 
  __in  DWORD Flags
);

· En el caso de que no se pueda usar un evento nunca hacer polling más a menudo que un segundo.

Para el caso de que tengamos que hacer poolling constantemente Windows 7 incluye una API nueva para hacer temporizadores agrupados (coalescing timers). Veamos que son estos temporizadores agrupados.

clip_image004

clip_image006

En las dos gráficas podemos ver fechas de dos colores verdes y naranjas. Las fechas verdes que son constantes en el tiempo representan los eventos periódicos del temporizador del sistema, del kernel. Las flechas naranjas representan los eventos de temporizador personalizados, que se producen en el sistema, es decir temporizadores que los programadores programan. Lo que podemos ver entre estas dos graficas en que en la gráfica de abajo, que representa a Windows 7, podemos ver como los temporizadores naranjas se han alineado con los temporizadores verdes, es decir ahora los eventos se retrasan para agruparse cuando el sistema se levanta del estado de Ilde. Esto permite que entre evento y evento de color verder (kernel), el sistema se encuentre en completo idle y así que procesador pueda descansar durante ese periodo de tiempo, y solo cuando el procesador necesita ser “levantado” para ejecutar el evento del sistema (verde) aprovechamos y ejecutamos los eventos personalizados (naranjas) que tengamos pendientes.

Como podéis ver es una solución muy ingeniosa para optimizar nuestro escenario más habitual que como dijimos antes es el Idle del pc. Esto no significa que tengamos que cambiar todos nuestros temporizadores a temporizadores agrupados (coalescing timers), sino que si estamos haciendo alguna tarea en segundo plano, como monitorizar una web, o un dispositivo, podemos usar estos temporizadores para hacer nuestra aplicación más óptima con el consumo energético. En cualquier otro caso podemos seguir usando los temporizadores normales.

Este tipo de temporizadores es muy util para usarse en servicios de sistema que necesitan monitorizar algo en segundo plano.

Para crear un temporizador tenemos que llamar a la función CreateWaitableTimerEx

HANDLE WINAPI CreateWaitableTimerEx(  
    __in_opt  LPSECURITY_ATTRIBUTES lpTimerAttributes,  
    __in_opt  LPCTSTR lpTimerName,  
    __in      DWORD dwFlags, 
    __in      DWORD dwDesiredAccess
);

Una vez llamado tenemos que llamar a la función SetWaitableTimerEx para inicializar el temporizador.

BOOL SetWaitableTimerEx(  
    __in  HANDLE hTimer, 
    __in  const LARGE_INTEGER *lpDueTime, 
    __in  LONG lPeriod,  
    __in  PTIMERAPCROUTINE pfnCompletionRoutine, 
    __in  LPVOID lpArgToCompletionRoutine,  
    __in  PREASON_CONTEXT WakeContext,  
    __in  ULONG TolerableDelay
);

Quiero resaltar en esta función SetWaitableTimerEx, que el último parámetro ULONG TolerableDelay es el tiempo máximo en el que el temporizador puede ser retrasado para poder ser agrupado, así podemos indicar un tiempo por el cual el temporizador puede esperar sin problema, pasado este tiempo el sistema ejecutará el temporizador como un temporizador normal.

O podemos abrir un temporizador existente llamando a OpenWaitableTimer

HANDLE WINAPI OpenWaitableTimer(  
    __in  DWORD dwDesiredAccess,  
    __in  BOOL bInheritHandle,  
    __in  LPCTSTR lpTimerName
);

Para los programadores administrador (.NET) incluyo código de una clase que implementa esta funcionalidad:

public sealed class CoalescingTimer : IDisposable
    {
      private IntPtr _timer;
    
      public CoalescingTimer(string name)
      {
          _timer = UnsafeNativeMethods.CreateWaitableTimerEx(IntPtr.Zero, name, 0, TIMER_MODIFY_STATE);
      }
    
      public event EventHandler Tick;
    
      public void Set(Int64 dueTime, int period, string reason, uint tolerableDelay)
      {
          UnsafeNativeMethods.POWER_REQUEST_CONTEXT reasonContext = new UnsafeNativeMethods.POWER_REQUEST_CONTEXT();
          reasonContext.Version = UnsafeNativeMethods.POWER_REQUEST_CONTEXT_VERSION;
          reasonContext.Flags = UnsafeNativeMethods.POWER_REQUEST_CONTEXT_SIMPLE_STRING;
          reasonContext.SimpleReasonString = reason;
          _registeredDelegate = new CoalescingTimerProc(OnTimer);
          bool success = UnsafeNativeMethods.SetWaitableTimerEx(_timer, ref dueTime, period, _registeredDelegate, IntPtr.Zero, ref reasonContext, tolerableDelay);
      }
    
      private CoalescingTimerProc _registeredDelegate;
    
      private void OnTimer(IntPtr argument, uint timerLowValue, uint timerHighValue)
      {
          if (Tick != null)
              Tick(this, EventArgs.Empty);
      }
    
      const uint TIMER_MODIFY_STATE = 0x0002;
    
      public void Dispose()
      {
          UnsafeNativeMethods.CloseHandle(_timer);
      }
    }
    internal delegate void CoalescingTimerProc(
      IntPtr argument,
      uint timerLowValue,
      uint timerHighValue);
    
    internal sealed class UnsafeNativeMethods
    {
      [DllImport("kernel32.dll")]
      public static extern IntPtr CreateWaitableTimerEx(
          IntPtr lpSecurityAttributes,
          string timerName,
          uint flags,
          uint desiredAccess);
    
      [DllImport("kernel32.dll")]
      public static extern bool SetWaitableTimerEx(
          IntPtr hTimer,
          ref Int64 dueTime,
          int period,
          CoalescingTimerProc completionRoutine,
          IntPtr argument,
          ref UnsafeNativeMethods.POWER_REQUEST_CONTEXT reasonContext,
          uint tolerableDelay);
    
      [DllImport("kernel32.dll")]
      public static extern bool SetWaitableTimerEx(
          IntPtr hTimer,
          ref Int64 dueTime,
          int period,
          CoalescingTimerProc completionRoutine,
          IntPtr argument,
          ref UnsafeNativeMethods.POWER_REQUEST_CONTEXT_DETAILED reasonContext,
          uint tolerableDelay);
    
      [DllImport("kernel32.dll")]
      [return: MarshalAs(UnmanagedType.Bool)]
      public static extern bool CloseHandle(IntPtr handle);
      [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
      public struct POWER_REQUEST_CONTEXT
      {
          public UInt32 Version;
          public UInt32 Flags;
          [MarshalAs(UnmanagedType.LPWStr)]
          public string SimpleReasonString;
      }
  }

Luis.

3 comentarios sobre “Windows 7: Coalescing timers”

  1. Que caña de caracterísica. La clase que has puesto me va a venir muy bien, tengo alguna solución que hace uso muy intensivo de timers. Eso sí, tendré que abstraer el que maneje la situación de no estar en Windows 7, supongo que no será muy complicado que la clase pase a usar timers ‘normales’ si estamos en Vista o XP.

    Además supongo que con el tiempo acabarán abstrayendo este tema en la propias clases de timers del Framework.

    Por último, he estado jugueteando con tu clase y he tenido que añadir algunas declaraciones en la clase UnsafeNativeMethods que me da que te has comido al pegar en el blog. Las he sacado del SDK y creo que sean correctas, por si alguien las necesita, aunque no las he provado a fondo:

    public const int POWER_REQUEST_CONTEXT_VERSION = 0;
    public const int POWER_REQUEST_CONTEXT_SIMPLE_STRING = 0x1;
    public const int POWER_REQUEST_CONTEXT_DETAILED_STRING = 0x2;

    [DllImport(“kernel32.dll”)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool CloseHandle(IntPtr handle);
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct POWER_REQUEST_CONTEXT
    {
    public UInt32 Version;
    public UInt32 Flags;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string SimpleReasonString;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct PowerRequestContextDetailedInformation
    {
    public IntPtr LocalizedReasonModule;
    public UInt32 LocalizedReasonId;
    public UInt32 ReasonStringCount;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string[] ReasonStrings;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct POWER_REQUEST_CONTEXT_DETAILED
    {
    public UInt32 Version;
    public UInt32 Flags;
    public PowerRequestContextDetailedInformation DetailedInformation;
    }

    ¡Un saludo titán!

  2. Por cierto, para que la clase se comporte correctamente en Vista o XP, supongo que el camino sería controlar la excepción EntryNotFoundException y para a usar un timer normal en lugar de estas nuevas API.

    ¿Se te ocurre algo mejor?… Seguro que después de unas pintas algo se te ocurre jejejeje…

    ¡Un abrazo!

  3. Es cierto Rodrigo !!, es lo que tiene copiar y pegar.
    En principio esa clase es para que funcione en Windows 7 directamente, supongo que tendrás que utilizar un Shims o tendrás que chequear la versión de Windows, aunque esto último no es lo más recomendable.

    Saludos. Luis.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *