☰
php-webdriver 执行时间分析:从打点到基准监控的完整实战
2026/10/20 17:47:46 网站建设 项目流程

大概半年前,我接手了一套用 php-webdriver 写的 UI 回归测试。用例量不大,六十来条,但整套跑完要四十多分钟。一开始大家都没当回事,毕竟用例里要开页面、填表单、等回调,慢一点好像也正常。直到某次发布窗口收紧,流水线光测试就卡掉了大半天,才有人正儿八经坐下来问了一句:“时间到底花在哪了?”

这一问,就把问题拆成了两个方向:一是 php-webdriver 这条链路上哪些环节在消耗时间,二是这些耗时数据怎么采集、怎么量化、怎么变成后续排查的依据。市面上聊 Selenium 框架怎么写的文章很多,但聊执行时间分析、性能监控的少,聊 PHP 版本就更少了。所以我打算把这段时间踩过的坑、用过的土办法、最终沉淀下来的分析套路,完整整理成一篇实战向的长文,给同样在维护 Web UI 自动化的朋友一个能直接照抄的参考。

这套思路至少适合三类人:写着写着发现用例又慢又稳不住的维护者,想给自动化体系搭性能监控的新手,以及需要向团队解释“用例到底慢在哪”的倒霉蛋。

1. php-webdriver 性能分析的价值与场景

1.1 为什么执行时间会逐渐失控

自动化测试跑得慢,大多数时候不是被测系统真的慢,而是测试链路本身在堆积误差。php-webdriver 的本质是 PHP 客户端向浏览器驱动发 HTTP 命令,再由驱动调用浏览器内核执行动作,整个过程一次点击就要经历“PHP 组包 -> HTTP 传输 -> 驱动解析 -> 浏览器执行 -> 结果回传”五段路程。这五段里任何一段出现波动,最终体现到业务层都是“用例变慢”。

我见过最典型的失控场景是等待策略被滥用。早期同事为了让用例稳定,习惯在关键步骤后面加硬编码 sleep,一睡就是三秒五秒。单个动作加一次看不出来,六十个用例各加十次,凭空多出半小时。更隐蔽的是隐式等待和显式等待叠加,PHP 端设置了一个超长隐式等待,某一步又用 WebDriverWait 轮询,两套机制同时在 WebDriver 协议层生效,元素没出现时实际耗时等于两者最坏情况之和。

除此之外还有浏览器版本升级、驱动版本不匹配、无头模式下的资源加载策略变化、CI 机器资源被并行任务抢占等因素。这些因素叠加起来,就让执行时间变成了一笔糊涂账。想理清这笔账,第一步不是优化代码,而是把时间拆开、量出来。

1.2 建立时间构成模型是分析的前提

做执行时间分析,我建议先建立一个“时间分片”的模型,把一次自动化操作从发出指令到拿到结果切成几个观察段。可以类比点餐过程:PHP 侧计算和组装命令是“点菜”,HTTP 和驱动传输是“传菜”,浏览器执行动作是“做菜”,协议响应返回是“上菜”,PHP 端处理结果与断言是“装盘”。顾客感知的“这顿饭久”,可能是任何一个环节拖出来的,不拆开就永远在猜。

我自己常用的是四个分片:命令发送耗时、浏览器动作耗时、协议响应耗时、结果处理耗时。命令发送和结果处理在 PHP 进程内可测量;协议响应耗时可以通过拦截 WebDriver 命令获取;浏览器动作耗时则需要借助浏览器自带的性能接口来估算。四者加总,基本覆盖了用例执行的主链路。下表是各分片常见的耗时区间与采集方式,可以参考。

时间分片常见耗时典型瓶颈采集方式
PHP 组包与调度0.1ms - 5ms日志写入、数据处理hrtime 前后打点
HTTP 命令传输1ms - 50ms网络延迟、驱动队列积压自定义 execute 拦截
浏览器端动作10ms - 3000ms渲染阻塞、等待策略Performance API
响应与结果处理0.5ms - 10ms断言与日志过大PHP 端计时

这个模型不用做得很学术,能指导排查就够了。核心原则是:先拿到细粒度数据,再谈优化。我在后面给出的所有代码和方案,都是围绕这张表展开的。

2. 执行时间采集:从粗粒度到细粒度

2.1 基础计时:别再用 time(),用 hrtime()

