GraalVM Debugging and Monitoring Tools

GraalVM provides a set of tools for developers, integrators, and IT administrators to debug and monitor GraalVM and deployed applications.

Component Updater

GraalVM allows you to install additional components (languages). Use the Graal updater utility to manage the installation.

Debugger

GraalVM supports debugging of guest language applications and provides a built-in implementation of the Chrome DevTools Protocol. This allows you to attach compatible debuggers such as Chrome Developer Tools to GraalVM.

To debug guest language applications, pass the --inspect option to the command-line launcher, as in the following example with a node.js hello world program:

var http = require('http');

var server = http.createServer(function (request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.end("Hello World!\n");
});

server.listen(8000);

console.log("Server running at http://localhost:8000/");
  1. Save this program as HelloWorld.js and then run:
$ node --inspect --jvm HelloWorld.js
Debugger listening on port 9229.
To start debugging, open the following URL in Chrome:
    chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.1.1:9229/76fcb6dd-35267eb09c3
Server running at http://localhost:8000/
  1. Navigate to http://localhost:8000/ in your browser to launch the node application.

  2. Open the chrome-devtools:... link in a separate Chrome browser tab.

  3. Navigate to the HelloWorld.js file and submit a breakpoint at line 4.

  4. Refresh the node.js app and you can see the breakpoint hit.

You can inspect the stack, variables, evaluate variables and selected expressions in a tooltip, and so on. By hovering a mouse over the response variable, for instance, you can inspect its properties as can be seen in the screenshot below:

Consult the JavaScript Debugging Reference for details on Chrome DevTools debugging features.

This debugging process applies to all guest languages that GraalVM supports. Other languages such as R and Ruby can be debugged as easily as JavaScript, including stepping through language boundaries during guest language interoperability.

Inspect Options

Node Launcher

The node.js implementation that GraalVM provides accepts the same options as node.js built on the V8 JavaScript engine, such as:

--inspect[=[host:]<port number>]

Enables the inspector agent and listens on port 9229 by default. To listen on a different port, specify the optional port number.

--inspect-brk[=[host:]<port number>]

Enables the inspector agent and suspends on the first line of the application code. Listens on port 9229 by default, to listen on a different port, specify the optional port number. This applies to the node launcher only.

Other Language Launchers

Other guest language launchers such as js, python, Rscript and ruby accept the --inspect[=[host:]<port number>] option, but suspend on the first line of the application code by default.

--inspect.Suspend=(true|false)

Disables the initial suspension if you specify --inspect.Suspend=false.

Additional Common Inspect Options

All launchers accept also following additional options:

--inspect.Path=<path>

allows to specify a fixed path that generates a predictable connection URL. By default, the path is randomly generated.

--inspect.Remote=(true|false)

when true, the local host address is used instead of the loopback address. That allows remote connection of the inspector client. By default, the loopback address is used. This option became obsolete after being able to specify the host name in --inspect option and may be removed in future versions.

--inspect.Secure=(true|false)

when true, use TLS/SSL to secure the debugging protocol. Besides changing the WS (web socket) protocol to WSS, the HTTP endpoint that serves metadata about the debuggee is also changed to HTTPS. This is not compatible e.g. with chrome://inspect page, which is not able to provide the debuggee information and launch the debugger then. Launch debugging via the printed WSS URL directly.

Use the standard javax.net.ssl.* system options to provide information about keystore with the TLS/SSL encryption keys, or following options:

  • --inspect.KeyStore keystore file path,
  • --inspect.KeyStoreType keystore file type (defaults to JKS),
  • --inspect.KeyStorePassword keystore password,
  • --inspect.KeyPassword password for recovering keys, if it’s different from the keystore password.
--inspect.WaitAttached=(true|false)

when true, no guest language source code is executed until the inspector client is attached. Unlike --inspect.Suspend=true, the execution is resumed right after the client is attached. That assures that no execution is missed by the inspector client. It is false by default.

