Unveiling the Secret: The Technology Evolution of the Alipay Mini Program “V8 Worker”

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.

From Service Worker to V8 Worker

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 first part is responsible for the display of the mini program. The corresponding package is index.js, which is also named the Render.
  • The second part is responsible for the business logic and views update of the mini program. The corresponding package is index.worker.js, which is also named the Worker.

The frontend framework APPX is also divided into the Render (af-appx.min.js) and the Worker (af-appx.worker.min.js):

  • The Renders (index.js and af-appx.min.js) run on UCWebView or SystemWebView.
  • The Workers (index.worker.js and af-appx.worker.min.js) run on the Service Worker[1].

Service Worker

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.

WebView Worker

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.

V8 Worker

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:

  • It can solve the problems due to the serial initialization and execution of the Render and the Worker. WebViews and the V8 engine can initialize and execute the JS code of the Render and the Worker in parallel.
  • It can provide a secure execution environment for JS and isolate the framework JS from the service JS.
  • It allows JS objects to be easily injected into the mini program and the JavaScript application programming interface (JSAPI) to be bound.
  • It can support more data types, such as ArrayBuffer.
  • It can extend the capabilities of the Worker and provide features, such as the mini program plug-ins and multi-threaded Workers.
  • It can make full use of the capabilities of the V8 engine for performance optimization, for example, the V8 CodeCache.
  • It can provide the JS engine capabilities to services other than the mini program, for example, the V8 Native plug-in.
  • It allows you to customize the JS runtime parameters.

The Architecture of V8 Worker

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.

Introduction to and Getting Started with V8

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.

Basic Concepts of Embedded V8

1. Isolates

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.

2. Contexts

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.

3. Handles and Garbage Collection

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.

4. Templates

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.

5. Accessors

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:

  • Static Global Variables
  • Dynamic Variables

6. Interceptors

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:

  • Named Property Interceptor: It is called when a property is accessed with the property name in strings. For example, you use document.theFormName.elementName in your browser to access the property.
  • Indexed Property Interceptor: It is called when a property is accessed with its index. For example, you use document.forms.elements[0] in your browser to access the property.

7. Security Model

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.

The Mini Program Architecture Based on V8 Worker

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.

The Architecture with a Single V8 Context

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.

The JSAPI Call Process of the Render

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.

From the Container to the Render

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.

From the Render to the Container

The JSAPI call from the Render to the container is implemented through the Console.log[11] Web API.

The JSAPI Call Process of the Worker

From the Worker to the Container

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.

From the Container to the Worker

After the JSAPI operation is processed in the container, if any results are returned, they are returned to the Worker.

Communication Between the Render and the Worker

The Container Bus-Based Message Channel

Take sending messages from the Render to the Worker as an example the process is listed below:

  • The Render sends a postMessage, which needs to be serialized once and converted into a string.
  • WebChromeClient onConsoleMessage intercepts the message, deserializes it into a JSONObject, and sends it to bridge.sendToNative(event) of the container bus.
  • The container bus distributes the event.
  • The worker plug-in intercepts the postMessage event and sends it to the Worker.
  • V8 Worker deserializes the message into a string, converts it into a JS data type, and transmits it to the V8 context where the Worker is located.
  • In workerjs_v8_origin.js, the _invokeJS function is called. At this point, the Worker receives the message from the Render.

The MessageChannel-Based Message Channel

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.

V8 Worker Connects to JSI

Background

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.

Introduction to JSI

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:

  • It provides JSI APIs that are independent of specific JS engines and apply to all platforms.
  • It has implemented common features, such as Inspector, Code Cache, and JS Timer, allowing service users to focus on service development and shorten the development cycle.
  • JSI is responsible for handling the compatibility issue of JS engine versions and provides an imperceptible connection to services.
  • JSI directly uses libwebviewuc.so in the UC kernel, and no V8 copy is required.
  • JSI is co-constructed by Alipay and UC. The JSI access layer is implemented on the Ariver project and is exported to various services of the Group through Ariver.

JSI-Based V8 Worker

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:

  • Initialization on the Java side and Native side
  • Create JSEngine (corresponding to v8::isolate)
  • Create JSContext (corresponding to v8::Context)
  • Inject JS objects, including global constants, global functions, and global accessors, through Java or C++ APIs
  • Execute JS code
  • Trace analysis and Timer

How Does V8 Worker Solve JS Security Issues

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.

Isolation Through Multiple Contexts

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.

Multi-Threaded Worker with Multiple Isolates

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.

JS Engine Capability Output

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 V8 Native Plug-in

The following figure shows the framework of the V8 Native plug-in. The design idea is listed below:

  • Add a layer of C++ plug-in code to V8 Worker, define the Native plug-in interface, and load the dynamic link library of the service and manage the plug-in.
  • Expose the mini program JS execution environment (C++ interfaces based on JSI, jsi::JSEngine, and jsi::JSContext) to the plug-in user through the plug-in interface. Then, service users can instantly obtain the JS execution environment of the mini program, conveniently add custom JS objects, and bind custom JSAPI.
  • The V8 Worker notifies service users of the mini program lifecycle events through the plug-in interface.
  • Meanwhile, expose the PostTask interface to the plug-in service, allowing the plug-in service to execute the task in the JS thread of the mini program.

The plug-in service will gain the following capabilities by connecting to the V8 Native plug-in:

  • Obtain mini program lifecycle events
  • Obtain the JS execution environment of the mini program
  • Execute tasks in JS threads of the mini program
  • Access the JS object and JSAPI of the mini program
  • Inject custom JS objects and bind the custom JSAPI implemented through C++

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.

V8 Worker Performance Optimization

Parallel Initialization

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.

Code Caching

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:

  • Lazy Code Caching: Only generates the code cache for the hot functions that have been executed
  • Eager Code Caching: Generates the code cache for the entire JS code

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.

Reference

[1] https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API

[2] https://v8.dev/docs

[3] https://chromium.googlesource.com/v8/v8/+/branch-heads/6.8/samples/hello-world.cc

[4] https://v8.dev/docs

[5] https://docs.google.com/presentation/d/1OqjVqRhtwlKeKfvMdX6HaCIu9wpZsrzqpIVIwQSuiXQ/edit#slide=id.ge4ef702cb_2_67

[6] https://docs.google.com/presentation/d/1HgDDXBYqCJNasBKBDf9szap1j4q4wnSHhOYpaNy5mHU/edit#slide=id.g1357e6d1a4_0_58

[7] https://docs.google.com/presentation/d/1HgDDXBYqCJNasBKBDf9szap1j4q4wnSHhOYpaNy5mHU/edit#slide=id.g1357e6d1a4_0_58

[8] https://v8.dev/blog/code-caching

[9] https://developer.android.com/reference/android/webkit/WebView#loadUrl(java.lang.String)

[10] https://codesearch.alipay.com/source/xref/Android_wallet_master/android-phone-nebula-git/nebula/js/h5_bridge.js?r=78c30345

[11] https://developer.mozilla.org/en-US/docs/Web/API/Console/log

[12] https://codesearch.alipay.com/source/xref/Android_wallet_master/android-ariver/js/workerjs_v8_origin.js?r=b59d7f92

[13] https://codesearch.alipay.com/source/xref/Android_wallet_master/android-ariver/js/workerjs_v8_origin.js?r=b59d7f92

[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.

Original Source:

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store