Appium 事件时序(Event Timings):用 `appium:eventTimings` 与 Events API 精确度量会话与命令耗时
2026/9/13 8:07:59 网站建设 项目流程

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属性内部包含两类子属性:

  1. 以事件类型命名(event type)的属性:其值为一个时间戳数组,记录该事件每次发生的时间。因为同一事件在一个会话中可能发生多次(例如会话期间多次重启应用、多次触发某自定义事件),所以用数组保存全部发生时刻。
  2. commands属性:固定存在的命令执行记录,是一个对象数组,每个对象包含:
    • cmd:Appium 内部命令名(例如clickfindElement等);
    • startTime:命令开始处理的时间戳(JavaScript 毫秒时间戳);
    • endTime:命令处理结束的时间戳(JavaScript 毫秒时间戳)。

事件类型示例

Appium 基础驱动内置了几类会话级事件。在 packages/base-driver/lib/basedriver/driver.ts 中可以看到这些事件的常量定义:

事件常量事件类型名(event_type触发时机
EVENT_SESSION_INITnewSessionRequested新会话被请求(创建会话命令开始执行时)
EVENT_SESSION_STARTnewSessionStarted新会话启动完成(创建会话命令执行完毕时)
EVENT_SESSION_QUIT_STARTquitSessionRequested退出会话被请求(删除会话命令开始执行时)
EVENT_SESSION_QUIT_DONEquitSessionFinished退出会话完成(删除会话命令执行完毕时)

这些事件在executeCommand的入口与出口分别通过this.logEvent(...)记录(见 driver.ts 与 driver.ts),因此newSessionRequestednewSessionStarted两个时间戳之间的差值,就是一次新会话从被请求到完全建成的耗时。

事件类型并非只有内置的几种

需要明确:各个 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 APIPOST /session/:sessionId/appium/log_event)向 Appium 服务器发送一个自定义事件名,服务器会为其记录当前时间戳;
  • 之后通过Get Log Events APIPOST /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; }, {}); },

要点:

  • logCustomEventvendorevent以冒号拼接成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']}, }, },

可以看出:vendoreventlogCustomEvent必填参数,而getLogEventstype为可选参数。

六、源码级佐证:事件与命令耗时如何被记录

命令时序的采集点

在 packages/base-driver/lib/basedriver/driver.ts 的executeCommand主流程中可以看到:

  1. 命令开始执行前,先取const startTime = Date.now();(第 88 行);
  2. 命令执行完成后,再取const endTime = Date.now();(第 173 行);
  3. {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端点返回的原始时序数据保存下来,再交给该解析器分析。它适合批量产出报表的场景,例如将多条测试运行的事件时序汇总成统计报告,用于定位"哪条命令最耗时""哪次会话启动最慢"等。

八、实战建议与小结

综合以上内容,给出几条落地建议:

  1. 开启方式:创建会话时设置appium:eventTimings: true;新项目更推荐直接使用getLogEvents端点,因为 capability 已被标记为 deprecated;
  2. 采集时机:在quit之前调用 events 端点,以获取整个会话的完整事件与命令时序;
  3. 分析维度:事件时间戳数组用于分析会话生命周期各阶段耗时;commands数组用于统计各类命令的平均耗时与异常耗时,定位性能瓶颈;
  4. 自定义埋点:在关键业务步骤前后调用logCustomEvent(vendor, event),把业务语义(如"登录完成""数据加载完成")写入时序,便于结合命令耗时还原完整的用户路径时间线;
  5. 批量报表:需要持续产出性能报告时,使用官方 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),仅供参考

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

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

立即咨询