How to profile the JavaScript code of your Tabris.js Application

If your Tabris.js application feels slow or unresponsive in certain places, the first step is to identify (or narrow down) the possible cause of the performance issue. Of course sometimes it’s obvious, especially for a seasoned developer. A non-exhaustive list of candidates would be:

  • A lot of logging to an active console.
  • Loading many or very large modules.
  • Slow network connection.
  • Complex UI, e.g. too many widgets too deeply nested.
  • Expensive synchronous calculations.

One thing to keep in mind is that an application that is sideloaded and has the developer tools enabled will always be slower than in production. So maybe try a production build before you dig too deep into the above list. But assuming this does not fix the issue, you’ll have to start investigating. If you already have a suspicion, you could try to verify it with a console log like this:

Naturally, this is a very limited approach that requires a lot of trial and error. If you want to cut to the chase using a JS profiler, here is how you do it:

The example application

In this tutorial, we will assume that the issue is within your JavaScript code itself, something under your influence, not a fundamantal issue with the network, device, OS, or framework. For simplicity, we will use a deliberately badly coded plain JavaScript Tabris.js application as an example. Let’s say you have a simple CollectionView like this and it stutters when scrolling:

It looks like this:

Establishing a Debug Connection

First, we need to connect the profiler to the application. For that you need the Chrome (or Chromium) browser on your development machine and an Android device or emulator with a debug build of your application. In this example, we will use an emulator and sideload the app using the special IP In the developer UI, enable the option that makes the app wait for a debug connection on startup. On Chrome open:

… where is the IP of your Android device if you’re not using an emulator. Now the app should start executing.

Connection Troubleshooting

If you get this error message:

Try loading the page without the parameters first, so:

Then enter the full URL again.

If you get this message:

It most likely means that Chrome can not reach your device at all. On a real network, this may be a firewall issue. If you’re using an emulator, you may have to enable port forwarding (again) using adb. Also, some newer Android versions appear to have issues with WebSockets in emulators, so consider using an older image. Android Pie was tested successfully.

If the app hangs, check in Chrome if the debugger paused it.

Running the profiler

To profile your app, open the right-hand overflow menu in the gray bar and select “More tools”, then “JavaScript Profiler”.

The “Performance” tab is not going to work.

Now click start:

Interact with the app in the manner that exemplifies your issue (as scrolling in this example), then click “stop” in the same place as before.

You’ll now see an entry for the recorded session under “CPU PROFILES” on the left, and if it’s selected, you can pick “Chart”, “Heavy” or “Tree” on the top.

Understanding the Chart

The upper timeline represents the entire recorded session. There is a lot of activity in the middle section; this is where the CollectionView was scrolled. You can select a range to display in more detail below. Let’s zoom in far enough to get a general idea of what’s going on.

The green bars are JavaScript function calls that require significant time, essentially a stack trace, just from top to bottom. You can hover them for detailed information or click them to get to the source code. This is very useful in the case of anonymous functions.

The red bars are native code called from within the JavaScript VM. These are platform-specific parts of the Tabris.js framework that can’t be profiled using this tool.

As you can see there are mainly two functions at the top: _trigger and flush. You can ignore _notify as it will always be there and is not relevant to performance analysis. A lot of activity in Tabris.js will look like this, so it is useful to understand what these functions represent.

  • _trigger – This is where the framework passes an OS event on to JavaScript. Typically this will be a user input such as tapping a button, swiping, scrolling or entering text. It could also be a timer, network response, or some other asynchronous operation. All your own JS code will be executed within this function.
  • flush – This is where the native UI is updated. When a Tabris widget object is created or manipulated, this isn’t immediately passed on to the OS since there is a significant amount of overhead associated with a JS-to-native call. Instead, all changes are recorded and then transferred in this function.

You have no direct control over flush; its duration will correspond to the number of changes made to the UI. The internals of it will be optimized a bit in Tabris 3.8, but the core principle is always the same.

Let’s zoom in further into a single _trigger call:

As you can see almost all time is spent in updateCell, which is to be expected: CollectionView works by re-using the same widget (the “cell”, in our case a TextView) again and again to display different data as you scroll. This function is where the cell is configured on the fly to display a new item, in our example, a “pet” object.

The interesting part is the function below, getPets, which eats up all the time of updateCell and even contains some red bits. This is bad, as you want to have as few JS-to-native calls within your _trigger call as possible, though it can sometimes not be avoided.

The Details: Tree View

We can get even more information via “Tree View (Top Down)”. Here, the calls hierarchy grows from left to right (instead of top-to-bottom), and all functions’ calls over the entire session are aggregated; nothing is omitted. By drilling down on the most expensive function calls (as indicated by “Total Time”) we also arrive at getPets and can indeed confirm that it is responsible for a good 4 seconds of the 5 seconds spent in _notify.

Note that in this view, all the time spans have to be considered relative to one another, as they depend on the total time recorded. Look at the percentages if you compare across recordings.

Furthermore, we can see that about 2 seconds are spent in getPets itself, indicated by its “Self Time”, and another 2 in nested getItem calls.

This is Heavy

There is a third view “Heavy (Bottom Up)” that is useful to find evil functions like getPets immediately. Select it and then “Self Time” to list the functions that eat up the most time within themselves, not counting nested calls. Typically these would contain for or while loops and have a “Self Time” that makes up a significant part of its “Total Time”. You can extend the item to see all functions calling the “heavy” function, a branching stack trace if you will.

Note that such functions don’t always exist as execution time may be distributed evenly across many functions, or a function may actually be lightweight but called very often. Even in our case the “Self Time” of getPets only makes up about half of its “Total Time”.

The Fix

Now have a look at that troublesome function:

Yeah, this explains things. Every time this function is called, it accesses localStorage, which is one of the more expensive synchronous (blocking) methods in the framework. In addition, it parses an array (presumably) of unknown size and wraps all items in “Pet” instances.

Remember how this function was called?

That’s a lot of reading and parsing several times per updateCell.

Now, we could try to optimize getPets itself. However, experimenting a bit with the profiler will show that the performance gain would be tiny, even if we could get rid of the loop.

No, the implementation getPets is fine, the real fix is obviously to cache the results:

Another run with the profiler confirms the success:

As you can see the _trigger and updateCell calls aren’t even visible anymore, and now we have some gray areas between the _notify blocks, indicating the application is idle and giving the OS time to update the screen.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *