• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# JSVM 通用调优实践
2<!--Kit: NDK Development-->
3<!--Subsystem: arkcompiler-->
4<!--Owner: @yuanxiaogou; @string_sz-->
5<!--Designer: @knightaoko-->
6<!--Tester: @test_lzz-->
7<!--Adviser: @fang-jinxu-->
8
9## JSVM 调用结构
10
11小程序使用 JSVM 执行 JS 代码的过程大概可以分为 Native,JSVM-API,JSVM 三层:
12
13- Native:小程序运行 JS 的逻辑层,使用 JSVM 提供的接口完成 JS 代码编译,运行,code cache 生成等操作的逻辑排布和组合
14- JSVM-API:连接 native 和 v8 的接口兼容层,保持对不同版本 JS 引擎的兼容,提供 JS 引擎标准化的使用实践
15- JSVM:JS 引擎层,负责 JS 代码实际的编译运行
16
17使用 JSVM 的过程中,可能会因为多种原因产生不必要的开销,导致启动速度变慢。可以从以下三个层面进行分析。
18
19## 提升启动速度
20
21对于使用 JSVM 的应用启动场景,我们可以区分冷热启动用于分别进行不同的优化。
22首先是冷启动,是没有任何 profile 或者 cache 可以用于优化的场景,通常是首次启动;
23热启动则是已经充分预热,在多次启动之后获取了足量用于优化的 cache 的场景。
24
25### 减少 JS 引擎层的开销
26
27引擎层的开销很大程度上来源于编译。通过合理调整调用 JSVM-API 时传入的选项,可以降低主线程上 JS 引擎的编译开销。
28以下面的编译接口为例,其中 eagerCompile 这个参数的开关可以调控编译行为,通过在不同的启动场景打开这个选项可以实现优化效果。
29
30```cpp
31/**
32 * ...
33 * @param eagerCompile: Whether to compile the script eagerly.
34 * ...
35 */
36JSVM_EXTERN JSVM_Status OH_JSVM_CompileScript(JSVM_Env env,
37                                              JSVM_Value script,
38                                              const uint8_t* cachedData,
39                                              size_t cacheDataLength,
40                                              bool eagerCompile, // 开启全量编译
41                                              bool* cacheRejected,
42                                              JSVM_Script* result);
43```
44
45同时,code cache 的生成和使用也会对编译产生影响,这部分可以参考 [使用 code cache 加速编译](use-jsvm-about-code-cache.md)。
46
47**热启动:生成足够多的 code cache**
48
49热启动场景下,我们会在热启动前生成 code cache 以减少编译带来的开销。这个时候生成的 code cache 的覆盖率会影响 code cache 对热启动的优化效果。
50
51有一个简单的策略可以生成足够的 code cache:在生成 code cache 之前的那次编译中,打开 `eager compile` 选项。这样,V8 会在编译时进行全量编译,确保生成的 code cache 是全量的。
52
53这个方法会增加额外的编译时间开销,可能影响冷启动时间。后续将详细讨论 native 层的冷启动优化方法。
54
55**冷启动:使用 lazy compile 代替 eager compile**
56
57在冷启动时,`eager compile` 会增加不必要的编译时间。这其中主要的原因是没有拿到 v8 lazy compile 优化效果:v8 会将不在必经路径上的函数推迟编译,在实际运行到的时候才进行编译,这样会减少一些不被运行到函数的编译,从而优化冷启动的时间。
58
59因此,在冷启动时,可以通过关闭 `eager compile` 选项来避免阻塞主线程,从而获得足够的冷启动优化效果。
60
61### 在 native 层减少时间开销
62**冷启动:减少 code cache 的影响**
63
64上面在考虑减少 v8 层开销的时候,提到了为了热启动的性能可以开启 `eager compile` 进行编译,而为了冷启动性能却又需要关闭 `eager compile` 选项,看起来是矛盾的。为了解决这个矛盾,避免在冷热启动性能上的权衡,关键点是在 code cache 生成本身。
65
66首先,生成 code cache 需要进行前置编译,其次,生成 code cache 本身也会产生开销。
67
68在 native 层,要解决冷启动与生成 code cache 之间的矛盾,可以另起一个线程用于生成 code cache,这样可以避免生成 code cache 操作对冷启动的影响。
69
70有两个方法可以参考(以下伪代码仅用于展示逻辑流程,不涉及实际的 API 调用):
71
72- 将生成 code cache 必需的前置编译也放到新增的线程上,这样编译选项可以分开使用:生成 code cache 打开 `eager compile`,冷启动运行则关闭,这样做的缺点是可能进一步提高运行时的峰值资源占用,优点是 code cache 生成和运行可以完全解耦,不再需要考虑生成 code cache 的时间点。该流程的伪代码如下所示
73
74```
75async_create_code_cache() {
76  compile_with_eager_compile();
77  create_code_cache();
78  save_code_cache();
79}
80
81...
82
83if (has_code_cache) {
84  evaluate_script_with_code_cache();
85} else {
86  start_thread(async_create_code_cache());
87  evaluate_script_without_code_cache();
88}
89```
90
91
92- 在启动过程中的所有路径运行完之后,再启动新线程生成 code cache,这样不必使用 `eager compile` 也能获取足量的 code cache,同时保证热启动性能不受影响,这样做的缺点是生成 code cache 的时间点受限,优点是峰值资源占用相对更少,且不必生成过量的 code cache 导致 io 变慢。这个流程可以用如下所示的伪代码来表示
93
94```
95async_create_code_cache() {
96  compile_with_out_eager_compile();
97  create_code_cache();
98  save_code_cache();
99}
100
101...
102
103if (has_code_cache) {
104  evaluate_script_with_code_cache();
105} else {
106  evaluate_script_without_code_cache();
107}
108
109...
110
111if (script_run_completed) {
112  start_thread(async_create_code_cache());
113}
114```
115
116
117### 使用更高效的 JSVM-API
118
119在能达到相同效果时,使用更高效的 JSVM-API 是一种有效的性能优化方法,以下是一些具体的实践示例。
120
121**使用 IsXXX 代替 TypeOf**
122
123过去发现,针对仅需要判断对象类型的场景,存在一种相对低效的使用方法:
124
125从 OH_JSVM_TypeOf 接口获取类型后,再判断是否与某个类型相同。
126
127这种方法需要先查询 object 的类型,这种方法相对于直接使用 is 方法会更慢,因此我们新增了针对基础类型的 IsXXX 系列方法,用更高效的接口代替了相对低效的接口。下面的示例中使用到的 JSVM-API 可以参考 [JSVM 数据类型与接口说明](./jsvm-data-types-interfaces.md),这里仅展示调用的步骤。
128
129- 低效用例
130
131
132```cpp
133bool Test::IsFunction(JSVM_Env env, JSVM_Value jsvmValue) const {
134    // type judgment
135    JSVM_ValueType valueType;
136    OH_JSVM_TypeOf(*env, jsvmValue, &valueType);
137    return valueType == JSVM_FUNCTION;
138}
139```
140
141- 高效用例
142
143
144```cpp
145bool Test::IsFunction(JSVM_Env env, JSVM_Value jsvmValue) const {
146    // type judgment
147    bool result = false;
148    OH_JSVM_IsFunction(*env, jsvmValue, &result); // 可直接判断是否为Function类型
149    return result;
150}
151```
152
153以某生态应用小程序场景为例,这个优化可以带来的性能收益端到端有 150ms,总占比约 5%。
154
155**直接使用 OH_JSVM_CreateReference,避免创建冗余的 object**
156
157过去存在这样一种创建 reference 的路径:
158
159创建一个新的 object -> 设置 object 的值 -> 创建 object 的 reference。
160
161在已有值的情况下,直接创建值的引用即可。
162
163下面的示例中使用的 JSVM-API 可以参考 [JSVM 数据类型与接口说明](./jsvm-data-types-interfaces.md),这里仅展示调用的步骤。
164
165
166- 低效用例
167
168```cpp
169// (1) open handle scope
170JSVM_HandleScope scope;
171OH_JSVM_OpenHandleScope(*env, &scope);
172// (2) get JSVM_Value
173JSVM_Value jsvmValue;
174OH_JSVM_GetNull(*env, &jsvmValue);
175// (3) create and store Reference for JSVM_Value
176JSVM_Value wrappingObject;
177OH_JSVM_CreateObject(*env, &wrappingObject);
178OH_JSVM_SetElement(*env, wrappingObject, 1, jsvmValue);
179OH_JSVM_CreateReference(*env, wrappingObject, 1, &result->p_member->jsvmRef);
180// (4) close handle scope
181OH_JSVM_CloseHandleScope(*env, scope);
182```
183
184- 高效用例
185
186```cpp
187// (1) open handle scope
188JSVM_HandleScope scope;
189OH_JSVM_OpenHandleScope(*env, &scope);
190// (2) get JSVM_Value
191JSVM_Value jsvmValue;
192OH_JSVM_GetNull(*env, &jsvmValue);
193// (3) create and store Reference for JSVM_Value
194OH_JSVM_CreateReference(*env, jsvmValue, 1, &result->p_member->jsvmRef); // 可从任意对象类型直接创建Reference,代码更为简洁高效
195// (4) close handle scope
196OH_JSVM_CloseHandleScope(*env, scope);
197```
198
199同样以某生态应用小程序场景为例,这个改动减少了大量冗余的接口调用,最终带来的端到端时间收益有 100+ms,约 3%。
200