Advanced Debug Options

Following options are for language experts and language developers:

--inspect.Initialization=(true|false)

when true, inspect the language initialization phase. When initial suspension is active, suspends at the begining of language initialization and not necessarily at the begining of the application code. It’s false by default.

--inspect.Internal=(true|false)

when true, internal sources are inspected as well. Internal sources may provide language implementation details. It’s false by default.

Programmatic Launch of Inspector Backend

Embedders can provide the appropriate inspector options to the Engine/Context to launch the inspector backend. The following code snippet provides an example of a possible launch:

String port = "4242";
String path = "session-identifier";
String remoteConnect = "true";
Context context = Context.newBuilder("js")
            .option("inspect", port)
            .option("inspect.Path", path)
            .option("inspect.Remote", remoteConnect)
            .build();
String hostAdress = "localhost";
String url = String.format(
            "chrome-devtools://devtools/bundled/inspector.html?ws=%s:%s/%s",
            hostAdress, port, path);
// Chrome Inspector client can be attached by opening the above url in Chrome

Profiler

GraalVM provides Profiling command line tools that let you optimize your code through analysis of CPU and memory usage.

Most applications spend 80 percent of their runtime in 20 percent of the code. For this reason, to optimize the code, it is essential to know where the application spends its time. GraalVM provides simple command line tools for runtime and memory profiling to help you analyze and optimize your code.

