Jump to content

Very slow _ProcessGetName() implementation in certain conditions - (Moved)


Go to solution Solved by argumentum,

Recommended Posts

It's my first post here, but I'll get right into it in a direct fashion - remember, I'm aiming for the same thing as you do: a better AutoIt. 😉

The _ProcessGetName() function is extremely slow when placed within a loop - well, two loops in my case, a For ... Next one nested into a Do ... Until one, getting 10 standard properties for all existing windows in the system, in order to message them to another program for displaying and further handling. The compiled 32bit script made by v.3.3.16.1 takes around 8% of my CPU when grabbing the aforementioned window properties every second, with the function clearly the culprit since the CPU usage drastically drops to around 0.9% after commenting it out and replacing it with ... the same code as in the "...\Include\Process.au3" file from the AutoIt package, but placed differently.

I suppose that the difference lies in that I'm getting the ProcessList() array inside the 1st loop but outside the 2nd, while the included function inevitably gets the array where I call the former (so everything in the 2nd loop). For the record, I only noticed that my workaround code is the same as the included one when I began a deeper investigation into what could be the cause of it. Also, for the sake of comparison, the rival AutoHotKey works flawlessly after compiling the equivalent script for its v.1 on 32bit, with only 0.5% CPU usage taken in precisely the same circumstances (but, of course, its syntax is not exactly user friendly, as you probably already know).

I can provide code samples to illustrate the issue if needed, but you can easily replicate the behavior in about 25 lines or some simple stress test anyway, based on the description above - let me know if you still need the samples though. Naturally, the question / purpose / goal of the topic is to make you aware of the problem and hopefully determine a better / more efficient implementation of the included function, in terms of speed.

P.S. There is also another probably unrelated (though apparently common, if looking for other similar topics) issue about occasional warnings / errors regarding subscripts on quickly closing windows, but I'll skip this one, I don't want to seem like I'm criticizing your otherwise fine piece of software.

Link to comment
Share on other sites

2 hours ago, Yincognito said:

inside the 1st loop but outside the 2nd, while the included function inevitably gets the array where I call the former (so everything in the 2nd loop).

English ( or any spoken language for that matter ) is not as good as code. Post an example code. It's easier in the long run to have users replicate it and try to come up with a code solution/optimization.

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting.
autoit_scripter_blue_userbar.png

Link to comment
Share on other sites

1 hour ago, Danp2 said:

Pretty sure you are going to be better off using a revised copy of the code from Process.au3, which will allow you to call ProcessList only once instead of multiple times (once for each window).

Indeed, that's what I've been using as a workaround. The idea of this post was to let folks here know of the existing problem and who knows, maybe come up with a better solution for the Process.au3 code going forward. I'm just one user and I was lucky enough to rewrite the code more efficiently before finding out that the include file used the same (but restricted to where the function was used) code, but once more people are confronted with this, a faster alternative (if possible, of course) would probably be needed. The obvious one would be to include getting the associated process list in the implementation of WinList() itself, otherwise similar scenarios to mine would make the existing _ProcessGetName() unusable in terms of speed, depending on the sleep interval.

 

45 minutes ago, argumentum said:

English ( or any spoken language for that matter ) is not as good as code. Post an example code. It's easier in the long run to have users replicate it and try to come up with a code solution/optimization.

Sure thing!

Here is the .au3 code (uncommenting the existing comments as well as commenting out the ProcessList() line and the For loop would switch to the slow built in solution):

#NoTrayIcon

#include <WinAPI.au3>
; Slow Alternative: #include <Process.au3>
#include <SendMessage.au3>

AutoItSetOption("WinTitleMatchMode", -2)

