Stanimira Vlaeva

Embedding V8 in the real world

V8 is the JavaScript engine powering Google Chrome, Node.js and NativeScript. NativeScript embeds V8 to process JavaScript and dynamically call Android APIs. This enables developers to write Android applications in JavaScript and directly access the underlying OS. Come to this session to learn what challenges the NativeScript team met embedding V8 in a mobile framework and how you can power any C++ based application with one of the most sophisticated JavaScript engines.

Portrait photo of Stanimira Vlaeva

Transcript

STANIMIRA: Hello, everyone. And welcome to my session. I'm going to be talking about V8 in the real world, or more specifically in the native script framework. I'm Stanimira Vlaeva, I'm a software engineer and work on this cool open source project, NativeScript.

And I'm with web technologies, and find me on Twitter, the best place. Or in the karaoke after.

So, NativeScript. Our main topic today after V8, of course. What is it? What is it? How many people here have heard about NativeScript? Awesome.

Okay. NativeScript is a framework for building native mobile applications for Android and iOS using web technologies. Like Angular, Vue, or just plain JavaScript.

In short, it is a way to execute JavaScript in the mobile world. And build mobile applications with it. We will take a short overview of the architecture of the framework. At the bottom, of course, we have Android and iOS. Operating systems.

On top of that we have the NativeScript run times for Android and for iOS which provide the 100% native API access. But if you have had to build a native application for Android or iOS, you may have noticed the way to do that is quite different. The APIs are different and the way to build your user interface is different. Everything is completely different because they are two different worlds.

That's why NativeScript provides a common abstraction for these APIs. It is part of the framework and the NativeScript developer cans use that in order to build layouts or build user interface or even style their applications with CSS and this layer is written in JavaScript and you can use that so that you can have a single codebase and have different applications for Android and iOS.

NativeScript also has a very light application framework which provides us with native bindings, navigations and some other cool things. And if you need something a bit more sophisticated while building your applications, NativeScript also supports Angular and Vue JS.

Today we're gonna talk about the bottom most levels. The deep stuff. And more specifically, we're gonna talk about the Android runtime. The two runtimes are quite similar.

And the biggest difference between them is that the Android runtime uses V8 under the hood whereas the iOS run time uses another JavaScript engine. JavaScript Core.

But the principle of how they work is quite similar. We're gonna start by explaining how the native API access works. As you make have guessed from the name "NativeScript," this is kind of what we mostly brag about because we have 100% API access. And this is why you should be using NativeScript instead of whatever  anything else you choose.

The main advantage. How it works.

We'll start with the look at the application package of our NativeScript application. So, we have Android, some phone or some device that is running the Android operating system. And the NativeScript application is just a regular Android application and which has some NativeScript magic inside it. And the first part of the magic is, of course, the JavaScript code that the NativeScript developer wrote and shipped inside that application. The JavaScript code is not cross compiled or converted or anything like that, it stays JavaScript during the whole life cycle while the application is running.

We also have the NativeScript run time, both in the Java part. We're going to talk about them shipped together inside the application. And the last part, almost, is V8. Why do we need to ship V8 inside an Android application? Well, to execute JavaScript.

V8 is a JavaScript engine. It executes JavaScript. It is embedded in Chrome, Note, even Microsoft nowadays and of course in NativeScript. It was developed by Google.

It was created from the Chrome browser and it's one of the fastest JavaScript engines out there. Another reason why we chose V8 is because it has a cool API that we can use and plug into the runtime.

If you want to read a bit more about V8 and how it works, I highly recommend these two resources. The first one is a really amazing popup series which is a crash course just in time compilers. And the other one is a video which is very recent. It's called why the script? And describes the optimizations under the hood while it executes your JavaScript code.

It's from the V8 team. If you want to learn about modern JavaScript engines, these are two great resources to get started.

The next part of the NativeScript magic. The metadata generator. This is one of the very, very valid JavaScript code inside of NativeScript. But we have something that is not usually in the JavaScript language, right? Android. Where does this come from?