In this section, we use an example application to demonstrate the profiling capabilities that GraalVM offers. This example application uses a basic prime number calculator based on the ancient Sieve of Eratosthenes algorithm.

  1. Copy the following code into a new file named primes.js:

     class AcceptFilter {
         accept(n) {
             return true
         }
     }
    
     class DivisibleByFilter {
         constructor(number, next) {
             this.number = number;
             this.next = next;
         }
    
         accept(n) {
             var filter = this;
             while (filter != null) {
                 if (n % filter.number === 0) {
                     return false;
                 }
                 filter = filter.next;
             }
             return true;
         }
     }
    
     class Primes {
         constructor() {
             this.number = 2;
             this.filter = new AcceptFilter();
         }
    
         next() {
             while (!this.filter.accept(this.number)) {
                 this.number++;
             }
             this.filter = new DivisibleByFilter(this.number, this.filter);
             return this.number;
         }
     }
    
     var primes = new Primes();
     var primesArray = [];
     for (let i = 0; i < 5000; i++) {
         primesArray.push(primes.next());
     }
     console.log(`Computed ${primesArray.length} prime numbers. ` +
                 `The last 5 are ${primesArray.slice(-5)}.`);
    
  2. Run js primes.js.

    The example application should print output as follows:

     $> js primes.js
     Computed 5000 prime numbers. The last 5 are 48563,48571,48589,48593,48611.
    
    

    This code takes a moment to compute so let’s see where all the time is spent.

  3. Run js primes.js --cpusampler to enable CPU sampling.

    The CPU sampler tool should print output for the example application as follows:

     $ ./js primes.js --cpusampler
     Computed 5000 prime numbers. The last 5 are 48563,48571,48589,48593,48611.
     ---------------------------------------------------------------------------------------------------
     Sampling Histogram. Recorded 1184 samples with period 1ms
       Self Time: Time spent on the top of the stack.
       Total Time: Time the location spent on the stack.
       Opt %: Percent of time spent in compiled and therfore non-interpreted code.
     ---------------------------------------------------------------------------------------------------
      Name        |      Total Time     |  Opt % ||       Self Time     |  Opt % | Location
     ---------------------------------------------------------------------------------------------------
      next        |       1216ms  98.5% |  87.9% ||       1063ms  85.9% |  99.0% | primes.js~31-37:564-770
      accept      |        159ms  11.2% |  22.7% ||        155ms  12.5% |  14.8% | primes.js~13-22:202-439
      :program    |       1233ms 100.0% |   0.0% ||         18ms   1.5% |   0.0% | primes.js~1-47:0-1024
      constructor |          1ms   0.1% |   0.0% ||          1ms   0.1% |   0.0% | primes.js~7-23:72-442
     ---------------------------------------------------------------------------------------------------
    
    

    The sampler prints an execution time histogram for each JavaScript function. By default, CPU sampling takes a sample every single millisecond. From the result we can see that roughly 96 percent of the time is spent in the DivisibleByFilter.accept function.

     accept(n) {
         var filter = this;
         while (filter != null) {
             if (n % filter.number === 0) {
                 return false;
             }
             filter = filter.next;
         }
         return true;
     }
    
    

    Let’s find out more about this function by filtering the samples and include statements in the profile in addition to methods.

  4. Run js primes.js --cpusampler --cpusampler.Mode=statements --cpusampler.FilterRootName=*accept to collect statement samples for all functions that end with accept.

     $ js primes.js --cpusampler --cpusampler.Mode=statements --cpusampler.FilterRootName=*accept
     Computed 5000 prime numbers. The last 5 are 48563,48571,48589,48593,48611.
     ----------------------------------------------------------------------------------------------------
     Sampling Histogram. Recorded 1567 samples with period 1ms
       Self Time: Time spent on the top of the stack.
       Total Time: Time the location spent on the stack.
       Opt %: Percent of time spent in compiled and therfore non-interpreted code.
     ----------------------------------------------------------------------------------------------------
      Name         |      Total Time     |  Opt % ||       Self Time     |  Opt % | Location
     ----------------------------------------------------------------------------------------------------
      accept~16-18 |        436ms  27.8% |  94.3% ||        435ms  27.8% |  94.5% | primes.js~16-18:275-348
      accept~15    |        432ms  27.6% |  97.0% ||        432ms  27.6% |  97.0% | primes.js~15:245-258
      accept~19    |        355ms  22.7% |  95.5% ||        355ms  22.7% |  95.5% | primes.js~19:362-381
      accept~17    |          1ms   0.1% |   0.0% ||          1ms   0.1% |   0.0% | primes.js~17:322-334
     ----------------------------------------------------------------------------------------------------
    
    

    Roughly 30 percent of the time is spent in this if condition:

     if (n % filter.number === 0) {
         return false;
     }
    
    

    The if condition contains an expensive modulo operation, which might explain the runtime of the statement.

    Now let’s use the CPU tracer tool to collect execution counts of each statement.

  5. Run js primes.js --cputracer --cputracer.TraceStatements --cputracer.FilterRootName=*accept to collect execution counts for all statements in methods ending with accept.

     $ js primes.js --cputracer --cputracer.TraceStatements --cputracer.FilterRootName=*accept
     Computed 5000 prime numbers. The last 5 are 48563,48571,48589,48593,48611.
     -----------------------------------------------------------------------------------------
     Tracing Histogram. Counted a total of 351278226 element executions.
       Total Count: Number of times the element was executed and percentage of total executions.
       Interpreted Count: Number of times the element was interpreted and percentage of total executions of this element.
       Compiled Count: Number of times the compiled element was executed and percentage of total executions of this element.
     -----------------------------------------------------------------------------------------
      Name     |          Total Count |    Interpreted Count |       Compiled Count | Location
     -----------------------------------------------------------------------------------------
      accept   |     117058669  33.3% |         63575   0.1% |     116995094  99.9% | primes.js~15:245-258
      accept   |     117053670  33.3% |         63422   0.1% |     116990248  99.9% | primes.js~16-18:275-348
      accept   |     117005061  33.3% |         61718   0.1% |     116943343  99.9% | primes.js~19:362-381
      accept   |         53608   0.0% |          1857   3.5% |         51751  96.5% | primes.js~14:215-227
      accept   |         53608   0.0% |          1857   3.5% |         51751  96.5% | primes.js~13-22:191-419
      accept   |         48609   0.0% |          1704   3.5% |         46905  96.5% | primes.js~17:322-334
      accept   |          4999   0.0% |           153   3.1% |          4846  96.9% | primes.js~21:409-412
      accept   |             1   0.0% |             1 100.0% |             0   0.0% | primes.js~2-4:25-61
      accept   |             1   0.0% |             1 100.0% |             0   0.0% | primes.js~3:52-55
     -----------------------------------------------------------------------------------------
    
    

    Now the output shows execution counters for each statement, instead of timing information. Tracing histograms often provides insights into the behavior of the algorithm that needs optimization.

    Lastly, let’s look at the memory tracer tool for capturing allocations, for which GraalVM currently provides experimental support.

  6. Run js primes.js --memtracer to display source code locations and counts of reported allocations.

     $ js primes.js --memtracer
     Computed 5000 prime numbers. The last 5 are 48563,48571,48589,48593,48611.
     ------------------------------------------------------------
      Location Histogram with Allocation Counts. Recorded a total of 5013 allocations.
        Total Count: Number of allocations during the execution of this element.
        Self Count: Number of allocations in this element alone (excluding sub calls).
     ------------------------------------------------------------
      Name        |      Self Count |     Total Count |  Location
     ------------------------------------------------------------
      next        |     5000  99.7% |     5000  99.7% | primes.js~31-37:537-737
      :program    |       11   0.2% |     5013 100.0% | primes.js~1-46:0-966
      Primes      |        1   0.0% |        1   0.0% | primes.js~25-38:454-739
     ------------------------------------------------------------
    
    

    This output shows the number of allocations which were recorded per function. For each prime number that was computed, the program allocates one object in next and one in constructor of DivisibleByFilter. Allocations are recorded independently of whether they could get eliminated by the compiler. The Graal compiler is particularly powerful in optimizing allocations and can push allocations into infrequent branches to increase execution performance. The Graal team plans to add information about memory optimizations to the memory tracer in the future.