最早我给用例打点用的是 time(),后来发现两个问题:精度只有秒级,一个动作只要低于一秒就测不准;而且页面跳转时偶尔会有负值,容易误导。换成 hrtime(true) 之后,精度到了纳秒级,而且它是单调时钟,不受系统时间调整影响,跑一晚上数据都干净。

封装一个简单的计时器类非常容易,我贴一份可以直接用的结构:

<?php class Stopwatch { private array $points = []; public function start(string $name): void { $this->points[$name] = [ 'start' => hrtime(true), 'end' => null, ]; } public function stop(string $name): void { if (!isset($this->points[$name]) || $this->points[$name]['end'] !== null) { return; } $this->points[$name]['end'] = hrtime(true); } public function duration(string $name): float { $point = $this->points[$name] ?? null; if ($point === null || $point['end'] === null) { return 0.0; } // 转换为毫秒 return ($point['end'] - $point['start']) / 1e6; } public function snapshot(): array { $result = []; foreach ($this->points as $name => $point) { $result[$name] = [ 'start_ms' => $point['start'] / 1e6, 'duration_ms' => ($point['end'] - $point['start']) / 1e6, ]; } return $result; } }

用法也很简单,在每个动作前后各调一次。比如登录动作:

$watch->start('login'); $driver->findElement(WebDriverBy::name('username'))->sendKeys($user); $driver->findElement(WebDriverBy::name('password'))->sendKeys($pass); $driver->findElement(WebDriverBy::id('submit'))->click(); $watch->stop('login');

这套打点先落实到动作级,而不是用例级。因为用例级只能告诉你“慢”,动作级才能告诉你“哪一步慢”。我建议至少对以下动作单独打点:打开页面、登录、查询提交、列表加载、详情跳转、断言环节。等这些数据积累起来,你自然能看出哪些动作是常驻超时的。

2.2 浏览器真实耗时:用 Performance API 拿页面加载数据

PHP 侧计时能测出“调用了多久”,但测不出页面本身渲染多久。想知道页面 loading 是不是瓶颈,得问浏览器自己。php-webdriver 里所有 JS 都能通过 executeScript 执行,所以可以顺手把浏览器性能指标拉出来。

我最常用的是 window.performance.timing 里的几个关键时间点:navigationStart 是导航开始,domContentLoadedEventEnd 是 DOM 解析完成,loadEventEnd 是页面完全加载。把这三个取回来做差值,就能知道页面到底花了多久。示例代码:

$metrics = $driver->executeScript(<<<'JS' const t = window.performance.timing; return { dcl: t.domContentLoadedEventEnd - t.navigationStart, load: t.loadEventEnd - t.navigationStart, domReady: t.domContentLoadedEventEnd - t.domInteractive, }; JS);

这段脚本返回的 dcl 就是 DOM Ready 耗时,load 是页面完全加载耗时。对于前后端分离的单页系统,我一般更关注 dcl 而不是 load,因为很多懒加载资源在 load 之后还会继续请求,load 值会虚高。实测过一个后台管理系统,load 显示 4.2 秒,但界面在 1.5 秒时已经可用了,性能问题其实是接口响应慢而不是页面渲染慢。

如果还想看具体资源请求,可以用 performance.getEntriesByType('resource') 把每个 JS、CSS、图片、接口请求的时间和大小都抽出来,按耗时倒序排。这样做一次就能找到“页面卡在哪个大脚本上”。我后来就是靠这段脚本定位到一个 2MB 的埋点脚本,砍掉之后列表页加载直接下降了一秒多。

2.3 协议层拦截:精确到每个 WebDriver 命令

如果说前面两个都是“外围”采集,那拦截 WebDriver 命令就是“主链路”采集了。PHP 客户端每执行一次 findElement、click、sendKeys,本质都是发一个 HTTP 请求给驱动。把这些请求的耗时记下来,你能直接看到是哪个动作在服务器端回得慢。

实现方式很简单,继承 RemoteWebDriver,重写 execute 方法,在前后分别计时:

<?php use Facebook\WebDriver\Remote\RemoteWebDriver; class TimingWebDriver extends RemoteWebDriver { private array $commandLog = []; public function execute($command_name, $params = []) { $start = hrtime(true); try { return parent::execute($command_name, $params); } finally { $duration = (hrtime(true) - $start) / 1e6; $this->commandLog[] = [ 'command' => $command_name, 'duration_ms' => round($duration, 2), ]; } } public function getCommandLog(): array { return $this->commandLog; } }

初始化时用这个子类替代原类,比如 create 时返回 TimingWebDriver 实例,后续所有 findElement、click、get 就都会被记录。这份 commandLog 是排查瓶颈最值钱的数据:你会看到 findElement 一次要 200ms,明显异常,正常应该是个位数毫秒级;你会看到 get 命令要 3 秒,配合性能指标就能断定是页面加载慢还是驱动响应慢。

有一点要提醒:协议层耗时是“端到端”,既包含 HTTP 传输,也包含浏览器执行。它更适合用来横向比较不同命令的耗时分布,而不是精确区分浏览器本身的工作时间。要想精确定位,得结合 2.2 的 Performance API 数据一起看。

3. 定位执行瓶颈的实战技巧

3.1 等待策略是最容易忽视的隐形杀手

在自动化测试里,等待几乎是执行时间失控的第一大原因。php-webdriver 提供三种等待机制,很多项目把三种混用,结果越混越慢。

第一种是硬编码 sleep,它完全不管页面状态,固定等几秒。稳定性最差,速度也最慢。第二种是隐式等待,通过 manage()->timeouts()->implicitlyWait() 设置,作用于所有 findElement 调用,元素没找到时会轮询等待直到超时。第三种是显式等待,用一个 WebDriverWait 实例配合 expected condition,针对特定条件轮询,比如等待元素可见、可点击、文本出现。

最大的坑在于隐式等待和显式等待叠加。只要设置了隐式等待,WebDriverWait 里的每一次 findElement 也会先吃一遍隐式等待的超时时间。我踩过很疼的一次:登录页等待“欢迎语”出现,显式等待设置 10 秒,隐式等待设置的也是 10 秒,结果每次失败实际等 20 秒,整轮跑下来多了上千秒。排查半天才想起来是叠加。

所以我的建议非常明确:隐式等待最多设置 2 到 3 秒兜底,所有关键交互全部用显式等待;日志里标记每处等待实际消耗的时间,超过 1 秒就要标注原因。等后期用例稳定,隐式等待直接关掉,全部交给显式条件,速度会有肉眼可见的提升。

3.2 选择器设计对定位耗时的直接影响

很多慢用例查到最后,根因居然是选择器写得差。XPath 里 // 开头的绝对遍历非常消耗渲染进程的计算资源,尤其当页面 DOM 很深、节点很多的时候,一次 findElement 可能从几十毫秒膨胀到几百毫秒。

我用同一个页面做过一个小实验:页面上有一个表格,里面 300 个节点。用 //*[@id='table']//tr[contains(.,'目标')] 定位,平均耗时 180ms;改用 CSS 选择器 #table tr:nth-child(5),平均耗时只有 12ms。虽然绝对时间都不大,但一条用例里少说几十次 findElement,差距就完全拉开了。

从性能监控角度,我给的选择器优化原则是三条。第一,能用 id 就用 id,id 定位永远是最快的。第二,前端开发约好自定义属性,比如><?php $rows = loadAllRuns(); // 从数据库或目录加载原始记录 $groups = []; foreach ($rows as $row) { $groups[$row['action']][] = $row['duration_ms']; } foreach ($groups as $action => $durations) { sort($durations); $count = count($durations); $p50 = percentile($durations, 0.50); $p95 = percentile($durations, 0.95); $max = end($durations); printf("%-20s p50=%-8.2f p95=%-8.2f max=%-8.2f\n", $action, $p50, $p95, $max); } function percentile(array $arr, float $p): float { if (empty($arr)) { return 0.0; } $idx = (int) ceil($p * count($arr)) - 1; return $arr[$idx]; }

把这份报表贴到群里,比说一万句“最近跑得好慢”都有说服力。哪个动作超时,哪个动作在变慢,一眼就清楚了。

4.2 建立基准线和阈值告警

有了历史耗时数据,第二步就是建立基准线。我通常每次发布前跑三轮回归,把三轮中每个动作的中位数作为当前基线,存下来。下一轮跑完,直接和基线对比,超过基线的 150% 就标记为性能回归,超过 200% 直接中断流水线,方便第一时间定位。

这个 150% 和 200% 不是拍脑袋定的,而是根据历史波动的区间倒推的。纯 UI 自动化受环境波动影响,本身会有 20% 到 40% 的随机抖动,所以阈值设低了只会误报,设高了又发现不了问题。建议先积累两周数据,算出每个动作的正常波动范围,再倒推合适的百分比。我这边稳定之后用的是“p95 超过基线 p95 的 1.5 倍即失败”,误报率大约在 1% 左右。

这部分如果不想自己写,可以直接在 CI 流水线里加一个分析 job:跑完测试,读取 JSON,和上次基线对比,输出一份差异报告。报告里哪怕只说一句话“登录动作变慢 80%”,也比只贴个“测试通过”有用得多。慢,有时候比失败更值得警惕。

5. 常见问题与排查实录

5.1 PHP 端计时和 CI 平台计时对不上

有段时间我特别困惑:PHP 脚本里打点显示整套用例跑了 300 秒,但 CI 平台记录这条任务耗时 350 秒。中间差了 50 秒,哪来的?

后来把任务日志里的阶段时间拆开看才发现,差在“收尾阶段”。php-webdriver 正常跑完用例后,还需要关闭浏览器会话,也就是执行 quit 命令。这个命令会触发浏览器清理进程、关闭页面、回收资源,耗时可长可短。再加上日志上传、截图归档、驱动进程清理等外围操作,多出几十秒很正常。

所以做统计时一定要明确统计口径。我开始把所有用例收尾动作单独打点,不计入业务耗时。具体做法是:在 tearDown 里 stop('quit_and_cleanup'),然后在报表里把这段单独列出来。这样业务耗时和平台耗时之间的差异就解释清楚了,没有人再拿着两套数字扯皮。

5.2 偶发超时:崩溃、无头模式与懒加载

偶发超时是我最头疼的问题,因为它不是每次都出现,复现难度高,压测时还可能把资源耗尽直接打崩。最常见的三个根因,一个是浏览器进程异常退出,后面所有命令直接报连接错误;另一个是无头模式与有头模式渲染行为不一致,无头模式下页面懒加载策略会不同,某些资源默认不加载,导致等待条件永远不满足;还有一个是页面本身有懒加载,但等待条件只写了“元素存在”没写“元素可见”,元素挂在那里但位置和尺寸还没定,点击就点不准。

排查偶发超时,我的土办法是给 catch 块里加一个“现场快照”:发生超时或报错时,立刻截图、抓页面标题、记录当前的 URL、再拉一次 performance 指标,全塞进日志文件。跑个几天,把日志捞出来找共性,问题基本就能定位。没有现场数据的偶发问题,永远只是“偶发”,有了现场数据,大多数都能变成“必现”。

我还特别建议对长时间运行的浏览器会话做周期重启。跑四个小时后,浏览器进程的稳定性和响应速度都会下降,定期重启会话能大大降低偶发超时率。

5.3 我的几条独家避坑心得

最后聊几个常规文档里不会写的细节,都是我拿真金白银的测试时间换出来的。

第一,断言阶段不要打印大对象。有人在断言失败时把整个页面 HTML 或整个响应体 print 出来,一条日志几 MB,PHP 侧处理耗时直接翻了几倍。真要留现场,截图比打印 HTML 有用得多。

第二,尽量在脚本里避免图片对比类操作。需要做视觉回归时,把截图下载到本地再单独开进程对比,不要直接在测试主线程里做像素级比较,那会把单条用例耗时长拉高好几秒。

第三,日志系统要轻。每个动作都写一条 info 日志没问题,但不要给日志加远程上报、同步写库、对象序列化这些重操作。日志本身应该快,否则它就不是观察者,而是性能瓶颈的贡献者。

第四,所有自定义预期条件里,尽量用 JS 一次性完成状态判断,不要用“先 findElement 再 getText 再比较”这种三步走。JS 里 return document.querySelector(...).innerText === 'xxx' 一行就能完成,既省了 WebDriver 命令数,又减少了等待轮询的往返次数。

第五,顺手把浏览器的网络模拟和 CPU 降速关掉。有些项目为了模拟弱网环境,在浏览器驱动里设置了网络限速,但没区分环境,结果测试环境长期处于降速状态,性能数据完全失真。排查性能问题前,先确认没有这套人为降速配置。

我在实际项目中后期,已经把上面这套时间分片、命令日志、基准对比的机制固化成了一个小型脚本库。现在每次看到用例变慢,第一反应不是猜,而是翻命令日志,找 p95 异常的命令,配合浏览器性能指标和资源状态,通常十分钟内就能把问题锁定。自动化测试里的性能问题并不可怕,可怕的是没有数据、没有基准、全靠经验和感觉。把时间纳入自动化测试的一等公民,维护成本会肉眼可见地往下降。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询