This pages covers the various mechanisms currently available for debugging the GraalVM compiler.
All the parts of the GraalVM compiler (henceforth just "compiler") written in Java can be debugged with a standard Java debugger. While debugging with Eclipse is described here, it should not be too hard to adapt these instructions for another debugger.
The mx eclipseinit
command not only creates Eclipse project configurations but also creates an
Eclipse launch configuration (in
mx.compiler/eclipse-launches/compiler-attach-localhost-8000.launch
) that can be used to debug all
compiler code running in the VM. This launch configuration requires you to start the VM with the -d
global option which puts the VM into a state waiting for a remote debugger to attach to port 8000
:
mx -d vm -XX:+UseJVMCICompiler -version
Listening for transport dt_socket at address: 8000
Note that the -d
option applies to any command that runs the VM, such as the mx unittest
command:
mx -d vm unittest
Listening for transport dt_socket at address: 8000
Once you see the message above, you then run the Eclipse launch configuration:
- From the main menu bar, select Run > Debug Configurations... to open the Debug Configurations dialogue.
- In the tree of configurations, select the Remote Java Application > compiler-attach-localhost-8000 entry.
- Click the Debug button to start debugging.
At this point you can set breakpoints and perform all other normal Java debugging actions.
In addition to IDE debugging, there is support for printf style debugging.
The simplest is to place temporary usages of System.out
where ever you need them.
For more permanent logging statements, use the log(...)
methods in
DebugContext
.
A (nestable) debug scope is entered via one of the scope(...)
methods in that class. For example:
DebugContext debug = ...;
InstalledCode code = null;
try (Scope s = debug.scope("CodeInstall", method)) {
code = ...
debug.log("installed code for %s", method);
} catch (Throwable e) {
throw debug.handle(e);
}
The debug.log
statement will send output to the console if CodeInstall
is matched by the
-Djdk.graal.Log
option. The matching logic for this option is implemented in
DebugFilter
and documented in the
Dump help message. As
mentioned in the javadoc, the same matching logic also applies to the -Djdk.graal.Dump
,
-Djdk.graal.Time
, -Djdk.graal.Count
and -Djdk.graal.TrackMemUse
options.
Since DebugContext
objects are thread local, they need to be passed around as parameters. For
convenience, they may be available from objects such as a Graph
or a Node
but care needs to be
taken when such objects can be exposed to multiple threads. There are assertions in DebugContext
guarding against use in a thread different from the one in which it was instantiated.
JVMCI and GraalVM compiler options are specified by the jvmci.*
and graal.*
system properties
respectively. These must be specified on the JVM command line. Modifications to these properties by
application code are not seen by JVMCI and the compiler. A listing of all supported properties can be
obtained with -XX:+JVMCIPrintProperties
.
The compiler supports metrics in the form of counters, timers and memory trackers.
Each metric has a unique name. Metrics are collected per-compilation.
At shutdown, they are aggregated across all compilations and reported to the console.
This ouput can be redirected to a file via the -Djdk.graal.AggregatedMetricsFile
option.
To list the per-compilation metrics, use the -Djdk.graal.MetricsFile
option. If not specified,
per-compilation metrics are not reported.
Metrics are represented in the code via fields or variables of type CounterKey
, TimerKey
or MemUseTrackerKey
.
As implied by the Key
suffix, these objects do not store the value of a metric.
The value is stored in a DebugContext
object. How to use each of these metrics is described below.
- A
CounterKey
counts some quantity. For example:
// declaration
private static final CounterKey ByteCodesCompiled = DebugContext.counter("ByteCodesCompiled");
// usage
DebugContext debug = ...;
long compiled = ... ;
ByteCodesCompiled.add(debug, compiled);
- A
TimerKey
records the time spent in a scope. For example:
// declaration
private static final TimerKey CompilationTime = DebugContext.timer("CompilationTime");
// usage
DebgContext debug = ...;
try (DebugCloseable scope = CompilationTime.start(debug)) {
...
}
// time spent in the above scope will be added to the `CompilationTime` value in `debug`
When reporting a timer, two values are reported. The accumulated time represents the real time spent in the scope. The flat time is the amount of time spent within the scope minus the time spent in any nested timer scopes. For example:
CompilationTime_Accm=100.0 ms
CompilationTime_Flat=10.0 ms
- A
MemUseTrackerKey
tracks the memory allocation in a scope. For example:
// declaration
private static final MemUseTrackerKey CompilationMemory = DebugContext.memUseTracker("CompilationMemory");
// usage
DebgContext debug = ...;
try (DebugCloseable a = CompilationMemory.start(debug)) {
...
}
// the bytes allocated in the above scope will be added to the `CompilationMemory` value in `debug`
Like TimerKey
, an accumulated and flat value is reported for memory usage tracker.
For example:
CompilationMemory_Accm=100000 bytes
CompilationMemory_Flat=1000 bytes
For both the -Djdk.graal.AggregatedMetricsFile
and the -Djdk.graal.MetricsFile
option, the output format
is determined by the file name suffix. A .csv
suffix produces a semi-colon separated format that
is amenable to automatic data processing.
The columns in the -Djdk.graal.MetricsFile
CSV output are:
- compilable: A textual label such as the fully qualified name of the method being compiled. For Truffle compilations, this will be a name describing the guest language function/method being compiled.
- compilable_identity: The identity hash code of the compilable. This is useful when the label may not uniquely identify the compilation input. For example, when a Java method is in a class loaded multiple times by different class loaders or is redefined via JVMTI, its label may not change but its identity will.
- compilation_nr: The number of times metrics have been dumped for a compilation of compilable_identity. This is guaranteed to be asymptotically increasing since dumping of per-compilation metrics is serialized on a global lock.
- compilation_id: An identifier for the compilation that is highly likely (but not guaranteed) to be unique. Note that it may not be purely numeric.
- metric_name: The name of the metric being reported.
- metric_value: The metric value being reported.
- metric_unit: The unit of measurement for the value being reported. This will be the empty string for a counter.
Example -Djdk.graal.MetricsFile
output:
java.lang.String.hashCode()int;1272077530;0;HotSpotCompilation-95;PhaseNodes_PhaseSuite;1
java.lang.String.hashCode()int;1272077530;0;HotSpotCompilation-95;PhaseNodes_GraphBuilderPhase;1
java.lang.String.hashCode()int;1272077530;0;HotSpotCompilation-95;PhaseCount_GraphBuilderPhase;1
java.lang.String.hashCode()int;1272077530;0;HotSpotCompilation-95;PhaseMemUse_GraphBuilderPhase_Accm;896536
java.lang.String.hashCode()int;1272077530;0;HotSpotCompilation-95;PhaseMemUse_GraphBuilderPhase_Flat;896536
java.lang.String.hashCode()int;1272077530;0;HotSpotCompilation-95;PhaseTime_GraphBuilderPhase_Accm;31095000
java.lang.String.hashCode()int;1272077530;0;HotSpotCompilation-95;PhaseTime_GraphBuilderPhase_Flat;27724000
The columns in the -Djdk.graal.AggregatedMetricsFile
CSV output are:
- metric_name: The name of the metric being reported.
- metric_value: The metric value being reported.
- metric_unit: The unit of measurement for the value being reported. This will be the empty string for a counter.
Example -Djdk.graal.AggregatedMetricsFile
output:
AllocationsRemoved;6446;
BackEnd_Accm;7931303;us
BackEnd_Flat;91919;us
DuplicateGraph_Accm;1935356;us
DuplicateGraph_Flat;1935356;us
LIRPhaseMemUse_AllocationStage_Accm;499763160;bytes
LIRPhaseMemUse_AllocationStage_Flat;17354112;bytes
Note that -Djdk.graal.MetricsFile
produces per-compilation output, not per-method output.
If a method is compiled multiple times, post-processing of the CSV output will be required
to obtain per-method metric values.
Specifying one of the debug scope options (i.e., -Djdk.graal.Log
, -Djdk.graal.Dump
, -Djdk.graal.Count
,
-Djdk.graal.Time
, or -Djdk.graal.TrackMemUse
) can generate a lot of output. Typically, you are only
interesting in compiler output related to a single or few methods. In this case, use the
-Djdk.graal.MethodFilter
option to specify a method filter. The matching logic for this option is
described in
MethodFilter
and documented in the
MethodFilter help message.
Alternatively, you may only want to see certain metrics. The -Djdk.graal.Timers
, -Djdk.graal.Counters
and -Djdk.graal.MemUseTrackers
exist for this purpose. These options take a comma separated list of
metrics names. Only the named metrics will be activated and reported. To see the available metric
names, specify -Djdk.graal.ListMetrics=true
. At VM shutdown this option lists all the metrics that
were encountered during the VM execution. It does not list metrics registered on non-executed paths
since metric registration is lazy. For example, to see all the metrics available in a boot strap:
mx vm -XX:+UseJVMCICompiler -XX:+BootstrapJVMCI -Djdk.graal.ListMetrics=true -version
In addition to logging, there is support for generating (or dumping) more detailed visualizations of certain compiler data structures. Currently, there is support for dumping:
- HIR graphs (i.e., instances of Graph) to the Ideal Graph Visualizer (IGV), and
- LIR register allocation and generated code to the C1Visualizer
Dumping is enabled via the -Djdk.graal.Dump
option. The dump handler for generating C1Visualizer
output will also generate output for LIR graphs if the -Djdk.graal.PrintBackendCFG=true
option is specified
(in addition to the -Djdk.graal.Dump
option).
By default, -Djdk.graal.Dump
output is written to the local file system in a directory determined
by the -Djdk.graal.DumpPath
option (default is $CWD/graal_dumps
). You can send dumps directly to
the IGV over a network socket with the -Djdk.graal.PrintGraph=Network
option. The -Djdk.graal.PrintGraphHost
and -Djdk.graal.PrintGraphPort
options determine where the dumps are sent. By default, they are
sent to 127.0.0.1:4445 and IGV listens on port 4445 by default.
By default, the graph dumping code dumps every graph at a particular level even if the graph hasn't
changed since the previous dump. Internally the compiler tracks when changes are made to the edges
of the graph and setting the flag -Djdk.graal.PrintUnmodifiedGraphs=false
will cause it to elide graph
dumps when no edges have changed since the previous dump. This can greatly reduce the number of
graphs dumped at high dump levels, though the resulting output can be a little harder to understand
without the context of the other graph names.
C1Visualizer output is written to *.cfg
files. These can be opened via File -> Open Compiled Methods... in C1Visualizer.
The IGV can be launched with mx igv
and the C1Visualizer can be launched via mx c1visualizer
.
Various other VM options are of interest to see activity related to compilation:
-XX:+PrintCompilation
or-Djdk.graal.PrintCompilation=true
for notification and brief info about each compilation-XX:+TraceDeoptimization
can be useful to see if excessive compilation is occurring
To see the compiler data structures used while compiling Node.updateUsages
, use the following command:
> mx vm -XX:+UseJVMCICompiler -XX:+BootstrapJVMCI -XX:-TieredCompilation -Djdk.graal.Dump= -Djdk.graal.MethodFilter=Node.updateUsages -version
Bootstrapping JVMCI....Dumping debug output in /Users/dsimon/graal/graal/compiler/dumps/1497910458736
................................................ in 38177 ms (compiled 5206 methods)
java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b31)
Java HotSpot(TM) 64-Bit Server VM (build 25.212-b31-jvmci-20-b01, mixed mode)
> find dumps/1497910458736 -type f
dumps/1497910458736/HotSpotCompilation-539[jdk.graal.compiler.graph.Node.updateUsages(Node, Node)].bgv
dumps/1497910458736/HotSpotCompilation-539[jdk.graal.compiler.graph.Node.updateUsages(Node, Node)].cfg
As you become familiar with the scope names used in the compiler, you can refine the -Djdk.graal.Dump
option
to limit the amount of dump output generated. For example, the "CodeGen"
and "CodeInstall"
scopes are active during code generation and installation respectively. To see the machine code (in
the C1Visualizer) produced during these scopes:
mx vm -Djdk.graal.Dump=CodeGen,CodeInstall -Djdk.graal.MethodFilter=NodeClass.get -version
You will notice that no IGV output is generated by this command.
Alternatively, you can see the machine code using HotSpot's PrintAssembly support:
mx hsdis
mx vm -XX:+UseJVMCICompiler -XX:+BootstrapJVMCI -XX:-TieredCompilation -XX:CompileCommand='print,*Node.updateUsages' -version
The first step above installs the hsdis
disassembler and only needs to be performed once.