Func SendBang($szBang)
  Local Const $hWnd = WinGetHandle("[CLASS:RainmeterMeterWindow]")
  If $hWnd Then
    Local Const $iSize = StringLen($szBang) + 1
    Local Const $pMem = DllStructCreate("wchar[" & $iSize & "]")
    Local Const $pCds = DllStructCreate("dword;dword;ptr")
    Local Const $WM_COPYDATA = 0x004A
    DllStructSetData($pMem, 1, $szBang)
    DllStructSetData($pCds, 1, 1)
    DllStructSetData($pCds, 2, ($iSize * 2))
    DllStructSetData($pCds, 3, DllStructGetPtr($pMem))
    _SendMessage($hWnd, $WM_COPYDATA, 0, DllStructGetPtr($pCds))
  EndIf
EndFunc

Func GetResult()
  Local $Action = $CmdLine[1], $Separator = $CmdLine[2], $Interval = $CmdLine[3]
  Do
    Local $Output = ""
    Local $WList = WinList()
    Local $PList = ProcessList()
    Local $WTitle, $WHandle, $WClass, $WState, $WPosition, $WX, $WY, $WW, $WH, $PID, $PName
    For $i = 1 To $WList[0][0]
      $WTitle = $WList[$i][0]
      $WHandle = $WList[$i][1]
      If $WHandle Then
        $WClass = _WinAPI_GetClassName($WHandle)
        $WState = WinGetState($WHandle)
        $WPosition = WinGetPos($WHandle)
        $WX = $WPosition[0]
        $WY = $WPosition[1]
        $WW = $WPosition[2]
        $WH = $WPosition[3]
        $PID = WinGetProcess($WHandle)
        $PName = ""
        ; Slow Alternative: $PName = _ProcessGetName($PID)
        For $j = 1 to $PList[0][0]
          If $PList[$j][1] = $PID Then $PName = $PList[$j][0]
        Next
        $Output = $Output & $WTitle & @TAB & $WClass & @TAB & $WHandle & @TAB & $WState & @TAB & $WX & @TAB & $WY & @TAB & $WW & @TAB & $WH & @TAB & $PID & @TAB & $PName & $Separator
      EndIf
    Next
    SendBang(StringReplace($Action, "$Output", $Output))
    Sleep($Interval)
  Until $Interval < 0
EndFunc

GetResult()

And here is the .ahk v.1 code, for comparison (I know it isn't needed here, but that's why I pursued both, to choose the one that fares better in as many aspects as possible):

#NoTrayIcon
#Requires AutoHotkey v1.0

DetectHiddenWindows, On

SendBang(ByRef szBang)
{
  TargetWindowClass := "RainmeterMeterWindow"
  iSize := (StrLen(szBang) + 1) * (A_IsUnicode ? 2 : 1)
  VarSetCapacity(Cds, 3 * A_PtrSize, 0)
  NumPut(1, Cds)
  NumPut(iSize, Cds, A_PtrSize)
  NumPut(&szBang, Cds, 2 * A_PtrSize)
  SendMessage, 0x4a, 0, &Cds,, ahk_class %TargetWindowClass%
  return ErrorLevel
}

GetResult()
{
  Action := A_Args[1], Separator := A_Args[2], Interval := A_Args[3]
  Loop
  {  
    Output := ""
    WinGet, windows, List
    Loop, % windows
    {
      WHandle := windows%A_Index%
      WinGetTitle, WTitle, % "ahk_id " WHandle
      WinGetClass, WClass, % "ahk_id " WHandle
      WinGetPos, WX, WY, WW, WH, % "ahk_id " WHandle
      WinGet, PID, PID, % "ahk_id " WHandle
      WinGet, PName, ProcessName, % "ahk_id " WHandle
      WinGet, WState, MinMax, % "ahk_id " WHandle
      Output := % Output WTitle "`t" WClass "`t" WHandle "`t" WState "`t" WX "`t" WY "`t" WW "`t" WH "`t" PID "`t" PName Separator
    }
    SendBang(StrReplace(Action, "$Output", Output))
    Sleep Interval
    If (Interval < 0)
      Break
  }
}

GetResult()

 

