SpinWait.Spin - пока не потребуется НАМНОГО больше времени, чем таймаут, для выхода в ожидании существования элемента селния

У меня есть относительно простой способ дождаться, пока элемент не появится и не отобразится. Метод обрабатывает ситуацию, когда для данного By возвращается более одного элемента (обычно мы ожидаем отображения только одного из них, но в любом случае метод вернет первый найденный отображаемый элемент).

Проблема, с которой я сталкиваюсь, заключается в том, что, когда на странице нет соответствующего элемента (вообще), он занимает больше времени *, чем указано в TimeSpan, и я не могу понять, почему.

* Я только что тестировал с таймаутом 30 секунд, и это заняло чуть больше 5 минут.

код:

    /// <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://stackru.com/questions");
       var nonExistenetElementBy = By.CssSelector("#custom-header99");
       FindDisplayedElement(nonExistenetElementBy , 10);
    }

Если вы запустите тест (с таймаутом 10 секунд), вы обнаружите, что для фактического выхода требуется около 100 секунд.

Похоже, это может иметь какое-то отношение к сочетанию ожидания наследования, встроенного в WebDriver.FindElements(), заключенного внутри SpinWait.WaitUntil().

Хотел бы услышать, что вы думаете об этой загадке.

Ура!

2 ответа

Решение

Проведя дополнительное тестирование, я обнаружил, что уменьшение времени ожидания неявного ожидания 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);
        }
    }

Это потому что 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;
}

Отметьте условие под комментарием "ЗДЕСЬ" выше. Он только проверяет, истек ли тайм-аут, если spinWait.NextSpinWillYieldвозвращает истину. Это означает следующее: если следующее вращение приведет к переключению контекста и истечет время ожидания - сдавайтесь и возвращайтесь. А в противном случае - продолжайте крутить, даже не проверив таймаут.

NextSpinWillYieldрезультат зависит от количества предыдущих вращений. В основном эта конструкция вращает X количество раз (я полагаю, 10), затем начинает уступать (отдавать текущий временной отрезок потока другим потокам).

В вашем случае условие внутри SpinUntilтребуется ОЧЕНЬ много времени для оценки, что полностью противоречит дизайну SpinWait - он ожидает, что оценка состояния вообще не займет времени (и там, где SpinWait действительно применим, это правда). Допустим, в вашем случае одна оценка состояния занимает 5 секунд. Затем, даже если тайм-аут равен 1 секунде, он сначала будет вращаться 10 раз (всего 50 секунд), прежде чем даже проверять тайм-аут. Это потому, что SpinWait не предназначен для того, для чего вы пытаетесь его использовать. Из документации:

System.Threading.SpinWait - это облегченный тип синхронизации, который можно использовать в низкоуровневых сценариях, чтобы избежать дорогостоящих переключений контекста и переходов ядра, которые требуются для событий ядра. На многоядерных компьютерах, когда ожидается, что ресурс не будет удерживаться в течение длительного периода времени, может быть более эффективным, чтобы ожидающий поток вращался в пользовательском режиме в течение нескольких десятков или нескольких сотен циклов, а затем повторил попытку получить ресурс.. Если ресурс доступен после отжима, то вы сэкономили несколько тысяч циклов. Если ресурс по-прежнему недоступен, значит, вы потратили всего несколько циклов и все еще можете войти в режим ожидания на основе ядра. Эту комбинацию вращения и ожидания иногда называют двухфазной операцией ожидания.

На мой взгляд, все это не применимо к вашей ситуации. В другой части документации говорится, что "SpinWait обычно не используется для обычных приложений".

В этом случае с таким большим временем оценки условия - вы можете просто запустить его в цикле без дополнительного ожидания или вращения и вручную проверять, истек ли тайм-аут на каждой итерации.

Другие вопросы по тегам