SpinWait.SpinUntil 在等待 Selenium 元素存在时花费比超时长得多的时间退出

SpinWait.SpinUntil taking MUCH longer than timeout to exit while waiting for Selnium element to exist

我有一个相对简单的方法来等待元素存在并显示。该方法处理多个元素被 return 给定 By 的情况(通常我们只希望显示其中一个,但无论如何该方法将 return 找到的第一个显示元素).

我遇到的问题是,当页面上(根本没有)匹配元素时,它花费的时间比指定的 TimeSpan 多*,我不知道为什么。

*我刚刚测试了30s的超时,用了5m多一点

代码:

    /// <summary>
    /// Returns the (first) element that is displayed when multiple elements are found on page for the same by
    /// </summary>
    public static IWebElement FindDisplayedElement(By by, int secondsToWait = 30)
    {
        WebDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(secondsToWait);
        // Wait for an element to exist and also displayed
        IWebElement element = null;
        bool success = SpinWait.SpinUntil(() =>
        {
            var collection = WebDriver.FindElements(by);
            if (collection.Count <= 0)
                return false;
            element = collection.ToList().FirstOrDefault(x => x.Displayed == true);
            return element != null;
        }
        , TimeSpan.FromSeconds(secondsToWait));

        if (success)
            return element;
        // if element still not found
        throw new NoSuchElementException("Could not find visible element with by: " + by.ToString());
    }

你可以这样称呼它:

    [Test]
    public void FindDisplayedElement()
    {
       webDriver.Navigate().GoToUrl("https://whosebug.com/questions");
       var nonExistenetElementBy = By.CssSelector("#custom-header99");
       FindDisplayedElement(nonExistenetElementBy , 10);
    }

如果你 运行 测试(超时 10 秒),你会发现实际退出大约需要 100 秒。

看起来它可能与 WebDriver.FindElements() 中内置的继承等待的混合包含在 SpinWait.WaitUntil() 中。

想听听你们对这个难题的看法。

干杯!

那是因为SpinWait.WaitUntil大致实现如下:

public static bool SpinUntil(Func<bool> condition, TimeSpan timeout) {
    int millisecondsTimeout = (int) timeout.TotalMilliseconds;
    long num = 0;
    if (millisecondsTimeout != 0 && millisecondsTimeout != -1)
        num = Environment.TickCount;
    SpinWait spinWait = new SpinWait();
    while (!condition())
    {
        if (millisecondsTimeout == 0)
            return false;
        spinWait.SpinOnce();
        // HERE
        if (millisecondsTimeout != -1 && spinWait.NextSpinWillYield && millisecondsTimeout <= (Environment.TickCount - num))
            return false;
    }
    return true;
}

注意上面“此处”评论下的条件。它只检查超时是否已过期 IF spinWait.NextSpinWillYield returns true。这意味着:如果下一次旋转将导致上下文切换并且超时已过期 - 放弃并 return。但除此之外 - 甚至不检查超时就继续旋转。

NextSpinWillYield 结果取决于之前的旋转次数。基本上这个构造旋转了 X 次(我相信是 10 次),然后开始屈服(放弃当前线程时间片给其他线程)。

在你的情况下,SpinUntil 中的条件需要很长时间来评估,这完全违反了 SpinWait 的设计 - 它期望条件评估根本不需要时间(并且 SpinWait 实际适用的地方 - 这是真的).假设在您的情况下,一次条件评估需要 5 秒。然后,即使超时是 1 秒 - 它会先旋转 10 次(总共 50 秒),然后再检查超时。那是因为 SpinWait 不是为您尝试使用它的目的而设计的。来自 documentation:

System.Threading.SpinWait is a lightweight synchronization type that you can use in low-level scenarios to avoid the expensive context switches and kernel transitions that are required for kernel events. On multicore computers, when a resource is not expected to be held for long periods of time, it can be more efficient for a waiting thread to spin in user mode for a few dozen or a few hundred cycles, and then retry to acquire the resource. If the resource is available after spinning, then you have saved several thousand cycles. If the resource is still not available, then you have spent only a few cycles and can still enter a kernel-based wait. This spinning-then-waiting combination is sometimes referred to as a two-phase wait operation.

我认为

None其中适用于您的情况。文档的另一部分指出“SpinWait 通常对普通应用程序没有用”。

在这种情况下,如果条件评估时间如此之长 - 您可以 运行 循环进行,无需额外等待或旋转,并在每次迭代时手动检查超时是否已过期。

做一些进一步的测试,我发现将 WebDriver 隐式等待超时减少到一个较低的数字(例如 100 毫秒)可以解决这个问题。这对应于解释 @Evk 为什么使用 SpinUntil 不起作用。

我已将函数改为使用 WebDriverWait(如图 ),现在它可以正常工作了。这完全消除了使用隐式等待超时的需要。

    /// <summary>
    /// Returns the (first) element that is displayed when multiple elements are found on page for the same by
    /// </summary>
    /// <exception cref="NoSuchElementException">Thrown when either an element is not found or none of the found elements is displayed</exception>
    public static IWebElement FindDisplayedElement(By by, int secondsToWait = DEFAULT_WAIT)
    {
        var wait = new WebDriverWait(WebDriver, TimeSpan.FromSeconds(secondsToWait));
        try
        {
            return wait.Until(condition =>
            {
                return WebDriver.FindElements(by).ToList().FirstOrDefault(x => x.Displayed == true);
            });
        }
        catch (WebDriverTimeoutException ex)
        {
            throw new NoSuchElementException("Could not find visible element with by: " + by.ToString(), ex);
        }
    }