You can disregard the rest of the code if you want, it's just sending a message to another program (Rainmeter), where I can display these in a "skin" / "widget" and send various other messages to desired windows from this list via a WindowMessagePlugin in order to control some music player, some HWiNFO sensor window and other similar productive / entertainment apps in the system (yes, I know I can do this directly from an .au3 / .ahk script, but like this I don't have to bother with creating the interface anymore). The chosen script is run with 3 parameters from the said application, i.e. a specific action / "bang" / command to send data back to the skin, a separator that's usually the newline character, and the interval at which this list is sent to the skin in milliseconds (-1 if only one pass through the outer loop is desired).

Link to comment
Share on other sites

$PName = _ProcessGetName_FromTasklist($PID, $aPids)

Func _ProcessGetName_FromTasklist($PID, ByRef $aPids, $iLoop = 0) ; pass $aPids as a string to init the array
    If UBound($aPids) < 2 Then
        Local $hTimer = TimerInit(), $iLines = 0, $aTemp, $sOutput, $iPID = Run(@ComSpec & ' /C tasklist /fo list', @TempDir, @SW_HIDE, $STDOUT_CHILD)
        ProcessWaitClose($iPID)
        $aTemp = StringSplit(StdoutRead($iPID), @CRLF, 1)
        Dim $aPids[UBound($aTemp)][6]
        For $n = 2 To $aTemp[0] Step 6
            $iLines += 1
            $aPids[$iLines][0] = justTheData($aTemp[$n + 0])
            $aPids[$iLines][1] = justTheData($aTemp[$n + 1])
            $aPids[$iLines][2] = justTheData($aTemp[$n + 2])
            $aPids[$iLines][3] = justTheData($aTemp[$n + 3])
            $aPids[$iLines][4] = justTheData($aTemp[$n + 4])
            $aPids[$iLines][5] = justTheData($aTemp[$n + 5])
        Next
        ReDim $aPids[$iLines + 1][6]
        $aPids[0][0] = $iLines
        ConsoleWrite('> $iLines >' & $iLines & @TAB & TimerDiff($hTimer) & @CRLF)
    EndIf
    For $n = 1 To $aPids[0][0]
        If $PID = $aPids[$n][1] Then Return $aPids[$n][0]
    Next
    If $iLoop Then Return ""
    $aPids = "" ; refresh the list only if not found ; you're not starting processes every second
    Return _ProcessGetName_FromTasklist($PID, $aPids, $iLoop + 1)
EndFunc

Func justTheData($sStr)
    Return StringStripWS(StringTrimLeft($sStr, StringInStr($sStr, ":") + 1), 3)
EndFunc

..it's faster than one by one. CPU wise, is not much better.

Edit: added to refresh the array only on not found. Makes it easier on the CPU.

Edited by argumentum
better code

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting.
autoit_scripter_blue_userbar.png

Link to comment
Share on other sites

  • Moderators

Moved to the appropriate forum.

Moderation Team

Public_Domain.png.2d871819fcb9957cf44f4514551a2935.png Any of my own code posted anywhere on the forum is available for use by others without any restriction of any kind

Open spoiler to see my UDFs:

Spoiler

ArrayMultiColSort ---- Sort arrays on multiple columns
ChooseFileFolder ---- Single and multiple selections from specified path treeview listing
Date_Time_Convert -- Easily convert date/time formats, including the language used
ExtMsgBox --------- A highly customisable replacement for MsgBox
GUIExtender -------- Extend and retract multiple sections within a GUI
GUIFrame ---------- Subdivide GUIs into many adjustable frames
GUIListViewEx ------- Insert, delete, move, drag, sort, edit and colour ListView items
GUITreeViewEx ------ Check/clear parent and child checkboxes in a TreeView
Marquee ----------- Scrolling tickertape GUIs
NoFocusLines ------- Remove the dotted focus lines from buttons, sliders, radios and checkboxes
Notify ------------- Small notifications on the edge of the display
Scrollbars ----------Automatically sized scrollbars with a single command
StringSize ---------- Automatically size controls to fit text
Toast -------------- Small GUIs which pop out of the notification area

 

Link to comment
Share on other sites

