Skip to content

跨端运行时按需测速

Mpx 在小程序、Web、React Native 上都有实例初始化、模板执行、生命周期和卸载等框架热路径,平台 Profiler 难以直接表达 Mpx 自身的语义阶段。@mpxjs/perf 提供跨端共用、可按构建关闭且关闭态零残留的显式性能探针。

三类统计能力

统计类型API输出典型用途
区段聚类统计aggrStart / aggrEndMap<string, AggrResult>高频 render、hook、函数的 count/sum/avg/max
区段序列统计traceStart / traceEndTraceTimeline模块火焰图、嵌套调用 profile、异步区段瀑布图
点序列统计markMarkTimeline数据就绪、首次渲染、页面可交互等里程碑

start/end 控制整个录制窗口,Reporter 在窗口结束时同步收到三类结果。聚类统计不保留逐次样本,适合高频热路径;trace 和 mark 会保留事件对象,只适合有限插桩点和诊断窗口。

设计原则

@mpxjs/perf 采用「编译期常量开关 + 运行时探针实现 + tree-shaking 兜底」三层结构:

  1. MpxWebpackPlugin 通过 DefinePlugin 注入 __mpx_perf__ 和分组常量。
  2. 探针调用直接包在 if (__mpx_perf_framework__) / if (__mpx_perf_user__) 字面量条件里。
  3. Terser 消除关闭分组的调用点,webpack tree-shaking 继续剔除失活的实现模块。

关闭态产物中不会保留探针实现、名称字符串或模块依赖。

该方案不支持线上动态开关

线上开关意味着探针字节必须进入产物,与关闭态零残留目标冲突。线上诊断需重新构建一个开启探针的内测包。

配置入口

mpx.config.jspluginOptions.mpx.plugin 下配置 perf

js
const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
  pluginOptions: {
    mpx: {
      plugin: {
        perf: {
          enable: !!process.env.MPX_PERF,
          probes: ['framework', 'user']
        }
      }
    }
  }
})

配置项说明

字段类型说明
enableboolean总开关。false 或不传时整套探针关闭。
probesstring[]当前支持 'framework' / 'user'。空数组等价于关闭,未知分组会在编译期报错。
分组控制对象典型用途
framework框架内建探针,如 instance:* / scheduler:* / lifecycle:* / 基础组件 render调试 Mpx 框架自身热路径
user业务自定义探针定位业务流程和函数耗时

两个分组独立 DCE,但共享同一个录制窗口和 Reporter。

录制窗口

ts
import { start, end } from '@mpxjs/perf'

router.beforeEnter('/goods/:id', () => {
  if (__mpx_perf__) {
    start({
      markLimit: 2048,
      traceLimit: 4096
    })
  }
})

router.beforeLeave('/goods/:id', () => {
  if (__mpx_perf__) end()
})
ts
interface PerfStartOptions {
  markLimit?: number
  traceLimit?: number
}
  • markLimitMarkTimeline 的总事件上限,包含 start/end 边界,默认 1024,必须是不小于 2 的整数。
  • traceLimit 是 trace 区段上限,默认 1024,必须是非负整数;传 0 可关闭当前窗口的 trace 存储。
  • 无效值回退为 1024。
  • 录制中重复调用 start(options) 保持幂等,不清空数据,也不修改当前容量。
  • 新窗口重新读取配置,不继承上一窗口的自定义容量。

start() 自动生成 { name: 'start', start: 0, timestamp: startedAt }end() 自动生成同结构的 end 边界。即使没有显式探针,完整窗口也会触发 Reporter。

区段聚类统计

聚类 API 将同名样本实时合并为 { count, sum, avg, max },不保存逐次耗时。

id 模式

默认 id 模式支持嵌套、乱序结束和同名并发,也是同步高频路径的首选:

ts
import { aggrStart, aggrEnd } from '@mpxjs/perf'