Well, let's imagine that when your computer, you have some native library. For example, Android SDK. And you use that inside your NativeScript application. While your application is being built, NativeScript runs a special tool called the metadata generator which traverses that native library and gets information about the APIs.

It gets information about all the global packages, about every single class, about how these classes can be instantiated, about every method in these classes and what are the meta signatures. Basically, it gets information how every single method and API can be used.

That is saved inside a compact runtime binary which is, again, shipped inside the application.

So, we have information about how we can create stuff in Java inside the metadata. And the metadata of course is shipped together with the whole application as well? And what happens at launch time? We initialize V8 which can execute the JavaScript code. We load the metadata from the files saved inside the application and we attach source and callbacks. And the callbacks are the most important part about embedding V8. They are our way to plug into the JavaScript code and do all sorts of stuff.

Let's start by explaining some stuff about these callbacks and how they actually work together with the metadata to provide access with the native APIs. Okay, we have this expression, Android media recorder. We are trying to execute that JavaScript code. The NativeScript runtime has read the metadata and found out that there is an Android global package.

That's why it has created a global object inside the running V8 instance for Android. It also has attached some callbacks to that object. Like the package getter callback so that when we query for Android.media, the NativeScript runtime plugs in with that callback.

The callback will be executed. And inside the callback the NativeScript runtime will try to find Android.media inside the metadata. It returns something, some information, for example, some information that Android.media has some media recorder. And it also has a package getter callback attached. So, when that callback is called, we find the media recorder inside the Android media package in the metadata.

And this time we return a constructer function because this is actually a class. And why is this constructer function so important? Well, because when it's invoked with new, it actually contains a constructer callback. Again, attached by the NativeScript runtime. And this is where the actual magic happens.

Because the NativeScript runtime creates a native Java object. But how does that happen?

Well, we use JNA, Java native interface, and this is a bridge between V8 and the running Android runtime. So, we can save functions back and forth between the two.

So, we create a native object. Then we create the JavaScript proxy object that we're going to discuss a bit later and we return the proxy object to the JavaScript world. If it's right to access something inside that proxy, well, actually this proxy object is not very simple. It's not a plain object.

It creates some callbacks. Contains some callbacks as well.

So, when we try to access this random field, we know that this field exists in the Java world so that we have attached a field getter callback. And the field getter callback actually queries the original Java object. But there is a slight complication here. Okay, we can get the result from the Java world. But the data type is different from the JavaScript data type, right? So, Java run string is not something we can assign to a JavaScript variable.

And that is why there is a marshaling service. To convert it from Java so to JavaScript and vice versa. At this point, you would say, wouldn't that be terribly slow to convert everything? Obviously, it will be, if it's to convert object, it's not a good idea.

This is another reason why proxies are quite useful. So, for objects, we just create a plain JavaScript object which has the same methods with the same signatures. And the same members as well. And inside that we have callbacks. So, that when you call some method with the same name on the JavaScript object, the callback will be called and the NativeScript runtime will call the original Java method for JNA.

And this is a very cheap operation. Creating new JavaScript objects. Instead of converting data. If you call a method, same story. A method callback is triggered.

We call the original Java method. The result is marshallized again and returned back to the JavaScript world. If we have arguments in that method, the arguments will be converted to Java data format. And then they will be  the Java method will be called with deconverted arguments.

Okay. Let's see just a quick overview of all these callbacks, if they are confused you so far. We try to instantiate new object and assign that to a JavaScript variable. We call the constructer callback. If you want to create a new instance of the class through JNA.

The instance is returned. And because it's an object, the NativeScript runtime creates a JavaScript proxy object. Then we try to call some methods on that proxy.

We call actually the method callback without knowing that we are calling it. Everything is hidden. It happens behind the scenes. But the method callback then calls the original Java method.