Tool Reference

Use the --help:tools option in all guest language launchers to display reference information for the CPU sampler, the CPU tracer, and the memory tracer.

The current set of available options is as follows:

CPU Sampler Command Options

  • --cpusampler: enables the CPU sampler. Disabled by default.
  • --cpusampler.Delay=<Long>: delays the sampling for the given number of milliseconds (default: 0).
  • --cpusampler.FilterFile=<Expression>: applies a wildcard filter for source file paths. For example, *program*.sl. The default is ∗.
  • --cpusampler.FilterLanguage=<String>: profiles languages only with the matching mime-type. For example, +. The default is no filter.
  • --cpusampler.FilterRootName=<Expression>: applies a wildcard filter for program roots. For example, Math.*. The default is ∗.
  • --cpusampler.Mode=<Mode>: describes level of sampling detail. Please note that increased detail can lead to reduced accuracy.
    • exclude_inlined_roots samples roots excluding inlined functions (enabled by default);
    • rootssamples roots including inlined functions;
    • statements samples all statements.
  • --cpusampler.Output=<Output>: prints a ‘histogram’ or ‘calltree’ as output. The default is ‘histogram’.
  • --cpusampler.Period=<Long>: specifies the period, in milliseconds, to sample the stack.
  • --cpusampler.SampleInternal: captures internal elements. The default is false.
  • --cpusampler.StackLimit=<Integer>: specifies the maximum number of stack elements.

CPU Tracer Command Options

  • --cputracer: enables the CPU tracer. Disabled by default.
  • --cputracer.FilterFile=<Expression>: applies a wildcard filter for source file paths. For example, *program*.sl. The default is ∗.
  • --cputracer.FilterLanguage=<String>: profiles languages only with the matching mime-type. For example, +. The default is no filter.
  • --cputracer.FilterRootName=<Expression>: applies a wildcard filter for program roots. For example, Math.*. The default is ∗.
  • --cputracer.TraceCalls: captures calls when tracing. The default is false.
  • --cputracer.TraceInternal: traces internal elements. The default is false.
  • --cputracer.TraceRoots=<Boolean>: captures roots when tracing. The default is true.
  • --cputracer.TraceStatements: captures statements when tracing. The default is false.