@argumentum: Thank you very much for the code. I'm on my phone ATM, but I'll try it when I get back to my laptop. On a first look, I agree that while it may be a faster approach to iterate the array, in practice the CPU usage won't drop drastically given the tasklist disk access and string manipulation. I'll never know until I try it though, so I might be wrong.

@Melba23: Thanks for moving the topic appropriately. As someone new to this forum, I posted this in the technical discussion thinking it's the AutoIt built-in function for this that first needs help in this specific context, and only then myself (just my honest assessment based on evidence, not criticizing in any way), but if you say so...

Link to comment
Share on other sites

19 hours ago, argumentum said:
$PName = _ProcessGetName_FromTasklist($PID, $aPids)

Func _ProcessGetName_FromTasklist($PID, ByRef $aPids, $iLoop = 0) ; pass $aPids as a string to init the array
    If UBound($aPids) < 2 Then
        Local $hTimer = TimerInit(), $iLines = 0, $aTemp, $sOutput, $iPID = Run(@ComSpec & ' /C tasklist /fo list', @TempDir, @SW_HIDE, $STDOUT_CHILD)
        ProcessWaitClose($iPID)
        $aTemp = StringSplit(StdoutRead($iPID), @CRLF, 1)
        Dim $aPids[UBound($aTemp)][6]
        For $n = 2 To $aTemp[0] Step 6
            $iLines += 1
            $aPids[$iLines][0] = justTheData($aTemp[$n + 0])
            $aPids[$iLines][1] = justTheData($aTemp[$n + 1])
            $aPids[$iLines][2] = justTheData($aTemp[$n + 2])
            $aPids[$iLines][3] = justTheData($aTemp[$n + 3])
            $aPids[$iLines][4] = justTheData($aTemp[$n + 4])
            $aPids[$iLines][5] = justTheData($aTemp[$n + 5])
        Next
        ReDim $aPids[$iLines + 1][6]
        $aPids[0][0] = $iLines
        ConsoleWrite('> $iLines >' & $iLines & @TAB & TimerDiff($hTimer) & @CRLF)
    EndIf
    For $n = 1 To $aPids[0][0]
        If $PID = $aPids[$n][1] Then Return $aPids[$n][0]
    Next
    If $iLoop Then Return ""
    $aPids = "" ; refresh the list only if not found ; you're not starting processes every second
    Return _ProcessGetName_FromTasklist($PID, $aPids, $iLoop + 1)
EndFunc

Func justTheData($sStr)
    Return StringStripWS(StringTrimLeft($sStr, StringInStr($sStr, ":") + 1), 3)
EndFunc

..it's faster than one by one. CPU wise, is not much better.

Edit: added to refresh the array only on not found. Makes it easier on the CPU.

Well, what do you know... it's actually faster than my workaround - well done! It takes around 0.77% of the CPU, so it's better than the 0.9% of my method. I take back what I said earlier after just seeing it, it seems it's actually quite practical. Taking one of the ideas from your code (that I didn't apply earlier to mine), exiting the For loop early once a matching PID is found appears to produce the same effect (same 0.77% CPU as a maximum):

Do
    ...
    Local $PList = ProcessList()
    ...
    For ...
      ...
        $PName = ""
        For $j = 1 to $PList[0][0]
            If $PList[$j][1] = $PID Then
              $PName = $PList[$j][0]
              ExitLoop
            EndIf
        Next
      ...
    Next
    ...
  Until ...

Not sure if sorting the array first (or using a dictionary?) would make it even faster, given the array's simplicity. What do you guys think?

Edited by Yincognito
Further clarification.
Link to comment
Share on other sites

1 hour ago, argumentum said:

I wouldn't but try all the ideas that come to mind. Go figure. We may just learn something interesting :)

So true. Well, I just made myself a PNames[] map out of the ProcessList() array from the outer loop, and got rid entirely of the For from the inner loop:

Func GetResult()
  Local $Action = $CmdLine[1], $Separator = $CmdLine[2], $Interval = $CmdLine[3]
  Do
    Local $Output = ""
    Local $WList = WinList()
    Local $PList = ProcessList()
    Local $WTitle, $WHandle, $WClass, $WState, $WPosition, $WX, $WY, $WW, $WH, $PID, $PName, $PNames[]
    For $j = 1 to $PList[0][0]
      $PNames[$PList[$j][1]] = $PList[$j][0]
    Next
    For $i = 1 To $WList[0][0]
      $WTitle = $WList[$i][0]
      $WHandle = $WList[$i][1]
      If $WHandle Then
        $WClass = _WinAPI_GetClassName($WHandle)
        $WState = WinGetState($WHandle)
        $WPosition = WinGetPos($WHandle)
        $WX = $WPosition[0]
        $WY = $WPosition[1]
        $WW = $WPosition[2]
        $WH = $WPosition[3]
        $PID = WinGetProcess($WHandle)
        $PName = $PNames[$PID]
        $Output = $Output & $WTitle & @TAB & $WClass & @TAB & $WHandle & @TAB & $WState & @TAB & $WX & @TAB & $WY & @TAB & $WW & @TAB & $WH & @TAB & $PID & @TAB & $PName & $Separator
      EndIf
    Next
    SendBang(StringReplace($Action, "$Output", $Output))
    Sleep($Interval)
  Until $Interval < 0
EndFunc

Result: 0.26% CPU usage. Now the code's speed is even better than the AutoHotkey one (twice as fast)! 😎

By the way, in case maps are by any chance removed from AutoIt going forward (hopefully not, judging by the above), declaring $PNames[4294967295] also works... 🤣

EDIT: I still have to deal with the pesky "Subscript used on non-accessible variable" error though - any chance of getting rid of it? Or should I make another topic just for that?

AutoItSubscriptError.jpg.7d80105c72594de40ab57cb8bebbd635.jpg

P.S. Suggestion: if the _ProcessGetName() function from <Process.au3> stays the same in the future, some note in the Remarks section of its page should mention users to be aware of its high CPU usage when using it in multiple loops updated frequently and attempt alternative implementations. Just in case others stumble upon this effect in their scripts. 😉

Edited by Yincognito
Added related error.
Link to comment
Share on other sites

...
  Local $Output, $WList, $PList
  Local $WTitle, $WHandle, $WClass, $WState, $WPosition, $WX, $WY, $WW, $WH, $PID, $PName, $PNames
  Do
    $Output = ""
    $WList = WinList()
    $PList = ProcessList()
    Dim $PNames[]
...

..so you don't declare (Local/Global) in a loop.

As far as maps, they have been there for quite some time. No user here foresees them going away.

37 minutes ago, Yincognito said:

P.S. Suggestion: if the _ProcessGetName() function ...

Func _ProcessGetName($iPID)
    Local $aProcessList = ProcessList()
    For $i = 1 To UBound($aProcessList) - 1
        If $aProcessList[$i][1] = $iPID Then
            Return $aProcessList[$i][0]
        EndIf
    Next
    Return SetError(1, 0, "")
EndFunc   ;==>_ProcessGetName

Do look at the UDFs. They are not magical. In this case you're writing a _ProcessGetNameEx_MyWay(), so to say :) 

Edit: oops

Edited by argumentum
oops

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting.
autoit_scripter_blue_userbar.png

Link to comment
Share on other sites

42 minutes ago, Yincognito said:

EDIT: I still have to deal with the pesky "Subscript used on non-accessible variable" error though - any chance of getting rid of it?

Well, that is an error on your logic ... somewhere. Do write a separate script to debug that function. Is not a bad idea. But once you've got the bug figured out, re-incorporate the function back to the main script. No need to have 2 executables.

Edited by argumentum
spelling

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting.
autoit_scripter_blue_userbar.png

Link to comment
Share on other sites

48 minutes ago, argumentum said:

..so you don't declare (Local/Global) in a loop.

Good idea, thanks (was unsure about the Dim / Redim impact, so tried to avoid it earlier).

48 minutes ago, argumentum said:

As far as maps, they have been there for quite some time. No user here foresees them going away.