The result that we can get is returned through JNA and marshallized and returned back to the JavaScript world. That's all the communication magic that happens.

What you may be wondering at this point what happens with these objects. Like we create JavaScript objects. We also create Java objects. They are collected in some way.

So, we actually have to take care of their life cycle. And in JavaScript we don't have to manually manage the memory. There is a garbage collector that runs.

And it's always to retrieve the memory of the unused objects. It also has a nondeterministic nature. We can't be sure when the garbage collector will run. And the other kind of complication is that, well, the Android runtime also has a garbage collector. It's pretty funny.

So, we have two garbage collectors running. We have objects in both worlds. And that's one of the biggest challenges of the NativeScript runtime. We have to kind of try to synchronize that. We have to ensure that no object is collected if there is a living counterpart.

For example, if you create some Java object through JavaScript, and then try to access it, if the Android garbage collector collected the native Java object, that sounds really cool because you will try to access something is that doesn't exist, and the application will crash. Like, it will crash. Yeah. You're running a mobile application and it's not really cool user experience.

Okay. In order to plugin into the life cycle, we use finalizer callbacks so that when the garbage collector of V8 marks something that  for collecting, says that some object doesn't have living instances anywhere and it should be collected, the finalize of the callback will be called. And this is the place where the script runtime is plugged into. We have strong and weak references.

Let's see how these actually look like. We have the same example as before.

First, we create the native object. Then we create the JavaScript proxy. And then the NativeScript runtime has two collections. One for strong references and one for weak references.

When the objects are first created, we create a strong reference or a link, if you would like to call that, between the two objects. And if that's confusing, okay, the proxy lives inside V8. The original object lives inside the Android runtime and the references live inside the NativeScript runtime.

All right. Time to collect stuff. Some garbage collector runs. We can't really say for sure if it's gonna be the V8 garbage collector or the Android runtime garbage collector. But say in this example that V8 will decide to collect the memory first.

So, there is no one in the JavaScript world using the JavaScript proxy recorder. And that's why it's marked for collection. But at this point the finalizer callback is called. And the NativeScript runtime sees that there is a living strong reference.

That's why the strong reference is turned into a weak reference. And we instruct V8 not to collect that object.

The next time when the Android garbage collector runs, it decides to mark the recorder object for collection because no one in the Java world is using it. And sees there's a weak reference. And because it's a weak reference, this object will be collected. So, let's say that at some time the V8 garbage collector runs again. Well, now there is a weak reference.

And the weak reference doesn't point to anything. And because we don't have anything out there in the Java world, this object can also be collected. It's marked for collection and now we can collect it.

But if we had two consecutive garbage collector collection runs inside V8 and we still had a weak reference to a living object that wasn't created by the Java garbage collector, the V8 object wouldn't have been collected as well. So, this is a normal cycle. And as you could imagine, there are some challenges that happen because we have two running garbage collectors. Well, we could get out of memory exceptions.

Usually the objects that were created in the Android application are not really big. So, we wouldn't have that happening for a hello world application, right?

But the problem is that, of course, yeah, we have a few garbage collection cycles that should be run in order for some memory to be retrieved back. And if we create some big objects, this can cause problems. Because the memory is not retrieved on time.

For example, we can have images. And an image  let's say that this Java array in the Java world. The Java array is quite big. Whereas the JavaScript proxy is not so big. It's actually just a plain object with some callbacks attached.

So, it actually looks like that, memorywise. We have a lot of memory in the Java world. We have a really plain JavaScript proxy. And the Java  the Android garbage collector is actually dependent on the V8 garbage collector in order to retrieve this huge chunk of memory.

So, at this point the V8 garbage collector, even if you have tens of thousands of these small, plain proxies, it doesn't have pressure to be run. Because we don't really take a lot of memory in the running JavaScript virtual machine. So, V8 doesn't really have a reason to trigger garbage collection. If that doesn't happen on time, well, we may cause out of memory exceptions. Because we're taking too much space in the Java virtual machine.