function expensiveCompute (data) {
  let id = -1
  if (__mpx_perf_user__) id = aggrStart('myBiz:list:filter')
  const result = data.filter(/* ... */).sort(/* ... */)
  if (__mpx_perf_user__) aggrEnd(id)
  return result
}

未录制时 aggrStart(name) 返回 -1 且不读取时钟。录制中使用数组槽位和 free list,稳态零对象、零闭包分配。

name 模式

开始和结束不方便传递 id 时,显式传入 useName: true

ts
if (__mpx_perf_user__) aggrStart('goods:request', true)

loadPageData().finally(() => {
  if (__mpx_perf_user__) aggrEnd('goods:request')
})

name 模式允许在录制窗口外保存起点,只有 aggrEnd(name) 发生在录制中时样本才会进入结果。后一次同名 start 会覆盖前一次起点,同名并发必须改用 id 模式。

区段序列统计

trace 保存每次区段的开始位置和持续时长,可还原模块火焰图或异步瀑布图:

ts
import { traceStart, traceEnd } from '@mpxjs/perf'

let appId = -1
let routerId = -1
if (__mpx_perf_user__) {
  appId = traceStart('module:app')
  routerId = traceStart('module:router')
}

if (__mpx_perf_user__) {
  traceEnd(routerId, { moduleId: 42 })
  traceEnd(appId, { moduleId: 1 })
}

trace 在 start 时预留事件位置,在 end 时回填 duration / info,因此嵌套区段始终按 start 顺序输出。未完成区段在窗口结束时从 events 移除,并计入 incomplete

跨作用域且不方便传递 id 时可以使用 name 模式:

ts
if (__mpx_perf_user__) traceStart('request:goods', true)

loadPageData().finally(() => {
  if (__mpx_perf_user__) {
    traceEnd('request:goods', { status: 'fulfilled' })
  }
})

trace 必须在录制窗口中 start。name 模式同名覆盖时,被覆盖的旧区段会计入 incomplete;同名并发、递归和严格嵌套必须使用 id 模式。上一窗口未消费的旧 id 不能结束下一窗口的新事件。

点序列统计

mark(name, info?) 记录瞬时里程碑,不产生聚合桶:

ts
import { mark } from '@mpxjs/perf'

if (__mpx_perf_user__) {
  mark('goods:data-ready', {
    source: 'cache',
    itemCount: 20
  })
}

同名 mark 仍是多条独立事件。info 按引用保存,不复制、不校验、不序列化;建议只传小型、可序列化的诊断字段,不要传组件实例、完整 props、响应体或大数组。

数据结构

ts
interface AggrResult {
  count: number
  sum: number
  avg: number
  max: number
}

interface MarkEvent {
  name: string
  start: number
  timestamp: number
  info?: unknown
}

interface MarkTimeline {
  events: MarkEvent[]
  dropped: number
}

interface TraceEvent {
  name: string
  start: number
  timestamp: number
  duration: number
  info?: unknown
}

interface TraceTimeline {
  events: TraceEvent[]
  dropped: number
  incomplete: number
}

start 是相对当前录制窗口起点的毫秒偏移。timestampperformance.now()、Hermes nativePerformanceNow()Date.now() 返回的原始值,不保证是 Unix epoch;同一运行环境和窗口中的值可以直接比较。

mark 和 trace 使用独立容量。达到上限后只增加对应 dropped,不保存事件、时间、name 或 info。mark 始终为 end 边界预留最后一个位置。

Reporter

ts
type Reporter = (
  aggregates: Map<string, AggrResult>,
  marks?: MarkTimeline,
  traces?: TraceTimeline
) => void

通过正常 start/end 完成的窗口始终传入 marks 和 traces;后两个参数保持可选,兼容旧的一、二参数 Reporter 和外部手动调用。

ts
import { setReporter } from '@mpxjs/perf'
import type {
  AggrResult,
  MarkTimeline,
  TraceTimeline
} from '@mpxjs/perf'