Great news, thanks for letting me know. I actually wanted to ask this, by the way, but you read my mind.

48 minutes ago, argumentum said:

Do look at the UDFs. They are not magical. In this case you're writing a _ProcessGetNameEx_MyWay(), so to say :) 

I don't think I wrote that, since the function body is split between the loops in my codes, which is precisely why it's faster. Unfortunately, you can't do that with an UDF. So if I make an UDF out of the above, it will be equally slow, since ProcessList() will be called in the inner loop, i.e. where I need the process name.

37 minutes ago, argumentum said:

Well, that is an error on your logic ... somewhere. Do write a separate script to debug that function. Is not a bad idea. But once you've got the bug figured out, re-incorporate the function back to the main script. No need to have 2 executables.

That is the whole point of the "If $WHandle Then" part above. Despite the code being quite straightforward, I tried lots of ways to make the error go away to no avail, and short of having a check on every array via IsArray / IsObj or similar (which is preposterous), I can't figure out why or where it happens. Not sure if it's an logical error on my part, since the error happens randomly / occasionally making it difficult to identify the culprit (and for roughly the same approach in AutoHotKey, it didn't happen once). Anyway, I'll probably get to the bottom of it, I'll just comment out stuff part by part and be done with it (logically speaking, just about the only place where it could happen given the already clear WList iteration, is when accessing the WinGetPos() elements, which could be on occasion less than 4?)...

 

Edited by Yincognito
Link to comment
Share on other sites

  • Solution
47 minutes ago, Yincognito said:

That is the whole point of the "If $WHandle Then" part above.

The handle should use "IsHWnd($WHandle)". But the error is in the array. I would use "if ubound($array) < 2 then ContinueLoop", hence the Sleep() would be placed up higher, in case of a ContinueLoop.
I do test the return value (an array in this case), even if the there is no @error return in the function, I test for failure regardless. That's what I do.

51 minutes ago, Yincognito said:

I don't think I wrote that, since the function body is ...

That function is in the include. Just because is an include/udf/whatnot, does not mean that is efficient for the use case in the code.

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting.
autoit_scripter_blue_userbar.png

Link to comment
Share on other sites

1 hour ago, argumentum said:

Well, that is an error on your logic ... somewhere.

Turns out that it's an error alright, just not on my logic. I "reverse engineered" the line number where the error occurs (simulated an error on my last line and then subtracted the difference between the error messages' line numbers), and it's exactly where I thought it would be: when accessing the WinGetPos() elements - specifically, the Y. So basically, WinGetPos() will sometimes yield the X, i.e. WinGetPos()[0], but not the Y (and probably not the other coordinates either) - so your solution to check the ubound() of the array is spot on - thanks again.

Still, it's against the documentation of the function, which states that the return value is always a 4 element array, except when the window is not found (which can't be true, since I iterate precisely through all existing windows in the first place, and secondly because the "X" or whatever of such a window is actually returned without any error)...

Link to comment
Share on other sites

Link to comment
Share on other sites

4 hours ago, argumentum said:

The handle should use "IsHWnd($WHandle)". But the error is in the array. I would use "if ubound($array) < 2 then ContinueLoop", hence the Sleep() would be placed up higher, in case of a ContinueLoop.
I do test the return value (an array in this case), even if the there is no @error return in the function, I test for failure regardless. That's what I do.

2 hours ago, Nine said:

Hmm. doubt that.  Unless you can make a repro that we can actually run, my take is that the window does not exist anymore when you try to get its position.

1 hour ago, Danp2 said:

Check @error following the call to WinGetPos.

Well, I understand your point of view since I've been in a similar position helping and without replicating and testing things yourself is hard to both believe and provide a solution. Following @argumentum's reply about the "If $WHandle Then" part being futile I got rid of it, only assigned WPosition[] array values to my variables "if UBound($WPosition) = 4" and still got errors about subscripts being either "non-accessible" or "badly formatted" in the $WPosition[N] lines, despite accessing the array specifically avoided in the code if its size was under 4 (I even spawned my own MsgBox telling me the lower size to verify if I succeeded in bypassing the error).

