Skip to content

[GR-54697] Implement debuginfo generation at image-runtime. #10522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions substratevm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ This changelog summarizes major changes to GraalVM Native Image.
* (GR-57827) Security providers can now be initialized at run time (instead of build time) when using the option `--future-defaults=all` or `--future-defaults=run-time-initialized-jdk`.
Run-time initialization of security providers helps reduce image heap size by avoiding unnecessary objects inclusion.
* (GR-48191) Enable lambda classes to be registered for reflection and serialization in _reachability-metadata.json_. The format is detailed [here](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ReachabilityMetadata.md).
* (GR-54697) Parallelize debug info generation and add support for run-time debug info generation. `-H:+RuntimeDebugInfo` adds a run-time debug info generator into a native image for use with GDB.

## GraalVM for JDK 24 (Internal Version 24.2.0)
* (GR-59717) Added `DuringSetupAccess.registerObjectReachabilityHandler` to allow registering a callback that is executed when an object of a specified type is marked as reachable during heap scanning.
Expand Down
1,151 changes: 710 additions & 441 deletions substratevm/debug/gdbpy/gdb-debughelpers.py

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions substratevm/debug/include/gdb_jit_compilation_interface.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

#ifndef SVM_NATIVE_GDBJITCOMPILATIONINTERFACE_H
#define SVM_NATIVE_GDBJITCOMPILATIONINTERFACE_H

// This header specifies the types used by the GDB JIT compilation interface (see https://sourceware.org/gdb/current/onlinedocs/gdb.html/Declarations.html#Declarations)
// The implementation of the JIT compilation interface is located in com.oracle.svm.core.debug.GdbJitInterface.

#include <stdint.h>

typedef enum
{
JIT_NOACTION = 0,
JIT_REGISTER,
JIT_UNREGISTER
} jit_actions_t;

struct jit_code_entry
{
struct jit_code_entry *next_entry;
struct jit_code_entry *prev_entry;
const char *symfile_addr;
uint64_t symfile_size;
};

struct jit_descriptor
{
uint32_t version;
/* This type should be jit_actions_t, but we use uint32_t
to be explicit about the bitwidth. */
uint32_t action_flag;
struct jit_code_entry *relevant_entry;
struct jit_code_entry *first_entry;
};

#endif
2 changes: 2 additions & 0 deletions substratevm/mx.substratevm/gdb_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ def check(self, text, skip_fails=True):
for i in range(0, num_rexps):
rexp = rexps[i]
match = None
if skip_fails:
line_idx = 0
while line_idx < num_lines and match is None:
line = lines[line_idx]
match = rexp.match(line)
Expand Down
133 changes: 116 additions & 17 deletions substratevm/mx.substratevm/mx_substratevm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,20 @@ def build_debug_test(variant_name, image_name, extra_args):
mx.run([os.environ.get('GDB_BIN', 'gdb'), '--nx', '-q', '-iex', 'set pagination off', '-ex', 'python "ISOLATES=True"', '-x', testhello_py, hello_binary])


def gdb_base_command(logfile, autoload_path):
return [
os.environ.get('GDB_BIN', 'gdb'),
'--nx',
'-q', # do not print the introductory and copyright messages
'-iex', 'set pagination off', # messages from enabling logging could already cause pagination, so this must be done first
'-iex', 'set logging redirect on',
'-iex', 'set logging overwrite off',
'-iex', f"set logging file {logfile}",
'-iex', 'set logging enabled on',
'-iex', f"set auto-load safe-path {autoload_path}",
]


def _gdbdebughelperstest(native_image, path, with_isolates_only, args):

# ====== check gdb version ======
Expand Down Expand Up @@ -1094,15 +1108,6 @@ def _gdbdebughelperstest(native_image, path, with_isolates_only, args):
'com.oracle.svm.test.debug.helper.ClassLoaderTest'
]

gdb_args = [
os.environ.get('GDB_BIN', 'gdb'),
'--nx',
'-q', # do not print the introductory and copyright messages
'-iex', 'set pagination off', # messages from enabling logging could already cause pagination, so this must be done first
'-iex', 'set logging redirect on',
'-iex', 'set logging overwrite off',
]

