Appium 事件时序(Event Timings):用appium:eventTimings与 Events API 精确度量会话与命令耗时
【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium
Appium 内置了一套事件时序(Event Timings)采集机制,可以在会话过程中记录"何时发生了什么"以及"每条命令耗时多长",是定位会话启动慢、命令执行慢等性能问题的利器。本文基于packages/appium/docs/zh/guides/event-timing.md展开,结合packages/base-driver源码与测试,完整讲解appium:eventTimings能力、events响应结构、自定义事件上报以及常见分析用法,读完即可在自己的 Appium 测试工程中落地一套命令级耗时统计方案。
一、概述:Appium 能记录哪些时序信息
Appium 具备记录两类时序信息的能力:
- 会话启动(startup)阶段的节点事件:例如"新会话被请求"、"新会话已启动",每个事件会记录其发生的精确时间戳;
- 命令(command)执行耗时:会话期间执行的每一条 Appium 内部命令,都会记录其开始处理与结束处理的时间。
这是一个高级(advanced)功能,需要显式开启。其开关是一个名为appium:eventTimings的 capability,将其设为true即可在会话期间采集事件时序。
注意:从当前仓库的会话能力文档(packages/appium/docs/zh/reference/session/caps.md)可以看到,
appium:eventTimings目前被标记为deprecated(已弃用),官方建议改用getLogEvents端点来获取事件时序数据。不过该 capability 的语义、响应结构及数据格式仍然与下方讲解一致,理解它可以帮你解读历史测试数据与老代码。
二、开启能力:appium:eventTimingscapability
在创建会话时,通过 desired capabilities 传入该能力即可:
{ "platformName": "Android", "appium:automationName": "UiAutomator2", "appium:eventTimings": true }开启后,调用POST /session/:id/appium/events(即各语言客户端中的driver.logs.events或类似方法,具体名称视客户端而定)时,其响应会被额外装饰(decorate)一个events属性,其中包含会话截至目前的所有事件时序数据。
客户端调用示意
以 JS 客户端(WebdriverIO / Appium 官方客户端)为例,典型调用路径是:
// 通过 logs API 获取事件时序 const events = await driver.logs.events('session');(不同客户端的方法名可能略有差异,请以你所使用客户端文档为准。)
重要前提:只能拿到"已经发生"的事件
需要特别强调:/session/:id/appium/events只返回在调用发生那一刻之前已经记录的事件。因此:
- 想获取整个会话的完整时序,最佳调用时机是即将退出会话(quit)之前;
- 在会话中途多次调用,可以拿到会话不同阶段的增量数据。
三、events响应结构详解
开启 capability 后,/session/:id/appium/events的响应会被装饰为如下结构(events属性):
{ "<event_type>": [<occurence_timestamp_1>, ...], "commands": [ { "cmd": "<command_name>", "startTime": <js_timestamp>, "endTime": <js_timestamp> } ] }events属性内部包含两类子属性:
- 以事件类型命名(event type)的属性:其值为一个时间戳数组,记录该事件每次发生的时间。因为同一事件在一个会话中可能发生多次(例如会话期间多次重启应用、多次触发某自定义事件),所以用数组保存全部发生时刻。
commands属性:固定存在的命令执行记录,是一个对象数组,每个对象包含:cmd:Appium 内部命令名(例如click、findElement等);startTime:命令开始处理的时间戳(JavaScript 毫秒时间戳);endTime:命令处理结束的时间戳(JavaScript 毫秒时间戳)。
事件类型示例
Appium 基础驱动内置了几类会话级事件。在 packages/base-driver/lib/basedriver/driver.ts 中可以看到这些事件的常量定义:
| 事件常量 | 事件类型名(event_type) | 触发时机 |
|---|---|---|
EVENT_SESSION_INIT | newSessionRequested | 新会话被请求(创建会话命令开始执行时) |
EVENT_SESSION_START | newSessionStarted | 新会话启动完成(创建会话命令执行完毕时) |
EVENT_SESSION_QUIT_START | quitSessionRequested | 退出会话被请求(删除会话命令开始执行时) |
EVENT_SESSION_QUIT_DONE | quitSessionFinished | 退出会话完成(删除会话命令执行完毕时) |
这些事件在executeCommand的入口与出口分别通过this.logEvent(...)记录(见 driver.ts 与 driver.ts),因此newSessionRequested与newSessionStarted两个时间戳之间的差值,就是一次新会话从被请求到完全建成的耗时。
事件类型并非只有内置的几种
需要明确:各个 driver 会自定义自己的事件类型,因此官方无法提供一份穷举清单。最可靠的做法是——在真实会话中实际调用一次events端点,直接检查返回的响应,即可看到该 driver 在当前环境下暴露的全部事件类型。
四、数据能用来做什么
拿到events数据后,可以做很多有价值的分析:
- 计算事件之间的时间间隔:例如
newSessionStarted减去newSessionRequested,得到建会话耗时;quitSessionFinished减去quitSessionRequested,得到退出会话耗时; - 还原严格的事件时间线:把所有事件的时间戳按时间排序,还原整个会话从创建到销毁的完整时间轴;
- 统计某类命令的平均耗时:对
commands数组按cmd分组,计算endTime - startTime的均值、最大值、最小值,找出性能瓶颈命令。
例如,一段简单的伪代码思路(具体语言以你的测试框架为准):
// 按命令名聚合耗时统计 const timings = {}; for (const c of events.commands) { const duration = c.endTime - c.startTime; timings[c.cmd] = timings[c.cmd] || []; timings[c.cmd].push(duration); } // 之后即可计算各命令的平均耗时、P95 等指标五、添加自定义事件(Custom Event)
除了内置事件,Appium 还允许你自定义事件,并将其纳入事件时序数据:
- 通过Log Custom Event API(
POST /session/:sessionId/appium/log_event)向 Appium 服务器发送一个自定义事件名,服务器会为其记录当前时间戳; - 之后通过Get Log Events API(
POST /session/:sessionId/appium/events)即可取回这些命名事件的时间戳。
自定义事件在时序数据中同样以"事件类型名 → 时间戳数组"的形式出现,事件类型名为vendor:event的拼接格式。
底层实现:事件如何被存储
在 packages/base-driver/lib/basedriver/commands/event.ts 中可以看到两个核心方法的实现:
async logCustomEvent<C extends Constraints>(this: BaseDriver<C>, vendor: string, event: string): Promise<void> { this.logEvent(`${vendor}:${event}`); }, async getLogEvents<C extends Constraints>( this: BaseDriver<C>, type: string | string[], ): Promise<Partial<EventHistory>> { if (util.isEmpty(type)) { return this.eventHistory; } const typeList = Array.isArray(type) ? type : [type]; return Object.entries(this.eventHistory).reduce<Partial<EventHistory>>((acc, [eventType, eventTimes]) => { if (typeList.includes(eventType)) { acc[eventType] = eventTimes; } return acc; }, {}); },要点:
logCustomEvent将vendor与event以冒号拼接成vendor:event后写入事件历史,vendor 前缀用于命名空间隔离,避免不同工具/厂商的自定义事件互相冲突;getLogEvents支持按type过滤:不传或传空值返回全部事件;传字符串返回单个事件类型;传数组可同时过滤多个事件类型(精确匹配事件类型名,不是子串匹配)。
路由定义
这两个 API 在协议层面对应的路由定义位于 packages/base-driver/lib/protocol/routes/appium.ts:
'/session/:sessionId/appium/events': { POST: {command: 'getLogEvents', payloadParams: {optional: ['type']}}, }, '/session/:sessionId/appium/log_event': { POST: { command: 'logCustomEvent', payloadParams: {required: ['vendor', 'event']}, }, },可以看出:vendor与event是logCustomEvent的必填参数,而getLogEvents的type为可选参数。
六、源码级佐证:事件与命令耗时如何被记录
命令时序的采集点
在 packages/base-driver/lib/basedriver/driver.ts 的executeCommand主流程中可以看到:
- 命令开始执行前,先取
const startTime = Date.now();(第 88 行); - 命令执行完成后,再取
const endTime = Date.now();(第 173 行); - 将
{cmd, startTime, endTime}压入this._eventHistory.commands数组(第 179 行)。
也就是说,每条 Appium 内部命令的startTime/endTime覆盖的是从命令进入驱动执行队列到处理完成的完整耗时,这与events响应中commands数组的元素结构一一对应。类型定义见 packages/types/lib/commands/basedriver.ts:
export interface EventHistory { commands: EventHistoryCommand[]; [key: string]: any; } export interface EventHistoryCommand { cmd: string; startTime: number; endTime: number; }会话事件在命令前后被写入
同样在executeCommand中:
- 命令进入前,若为
createSession则记录newSessionRequested,若为删除会话则记录quitSessionRequested(driver.ts); - 命令完成后,若为
createSession则记录newSessionStarted,若为删除会话则记录quitSessionFinished(driver.ts)。
另外,当appium:eventTimings开启时,GET /session/:id的会话信息响应中还会附带events字段(见 driver.ts),这是历史行为;官方在事件时序文档(event-timing.md)中特别提示:过去事件数据曾作为GET /session/:id响应的一部分返回,现在已不再这样做,请统一通过 events 端点获取。
测试验证
事件相关逻辑有完善的单元测试覆盖,见 packages/base-driver/test/unit/basedriver/commands/event.spec.ts:
logCustomEvent('myorg', 'myevent')后,事件历史中会出现myorg:myevent键(第 12-13 行),印证了vendor:event的存储格式;getLogEvents()不带参数返回全部事件,包括内置事件与自定义事件(第 15-22 行);getLogEvents('testCommand')只返回匹配该类型的事件(第 36-43 行);- 传入数组
['testCommand', 'testCommand2']可同时过滤多个类型(第 52-61 行); - 过滤是精确匹配:传入
testCommandDummy不会匹配到testCommand(第 45-50 行),传入不存在的noEventName返回空对象(第 72-77 行)。
这些测试用例同时给出了getLogEvents过滤语义的权威说明:类型名必须精确匹配(全等),既不是前缀也不是子串匹配。
七、配套工具:Appium 官方事件解析器
Appium 团队维护了一个事件时序解析工具(event timings parser),可直接用于从事件时序输出中生成各类报告:appium/appium-event-parser(详见 event-timing.md)。
使用思路是:先把events端点返回的原始时序数据保存下来,再交给该解析器分析。它适合批量产出报表的场景,例如将多条测试运行的事件时序汇总成统计报告,用于定位"哪条命令最耗时""哪次会话启动最慢"等。
八、实战建议与小结
综合以上内容,给出几条落地建议:
- 开启方式:创建会话时设置
appium:eventTimings: true;新项目更推荐直接使用getLogEvents端点,因为 capability 已被标记为 deprecated; - 采集时机:在
quit之前调用 events 端点,以获取整个会话的完整事件与命令时序; - 分析维度:事件时间戳数组用于分析会话生命周期各阶段耗时;
commands数组用于统计各类命令的平均耗时与异常耗时,定位性能瓶颈; - 自定义埋点:在关键业务步骤前后调用
logCustomEvent(vendor, event),把业务语义(如"登录完成""数据加载完成")写入时序,便于结合命令耗时还原完整的用户路径时间线; - 批量报表:需要持续产出性能报告时,使用官方 event-parser 工具处理导出的时序数据。
事件时序功能让 Appium 从"只能断言功能是否通过"升级为"还能量化每一步花了多久",是性能分析、CI 稳定性排查与容量评估中非常实用的能力。
【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考