Some solutions or more like strategies to overcome this. Because there is no straightforward deterministic solution. Because of the nature of the problem. The first one, there is an API provided by V8 that lets us instruct V8 about the memory that is allocated inside of it.

So, in our case we can say to V8, okay, the Android application that is running actually uses this amount of memory. And this memory is used because you have created some JavaScript objects.

And the JavaScript objects are still pointing to living instances in the Java world. So, this should hint V8 to garbage collection more often because it's aware that there is more memory freed. I mean, it works in practice, but we can still get out of memory exceptions. Another important thing, we are doing this internally inside of a NativeScript runtime so the NativeScript developers don't have to use that. And it is a technique used internally.

Another solution. We can force garbage collection, of course. We can say, V8, come on, run garbage collection. Mark these objects as free to be retrieved. Make these strong references weak.

Then we can run the Android garbage collector. And then run the V8 garbage collector again. This is not the best thing ever because it doesn't guarantee that the garbage collection will be run. It kind of schedules it or hints it that it will be run.

But we don't have a guarantee that it will be run. And you don't have a guarantee that it will be run in that order as well. And it's not the cheapest option out there. You are checking the objects and seeing if they have living references. It may have the opposite effect.

So, this is not the best solution ever. You can do it. It is some strategy, but we don't really recommend using that.

Okay. Let's take a look at this again. We have a strong reference. What didn't have references? And what if we had the control over things like this Java object can't be collected because I'm not using it anymore, it can be collected. I'm not using it in the JavaScript world.

Well, the NativeScript runtime releases a function, release native counterpart and we need to run the object, and it basically destroys all these references. So, we invoke that. We basically instruct that we're no longer using this native object and it can be retrieved.

So, whenever the next Android garbage collector runs, it is no longer dependent on V8's garbage collector. It can mark this object and say retrieve it. And as the last part of the presentation, something like a bit simpler. What is the point? Well, the JavaScript code in NativeScript is run and executed from a single thread. Which actually happens to be the main user interface thread.

And if you see where I'm going, this can cause some problems with log and junk. So, you can see some glitches while your mobile application is being used. And this is not the best experience for a native mobile application as well. So, first you know what is junk, probably. It is the percentage of frames that are dropped while you are doing some calculations.

We're not gonna focus on that. It's important to know that in the NativeScript application, if you are just building user interface, you are creating native Android and iOS widgets. So, you shouldn't experience junk in a native list view when scrolling, for example. If you are creating animations, same thing.

You have many ways of creating in NativeScript, with Angular, CSS, with JavaScript. But internally it's actually creating native applications. So, you shouldn't have any problem while running animations.

The other thing that is commonly  we're commonly asked for. If you're creating an HTTP request, the plugin that you're gonna use creates a background thread in the Java world which wouldn't freeze the main UI thread. But you may see some junk when you're executing CPUintensive operations. And the same thing would happen if you are executing CPUintensive Java code in an Android application.

What is the solution? Well, worker threads. Essentially background threads to unlock the main thread.

We don't have JavaScript memory sharing, but we have a way to communicate between the worker thread and the main UI thread. And the final thing, I'm going to ask you a question. You have to be patient for 30 more seconds. What is a worker thread in NativeScript? Two hints.

Is it an isolate? Isolate is V8's way to isolate and executes some memory  sorry  to isolate some memory for a code that's being executed. They can run in parallel and we don't have memory sharing. Context. One isolate can have multiple contexts.

We don't have member isolation and we can't run contexts in parallel. Also, you have to explicitly specify the context that some code is being executed on.

Isolates or context? Isolates. Contexts. Okay. That's okay. Isolates.

Okay. So, isolate. All right. So, this was about NativeScript and V8. If you want to meet me afterwards, you can find me in Twitter, and I'll be coming to you.

And I also want to thank my colleague who helped me with this presentation. And this is his handle on Twitter as well. So, thanks a lot.

[ Applause ]