GCuser99 / SeleniumVBA

A comprehensive Selenium wrapper for browser automation developed for MS Office VBA running in Windows
MIT License
89 stars 18 forks source link

VBA.Timer midnight issue #67

Closed 6DiegoDiego9 closed 1 year ago

6DiegoDiego9 commented 1 year ago

During last days I kept an always-on SeleniumVBA loop running with a 2000ms sleep each run. Each morning, I found the script hung. After the nth night crash, I investigated and found that it was simply waiting for the VBA.Timer to reach an overlimit number equivalent to 24:00:02 that could never reach because at midnight VBA.Timer resets to 0.

I thought about a couple of solutions to this bug:

Since I already fine-tuned my all-purpose Sleep procedure for my master library, reaching my goal of <0.0% CPU usage, DoEvents, accuracy +-<10ms, I thought it would be a good idea to move it to the WebShared module for use by SeleniumVBA too, as in my last pull request.

I was also about to modify your WaitUntilReady procedure:

'@Description("Waits until element is interactable")
Public Function WaitUntilReady(element As WebElement, Optional ByVal maxWaitTimeMS As Long = 30000) As WebElement
    'waits until element is interactable, returns the input element for further action
    'such as "Click" on same line
    'see https://www.w3.org/TR/webdriver/#element-displayedness
    Dim startTime As Single
    Dim nowTime As Single
    Dim endTime As Single

    startTime = VBA.Timer
    nowTime = startTime
    endTime = startTime + maxWaitTimeMS / 1000#

    Do While nowTime < endTime
        If element.IsDisplayed Then Exit Do
        nowTime = VBA.Timer()
        Dim elapsedTime As Single
        If nowTime < startTime Then
            endTime = endTime - elapsedTime
            startTime = 0
        End If
        elapsedTime = nowTime - startTime
        DoEvents    'yield to other processes.
    Loop

    Set WaitUntilReady = element
End Function

using the Windows API functions, like this:

'@Description("Waits until element is interactable")
Public Function WaitUntilReady(element As WebElement, Optional ByVal maxWaitTimeMS As Long = 30000) As WebElement
    'waits until element is interactable, returns the input element for further action
    'such as "Click" on same line
    'see https://www.w3.org/TR/webdriver/#element-displayedness

    Dim cTimeStart As Currency, cTimeNow As Currency
    Dim dTimeElapsed As Currency, cMaxWaitTimeMS As Currency

    getTime cTimeStart

    Static cPerSecond As Currency
    If cPerSecond = 0 Then getFrequency cPerSecond
    cMaxWaitTimeMS = CCur(maxWaitTimeMS) * (cPerSecond / 1000)

    Do
        If element.IsDisplayed Then Exit Do
        getTime cTimeNow
        DoEvents 'yield to other processes
    Loop Until (cTimeNow - cTimeStart) >= cMaxWaitTimeMS

    Set WaitUntilReady = element
End Function

or at least using the VBA.Date to VBA.Timer solution, but I didn't understand its "If nowTime < startTime" branch: I can only think of a nowTime that is less than startTime when midnight passes, however I can't understand what the following lines accomplish. They don't seem to approach the midnight issue.

So, I leave my pull request and this last function modification for your review @GCuser99.

GCuser99 commented 1 year ago

@6DiegoDiego9 thanks for your tireless (?) work in catching the mid-night error - I have to admit that I never tested it at mid-night. Sorry about that.

I'm ok with your new Sleep and WaitUntilReady methods. For the Sleep method, what advantage does it have over just using the API sleep? Would it only be the inclusion of DoEvents? Or are there other advantages?

Also, I noticed that you commented out the Private Module statement in WebShared.bas. Did you have a compelling reason for that? I put that line in there because I didn't like having WebShared methods exposed to calling projects - seems like all of that is under-the-hood type stuff.

Thanks!

6DiegoDiego9 commented 1 year ago

API sleep: yes, the missing DoEvents was the only reason why I didn't use it, if I don't forget other reasons.


Option Private Module: hmm yes you're right, we should better manage that. Unfortunately the the "Friend" keyword doesn't work in standard modules. Although I've no problem redeclaring getFrequency, getTime in another module where I need them for all-purpose high-precision timers, I would dislike to duplicate an entire all-purpose procedure like Sleep.

Hmmm, what about making WebShared a pre-declared public class so that we have the full choice of public, friend and private keywords? To avoid to explicitly type the class name every time, I/we could then eventually put a Sleep procedure in a standard module that calls WebShared.Sleep.