However, after applying both parts of @argumentum's advice, it "seems" that the issue was solved (must test this thoroughly though). This is how my code looks like now:

#NoTrayIcon

#include <WinAPI.au3>
#include <SendMessage.au3>

AutoItSetOption("WinTitleMatchMode", -2)

Func SendBang($szBang)
  Local Const $hWnd = WinGetHandle("[CLASS:RainmeterMeterWindow]")
  If $hWnd Then
    Local Const $iSize = StringLen($szBang) + 1
    Local Const $pMem = DllStructCreate("wchar[" & $iSize & "]")
    Local Const $pCds = DllStructCreate("dword;dword;ptr")
    Local Const $WM_COPYDATA = 0x004A
    DllStructSetData($pMem, 1, $szBang)
    DllStructSetData($pCds, 1, 1)
    DllStructSetData($pCds, 2, ($iSize * 2))
    DllStructSetData($pCds, 3, DllStructGetPtr($pMem))
    _SendMessage($hWnd, $WM_COPYDATA, 0, DllStructGetPtr($pCds))
  EndIf
EndFunc

Func GetResult()
  Local $Action = $CmdLine[1], $Separator = $CmdLine[2], $Interval = $CmdLine[3]
  Local $Output, $WList, $PList, $PNames, $ACount, $WTitle, $WHandle, $WClass, $WState, $WPosition, $WX, $WY, $WW, $WH, $PID, $PName
  Do
    Dim $Output = "", $WList = WinList(), $PList = ProcessList(), $PNames[]
    For $j = 1 to $PList[0][0]
      $PNames[$PList[$j][1]] = $PList[$j][0]
    Next
    For $i = 1 To $WList[0][0]
      $WTitle = $WList[$i][0]
      $WHandle = $WList[$i][1]
      If IsHWnd($WHandle) Then
        $WClass = _WinAPI_GetClassName($WHandle)
        $WState = WinGetState($WHandle)
        $WPosition = WinGetPos($WHandle)
        Dim $WX = -32000, $WY = -32000, $WW = -32000, $WH = -32000
        If (IsArray($WPosition)) And (UBound($WPosition) = 4) Then
          Dim $WX = $WPosition[0], $WY = $WPosition[1], $WW = $WPosition[2], $WH = $WPosition[3]
        EndIf
        $PID = WinGetProcess($WHandle)
        $PName = $PNames[$PID]
        $Output = $Output & $WTitle & @TAB & $WClass & @TAB & $WHandle & @TAB & $WState & @TAB & $WX & @TAB & $WY & @TAB & $WW & @TAB & $WH & @TAB & $PID & @TAB & $PName & $Separator
      EndIf
    Next
    SendBang(StringReplace($Action, "$Output", $Output))
    Sleep($Interval)
  Until $Interval < 0
EndFunc

GetResult() ; Editor Line 55 = Binary Line 13069