Memory Tracer Command Options

  • --memtracer: enables the memory tracer. Disabled by default.
  • --memtracer.FilterFile=<Expression>: applies a wildcard filter for source file paths. For example, *program*.sl. The default is ∗.
  • --memtracer.FilterLanguage=<String>: profiles languages only with the matching mime-type. For example, +. The default is no filter.
  • --memtracer.FilterRootName=<Expression>: applies a wildcard filter for program roots. For example, Math.*. The default is ∗.
  • --memtracer.Output=<Format>: prints a ‘typehistogram’, ‘histogram’, or ‘calltree’ as output. The default is ‘histogram’.
  • --memtracer.StackLimit=<Integer>: sets the maximum number of maximum stack elements.
  • --memtracer.TraceCalls: captures calls when tracing. The default is false.
  • --memtracer.TraceInternal: captures internal elements. The default is false.
  • --memtracer.TraceRoots=<Boolean>: captures roots when tracing. The default is true.
  • --memtracer.TraceStatements: captures statements when tracing. The default is false.

Monitoring Agent

GraalVM gives you a dedicated Agent application that comes with rich monitoring features to allow developers, integrators, and IT staff to monitor the performance of user programs and the health status of the virtual machine.

The GraalVM monitoring component is called the Agent and can be easily enabled by providing the command line option --agent to the executables.

Disclaimer: The Agent is currently an experimental feature provided by the GraalVM Enterprise Edition available for download from the Oracle Technology Network.

GraalVM Instruments

The Agent uses instruments that implement the Truffle Instrumentation API.

In future, additional instrumentation tools can be added either by the GraalVM development team or third party developers.

Running an Example for Agent Monitoring

Get familiar with using the GraalVM Agent with this Hello World example.

  1. Save the following code snippet as HellWorld.js:

     var http = require('http');
    
     var server = http.createServer(function (request, response) {
       let host = request.headers.host;
       let index = host.indexOf(':');
       if (index > 0) {
         host = host.substring(0, index);
       }
       response.writeHead(200, {"Content-Type": "text/plain"});
       response.end("Hello "+host+"!\n");
     });
    
     server.listen(8000);
    
     console.log("Server running at http://localhost:8000/");
    
  2. Launch the Graal Enterprise Monitoring Agent Server gemasrv using this command:

     gemasrv
     Agent http server is running in unsecure mode!
     Use Java VM properties:
         -Dkey.store.file.path to specify the path to the .jsk file.
         -Dkey.store.file.password to specify the password used to unlock the keystore
         -Dkey.store.key.recover.password to specify the password for recovering keys. If is not specified the key.store.file.password is used.
     Agent http server started on port 8080, reachable at:
     http://localhost:8080/info
    

    The program starts without SSL, with a warning about unsecure mode, and the Monitoring Agent web application is available at http://localhost:8080/.

    When you open this page, you can see there are no virtual machines attached yet.

    Note: To run the server on a different port, use the --port <port_number> option.

  3. Launch GraalVM node to monitor HellWorld.js using this command:

     node --agent HelloWorld.js
     Server running at http://localhost:8000/
    

    The GraalVM starts and attaches itself to the Monitoring Agent Server running by default on the localhost:8080 port. To attach the GraalVM to a server running on a different host:port, use the --agent=<[host:]port> option.

    Now, when you open the Monitoring Agent’s page, you can see the virtual machine is running with some system information displayed by default.

  4. In the Agent window, select Polyglot Engine - 1 from the second drop-down box at the top. You should see the sources loaded in the Polyglot Engine and the available instruments as shown in the following image:

The Agent window shows loaded sources on the left where the HelloWorld.js source can be found under the FileSystem node . The area in the middle provides detailed information about sources or gathered data from instruments. The right-most column allows you to enable various loaded instruments and set their properties.

Let’s take a look at CPU sampling with our script to see what takes the most time.

  1. Enable Agent CPU Sampler.
  2. In the settings section, select Start Sampling.
  3. Reload the application at http://localhost:8000/ so that the Agent can gather the data.
  4. Return to the Agent window.
  5. In the CPU Sampler section, select Stop Sampling.

The Agent page refreshes and results should display as follows:

It is also possible to see detailed data in the source code. To do this, open the HelloWorld.js file in the Agent tab and the select Agent CPU Sampler from the drop-down box. Sources should display with data available as follows:

JVM Mode Only Instruments

Some monitoring instruments, such as the Specialization Instrument, are available in --jvm mode only for the GraalVM launchers.

In this case, you should launch HelloWorld.js as follows:

node --jvm --agent HelloWorld.js

Then do the following:

  1. Open Agent and enable Agent Specializations from the list of instruments.
  2. Start Tracing, using the button below.
  3. Reload the HelloWorld.js application in your browser.
  4. Return to the Agent window and Stop Tracing.
  5. Open the HelloWorld.js and select Agent Specializations from the drop-down box next to the filename tab.

Data such as the following displays specializations of various JavaScript statements:

Heap Viewer

GraalVM comes with Graal VisualVM, an enhanced version of the popular VisualVM tool which includes special heap analysis features for the supported guest languages. These languages and features are currently available:

  • Java: Heap Summary, Objects View, Threads View, OQL Console
  • JavaScript: Heap Summary, Objects View, Thread View
  • Python: Heap Summary, Objects View
  • Ruby: Heap Summary, Objects View, Threads View
  • R: Heap Summary, Objects View

Starting Graal VisualVM

To start Graal VisualVM execute jvisualvm. Immediately after the startup, the tool shows all locally running Java processes in the Applications area, including the VisualVM process itself.

NOTE: Substrate VM processes are only displayed in the Applications area if created by the native-image tool with the -H:+AllowVMInspection flag. Guest language REPL process must be started with the --jvm flag to monitor it using Graal VisualVM.

Getting Heap Dump

Let’s say you are trying to analyze a Ruby application. To get a heap dump, first start your application and let it run for a few seconds to warm up.

Then right-click its process in VisualVM and invoke the Heap Dump action. A new heap viewer for the Ruby process opens.

NOTE: See the Generating Native Heap Dumps page for details on creating heap dumps from a Substrate VM process.

Analyzing Objects

Initially the Summary view for the Java heap is displayed. To analyze the Ruby heap, click the leftmost (Summary) dropdown in the heap viewer toolbar, choose the Ruby Heap scope and select the Objects view. Now the heap viewer displays all Ruby heap objects, aggregated by their type.

Expand the Proc node in the results view to see a list of objects of this type. Each object displays its logical value as provided by the underlying implementation. Expand the objects to access their variables and references, where available.

Now enable the Preview, Variables and References details by clicking the buttons in the toolbar and select the individual ProcType objects. Where available, the Preview view shows the corresponding source fragment, the Variables view shows variables of the object and References view shows objects referring to the selected object.

Last, use the Presets dropdown in the heap viewer toolbar to switch the view from All Objects to Dominators or GC Roots. To display the heap dominators, retained sizes must be computed first, which can take a few minutes for the server.rb example. Select the Objects aggregation in the toolbar to view the individual dominators or GC roots.

Analyzing Threads

Click the leftmost dropdown in the heap viewer toolbar and select the Threads view for the Ruby heap. The heap viewer now displays the Ruby thread stack trace, including local objects. The stack trace can alternatively be displayed textually by clicking the HTML toolbar button.