【问题标题】:SpinWait.SpinUntil taking MUCH longer than timeout to exit while waiting for Selnium element to existSpinWait.SpinUntil 在等待 Selenium 元素存在时退出的时间比超时时间长得多
【发布时间】:2021-02-22 01:16:30
【问题描述】:

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

我遇到的问题是,当页面上(根本)没有匹配元素时,它所花费的时间比指定的 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://stackoverflow.com/questions");
       var nonExistenetElementBy = By.CssSelector("#custom-header99");
       FindDisplayedElement(nonExistenetElementBy , 10);
    }

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

看起来这可能与封装在 SpinWait.WaitUntil() 中的 WebDriver.FindElements() 中内置的继承等待混合有关。

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

干杯!

【问题讨论】:

    标签: c# selenium-webdriver nunit parallel-testing spinwait


    【解决方案1】:

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

    我已将函数更改为使用 WebDriverWait(如 in this answer to a different question 所示),现在它可以正常工作了。这完全消除了使用隐式等待超时的需要。

        /// <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);
            }
        }
    

    【讨论】:

      【解决方案2】:

      那是因为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 返回 true 时检查超时是否已过期。这意味着:如果下一次旋转将导致上下文切换并且超时已过期 - 放弃并返回。但除此之外 - 继续旋转,甚至不检查超时。

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

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

      System.Threading.SpinWait 是一种轻量级同步类型, 您可以在低级场景中使用以避免昂贵的上下文 内核事件所需的开关和内核转换。 在多核计算机上,当资源预计不会保留 很长一段时间,等待线程可以更有效率 在用户模式下旋转几十或几百个周期,然后 重试获取资源。如果资源在之后可用 旋转,那么您节省了数千个周期。如果资源 仍然不可用,那么您只用了几个周期就可以 仍然进入基于内核的等待。这种旋转然后等待 组合有时被称为两阶段等待操作。

      在我看来,这些都不适用于您的情况。文档的另一部分指出“SpinWait 通常不适用于普通应用程序”。

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

      【讨论】:

      • 谢谢。这解释了我的其他发现(请参阅我提供的答案)。 C# 中是否有内置的线程友好机制用于长时间等待?
      • 另外,我应该在我的问题中解释说,我不想在每次需要等待页面上发生某些事情时自己建立超时机制。
      • 好吧,我看到你为你的情况找到了这样的机制。无法发出信号的长时间等待的情况非常罕见。一般来说,如果等待时间很长 - 另一方(执行长时间操作)应该发出完成的信号,而不是调用者必须轮询它。在这里不适用,是的,但在大多数其他情况下它是。
      猜你喜欢
      • 1970-01-01
      • 2011-03-24
      • 2017-10-17
      • 2018-02-12
      • 1970-01-01
      • 2012-06-14
      • 2019-12-02
      • 2012-10-14
      • 1970-01-01
      相关资源
      最近更新 更多