By Kuanggu
Introduction: This article shares the accumulated experience and summary of our work on the Alipay mini program V8 Worker, including the technology evolution, architecture, basic features, the output of JavaScript (JS) engine capabilities, and some optimization solutions. Discussions and corrections are welcomed.
This topic describes the technology evolution from Service Worker to V8 Worker for Alipay mini programs.
As we all know, the source code of an Alipay mini program can be packaged into two parts:
The frontend framework APPX is also divided into the Render (af-appx.min.js) and the Worker (af-appx.worker.min.js):
The Service Worker is provided by the browser kernel and is designed to serve as a proxy server between Web applications and browsers. The Service Worker runs in a separate worker context, so it cannot access the document object model (DOM). Compared with the main JS thread that drives applications, the Service Worker runs on a different thread and therefore does not cause blocking.
However, the startup of the Service Worker and the startup of the Render are serial. The startup of the Service Worker is initiated by the JS of the Render only after the WebView is started. This is a major performance bottleneck for the mini program.
To solve the performance problems caused by the serial initialization and execution of the Worker and the Render, the mini program team tried to use WebViews to execute the Worker. When the mini program is started, two new WebViews are created. One WebView is used to render the Render, and the other WebView is specifically used to execute the JS of the Worker. However, an exclusive WebView is undoubtedly overqualified to execute the JS of the Worker. In addition, using a WebView consumes a large amount of resources.
The serial initialization of the Service Worker affects the startup performance of the mini program. The WebView Worker is not lightweight enough to run the mini program Worker code. Using a proprietary JS engine to do the work of the Worker was the best choice, so the V8 Worker was created.
The following figure shows the basic structure of the V8 Worker of the mini program, which will be elaborated on later in this article.
Using the V8 engine to run the Worker has the following benefits:
This topic describes the V8 Worker project structure and the mini program architecture based on V8 Worker. If you are not familiar with V8 engines, here is a brief introduction to V8 and links to learning materials.
Before we introduce V8 Worker, let's briefly learn about the V8 engine[2]. If you are familiar with V8, please skip this part.
V8 is Google's open-source high-performance JS and WebAssembly engine and is used in projects, such as the Chrome browser and Node.js. The threshold for learning V8 is still relatively high. Here, we only introduce the basic concepts of V8 that you need to know to read this article, as well as the official embedded V8 HelloWorld code. Some learning links are below.
An isolate is similar to a process in an operating system. Processes are completely isolated from each other. A process has multiple threads, and processes do not share resources with each other. The same is true for isolates. Isolate1 and Isolate2 are two virtual machine instances with their own stacks and are completely isolated from each other.
In V8, a context is an execution environment, which makes it possible to execute isolated and unrelated JS code in a V8 instance. You must explicitly specify a context for the JS code you are about to execute.
This is because JS provides some built-in tool functions and objects, which can be modified by the JS code. For example, if two completely unrelated JS functions are modifying a global object in the same way, an unexpected result is likely to occur.
A handle provides a reference to a JS object's location in the heap. The V8 garbage collector reclaims memory used by objects that can no longer be accessed. During the garbage collection process, the garbage collector often moves objects to different locations in the heap. When the garbage collector moves the object, it also updates all corresponding handles for the object with the object's new location.
When an object cannot be accessed from JS, and no handles reference it the object will be considered to be "garbage." The garbage collector will gradually remove all objects determined to be "garbage" from the heap. The V8 garbage collection mechanism is the key to V8's performance.
Local handles are stored on a stack and are deleted when the destructor of the stack is called. The lifecycle of these handles depends on the handle scope. When a function is called, the corresponding handle scope is created. When a handle scope is deleted, the garbage collector will reclaim objects referenced by handles in the handle scope if the objects are no longer accessible from JS or no other handles point to them. The examples in the "Getting Started Guide" use this type of handles.
A persistent handle is a reference to a heap-allocated JS object, which is the same as a local handle. However, it has two attributes, which differ in the lifecycle management of the reference they handle. When you want to hold a reference to an object and the reference exceeds the period or scope of the function call, or the lifecycle of the reference is inconsistent with the C++ scope, you need to use the persistent handle. For example, Google Chrome uses persistent handles to reference DOM nodes. A persistent handle supports weak references, that is, PersistentBase::SetWeak
. A weak persistent handle can trigger a callback from the garbage collector when references to an object are only from weak persistent handles.
In a context, a template is a model for JS functions and objects. You can use a template to encapsulate C++ functions and data structures in a JS object so that it can be operated by JS code. For example, Chrome uses templates to encapsulate C++ DOM nodes as JS objects and install functions in the global namespace. You can create a set of templates and reuse the set in each created context. You can create as many templates as you require. However, in any context, only one instance of any template can be created.
In JS, there is a strong duality between functions and objects. To create a new object type in C++ or Java, you need to define a class. In JS, however, you need to create a function and use the function as a constructor to generate an object instance. The internal structure and functionality of a JS object are largely determined by the function that constructed it. These attributes are also reflected in the design of V8 templates. Therefore, V8 has two types of templates:
1. Function Templates
A function template is a model for a JS function. You can call the GetFunction method of a template to create a JS instance of the template in the specified context. You can also associate a C++ callback with a function template that is called when the JS function instance is executed.
2. Object Templates
Each function template is associated with an object template. It is used to configure objects created with this function as a constructor.
An accessor is a C++ callback that calculates and returns a value when an object property is accessed by JS code. Accessors are configured through the SetAccessor method of an object template. This method receives the name of the property and the callback function associated with it, and is triggered when JS reads and writes the property.
The complexity of an accessor depends on the type of data you access:
You can configure a callback to be called when any property of the corresponding object is accessed. This is an interceptor. In terms of efficiency, interceptors can be divided into two types:
document.theFormName.elementName
in your browser to access the property.document.forms.elements[0]
in your browser to access the property.In V8, the same origin is defined as the same context. By default, you are not allowed to access any context other than the one from which you are calling. If you must access another context, you need to use a security token or a security callback. The security token can be of any value but is generally a unique canonical string. When you create a context, you can specify a security token using SetSecurityToken
. If you do not specify a security token, V8 will automatically generate one for the context.
This topic describes the mini program architecture based on V8 Worker in detail, including the JSAPI process details of the Render and V8 Worker and how the Render and the Worker communicate directly.
As shown in the preceding figure, in the initial development of V8 Worker, one mini program occupies one V8 isolate, and one V8 isolate only has one V8 context. The frontend framework APPX code appx.worker.min.js
of the mini program and the service code index.worker.js
of the mini program run on the same V8 context on the same V8 isolate. This design will incur JS security issues. The service JS code can access the internal JS object and internal JSAPI, which are injected into APPX, in the form of splices. In the same V8 context, it is impossible to isolate the execution environment of the service JS code from that of the APPX framework JS code. We will explain how to resolve this security problem later.
As shown in the preceding figure, the direct two-way traffic transmission of the Render and Nebula is realized through Console.log and loadUrl[9] in the WebView.
The container loads and runs the JS code of the Render through the loadUrl of the WebView. Before the WebView runs the JS code (af-appx.min.js
and index.js
) of the Render, it needs to inject the global JS objects needed by the APPX framework in advance, such as window.AlipayJSBridge
[10], for JSAPI calls.
The JSAPI call from the Render to the container is implemented through the Console.log[11] Web API.
Similar to the Render, when the V8 Worker is initialized, it is necessary to inject the global JS object, AlipayJSBridge, into the V8 Worker environment. The definition of AlipayJSBridge is in workerjs_v8_origin.js
[12] and workerjs_v8_origin.js
[13] has been loaded in V8 Worker in advance.
AlipayJSBridge = {
//xxxxx
call: function (func, param, callback) {
nativeFlushQueue(func, viewId, JSON.stringify(msg), extraData);
}
//xxxxx
}
Meanwhile, we have injected the nativeFlushQueue
API into the V8 Worker environment in advance and have bound the Java callback of this API
mV8Runtime.registerJavaMethod(new AsyncJsapiCallback(this), "__nativeFlushQueue__");
This way, the Worker calls AlipayJSBridge.call()
in JSAPI and finally calls back to the AsyncJsapiCallback()
on the container side.
After the JSAPI operation is processed in the container, if any results are returned, they are returned to the Worker.
Take sending messages from the Render to the Worker as an example the process is listed below:
onConsoleMessage
intercepts the message, deserializes it into a JSONObject, and sends it to bridge.sendToNative(event)
of the container bus.workerjs_v8_origin.js
, the _invokeJS
function is called. At this point, the Worker receives the message from the Render.You can see that a message needs to undergo multiple times of serialization and deserialization from the Render to the Worker if the container bus-based message channel is used, which is very time-consuming. It not only affects the startup speed of a mini program, but it also affects the frame rate of the mini program because interactive events, such as the sliding of the mini program, involve a large number of messages between the Worker and the Render.
Therefore, the MessageChannel-based message channel was created.
MessageChannel allows us to create a new message channel and send data using its two messagePort properties. As shown in the following figure, MessageChannel creates a pipeline, and the two ends of the pipeline respectively represent a messagePort, both of which can send data to the other end through portMessage and accept the data from the other end through onMessage. With the features of MessageChannel, the communication between the Render and the Worker can be done without going through the Nebula bus, which reduces the serialization and deserialization of messages.
With the increasing use of the V8 engine in Alipay services and the services of the entire group, the upgrade and maintenance of the V8 engine become more complicated and important. Each service may use a different interface, which needs to be re-adapted when the V8 engine is upgraded. Moreover, as mentioned earlier, the V8 engine is currently provided by the UCWebView kernel, and to use V8, a new copy is required.
How can we solve these problems? All problems in computer science can be solved by another level of indirection. Therefore, JavaScript Interface (JSI) was born.
JSI is an encapsulation of JS engines, such as V8 and JSC. It provides service users with basic, complete, stable, JS engine-free Java APIs and Native APIs that are compatible with later versions.
The benefits brought by JSI are listed below:
libwebviewuc.so
in the UC kernel, and no V8 copy is required.The following figure shows the architecture of JSI-based V8 Worker. Compared with the V8 Worker based on J2V8[14], the JSI-based V8 Worker only needs to load the V8 engine through the Java interface of JSI for mini programs, mini games, Cube, and other services. When U4 Linker is used in JSI to load libwebviewuc.so
, libwebviewuc.so
in the UC WebView SDK can be reused, and no copy is required. This solves the conflicts of global variables of libwebviewuc.so
in the case that libwebviewuc.so
and UC WebView coexists in the same process. JSI provides both Java and C++ encapsulation APIs to facilitate service users to access.
The JSI access document details on how to quickly use the JS engine with JSI are listed below:
As mentioned in the previous section, V8 Worker that uses the structure of a single V8 isolate and a single V8 context will incur JS security issues and cannot isolate the execution environment of the service JS code from that of the frontend framework JS code. The following section describes the multi-context V8 Worker and multi-isolate multi-threaded Workers.
The following figure shows a V8 Worker with an architecture of multiple contexts for isolation. For the same mini program, under the same V8 isolate, APPX Context, Biz Context, and Plugin Context (jsi::JSContext corresponds to v8::Context) are created respectively for the mini program frontend framework script (af-appx.worker.minjs
), the mini program service script (index.worker.js
), and the mini program plug-in[15] script (plugin/index.worker.js
). The same mini program may have multiple mini program plugins, each of which is assigned a separate V8 context as the execution environment.
As described in the V8 context security model[16], the same origin is defined as the same context. By default, different contexts cannot be accessed from each other unless the security token is set through SetSecurityToken
. Based on this feature of contexts, we isolate the JS execution environments of the frontend framework, mini program services, and mini program plug-ins in a secure manner.
In a mini program, some tasks that are processed asynchronously can be placed in the background Worker thread to run. After the task is completed, the result is returned to the main thread of the mini program. This is called a multi-threaded Worker.
The preceding figure shows the framework of a multi-threaded Worker. The main thread of the mini program's Worker runs on a separate V8 isolate, while the service JS, APPX framework JS, and plug-in JS run in their respective V8 contexts. Meanwhile, for each Worker task, a separate Worker thread will be initiated, and a separate V8 isolate and V8 context instance will be created. Each Worker task and the tasks in the main thread of the mini program are isolated from each other through threads and isolates.
Isolation through isolates means isolation of the V8 heaps. Therefore, data cannot be directly transmitted between the Worker main thread and the background Worker thread. If you want to implement the direct data transmission between the Worker main thread and the background Worker thread, the data needs to be serialized and deserialized. Serialization is the process of copying data from the source V8 heap to the C++ heap, and deserialization is the process of copying data from the C++ heap to the destination V8 heap. The Worker main thread and the background Worker thread transmit data through postMessage for serialization and onMessage for deserialization.
You may want to give the JS engine capability at the C++ layer to some other Alipay services, such as Native GCanas, and meanwhile spare the services from the need to connect the JS engine. In this case, you will need the V8 Worker to be able to output the JS execution environment of the mini program. The V8 Native plug-in is one of the solutions.
The following figure shows the framework of the V8 Native plug-in. The design idea is listed below:
The plug-in service will gain the following capabilities by connecting to the V8 Native plug-in:
Since the plug-in service can directly obtain the JS execution environment of the mini program, the plug-in service must be reliable. Otherwise, it will incur security issues. Therefore, whitelist management and switch control of the plug-in are required at the V8 Worker Java layer.
The reason for the initial use of V8 Worker was to solve the serial initialization and execution problems of the Render and the Worker of the mini program.
The preceding figure shows how V8 code caching works. Since JS is a Just-in-Time (JIT) language, V8 needs to parse and compile it before it is executed. Therefore, the execution efficiency of JS has always been a problem. To address this problem, you can use the V8 code caching. When the JS code is executed for the first time, the bytecode cache of the JS code is generated and stored on the local disk. When the same JS code is run for the second time, V8 can use the saved bytecode cache to rebuild the compilation result, so there is no need to recompile the code. With the code cache, the JS code will be executed faster.
V8 code caching is divided into two types:
The cache generated through eager code caching is more comprehensive, and the code cache hit rate of hotspot functions is higher. In addition, the size of the cache will be larger. Therefore, it takes longer to load the cache from the disk for the second time of execution. The V8 official report claims that eager code caching will reduce the time spent in parsing and compiling JS code by 20% to 40% compared with lazy code caching. We found through experiments that eager code caching does not bring better results than the lazy code caching that is currently used by UC since the size of the cache greatly affects the performance. However, through Trace analysis, the JS execution time is still significantly reduced when eager code caching is used, compared with when no caching is used.
[1] https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
[3] https://chromium.googlesource.com/v8/v8/+/branch-heads/6.8/samples/hello-world.cc
[8] https://v8.dev/blog/code-caching
[9] https://developer.android.com/reference/android/webkit/WebView#loadUrl(java.lang.String)
[11] https://developer.mozilla.org/en-US/docs/Web/API/Console/log
[14] https://github.com/eclipsesource/J2V8
[15] https://opendocs.alipay.com/mini/plugin/plugin-introduction
[16] https://v8.dev/docs/embed#security-model
The views expressed herein are for reference only and don't necessarily represent the official views of Alibaba Cloud.
Alibaba Cloud Achieved Large-Scale Implementation of Serverless in Core Business Scenarios
2,599 posts | 763 followers
FollowAlibaba Clouder - April 17, 2020
AlibabaCloud_Network - November 12, 2018
Alibaba Clouder - March 16, 2020
Alibaba Clouder - January 22, 2020
Alibaba Clouder - October 16, 2020
zcm_cathy - November 11, 2019
2,599 posts | 763 followers
FollowProvides a control plane to allow users to manage Kubernetes clusters that run based on different infrastructure resources
Learn MoreA secure image hosting platform providing containerized image lifecycle management
Learn MoreAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreBuild your cloud drive to store, share, and manage photos and files online for your enterprise customers
Learn MoreMore Posts by Alibaba Clouder