Page 1 of 1

[Solved] User idle detection problem

Posted: Sun Feb 25, 2018 10:21 pm
by Dude
[Edit] Solved - See my post further below. :)

I need my app to know when the user hasn't used the PC (ie. typed or used the mouse) for 60 seconds. However, the small test code below gives wrong results: waiting for 5000 ms actually results in 5800 or 6100 ms, and waiting for 60000 ms (one minute) results in 70000 ms instead (a massive 10 seconds too long). The plan is that my app will call GetIdleTime() once per minute in a thread, and if unused, the thread will write something to a log.

Important note: The code below on PureBasic v5.20 (32-bit) works as expected and the timing is correct. But I'm using PureBasic v5.62 (32-bit). I'm guessing the issue is something to do with GetTickCount_()? My PC hasn't been running for 49.7 days yet. :)

Code: Select all

Procedure GetIdleTime()
  lipi.LASTINPUTINFO\cbSize=SizeOf(LASTINPUTINFO)
  GetLastInputInfo_(@lipi)
  ProcedureReturn (GetTickCount_()-lipi\dwTime)
EndProcedure

Debug "Press Ctrl to start timing..."

Repeat
  Sleep_(1)
Until GetAsyncKeyState_(#VK_CONTROL)<>0

Debug "Timing (don't use your PC)..."

start.q=ElapsedMilliseconds()

Repeat
  Sleep_(1)
Until GetIdleTime()>5000

Debug Str(ElapsedMilliseconds()-start)+" ms actually passed :("

Re: User idle detection problem

Posted: Sun Feb 25, 2018 11:20 pm
by Franky666
I don't have Windows so I can't test that.

But looking into the MSDN documentation of these two API functions might be helpful here:

https://msdn.microsoft.com/de-de/librar ... s.85).aspx
The elapsed time is stored as a DWORD value. Therefore, the time will wrap around to zero if the system is run continuously for 49.7 days. To avoid this problem, use the GetTickCount64 function. Otherwise, check for an overflow condition when comparing times.
They shifted the value to use the full 32-bit DWORD range (the full negative value is the zero point). The API equivalent to ElapsedMiliseconds() is GetTickCount64() (I believe).

https://msdn.microsoft.com/en-us/librar ... s.85).aspx
The tick count when the last input event was received (see LASTINPUTINFO) is not guaranteed to be incremental. In some cases, the value might be less than the tick count of a prior event. For example, this can be caused by a timing gap between the raw input thread and the desktop thread or an event raised by SendInput, which supplies its own tick count.
This could explain your issue.

Probably, Windows throttles the input thread a bit if there is no input for longer time. That would cause bigger differences.

If you need it bit more accurate, you could try to compare between return values of GetLastInputInfo() instead of comparing it to the tick clock. As long as there is no change between calls, there was no input since that time.
Do your own time measuring to check the difference (like you do it to measure the runtime of the main loop). Everytime there is a difference between GetLastInputInfo() calls, reset the measuring (startTime = ElapsedMilliseconds()). If your own time measuring reaches 5000ms, then the user is idle. :mrgreen:


Comparing clock values across different threads tends to be inaccurate.

Edit: I made my idea into code. But it is with a simulated API call, because I don't have Windows to use the real thing.

Code: Select all

OpenWindow(0,0,0,320,200,"test bla bla")
Global lastInputTimeStamp.q = ElapsedMilliseconds()
Procedure.q LastInputTime()
  ProcedureReturn lastInputTimeStamp
EndProcedure
  
thisInputThing.q = 0
lastInputThing.q = LastInputTime()
startTimer.q = ElapsedMilliseconds()

Repeat
  thisInputThing = LastInputTime()
  If lastInputThing <> thisInputThing  
    ;there was input, reset the timer
    startTimer.q = ElapsedMilliseconds()
    lastInputThing = thisInputThing
  EndIf
  
  Debug ElapsedMilliseconds() - startTimer
  WaitWindowEvent(1)
  
  If WindowMouseX(0) <> -1 And WindowMouseY(0) <> -1 And (WindowMouseX(0) <> lastWindowMouseX Or WindowMouseY(0) <> lastWindowMouseY)
    lastInputTimeStamp = ElapsedMilliseconds() - Random(500000,100) ;make the time artifically inaccurate
    lastWindowMouseX = WindowMouseX(0)
    lastWindowMouseY = WindowMouseY(0)
  EndIf
Until ElapsedMilliseconds() - startTimer >= 5000
Debug "Tadaaaa!!!"
__________________________________________________
URL tags added
06.03.2018
RSBasic

Re: User idle detection problem

Posted: Mon Feb 26, 2018 4:06 am
by BasicallyPure
Dude wrote:waiting for 5000 ms actually results in 5800 or 6100 ms, and waiting for 60000 ms (one minute) results in 70000 ms instead (a massive 10 seconds too long).
Hi,
Perhaps this 'AccuTimer' trick would help solve your problem.

http://www.purebasic.fr/english/viewtop ... 12&t=55513

Re: User idle detection problem

Posted: Mon Feb 26, 2018 5:07 am
by nco2k
Sleep_() is pretty inaccurate by default. in order to improve the accuracy, you could use timeBeginPeriod_() and timeGetTime_() instead of GetTickCount_(), as it produces more predictable results. or of course you could stick with ElapsedMilliseconds(), which uses QueryPerformanceCounter_(). however, using timeBeginPeriod_(timecaps\wPeriodMin) will cause the scheduler to switch between tasks more frequently, and therefore could have a negative impact on battery life and overall system performance. it also depends greatly on the used hardware and operating system. and last but not least, running with the debugger enabled, also affects the results.

there are lots of ways how to achieve this, especially when dealing with threads. you can find a lot of information here on the forum and on msdn. every method has pros and cons. do some research and decide for yourself.

Code: Select all

Global MMTimerResolution.l

Procedure StopMMTimerPeriod()
  If MMTimerResolution
    timeEndPeriod_(MMTimerResolution)
    MMTimerResolution = 0
  EndIf
EndProcedure

Procedure StartMMTimerPeriod()
  Protected Result, timecaps.TIMECAPS
  If timeGetDevCaps_(@timecaps, SizeOf(TIMECAPS)) = #MMSYSERR_NOERROR
    If timecaps\wPeriodMin = 0
      timecaps\wPeriodMin = 1
    EndIf
    If timeBeginPeriod_(timecaps\wPeriodMin) = #TIMERR_NOERROR
      MMTimerResolution = timecaps\wPeriodMin
      Result = MMTimerResolution
    EndIf
  EndIf
  ProcedureReturn Result
EndProcedure

Define PBTimer.q, MMTimer.l

If StartMMTimerPeriod()
  
  Debug "TimerResolution: "+Str(MMTimerResolution)+" ms"
  
  PBTimer = ElapsedMilliseconds()
  
  MMTimer = timeGetTime_()
  Repeat
    Sleep_(1)
  Until timeGetTime_() - MMTimer >= 5000
  
  PBTimer = ElapsedMilliseconds() - PBTimer
  
  Debug "ElapsedMilliseconds: "+Str(PBTimer)
  
  StopMMTimerPeriod()
  
EndIf : End
as for the idle detection itself, this thread has some pretty interesting approaches: https://www.codeproject.com/Articles/91 ... nd-without

c ya,
nco2k

Re: User idle detection problem

Posted: Mon Feb 26, 2018 12:37 pm
by Dude
[Solved] Thanks to all who replied. :)

I was able to use your posts to come up with this working solution. On my PC, it correctly shows 60000 when I do a 60-second idle test, instead of 70000. :)

Code: Select all

Global idlestamp.q,idlebefore=-1,idlenow

Procedure HasBeenIdleFor(ms)
  lipi.LASTINPUTINFO\cbSize=SizeOf(LASTINPUTINFO)
  GetLastInputInfo_(@lipi)
  idlenow=lipi\dwTime
  If idlenow<>idlebefore
    idlebefore=idlenow
    idlestamp=ElapsedMilliseconds()+ms
  ElseIf ElapsedMilliseconds()>idlestamp
    result=1
  EndIf
  ProcedureReturn result
EndProcedure

Debug "Press Ctrl to start timing..."

Repeat
  Sleep_(1)
Until GetAsyncKeyState_(#VK_CONTROL)<>0

Debug "Timing (don't use your PC)..."

start.q=ElapsedMilliseconds()

Repeat
  Sleep_(1)
Until HasBeenIdleFor(5000)

Debug Str(ElapsedMilliseconds()-start)+" ms"