# 子衿
**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
```

## 四、用法
### 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 | 图形线程最长连续丢帧。 |