def run_debug_test(image_name: str, testfile: str, source_path: str, with_isolates: bool = True,
build_cinterfacetutorial: bool = False, extra_args: list = None,
skip_build: bool = False) -> int:
Expand Down Expand Up @@ -1138,7 +1143,7 @@ def run_debug_test(image_name: str, testfile: str, source_path: str, with_isolat
else:
build_args += ['-o', join(build_dir, image_name)]

mx.log(f"native_image {' '.join(build_args)}")
mx.log(f"native-image {' '.join(build_args)}")
native_image(build_args)

if build_cinterfacetutorial:
Expand All @@ -1151,18 +1156,13 @@ def run_debug_test(image_name: str, testfile: str, source_path: str, with_isolat
else:
c_command = ['cl', '-MD', join(tutorial_c_source_dir, 'cinterfacetutorial.c'), '-I.',
'libcinterfacetutorial.lib']
mx.log(' '.join(c_command))
mx.run(c_command, cwd=build_dir)
if mx.get_os() == 'linux':
logfile = join(path, pathlib.Path(testfile).stem + ('' if with_isolates else '_no_isolates') + '.log')
os.environ.update({'gdbdebughelperstest_logfile': logfile})
gdb_command = gdb_args + [
'-iex', f"set logging file {logfile}",
'-iex', 'set logging enabled on',
'-iex', f"set auto-load safe-path {join(build_dir, 'gdb-debughelpers.py')}",
os.environ.update({'gdb_logfile': logfile})
gdb_command = gdb_base_command(logfile, join(build_dir, 'gdb-debughelpers.py')) + [
'-x', testfile, join(build_dir, image_name)
]
mx.log(' '.join(gdb_command))
# unittest may result in different exit code, nonZeroIsFatal ensures that we can go on with other test
return mx.run(gdb_command, cwd=build_dir, nonZeroIsFatal=False)
return 0
Expand All @@ -1189,6 +1189,82 @@ def run_debug_test(image_name: str, testfile: str, source_path: str, with_isolat
mx.abort(status)


def _runtimedebuginfotest(native_image, output_path, with_isolates_only, args=None):
"""Build and run the runtimedebuginfotest"""

args = [] if args is None else args

test_proj = mx.dependency('com.oracle.svm.test')
test_source_path = test_proj.source_dirs()[0]

test_python_source_dir = join(test_source_path, 'com', 'oracle', 'svm', 'test', 'debug', 'helper')
test_runtime_compilation_py = join(test_python_source_dir, 'test_runtime_compilation.py')
test_runtime_deopt_py = join(test_python_source_dir, 'test_runtime_deopt.py')
testdeopt_js = join(suite.dir, 'mx.substratevm', 'testdeopt.js')

# clean / create output directory
if exists(output_path):
mx.rmtree(output_path)
mx_util.ensure_dir_exists(output_path)

# Build the native image from Java code
build_args = [
'-g', '-O0',
# set property controlling inclusion of foreign struct header
'-DbuildDebugInfoTestExample=true',
'--native-compiler-options=-I' + test_source_path,
'-o', join(output_path, 'runtimedebuginfotest'),
'-cp', classpath('com.oracle.svm.test'),
# We do not want to step into class initializer, so initialize everything at build time.
'--initialize-at-build-time=com.oracle.svm.test.debug.helper',
'--features=com.oracle.svm.test.debug.helper.RuntimeCompileDebugInfoTest$RegisterMethodsFeature',
'com.oracle.svm.test.debug.helper.RuntimeCompileDebugInfoTest',
] + svm_experimental_options([
'-H:DebugInfoSourceSearchPath=' + test_source_path,
'-H:+SourceLevelDebug',
'-H:+RuntimeDebugInfo',
]) + args

mx.log(f"native-image {' '.join(build_args)}")
runtime_compile_binary = native_image(build_args)

logfile = join(output_path, 'test_runtime_compilation.log')
os.environ.update({'gdb_logfile': logfile})
gdb_command = gdb_base_command(logfile, join(output_path, 'gdb-debughelpers.py')) + [
'-x', test_runtime_compilation_py, runtime_compile_binary
]
# unittest may result in different exit code, nonZeroIsFatal ensures that we can go on with other test
status = mx.run(gdb_command, cwd=output_path, nonZeroIsFatal=False)

def run_js_test(eager: bool = False):
jslib = mx.add_lib_suffix(native_image(
args +
svm_experimental_options([
'-H:+SourceLevelDebug',
'-H:+RuntimeDebugInfo',
'-H:+LazyDeoptimization' if eager else '-H:-LazyDeoptimization',
]) +
['-g', '-O0', '--macro:jsvm-library']
))
js_launcher = get_js_launcher(jslib)
logfile = join(output_path, 'test_runtime_deopt_' + ('eager' if eager else 'lazy') + '.log')
os.environ.update({'gdb_logfile': logfile})
gdb_command = gdb_base_command(logfile, join(output_path, 'gdb-debughelpers.py')) + [
'-x', test_runtime_deopt_py, '--args', js_launcher, testdeopt_js
]
# unittest may result in different exit code, nonZeroIsFatal ensures that we can go on with other test
return mx.run(gdb_command, cwd=output_path, nonZeroIsFatal=False)

# G1 does not work for the jsvm library
# avoid complications with '-H:+ProtectionKeys' which is not compatible with '-H:-SpawnIsolates' and '-H:-UseCompressedReferences'
if '--gc=G1' not in args and '-H:-UseCompressedReferences' not in args and '-H:-SpawnIsolates' not in args:
status |= run_js_test()
status |= run_js_test(True)

if status != 0:
mx.abort(status)


def _javac_image(native_image, path, args=None):
args = [] if args is None else args
mx_util.ensure_dir_exists(path)
Expand Down Expand Up @@ -1766,6 +1842,28 @@ def gdbdebughelperstest(args, config=None):
config=config
)


@mx.command(suite.name, 'runtimedebuginfotest', 'Runs the runtime debuginfo generation test')
def runtimedebuginfotest(args, config=None):
"""
runs a native image that compiles code and creates debug info at runtime.
"""
parser = ArgumentParser(prog='mx runtimedebuginfotest')
all_args = ['--output-path', '--with-isolates-only']
masked_args = [_mask(arg, all_args) for arg in args]
parser.add_argument(all_args[0], metavar='<output-path>', nargs=1, help='Path of the generated image', default=[join(svmbuild_dir(), "runtimedebuginfotest")])
parser.add_argument(all_args[1], action='store_true', help='Only build and test the native image with isolates')
parser.add_argument('image_args', nargs='*', default=[])
parsed = parser.parse_args(masked_args)
output_path = unmask(parsed.output_path)[0]
with_isolates_only = parsed.with_isolates_only
native_image_context_run(
lambda native_image, a:
_runtimedebuginfotest(native_image, output_path, with_isolates_only, a), unmask(parsed.image_args),
config=config
)


@mx.command(suite_name=suite.name, command_name='helloworld', usage_msg='[options]')
def helloworld(args, config=None):
"""
Expand Down Expand Up @@ -1858,6 +1956,7 @@ def build_and_test_java_agent_image(native_image, args):

native_image_context_run(build_and_test_java_agent_image, args)


@mx.command(suite.name, 'clinittest', 'Runs the ')
def clinittest(args):
def build_and_test_clinittest_image(native_image, args):
Expand Down
10 changes: 9 additions & 1 deletion substratevm/mx.substratevm/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@
"dependencies": [
"com.oracle.svm.common",
"com.oracle.svm.shaded.org.objectweb.asm",
"com.oracle.objectfile",
"SVM_CONFIGURE",
"espresso-shared:ESPRESSO_SVM",
],
Expand Down Expand Up @@ -738,7 +739,6 @@
"subDir": "src",
"sourceDirs": ["src"],
"dependencies": [
"com.oracle.objectfile",
"com.oracle.graal.reachability",
"com.oracle.svm.core.graal.amd64",
"com.oracle.svm.shaded.org.capnproto",
Expand Down Expand Up @@ -1152,6 +1152,10 @@
"jdk.internal.misc",
"sun.security.jca",
],
"jdk.internal.vm.ci" : [
"jdk.vm.ci.code",
"jdk.vm.ci.meta",
],
},
"checkstyle": "com.oracle.svm.test",
"checkstyleVersion" : "10.21.0",
Expand Down Expand Up @@ -1971,6 +1975,8 @@
"com.oracle.objectfile",
"com.oracle.objectfile.io",
"com.oracle.objectfile.debuginfo",
"com.oracle.objectfile.debugentry",
"com.oracle.objectfile.debugentry.range",
"com.oracle.objectfile.macho",
],

Expand Down Expand Up @@ -2101,6 +2107,7 @@
"dependency:com.oracle.svm.native.libchelper/*",
"dependency:com.oracle.svm.native.jvm.posix/*",
"dependency:com.oracle.svm.native.libcontainer/*",
"file:debug/include",
],
},
},
Expand All @@ -2109,6 +2116,7 @@
# on all other os's we don't want libc specific subdirectories
"include/": [
"dependency:com.oracle.svm.native.libchelper/include/*",
"file:debug/include/*",
],
"<os>-<arch>/": [
"dependency:com.oracle.svm.native.libchelper/<os>-<arch>/default/*",
Expand Down
47 changes: 47 additions & 0 deletions substratevm/mx.substratevm/testdeopt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

function add(a, b, test) {
if (test) {
a += b;
}
return a + b;
}

// trigger compilation add for ints and test = true
for (let i = 0; i < 1000 * 1000; i++) {
add(i, i, true);
}

// deoptimize with failed assumption in compiled method
// then trigger compilation again
console.log("deopt1")
for (let i = 0; i < 1000 * 1000; i++) {
add(i, i, false);
}

// deoptimize with different parameter types
console.log("deopt2");
add({f1: "test1", f2: 2}, {x: "x", y: {test: 42}}, false);
Loading