if (__mpx_perf__) {
  setReporter((
    aggregates: Map<string, AggrResult>,
    marks?: MarkTimeline,
    traces?: TraceTimeline
  ) => {
    MyAPM.report('mpx_perf_aggregates', aggregates)
    if (marks) MyAPM.report('mpx_perf_marks', marks)
    if (traces) MyAPM.report('mpx_perf_traces', traces)
  })
}

全局 Reporter 和 end(localReporter) 传入的局部 Reporter 会依次收到同一份 Map、marks 和 traces 引用。不要直接修改;需要改写时自行复制。

注册时机

setReporter 必须直接放在 if (__mpx_perf__) 中,确保自定义函数和闭包也能在关闭态被 DCE。

Console Reporter

默认 consoleReporter 分别输出 aggregates、traces 和 marks。createConsoleReporter({ sortBy, filter, header }) 可定制:

  • sortBy 只影响 aggregates。
  • filter 同时作用于 aggregate、trace 和显式 mark,内建 start/end 不会隐藏。
  • trace 和 mark 保持原始顺序。
  • 仅有事件包含 info 时显示 info 列。
  • mark dropped、trace dropped 和 trace incomplete 分别提示。
  • info 的 JSON 格式化失败不会中断业务或其他统计输出。

Chrome Trace 对接

TraceEvent 可映射为 Chrome Trace / Perfetto Complete Event:

ts
const chromeEvents = traces.events.map(event => ({
  name: event.name,
  ph: 'X',
  ts: event.timestamp * 1000,
  dur: event.duration * 1000,
  args: event.info
}))

核心包不内置文件写入或格式转换器,业务 Reporter 可按需补充 pid / tid / cat 等字段。MarkEvent 可用同一 timestamp * 1000 规则映射为 Instant Event。

API 参考

API说明
aggrStart(name, useName?)默认返回数字 id;传 true 改用 name 配对。
aggrEnd(idOrName)完成区段并实时聚合;无效或重复目标安全 noop。
traceStart(name, useName?)默认返回数字 id;传 true 改用 name 配对。
traceEnd(idOrName, info?)完成 trace 并保存可选 info。
mark(name, info?)追加独立、有序的点事件。
start(options?)创建录制窗口和三类容器;重复 start 幂等。
end(reporter?)关闭窗口,回填 avg、压缩未完成 trace 并触发 Reporter。
setReporter(r) / clearReporter()替换或清空全局 Reporter。
createConsoleReporter(opts?)创建可配置的 Console Reporter。
consoleReporter默认 Reporter。

旧 API 兼容

旧 API内部映射兼容语义
scopeStart(name)aggrStart(name)返回 id;未录制返回 -1
scopeEnd(id)aggrEnd(id)负 id 和重复结束安全 noop。
measureStart(name)aggrStart(name, true)后一次同名 start 覆盖前一次。
measureEnd(name)aggrEnd(name)命中后消费起点。

四个旧函数继续导出且没有移除版本,但新代码优先使用 aggrStart/aggrEnd。聚合结果类型已由 AggResult 更名为 AggrResult,不保留旧类型别名;MarkEvent.at 已更名为 start,并新增 timestamp / info

内置框架探针事件 schema

统一指标只描述 Mpx 语义,不使用 mini:web:rn: 平台前缀。Reporter 应结合 __mpx_mode__ 分平台建立基线;不同平台缺少某个阶段时不会产生该指标。现有框架探针继续通过兼容的 scopeStart/scopeEnd 采集聚合耗时。

实例、调度与生命周期

