c++desktop-applicationtimingwindows-11window-managers

Windows 11 application timing becomes uneven when "backgrounded"


I have a Windows application that is supposed to do something every 40 milliseconds precisely (actually it can be configured to anything, but 40 msec is a typical setting). It has plenty of time to do its tasks, so most of that time slice is spent idle, but it needs to report at precise intervals. We continually monitor to see whether it is taking more or less than 40 milliseconds to report in reality.

Under Windows 7, 8 or 10, it performed well. Under Windows 11, we have a new problem: the application performs well only as long as its windows are not hidden. If its windows are hidden behind the maximized window of another application (Firefox, notepad, anything), then after 5 seconds the timing becomes jagged - overshoots the 40 msec deadline, then undershoots to compensate, then overshoots again, and so on. If you alt-tab back to our application's window, you have to wait another 5 seconds but then the timing goes back to being smooth.

This happens only if the application is behind a maximized window: if the application's windows are hidden behind a non-maximized notepad window and there's just a little bit of bare desktop showing somewhere, then the timing stays smooth the whole time.

Seemingly, Windows 11's window manager is taking the decision, based on the maximization of notepad or whatever, to put our app into some kind of "background" mode. What is this mode? Is there some Windows API call my application can make, to prevent itself getting put into this mode?

We have found one way of making it worse: if we register the process using SetCurrentProcessExplicitAppUserModelID(), then we get different behavior: specifically, we get exactly 10 seconds' worth of good performance, then it goes bad and stays bad no matter whether we're in the foreground or background. That might be a clue but I'm not sure which direction it points, to fix the original problem.

In case it is relevant: our application is written in C++, compiled with MSVC, and uses Qt 6 for its windows.


Solution

  • The phenomenon appears to be called "process power throttling". The following paragraphs from Microsoft's SetProcessInformation() documentation seem relevant:

    ProcessPowerThrottling enables throttling policies on a process, which can be used to balance out performance and power efficiency in cases where optimal performance is not required.

    When a process opts into enabling PROCESS_POWER_THROTTLING_EXECUTION_SPEED, the process will be classified as EcoQoS. The system will try to increase power efficiency through strategies such as reducing CPU frequency or using more power efficient cores. EcoQoS should be used when the work is not contributing to the foreground user experience, which provides longer battery life, and reduced heat and fan noise. EcoQoS should not be used for performance critical or foreground user experiences. (Prior to Windows 11, the EcoQoS level did not exist and the process was labeled as LowQoS). If an application does not explicitly enable PROCESS_POWER_THROTTLING_EXECUTION_SPEED, the system will use its own heuristics to automatically infer a Quality of Service level. For more information, see Quality of Service.

    When a process opts into enabling PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION, any current timer resolution requests made by the process will be ignored. Timers belonging to the process are no longer guaranteed to expire with higher timer resolution, which can improve power efficiency. After explicitly disabling PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION, the system remembers and honors any previous timer resolution request by the process. By default in Windows 11 if a window owning process becomes fully occluded, minimized, or otherwise non-visible to the end user, and non-audible, Windows may automatically ignore the timer resolution request and thus does not guarantee a higher resolution than the default system resolution.

    We were actually able to fix the timing problem described in the question, by addressing just the second point:

    #ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION
    #  define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x4 // defined in Windows 11 headers but not in earlier versions
    #endif
    PROCESS_POWER_THROTTLING_STATE state = { 0 };
    state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
    state.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
    state.StateMask = 0;
    SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &state, sizeof(state));
    // (call fails in Windows 10, so just ignore the result)