# 子衿 **Repository Path**: yongyu0102/ZiJin ## Basic Information - **Project Name**: 子衿 - **Description**: 子衿是鸿蒙系统上的一款线上性能监控框架,目前支持监控崩溃、不响应、踩内存、启动时长、CPU高负载、主线程超时、资源泄露、滑动丢帧等。当发生这些异常,会将堆栈信息保存在本地,进而上报服务端。 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-12-12 - **Last Updated**: 2024-12-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 子衿 ## 一、存在的性能问题 应用发布上线,随着用户量越来越大,各种隐藏的性能问题不断的报出来,严重影响了应用的稳定性。为此需要需要一款线上性能监控框架,当发生问题时,能够将堆栈等各种信息保存下来,并及时的上报给服务端。
**子衿是鸿蒙系统上的一款线上性能监控框架,目前支持监控崩溃、不响应、踩内存、启动时长、CPU高负载、主线程超时、资源泄露、滑动丢帧等。当发生这些异常,会将堆栈信息保存在本地,进而上报服务端。** ## 二、子衿名称来源 子衿一词出自古代第一部诗歌总集《诗经》中的《郑风·子衿》。 ## 三、下载安装 ``` ohpm install @shijing/zijin ``` ![效果图](./zijin/screenshots/zijin.jpg) ## 四、用法 ### 4、1初始化子衿 初始化子衿需要尽可能的早,建议新建类,继承AbilityStage。 ``` export class ZiJinAbilityStage extends AbilityStage { onCreate(): void { } } ``` 在modules.json里面添加如下代码,用srcEntry指定ZiJinAbilityStage的文件路径 ``` "srcEntry": "./ets/ZiJinAbilityStage.ets", ``` 在ZiJinAbilityStage的onCreate里面初始化子衿。 ``` export class ZiJinAbilityStage extends AbilityStage { onCreate(): void { ZiJin.init(this.context) const builder = new ZiJinBuilder() builder.debug = BuildProfile.DEBUG;//可选,测试阶段配置有输出日志 builder.channel = "zijin";//可选,渠道 builder.versionCode = BuildProfile.VERSION_CODE;//可选,应用versionCode builder.versionName = BuildProfile.VERSION_NAME;//可选,应用versionName // 生成的文件保存在本地私有目录,需要上传到服务端,上传文件由接入方来上传 builder.uploadFileCallback = new UploadPerformanceFile() builder.dynamicParams = { getDeviceId: () => { return "";// 设备id,可选 }, getUserId: () => { return "user_id";//可选,用户标识,没有默认值。 } } ZiJin.start(builder); } } ``` ### 4、2上传文件 当发生异常,子衿会将日志保存到文件,需要接入方自行上传到服务端。新建UploadPerformanceFile类,实现UploadFileCallback接口,重写onUploadFile方法。子衿会调用onUploadFile方法。文件上传完成后,删除文件,防止重复上传。所有的文件都在同一个目录,可以将这些文件压缩成一个文件。上传完成后,如果有压缩文件,还需要将压缩文件删除。什么时候调用onUploadFile方法? * 每次启动的时候,子衿会去指定目录查询是否存在文件,如果存在,则会调用onUploadFile方法。 * 当发生异常,子衿会将日志文件保存到指定目录下,保存成功后调用onUploadFile方法。 **所有生成的文件都保存在/data/storage/el2/base/files/performance/目录,/data/storage/el2/base/files/performance/是沙箱路径,对应到真实的物理路径为/data/app/el2/100/base/<包名>/files/performance。可以在DevEco Studio的Device File Browser通过真实的物理路径查看文件。** ``` /** * 上传文件到服务端 */ export class UploadPerformanceFile implements UploadFileCallback { onUploadFile(paths: string[]): void { // 在子线程中上传文件 TaskPool.execute(uploadFile, paths) } } @Concurrent function uploadFile(paths: string[]) { /** * 添加上传文件代码..... * 所有的文件都在同一个目录,可以将这些文件压缩成一个文件。 * 上传完成后,如果有压缩文件,还需要将压缩文件删除,防止重复上传。 * 最好不要删除整个目录,因为可能会把还没上传的文件给删除了 */ let file = fs.openSync(paths[0], fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); // 文件所在目录 const dir = file.getParent() // 上传完成后,将下面的代码放开,删除文件 // paths.forEach(path => { // FileUtil.delete(path) // }) // 如果将文件压缩成一个文件,压缩文件也要删除 } ``` ## 五、监控 ### 5、1监控ArkTS崩溃 触发崩溃后,需要重启应用,才能检测到崩溃。可以在/data/app/el2/100/base/<包名>/files/performance目录下找到文件,文件名以app_crash开头。如下是生成的文件内容。 ``` Generated by HiviewDFX@OpenHarmony ================================================================ Device info:HUAWEI Pura 70 // 设备名称,这里是华为p70 Build info:ADY-AL00 5.0.0.70(SP3C00E68R5P6) // 设备型号 Fingerprint:4ff81b0ee4124cac4dd13f2c85fc6bc7e4440a5df292a807fa2eddab36a682d3 Module name:com.shijing.zijin // 包名 Version:1.0.0 // 版本名称 VersionCode:1000000 // 版本号 PreInstalled:No Foreground:Yes // 是否前台 Pid:53103 // 进程 Uid:20020041 Reason:SyntaxError // 崩溃原因,语法错误 Error name:SyntaxError Error message:Unexpected Text in JSON // 解析json发生异常,传入的json字符串有问题 Stacktrace: at click (entry/src/main/ets/pages/Index.ets:84:9) // 发生崩溃的代码位置,84行代码发生崩溃 at anonymous (entry/src/main/ets/pages/Index.ets:63:11) ``` 代码混淆后,崩溃堆栈也会被混淆,需要还原堆栈,如何还原堆栈,可以查看[鸿蒙JS崩溃分析](https://juejin.cn/post/7399530640860102708) ### 5、2监控C/C++崩溃 如下代码,触发了C/C++层空指针崩溃。堆栈显示调用Sub类的testSub函数发生空指针异常,说明Sub对象空了,同时又告知了代码行号。 ``` Generated by HiviewDFX@OpenHarmony ================================================================ Device info:HUAWEI Pura 70 Build info:ADY-AL00 5.0.0.70(SP3C00E68R5P6) Fingerprint:4ed5be0aad1290325db4beabb28ba9e3918c6a7c8458b395a8a8b48391c8afa7 Module name:com.shijing.zijin Version:1.0.0 VersionCode:1000000 PreInstalled:No Foreground:Yes Timestamp:2024-10-19 21:43:30.566 Pid:41574 Uid:20020041 Process name:com.shijing.zijin Process life time:11s // 空指针异常 Reason:Signal:SIGSEGV(SEGV_MAPERR)@000000000000000000 probably caused by NULL pointer dereference Fault thread info: Tid:41574, Name:m.shijing.zijin // 堆栈,崩溃代码所在行号 #00 pc 000000000000c1d4 /data/storage/el1/bundle/libs/arm64/libentry.so(Sub::testSub()+12)(398741510f8a6b6e524e04515c7293e3d4e059cf) #01 pc 0000000000005d68 /data/storage/el1/bundle/libs/arm64/libentry.so(NullPointerException(napi_env__*, napi_callback_info__*)+28)(398741510f8a6b6e524e04515c7293e3d4e059cf) #02 pc 0000000000039c08 /system/lib64/platformsdk/libace_napi.z.so(panda::JSValueRef ArkNativeFunctionCallBack/files/performance目录下找到文件,文件名以app_freeze开头。卡死的日志内容比较多,如下截取了关键日志,日志中的堆栈指明了卡顿发生在Index.ets的93行。 ``` Timestamp:2024-10-19 21:48:13:383 Tid:41913, Name:m.shijing.zijin #00 pc 000000000004110c /system/lib64/module/arkcompiler/stub.an(BCStub_HandleJmpImm8+324) // 卡顿发生在Index.ets的93行 #01 at anonymous (entry/src/main/ets/pages/Index.ets:93:11) #02 pc 000000000038d75c /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::InterpreterAssembly::Execute(panda::ecmascript::EcmaRuntimeCallInfo*)+216)(79cfee7771cefb9ff03377508b3ce349) #03 pc 00000000005826e0 /system/lib64/platformsdk/libark_jsruntime.so(panda::FunctionRef::CallForNapi(panda::ecmascript::EcmaVM const*, panda::JSValueRef*, panda::JSValueRef* const*, int)+336)(79cfee7771cefb9ff03377508b3ce349) #04 pc 0000000000054ff4 /system/lib64/platformsdk/libace_napi.z.so(napi_call_function+308)(9db46237a781298034c174cc6e506c04) ``` ### 5、4监控踩内存 踩内存是发生在C/C++层的一种崩溃,踩内存‌是指程序在运行时访问了它不应该访问的内存区域,例如内存访问越界、使用非法指针或栈溢出等。当发生踩内存后,应用会退出,需要重启应用,才能检测到踩内存。可以在/data/app/el2/100/base/<包名>/files/performance目录下找到文件,文件名以app_crash开头。 ``` Generated by HiviewDFX@OpenHarmony ================================================================ Device info:HUAWEI Pura 70 Build info:ADY-AL00 5.0.0.70(SP3C00E68R5P6) Fingerprint:492400be896afeb7ef1ec8feda0b921a5438df84e4b40736474e6ce004fbffa6 Module name:com.shijing.zijin Version:1.0.0 VersionCode:1000000 PreInstalled:No Foreground:Yes Timestamp:2024-10-19 21:56:36.036 Pid:43623 Uid:20020041 Process name:com.shijing.zijin Process life time:495s Reason:Signal:SIGTRAP(TRAP_BRKPT)@0x0000005b7b485d48 Fault thread info: Tid:43623, Name:m.shijing.zijin // IndexOutOfBounds函数发生越界 #00 pc 0000000000005d48 /data/storage/el1/bundle/libs/arm64/libentry.so(IndexOutOfBounds(napi_env__*, napi_callback_info__*)+20)(398741510f8a6b6e524e04515c7293e3d4e059cf) #01 pc 0000000000039c08 /system/lib64/platformsdk/libace_napi.z.so(panda::JSValueRef ArkNativeFunctionCallBack(panda::JsiRuntimeCallInfo*)+216)(9db46237a781298034c174cc6e506c04) #02 pc 00000000003f28ac /system/lib64/module/arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+40) #03 at click (entry/src/main/ets/pages/Index.ets:98:9) #04 at anonymous (entry/src/main/ets/pages/Index.ets:63:11) ``` 常见踩内存 | 类型 | 取值 | |-------------------------------|-------------------| | alloc-dealloc-mismatch | 内存分配和释放方式不匹配。 | | allocation-size-too-big | 当分配对堆来说太大时发现的错误。 | | calloc-overflow | calloc分配内存错误。 | | container-overflow | 容器溢出。 | | double-free | 释放已释放的内存。 | | dynamic-stack-buffer-overflow | 缓冲区访问超出堆栈分配对象的边界。 | | stack-use-after-return | 在返回后使用堆栈内存。 | | new-delete-type-mismatch | 内存释放大小与分配大小不一致。 | ### 5、5统计启动时长 每次启动,不管是冷启动还是热启动,都会检测启动耗时。可以在/data/app/el2/100/base/<包名>/files/performance目录下找到文件,文件名以app_launch开头。 ``` { "eventName": "APP_LAUNCH", "time": 1729346204746, "channel": "zijin", "versionCode": 1000000, "versionName": "1.0.0", "deviceId": "", "userId": "user_id", "bundleName": "com.shijing.zijin", "startType": 0, "iconInputTime": 1729346199565, "animationFinishTime": 618, "extendTime": 0 } ``` 上述字段含义 | 字段 | 含义 | |:--------------------|----------------------| | time | 事件生成时间,单位为毫秒。 | | versionCode | 应用版本。 | | versionName | 应用名称。 | | startType | 冷热启动。0:冷启动,1:热启动。 | | iconInputTime | 点击桌面应用图标离手时间,单位为毫秒。 | | animationFinishTime | 动效完成耗时,单位为毫秒。 | | extendTime | 开发者扩展接口调用完成耗时,单位为毫秒。 | ### 5、6监控CPU高负载 耗时操作需要放到子线程,但是如果子线程出现不正常的耗时操作,比如子线程出现了死循环,那CPU就出现了高负载,设备发烫。点击触发CPU高负载按钮后,应用保持在前台且屏幕处于亮屏状态,五到十分钟就可以在/data/app/el2/100/base/<包名>/files/performance目录下找到文件,文件名以cpu_usage_high开头。日志内容比较多,截取关键日志,日志显示worker.ets文件的11行有问题,我们在worker.ets文件开启死循环,虽然是在子线程中开启死循环,但死循环导致CPU高负载。 ``` Tid:63627, Name:WorkerThread #00 pc 00000000000416dc /system/lib64/module/arkcompiler/stub.an(BCStub_HandleStaV8+8) #01 at eatCpu (entry/src/main/ets/workers/worker.ets:11:5) #02 at anonymous (entry/src/main/ets/workers/worker.ets:6:3) ``` ### 5、7监控主线程超时 可以在/data/app/el2/100/base/<包名>/files/performance目录下找到文件,文件名以main_thread_jank开头。日志内容比较多,截取了两行关键日志,堆栈显示Index.ts文件发生主线程超时。需要注意的是我们用的是Index.ets,堆栈显示的是Index.ts,两者有一丢丢差距。 ``` 4 #40 at anonymous (entry|entry|1.0.0|src/main/ets/pages/Index.ts:131:17) 4 #41 at click (entry|entry|1.0.0|src/main/ets/pages/Index.ts:184:24) ``` ### 5、8监控资源泄漏 资源泄漏分为pss内存泄漏、js内存泄漏、fd资源泄漏、线程泄漏。发生资源泄露,等待15~30分钟,会上报应用内存泄漏事件。同一个应用,24小时内至多上报一次内存泄漏,如果短时间内要二次上报,需要重启设备。可以在/data/app/el2/100/base/<包名>/files/performance目录下找到文件,文件名以resource_overlimit开头。 ### 5、9监控滑动丢帧 当滑动列表发生卡顿,可以在/data/app/el2/100/base/<包名>/files/performance目录下找到文件,文件名以scroll_jank开头。 ``` { "eventName": "SCROLL_JANK", "time": 1729348305261, "channel": "zijin", "versionCode": 1000000, "versionName": "1.0.0", "deviceId": "", "userId": "user_id", "bundleName": "com.shijing.zijin", "abilityName": "EntryAbility", "beginTime": 1729348303790, "duration": 1445, "totalAppFrames": 78, "totalAppMissedFrames": 46, "maxAppFramesTime": 67, "maxAppSeqFrames": 31, "totalRenderFrames": 72, "totalRenderMissedFrames": 0, "maxRenderFrameTime": 8, "maxRenderSeqFrames": 0 } ``` 下面是字段含义。滑动丢帧并没有堆栈信息,对于滑动丢帧,可以通过Profiler来定位滑动卡顿,具体可查看[鸿蒙性能优化之卡顿优化](https://juejin.cn/post/7420978253491994674)。 | 字段 | 含义 | |-------------------------|------------------| | beginTime | 滑动开始时间,单位为毫秒。 | | duration | 持续时间,单位为毫秒。 | | totalAppFrames | 应用线程绘帧总数。 | | totalAppMissedFrames | 应用线程丢帧总数。 | | maxAppFramesTime | 应用线程最大单帧耗时,单位毫秒。 | | maxAppSeqFrames | 应用线程最长连续丢帧。 | | totalRenderFrames | 图形绘帧总数。 | | totalRenderMissedFrames | 图形丢帧总数。 | | maxRenderFrameTime | 图形最大单帧耗时,单位毫秒。 | | maxRenderSeqFrames | 图形线程最长连续丢帧。 |