指标小程序WebRN边界
instance:initbeforeCreate 完成后到 created 开始前的实例 state 初始化
instance:init:setup用户 setup(props, context) 同步调用
instance:render平台 render 同步执行;小程序覆盖非 vnode ReactiveEffect 主函数,Web 覆盖 Mpx 实例的 Vue _render;均从进入到正常返回
instance:render:getStyleRN __getStyle 同步执行
instance:render:getStyle:classRN class 样式解析
instance:render:getStyle:styleRN inline style 解析
instance:unmount前置卸载 hook 完成后的实例核心资源释放
scheduler:flush一次 Mpx scheduler 完整 drain,递归新增任务不重复计样本
lifecycle:<hook>按支持情况按支持情况按支持情况存在真实 option hook 或组合式 hook 时的一次 Page / Component 生命周期调度
lifecycle:app:<hook>按支持情况按支持情况按支持情况一次用户定义的 App 生命周期执行,含用户 mixin、不含内建 mixin

不采集宿主 setData 调用到 callback 的异步耗时,该阶段受宿主调度影响。这些指标存在包含关系,不能直接相加。例如 instance:init:setupinstance:init 的子阶段,RN 的 instance:render:getStyle 及其 class/style 子阶段包含在 instance:render 中。

内建低频 mark 节点为:

text
app:onLaunch:start
page:onLoad:start
page:onReady:start

三个节点都在对应生命周期开始前产生。page:onLoad:startpage:onReady:startinfo 均为 { route },用于标识当前页面。page:onReady:start 只表示目标平台开始调度 Mpx onReady 对应生命周期,不代表统一的 GPU 首帧或可交互时间。

基础组件 render

Web 的 mpx-viewmpx-textmpx-imagempx-scroll-view 每次真实 render 分别产生 view:rendertext:renderimage:renderscroll-view:render。这些指标代表组件 render 父阶段的同步整体耗时,排除 Vue scheduler、patch、DOM layout/paint、图片网络与解码、滚动事件及 observer callback。

RN 的 getStyle 使用 instance:render:getStyle 父阶段及 instance:render:getStyle:classinstance:render:getStyle:style 子阶段。RN 同时采集 view、simple-view、text、simple-text、image 与 scroll-view 的 :render 父阶段和 :render:<phase> 子阶段;phase 包括组件支持的 propsstyleinnerPropscreateElement。所有 RN 组件指标都排除 effect callback、手势 worklet 和事件后续执行、React commit 与 Native layout。组件父阶段与子阶段、父模板 render 可能存在包含关系,不应相加。

性能影响评估

能力未录制录制中
aggr id 模式状态判断后返回 -1数组槽位 + free list;稳态零对象、零闭包分配
aggr name 模式保存 name 起点Map set/get/delete;只保留聚合桶
trace状态判断后 noop每个被接受区段一个事件对象和一条进行中映射,默认最多 1024 条
mark状态判断后 noop每个显式 mark 一个事件对象,默认最多 1022 条,加边界后最多 1024 条

聚合与事件序列的取舍

高频 render 和函数耗时使用 aggr;需要火焰图或逐区段瀑布图时使用 trace;里程碑使用 mark。trace/mark 不能替代渲染循环中的聚类统计。

与现有工具的关系

  • 平台 Profiler:提供函数或宿主层采样;trace 提供 Mpx 语义明确的区段序列,同一平台内可按 performance 时钟对照分析。
  • Perfetto / Chrome Trace:Perf 的 trace 是稳定数据源,业务 Reporter 负责转换和补充进程、线程、分类字段。
  • 业务 APM:Perf 不替代 APM,只提供聚合、区段和里程碑数据。

Terser / Babel 兼容性约束

  • 最终构建依赖 @mpxjs/perfdist/index.js 保留顶层三元、sideEffects: false 与使用方 Terser 完成 DCE。
  • 探针调用必须直接置于 if (__mpx_perf_framework__) / if (__mpx_perf_user__) 字面量条件内。
  • 接入方需保留默认 Terser 的 dead_code / conditionals 优化。
  • Babel 不应提前破坏 __mpx_perf__ ? impl.x : noop.x 顶层三元结构。