【翻译】使用Tracy剖析React Native内部机制以实现巅峰性能

7 阅读9分钟

原文链接:www.callstack.com/blog/profil…

作者:Mariusz Pasiński

为何要分析 React Native 内部机制?(又名:何必费这个劲?)

有人可能会问,为什么 Meta 以外的人要分析 React Native 本身的 C++ 代码... 这确实是个好问题!或许你已在应用中解决了大部分显而易见的优化点,现在希望在 React Native 代码库中寻找类似的改进空间。不妨将 React Native 比作一辆汽车,而你的 React Native 应用就是乘坐它的乘客。让汽车跑得更快,乘客自然也能更快抵达目的地,对吧?这无疑将是一项巨大的贡献。

但可能还有另一个较少见的理由:你或许希望更全面地了解React Native内部机制的运作原理。这将帮助你识别性能瓶颈,并精准定位代码中的特定区域,以便集中精力使用调试器进行深入分析。

无论出于何种目的,欢迎阅读本文关于 React Native 代码性能分析的指南。我将快速演示如何使用 Tracy 性能分析工具实现这一目标!

统一解决方案:React Native的多平台性能分析

亲爱的原生移动开发者们,你们大多对常用工具和IDE已驾轻就熟。在iOS应用中,你们可能用过Xcode的性能分析器来排查性能瓶颈;同样地,若要在Android平台进行类似操作,你们或许使用过Android Studio的内置工具,或直接调用systrace/ftrace

这些工具固然优秀,但对我而言还不够完善😉 在之前的React Native性能分析导论中,我已阐述采样式与仪器式分析器的差异及其优劣。更令我欣喜的是,当存在单一工具能同时分析多平台应用,且无需为笔记本增加数GB负担时。若还能保存这些跟踪数据以便后续处理,那便是锦上添花。

若你读过我的上一篇文章,想必已知晓我将继续使用 Tracy——这款开源混合型远程分析工具,在 React Native 优化领域表现卓越!

当鼠标悬停在阻塞 JavaScript 线程的互斥量上时,Tracy 分析器的屏幕截图

如何使用Tracy进行React Native性能分析?

首先,我们需要下载至少两项内容:Tracy和React Native的源代码。我将从后者开始:

让我们深入React Native的源代码树,在此创建一个用于Tracy的新目录。由于我需要分析大量模块,越接近核心模块效果越好。最终选定ReactCommon目录——它同时被Android和iOS平台使用,堪称理想候选地!

现在在此创建tracy目录并开始填充内容:

为简化操作,我们将所有Tracy源代码集中存放于此。理论上,除public目录外其余内容均可移除。

iOS平台的具体步骤稍多一些……

其实我们已经快完成了!针对iOS,我们需要为Tracy创建一个podspec文件,以下是我准备好的内容(tracy.podspec):

我并非CocoaPods专家,因此添加了一些额外的变通方案来解决可能出现的细微问题。首先,官方引入并使用Tracy分析器的正确方式是这样写的:

但执行pod install后,公共头文件会被放置在以Pod名称命名的tracy目录下,实际路径变为:

因此我的临时"解决方案"是创建两个单行头文件作为"代理"指向正确文件:

这样就能解决问题,继续后续操作。待找到正式解决方案后我会更新本文。

Tracy通过相对路径包含头文件,因此我们必须保持Tracy public目录内的文件结构。

现在可在react_native_pods.rb文件中声明该Pod,只需添加以下代码:

我们需要定义全局变量TRACY_ENABLE来启用Tracy,否则它会被移除。该定义的具体值无关紧要,关键在于必须进行定义。

构建Tracy工具(服务器端)

我们还需要一个用于展示收集到的指标的应用程序。

测试所有组件

好,现在来验证整体功能是否正常。保持Tracy窗口开启,接着运行rn-tester应用程序。

无需特定操作流程,例如按特定顺序或在严格时间窗口内运行Tracy分析器和应用程序。您只需知道运行应用程序的设备IP地址(在iOS模拟器上运行时为localhost)。换言之,您随时可以将Tracy连接到应用程序!

若未显示任何内容,可能是您在构建应用时未定义 TRACY_ENABLE 环境变量(注意末尾没有字母 D!),或者设备在您的网络中不可访问——虽然可能性较低,但通过简单的 ping 命令即可验证。

如何使用Tracy性能分析器?

我们将通过对需要分析的特定代码片段进行插桩来使用Tracy。

若需在分析器中查看某个函数,只需在该函数开头添加 ZoneScoped;。可将作用域视为带有名称的性能标记对,并可选配名称、颜色等属性。作用域声明时(借助RAII机制)会记录开始时间,离开作用域时则测量结束时间。

使用ZoneScoped;宏创建的范围默认采用所在函数名称。若需自定义名称,可使用ZoneScopedN("WhateverBetterNameYouWant")变体,该变体接受自定义名称字符串——当单个函数内存在多个范围时尤为实用!没错,您可以在单个函数内声明多个嵌套范围!

Tracy同时具备帧级分析功能,这里的"帧"可指代任何逻辑工作单元。使用时只需通过FrameMark;宏标记帧起止点。我个人习惯在packages/react-native/React/Base/RCTDisplayLink.mm文件的_jsThreadUpdate函数末尾添加此行代码。通过此配置,我们得以实现逐帧分析——这正是 React Native 性能优化的精髓所在!

哪些地方值得进行仪器化?

嗯,这确实是个难题,因为这取决于你想要观察什么。我通常从 packages/react-native/ReactCommon/ReactInstance.cpppackages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp 开始监控。这些文件能显示 React Native 事件循环的运行时段。个人还会关注 Task.cppEventQueue.cpp

在开始性能分析时,我主要关注渲染和布局环节,因此重点监控了Yoga(其使用递归调用,可尝试calculateLayout 或 computeFlexBasisForChild)、ShadowNode以及大部分packages/react-native/ReactCommon/react/renderer/mounting/类——这正是我在早期X平台帖子(如这篇)中展示的内容。

哪些内容可以进行性能分析?

本节将简要说明可收集的指标类型,并以 React Native 代码为例演示具体操作方法。

系统默认会定期收集 CPU 使用率数据。前文已介绍过如何收集函数执行时长测量值。

但当时我忽略了一个特殊场景:若感兴趣的范围始于某个函数而终止于另一个函数怎么办?根据文档说明,这属于高级用例,需要调用Tracy的C API并使用TracyCZoneTracyCZoneEnd宏。

如第3.13.3.1节所述,我们需要在函数间手动传递类型为TracyCZoneCtx的不透明"上下文结构",可通过函数参数、返回值或TLS(线程局部存储)实现。但该功能仍存在限制,例如区域必须在启动它的线程中结束。

锁竞争

在上方截图中,您可能注意到Tracy分析器能显示锁被……锁定时的状态。此功能让我们能够调查多线程应用中的锁竞争问题。当然,Tracy本身并不会识别您(或其他)源代码中使用的锁——这些需要我们自行进行仪器化处理。

那么我具体做了什么?我打开了 packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.h 文件,在类声明的末尾附近发现了这行代码:

你只需用 TracyLockable 宏包裹互斥锁声明,如下所示:

别忘了包含 Tracy 的头文件(位于 tracy/Tracy.hpp)😅 重建并重启应用后,你就能看到这个锁被用于同步 MountingCoordinatorpush()pullTransaction() 方法了。很酷吧!

如果你是那些被困在C++17之前版本标准中的迷失灵魂之一,那么这里还有一个直接引用自Tracy文档的额外步骤:

标准的std::lock_guardstd::unique_lock封装器应使用LockableBase(type)宏作为其模板参数(除非你使用的是C++17,其模板参数推导功能已得到改进)。例如: std::lock_guard<LockableBase(std::mutex)> lock(m_lock);

更多详情请参阅Tracy文档第3.5节"标记锁定机制"。

线程命名

说到线程,告知Tracy我们创建的所有线程会很有帮助。虽然Tracy能察觉到存在线程,但无法推断其用途或名称。如同其他优秀的分析工具,你可以为Tracy提供提示,并为这些线程传递标签。

我的做法是在两个文件中添加相同的以下代码行:

分别位于:packages/react-native/React/CxxBridge/RCTCxxBridge.mmpackages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTJSThreadManager.mm。现在我们终于用上了规范的线程名称,而非数字标识符!

内存分配

本节开篇,我将展示一张截图:

Tracy内存分析器不仅能绘制内存分配量(如我截图中底部的橙色曲线),更会追踪每次内存分配与释放操作。这使其能列出所有活跃分配,助您发现内存泄漏。对于来自嵌入式领域的开发者——那些在MMU和虚拟内存普及前编写程序的人——它还能帮助识别堆碎片问题。老实说,这功能够酷吧?

我只需按照Tracy文档(第3.8节"内存分析")所述,重写全局newdelete运算符:

在过去,若要追踪内存分配,必须至少暴露两个宏——一个用于分配,另一个用于释放内存——同时这些宏还会携带源文件名(__FILE__)和行号(__LINE__)。这使得您能够追溯"泄漏"内存的责任方。

您还可以使用TracyAllocSTracyFreeS变体,它们会自动为您收集堆栈信息,从而精准定位责任归属!

所有功能在此完美融合!当您向Tracy分析器提供线程信息和内存分配数据,并设置足够的监控范围后,发现性能问题将变得轻而易举。内存分配将自动关联到区域和线程,您还能预览源代码。React Native性能分析所需的一切功能,尽在一款工具中! Tracy 显示活动内存分配的截图,包含关联区域和线程

各位朋友,本次讲解到此结束!我们快速浏览了 Tracy 在 React Native 代码性能分析场景下最重要的功能特性!强烈建议查阅官方文档——其中包含大量优质内容。您可在此获取文档(没错,是可下载的 PDF 格式)。下次再见!