Ballerina Profiler

Improvements in your Ballerina code might be required to increase its runtime performance, minimize code execution times, increase throughput, and reduce latency. To do that, you need to identify performance bottlenecks, implications of concurrent executions, which code segments take longer to execute, and areas to improve performance. This can be done by using a profiler.

Ballerina Profiler is a tool that monitors the Ballerina runtime and its operations such as function calls. It can be used to understand the behavior and troubleshoot the performance issues of a Ballerina program and optimize it.

Note: Ballerina Profiler is an experimental feature, which supports only a limited set of functionality. The commands associated with the tool might change in future releases.

Features

  • Profile a local session of a Ballerina program through offline instrumentation.

  • Perform CPU profiling, which finds out where the CPU time is going.

  • Generate a flame graph, which shows the function call stack and the execution times of each function call. It provides an interface for viewing runtime performance and the ability to search, diagnose, and find performance bottlenecks.

Note: Profiling is a high-powered activity with a heavy overhead and can slow down your application.

Profile a Ballerina program

To profile a Ballerina package, you can use the bal profile CLI command inside the root directory of the Ballerina package.

Consider the following step-by-step guide to profile a Ballerina package.

  1. Create a Ballerina package named sort using the bal new CLI command.
Copy
$ bal new sort 
  1. Go to the root directory of the Ballerina package.
Copy
$ cd sort
  1. Replace the contents of the main.bal file with the following Ballerina code, which creates an array of random integers, sorts them, and verifies the output.
Copy
import ballerina/io;
import ballerina/random;

public function main() returns error? {
    int[] arr = check createRandomIntArray(check float:pow(10, 2).cloneWithType(int));
    int[] sortedArr = bubbleSort(arr);
    boolean isSorted = isSortedArray(sortedArr);
    io:println("Is the array sorted? " + isSorted.toString());
}

public isolated function bubbleSort(int[] arr) returns int[] {
    int n = arr.length();
    int temp = 0;
    boolean swapped = false;
    foreach int i in 0 ... n - 2 {
        foreach int j in 1 ... n - 1 - i {
            if (arr[j - 1] > arr[j]) {
                temp = arr[j - 1];
                arr[j - 1] = arr[j];
                arr[j] = temp;
                swapped = true;
            }
        }
        if (!swapped) {
            break;
        }
    }
    return arr;
}

isolated function isSortedArray(int[] sortedArr) returns boolean {
    foreach int i in 0 ..< sortedArr.length() - 1 {
        if (sortedArr[i] > sortedArr[i + 1]) {
            return false;
        }
    }
    return true;
}

isolated function createRandomIntArray(int size) returns int[]|error {
    int[] array = [];
    foreach int i in 0 ..< size {
        array.push(check random:createIntInRange(0, int:MAX_VALUE));
    }
    return array;
}
  1. Run and profile the Ballerina package using the bal profile CLI command.
Copy
$ bal profile

You view the output below.

Compiling source
        profiler_demo/sort:0.1.0

Generating executable
        target/bin/sort.jar

================================================================================
Ballerina Profiler: Profiling...
================================================================================
Note: This is an experimental feature, which supports only a limited set of functionality.
[1/6] Initializing...
[2/6] Copying executable...
[3/6] Performing analysis...
[4/6] Instrumenting functions...
 ○ Instrumented module count: 31
 ○ Instrumented function count: 1016
[5/6] Running executable...
Is the array sorted? true
[6/6] Generating output...
 ○ Execution time: 3 seconds 
 ○ Output: target/bin/ProfilerOutput.html
-------------------------------------------------------------------------------
  1. Open the target/bin/ProfilerOutput.html file using a web browser window to examine the Ballerina Profiler output to find the slow-running functions and performance bottlenecks, which can then be addressed to improve the program’s overall performance.

Profiler output and available details

The flame graph generated by the Ballerina Profiler contains the following details.

  1. Function call stack

  2. Time taken to execute each function and its percentage

Profiler output

Flame graph also has the following features.

  1. Search a function.

Search function inside flame graph

  1. Clear the search.

  2. Click on a function call and zoom the view.

Click function and zoom flame graph

  1. Reset the view.

Examine the output and find performance bottlenecks

When you observe the flame graph generated by the Ballerina Profile for the above code, you can see the bubbleSort function has taken the most time to execute.

Bubble sort function time

Let's try to optimize the sorting function to increase the performance of the Ballerina application.

Optimize the Ballerina code

In this example, you will use another sorting algorithm to reduce the time taken to execute the application. Replace the main.bal file with the code below, which uses the merge sort algorithm instead of the bubble sort algorithm for sorting.

Copy
import ballerina/io;
import ballerina/random;

public function main() returns error? {
    int[] arr = check createRandomIntArray(check float:pow(10, 2).cloneWithType(int));
    int[] sortedArr = mergeSort(arr);
    boolean isSorted = isSortedArray(sortedArr);
    io:println("Is the array sorted? " + isSorted.toString());
}

