JSVM通用调优实践
==========

#### JSVM调用结构

小程序使用JSVM执行JS代码的过程可以分为 Native，JSVM-API，JSVM 三层：

*   Native：小程序运行JS的逻辑层，使用JSVM提供的接口完成JS代码编译，运行，code cache生成等操作的逻辑排布和组合
*   JSVM-API：连接native和v8的接口兼容层，保持对不同版本JS引擎的兼容，提供JS引擎标准化的使用实践
*   JSVM：JS引擎层，负责JS代码实际的编译运行

使用JSVM的过程中，可能会因为多种原因产生不必要的开销，导致启动速度变慢。可以从以下三个层面进行分析。

#### 提升启动速度

对于使用JSVM的应用启动场景，我们可以区分冷热启动用于分别进行不同的优化。

首先是冷启动，是没有任何profile或者cache可以用于优化的场景，通常是首次启动；

热启动则是已经充分预热，在多次启动之后获取了足量用于优化的cache的场景。

#### \[h2\]减少 JS 引擎层的开销

引擎层的开销很大程度上来源于编译。通过合理调整调用JSVM-API时传入的选项，可以降低主线程上JS引擎的编译开销。

以下面的编译接口为例，其中eagerCompile这个参数的开关可以调控编译行为，通过在不同的启动场景打开这个选项可以实现优化效果。

/\*\*
 \* ...
 \* @param eagerCompile: Whether to compile the script eagerly.
 \* ...
 \*/
JSVM\_EXTERN JSVM\_Status OH\_JSVM\_CompileScript(JSVM\_Env env,
                                              JSVM\_Value script,
                                              const uint8\_t\* cachedData,
                                              size\_t cacheDataLength,
                                              bool eagerCompile, // 开启全量编译
                                              bool\* cacheRejected,
                                              JSVM\_Script\* result);

同时，code cache的生成和使用也会对编译产生影响，这部分可以参考 [使用code cache加速编译](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/use-jsvm-about-code-cache)。

**热启动：生成足够多的code cache**

热启动场景下，我们会在热启动前生成code cache以减少编译带来的开销。这个时候生成的code cache的覆盖率会影响code cache对热启动的优化效果。

有一个简单的策略可以生成足够的code cache：在生成code cache之前的那次编译中，打开eager compile选项。这样，V8会在编译时进行全量编译，确保生成的code cache是全量的。

这个方法会增加额外的编译时间开销，可能影响冷启动时间。后续将详细讨论native层的冷启动优化方法。

**冷启动：使用lazy compile代替eager compile**

在冷启动时，eager compile会增加不必要的编译时间。这其中主要的原因是没有拿到v8 lazy compile优化效果：v8会将不在必经路径上的函数推迟编译，在实际运行到的时候才进行编译，这样会减少一些不被运行到函数的编译，从而优化冷启动的时间。

因此，在冷启动时，可以通过关闭eager compile选项来避免阻塞主线程，从而获得足够的冷启动优化效果。

#### \[h2\]在native层减少时间开销

**冷启动：减少code cache的影响**

上面在考虑减少v8层开销的时候，提到了为了热启动的性能可以开启eager compile进行编译，而为了冷启动性能却又需要关闭eager compile选项，看起来是矛盾的。为了解决这个矛盾，避免在冷热启动性能上的权衡，关键点是在code cache生成本身。

首先，生成code cache需要进行前置编译，其次，生成code cache本身也会产生开销。

在native层，要解决冷启动与生成code cache之间的矛盾，可以另起一个线程用于生成code cache，这样可以避免生成code cache操作对冷启动的影响。

有两个方法可以参考（以下伪代码仅用于展示逻辑流程，不涉及实际的API调用）：

*   将生成code cache必需的前置编译也放到新增的线程上，这样编译选项可以分开使用：生成code cache打开eager compile，冷启动运行则关闭，这样做的缺点是可能进一步提高运行时的峰值资源占用，优点是code cache生成和运行可以完全解耦，不再需要考虑生成code cache的时间点。该流程的伪代码如下所示

async\_create\_code\_cache() {
  compile\_with\_eager\_compile();
  create\_code\_cache();
  save\_code\_cache();
}



if (has\_code\_cache) {
  evaluate\_script\_with\_code\_cache();
} else {
  start\_thread(async\_create\_code\_cache());
  evaluate\_script\_without\_code\_cache();
}

*   在启动过程中的所有路径运行完之后，再启动新线程生成code cache，这样不必使用eager compile也能获取足量的code cache，同时保证热启动性能不受影响，这样做的缺点是生成code cache的时间点受限，优点是峰值资源占用相对更少，且不必生成过量的code cache导致io变慢。这个流程可以用如下所示的伪代码来表示

async\_create\_code\_cache() {
  compile\_with\_out\_eager\_compile();
  create\_code\_cache();
  save\_code\_cache();
}



if (has\_code\_cache) {
  evaluate\_script\_with\_code\_cache();
} else {
  evaluate\_script\_without\_code\_cache();
}



if (script\_run\_completed) {
  start\_thread(async\_create\_code\_cache());
}

#### \[h2\]使用更高效的JSVM-API

在能达到相同效果时，使用更高效的JSVM-API是一种有效的性能优化方法，以下是一些具体的实践示例。

**使用IsXXX代替TypeOf**

过去发现，针对仅需要判断对象类型的场景，存在一种相对低效的使用方法：

从OH\_JSVM\_TypeOf接口获取类型后，再判断是否与某个类型相同。

这种方法需要先查询object的类型，这种方法相对于直接使用is方法会更慢，因此我们新增了针对基础类型的IsXXX系列方法，用更高效的接口代替了相对低效的接口。下面的示例中使用到的JSVM-API可以参考 [JSVM数据类型与接口说明](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/jsvm-data-types-interfaces)，这里仅展示调用的步骤。

*   低效用例

bool Test::IsFunction(JSVM\_Env env, JSVM\_Value jsvmValue) const {
    // type judgment
    JSVM\_ValueType valueType;
    OH\_JSVM\_TypeOf(\*env, jsvmValue, &valueType);
    return valueType == JSVM\_FUNCTION;
}

*   高效用例

bool Test::IsFunction(JSVM\_Env env, JSVM\_Value jsvmValue) const {
    // type judgment
    bool result = false;
    OH\_JSVM\_IsFunction(\*env, jsvmValue, &result); // 可直接判断是否为Function类型
    return result;
}

以某生态应用小程序场景为例，这个优化可以带来的性能收益端到端有150ms，总占比约5%。

**直接使用OH\_JSVM\_CreateReference，避免创建冗余的object**

过去存在这样一种创建reference的路径：

创建一个新的object->设置object的值->创建object的reference。

在已有值的情况下，直接创建值的引用即可。

下面的示例中使用的JSVM-API可以参考 [JSVM数据类型与接口说明](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/jsvm-data-types-interfaces)，这里仅展示调用的步骤。

*   低效用例

// (1) open handle scope
JSVM\_HandleScope scope;
OH\_JSVM\_OpenHandleScope(\*env, &scope);
// (2) get JSVM\_Value
JSVM\_Value jsvmValue;
OH\_JSVM\_GetNull(\*env, &jsvmValue);
// (3) create and store Reference for JSVM\_Value
JSVM\_Value wrappingObject;
OH\_JSVM\_CreateObject(\*env, &wrappingObject);
OH\_JSVM\_SetElement(\*env, wrappingObject, 1, jsvmValue);
OH\_JSVM\_CreateReference(\*env, wrappingObject, 1, &result->p\_member->jsvmRef);
// (4) close handle scope
OH\_JSVM\_CloseHandleScope(\*env, scope);

*   高效用例

// (1) open handle scope
JSVM\_HandleScope scope;
OH\_JSVM\_OpenHandleScope(\*env, &scope);
// (2) get JSVM\_Value
JSVM\_Value jsvmValue;
OH\_JSVM\_GetNull(\*env, &jsvmValue);
// (3) create and store Reference for JSVM\_Value
OH\_JSVM\_CreateReference(\*env, jsvmValue, 1, &result->p\_member->jsvmRef); // 可从任意对象类型直接创建Reference，代码更为简洁高效
// (4) close handle scope
OH\_JSVM\_CloseHandleScope(\*env, scope);

同样以某生态应用小程序场景为例，这个改动减少了大量冗余的接口调用，最终带来的端到端时间收益有100+ms，约3%。