Yeah, the repro is a bit difficult to provide. I could provide the packed Rainmeter skin itself (containing the source scripts and their compiled versions), which can be installed with a double click once Rainmeter is installed as well, but I'm not sure if you'll manage to "determine" the script to throw the error (I quickly navigate between two browser tabs, trigger the context menu, select a few lines to copy them, and then open my Winamp only to quickly close it - I know, it's an entire "ritual", lol). Indeed, the window probably doesn't exist by that point, it's just that it's a bit inconvenient to always check for such things... in a loop that iterates only between existing windows to begin with (via the 1st For loop), if you know what I mean. One might ask what's the point in running that loop if you still have to check something that the loop supposedly already avoided.

P.S. I will let you know if these error(s) make a comeback to haunt me again. So far so good, thanks to @argumentum - fingers crossed. 🙂

Link to comment
Share on other sites

  • 2 weeks later...

As promised, I return to let you know that the results of my tests indicate that I indeed have to check for errors on each line to entirely get rid of the subscript errors, as hinted earlier as well in the post I'll mark as solution (yes, I know there are some redundancies in the code, but they are intentional). Even a single If encompassing multiple WinGet... assignments is not enough since the amount of work done inside it has an impact on the errors occurrence. That being said, it's not that bad, since apparently using If IsHWnd(...) Then ... is much more resource friendly than having If @error Then ContinueLoop after each line (the former has the same CPU usage whether the data retrieval interval is 25 ms or 1000 ms, while the latter naturally takes more CPU when on 25 ms):

#NoTrayIcon
#include <WinAPI.au3>
#include <SendMessage.au3>

Func SendBang($Bang)
  Local Const $hWnd = WinGetHandle("[CLASS:RainmeterMeterWindow]")
  If $hWnd Then
    Local Const $iLen = StringLen($Bang) + 1
    Local Const $pMem = DllStructCreate("wchar[" & $iLen & "]")
    Local Const $pCds = DllStructCreate("dword;dword;ptr")
    Local Const $WM_COPYDATA = 0x004A
    DllStructSetData($pMem, 1, $Bang)
    DllStructSetData($pCds, 1, 1)
    DllStructSetData($pCds, 2, ($iLen * 2))
    DllStructSetData($pCds, 3, DllStructGetPtr($pMem))
    _SendMessage($hWnd, $WM_COPYDATA, 0, DllStructGetPtr($pCds))
  EndIf
EndFunc

Func GetResult()
  Local $aAct = $CmdLine[1], $aSep = $CmdLine[2], $aInt = $CmdLine[3]
  Do
    Local $sOut = "", $pMap[], $pLis = ProcessList(), $wLis = WinList()
    For $j = 1 to $pLis[0][0]
      $pMap[$pLis[$j][1]] = $pLis[$j][0]
    Next
    For $i = 1 To $wLis[0][0]
      Local $hWnd = $wLis[$i][1], $pNum = -1, $pNam = -1, $wSty = -1, $wSet = -1, $wTit = -1, $wHan = -1, $wCla = -1, $wSta = -1, $wPos = -1, $wPro = -1
      If IsHWnd($hWnd) Then Dim $wHan = WinGetHandle($hWnd)
      If IsHWnd($wHan) Then Dim $wTit = WinGetTitle($wHan)
      If IsHWnd($wHan) Then Dim $wCla = _WinAPI_GetClassName($wHan)
      If IsHWnd($wHan) Then Dim $wSty = WinGetState($wHan), $wSta = BitAND($wSty,1)/1 & @TAB & BitAND($wSty,2)/2 & @TAB & BitAND($wSty,4)/4 & @TAB & BitAND($wSty,8)/8 & @TAB & BitAND($wSty,16)/16 & @TAB & BitAND($wSty,32)/32
      If IsHWnd($wHan) Then Dim $wSet = WinGetPos($wHan), $wPos = $wSet[0] & @TAB & $wSet[1] & @TAB & $wSet[2] & @TAB & $wSet[3]
      If IsHWnd($wHan) Then Dim $pNum = WinGetProcess($wHan), $pNam = $pMap[$pNum], $wPro = $pNum & @TAB & $pNam
      If IsHWnd($WHan) Then $sOut &= StringSplit($sOut, $aSep, 1)[0] & @TAB & $wTit & @TAB & $wCla & @TAB & $wHan & @TAB & $wSta & @TAB & $wPos & @TAB & $wPro  & @TAB & $aSep
    Next
    SendBang(StringReplace($aAct, "$Output", $sOut))
    Sleep($aInt)
  Until $aInt < 0
EndFunc

GetResult()

Overall, I'm happy with the results and grateful for the help I got here from everybody. I couldn't keep the CPU at the 0.26% I got earlier, but it's not that far at between 0.39% and 0.77% with all the error checking and everything (the StringSplit() understandably adds a bit more up to 1% on occasion, of course). I will mark @argumentum's reply about the error checking as the solution, because he led me to the most efficient path to fix these errors and was instrumental in me finding the dictionary approach that helped replace the _ProcessGetName() function when it came to the ID and name coupling. Thanks again!

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...