public isolated function mergeSort(int[] arr) returns int[] {
    int n = arr.length();
    int width = 1;
    while (width < n) {
        int l = 0;
        while (l < n) {
            int r = int:min(l + (width * 2 - 1), n - 1);
            int m = int:min(l + width - 1, n - 1);
            merge(arr, l, m, r);
            l += width * 2;
        }
        width *= 2;
    }
    return arr;
}

isolated function merge(int[] a, int l, int m, int r) {
    int n1 = m - l + 1;
    int n2 = r - m;
    int[] L = [];
    int[] R = [];
    foreach int i in int:range(0, n1, 1) {
        L[i] = a[l + i];
    }
    foreach int i in int:range(0, n2, 1) {
        R[i] = a[m + i + 1];
    }
    int i = 0;
    int j = 0;
    int k = l;
    while (i < n1 && j < n2) {
        if L[i] <= R[j] {
            a[k] = L[i];
            i += 1;
        } else {
            a[k] = R[j];
            j += 1;
        }
        k += 1;
    }
    while (i < n1) {
        a[k] = L[i];
        i += 1;
        k += 1;
    }
    while (j < n2) {
        a[k] = R[j];
        j += 1;
        k += 1;
    }
}

isolated function isSortedArray(int[] sortedArr) returns boolean {
    foreach int i in 0 ..< sortedArr.length() - 1 {
        if (sortedArr[i] > sortedArr[i + 1]) {
            return false;
        }
    }
    return true;
}

isolated function createRandomIntArray(int size) returns int[]|error {
    int[] array = [];
    foreach int i in 0 ..< size {
        array.push(check random:createIntInRange(0, int:MAX_VALUE));
    }
    return array;
}

When you profile the code again, you can see that you have reduced the time taken for sorting substantially and have improved the performance of the application.

Merge sort function time

Profile a Ballerina service

Consider the following step-by-step guide to profile a Ballerina package that contains an HTTP service.

  1. Create a new Ballerina package and replace the main.bal file with the below code.
Copy
import ballerina/http;

type Country record {
    string country;
    int population;
    string continent;
    int cases;
    int deaths;
};

type Data record {|
    string country;
    string continent;
    int population;
    decimal caseFatalityRatio;
|};

http:Client diseaseEp = check new ("https://disease.sh/v3");
final Country[] & readonly countries;

function init() returns error? {
    countries = check diseaseEp->/covid\-19/countries;
}

service /covid19/countries on new http:Listener(8080) {
    resource function get summary() returns json {
        Data[] listResult = from var {country, continent, population, cases, deaths} in countries
            where hasSignificantPopulation(population, deaths)
            let decimal caseFatalityRatio = <decimal>deaths / <decimal>cases * 100
            order by caseFatalityRatio descending
            limit 1000
            select {country, continent, population, caseFatalityRatio};
        return getTopSortedValues(listResult);
    }

    isolated resource function get names() returns json {
        return from var {country, population, deaths} in countries
            where hasNonZeroPopulation(population, deaths)
            select {country};
    }
}

isolated function getTopSortedValues(Data[] listResult) returns json[] {
    json[] sortedArray = listResult.sort("ascending", getKey);
    return getTopElements(sortedArray);
}

isolated function getTopElements(json[] sortedArray) returns json[] {
    return from var i in sortedArray
        limit 10
        select i;
}

isolated function hasSignificantPopulation(int population, int deaths) returns boolean {
    return population >= 100 && deaths >= 10;
}

isolated function hasNonZeroPopulation(int population, int deaths) returns boolean {
    return population >= 0 && deaths >= 0;
}

isolated function getKey(record {|string country; string continent; int population; decimal caseFatalityRatio;|} recordVal) returns string {
    return recordVal.country;
}
  1. Run the CLI command bal profile in the root directory to run the Ballerina package and profile it. This will run the service while profiling it.
Copy
$ bal profile
Compiling source
        profiler_demo/covid19_stats:0.1.0

Generating executable
        target/bin/covid19_stats.jar

================================================================================
Ballerina Profiler: Profiling...
================================================================================
Note: This is an experimental feature, which supports only a limited set of functionality.
[1/6] Initializing...
[2/6] Copying executable...
[3/6] Performing analysis...
[4/6] Instrumenting functions...
 ○ Instrumented module count: 44
 ○ Instrumented function count: 2935
[5/6] Running executable...
  1. Send 10 requests to each resource endpoint using the curl command.
Copy
$ curl localhost:8080/covid19/countries/names
Copy
$ curl localhost:8080/covid19/countries/summary
  1. Stop the service by sending the SIGINT signal to the Ballerina program. You can use Ctrl+C in the service running terminal to send the signal to it. This will stop both the Ballerina program and profiling it and will generate the profiler output.
[6/6] Generating output...
 ○ Execution time: 58 seconds 
 ○ Output: target/bin/ProfilerOutput.html
--------------------------------------------------------------------------------
  1. Open the target/bin/ProfilerOutput.html file using a web browser to observe the flame graph generated by the profiler.

Profiler output of a Ballerina service

By observing the flame graph you can see that the get summary resource function takes more time than the get names resource function. You can use the profiler output data to improve the performance of the Ballerina service.