Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
fd7498c
refactor: extract profiler-specific VMStructs members to ProfilerVMSt…
jbachorik Jun 28, 2026
29d1f64
refactor: replace ProfiledThread back-reference in crashProtectionAct…
jbachorik Jun 28, 2026
d4d80cb
refactor: replace ProfiledThread fast-path in isJavaThread with regis…
jbachorik Jun 28, 2026
512baa4
chore: vmStructs audit complete — no remaining profiler coupling
jbachorik Jun 28, 2026
fd90e1d
chore: vmStructs audit complete — no remaining profiler coupling
jbachorik Jun 28, 2026
1c86f7e
refactor: eliminate residual profiler coupling in vmStructs
jbachorik Jun 28, 2026
2330dca
build: add canonical vmStructs ABI symbol list
jbachorik Jun 28, 2026
67eb51f
build: add exportSymbolsFile input to NativeLinkTask for file-based A…
jbachorik Jun 28, 2026
ba5be07
refactor: move support-library source files into src/main/cpp/support/
jbachorik Jun 29, 2026
ff97ddc
build: extend NativeBuildPlugin with dual source-set support for libr…
jbachorik Jun 29, 2026
63d5710
refactor: complete Task 3.3 — split build works on macOS and Linux
jbachorik Jun 29, 2026
0c2f53b
fix: remove profiler.h/signalSafety.h from support library sources
jbachorik Jun 29, 2026
82acb6b
build: add gtest support-only test link infrastructure
jbachorik Jun 29, 2026
6f5ee75
ci: add libJavaSupport ABI symbol gate
jbachorik Jun 29, 2026
12f0ea4
feat: move JVMAccess JNI natives into libJavaSupport
jbachorik Jun 29, 2026
cd1ae5c
feat: LibraryLoader loads libJavaSupport or profiler; JVMAccess uses …
jbachorik Jun 29, 2026
241cfb1
test: verify JVMAccess loads only libJavaSupport
jbachorik Jun 29, 2026
82d8567
review: address sphinx feedback on library-split
jbachorik Jun 30, 2026
155e503
fix: formatting and scan-build include paths
jbachorik Jun 30, 2026
e055170
fix: break LibraryPatcher dependency from support lib
jbachorik Jun 30, 2026
f4f2712
fix: copy libJavaSupport.so for gtest binaries; guard J9/agent in Sup…
jbachorik Jun 30, 2026
f370bdd
fix: update JVMAccessTest for library-split architecture
jbachorik Jun 30, 2026
ff94a60
fix: skip support-only gtest wiring for sanitizer configs
jbachorik Jun 30, 2026
8e96cf7
fix: healthCheck0 lazily initialises VMStructs for standalone libJava…
jbachorik Jun 30, 2026
84f4fb6
fix: restrict support-only gtest to debug/release; use isActive for J…
jbachorik Jun 30, 2026
3860ddc
fix: gate JVMAccess.isActive on healthCheck0 result (J9/Zing)
jbachorik Jun 30, 2026
65b425a
fix: resolve HotSpot version for standalone JVMAccess flag reads
jbachorik Jun 30, 2026
7d37b95
fix: do not enter HotSpot mode for J9/Zing in VMStructs::init
jbachorik Jun 30, 2026
bee9d84
fix: harden LibraryLoader concurrency and resource handling
jbachorik Jun 30, 2026
984428f
ci: exclude split fuzzer compile tasks in assemble/publish
jbachorik Jun 30, 2026
337d6ad
refactor: extract ThreadContext base from ProfiledThread
jbachorik Jul 1, 2026
3858bf2
refactor: move otel_context to support
jbachorik Jul 1, 2026
22f52ff
refactor: split ContextApi (get/init → support, snapshot → profiler)
jbachorik Jul 1, 2026
eef5042
refactor: move context/process-ctx natives to support TU
jbachorik Jul 1, 2026
7d10bc3
refactor: add support-loadable ContextStorage; load OTelContext from …
jbachorik Jul 1, 2026
3c8190d
refactor: address review findings on thread-context factory split
jbachorik Jul 2, 2026
ed066ba
fix: avoid UAF in threadContext_defensive_ut CriticalSection scope
jbachorik Jul 2, 2026
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
24 changes: 24 additions & 0 deletions .github/workflows/test_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,30 @@ jobs:
echo "glibc-${{ matrix.java_version }}-${{ matrix.config }}-amd64" >> failures_glibc-${{ matrix.java_version }}-${{ matrix.config }}-amd64.txt
exit 1
fi
- name: Verify libJavaSupport ABI
# NOTE: this ABI gate only runs on the glibc/musl Linux matrix legs
# (nm -D against libJavaSupport.so). There is no equivalent check for
# macOS builds — the otel_thread_ctx_v1 export guarantee documented
# below is verified on Linux only.
if: success()
run: |
SUPPORT_LIB=$(find ddprof-lib/build/lib -name "libJavaSupport.so" | head -1)
[ -z "$SUPPORT_LIB" ] && echo "libJavaSupport.so not found" && exit 1
PROFILER_UNDEFINED=$(nm -u "$SUPPORT_LIB" | grep -E 'ProfiledThread|Profiler::|FlightRecorder' || true)
if [ -n "$PROFILER_UNDEFINED" ]; then
echo "ERROR: libJavaSupport.so has undefined profiler symbols:"
echo "$PROFILER_UNDEFINED"
exit 1
fi
# ContextExtractionToSupportPlan Phase E.4: otel_thread_ctx_v1 must be exported
# from libJavaSupport.so so external profilers can discover the OTEP thread
# context record with only the support library loaded (no profiler).
if ! nm -D "$SUPPORT_LIB" | grep -q 'otel_thread_ctx_v1'; then
echo "ERROR: libJavaSupport.so does not export otel_thread_ctx_v1"
nm -D "$SUPPORT_LIB" | grep -i otel || true
exit 1
fi
echo "ABI gate passed — no profiler symbols in libJavaSupport.so, otel_thread_ctx_v1 exported"
- name: Generate Unwinding Report
if: success() && matrix.config == 'debug'
run: |
Expand Down
4 changes: 2 additions & 2 deletions .gitlab/scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ echo "com.datadoghq:ddprof:${LIB_VERSION}" > version.txt
# Assemble task (always needed for artifact creation)
if [ "$MODE" = "assemble" ] || [ "$MODE" = "all" ]; then
echo "=== Assembling artifact ==="
./gradlew -Pskip-native -Pskip-tests -Pddprof_version="${LIB_VERSION}" -PbuildInfo.build.number=$CI_JOB_ID -Pwith-libs="$(pwd)/libs" :ddprof-lib:jar assembleAll --exclude-task compileFuzzer --exclude-task sign --max-workers=1 --no-build-cache --stacktrace --info --no-watch-fs --no-daemon
./gradlew -Pskip-native -Pskip-tests -Pddprof_version="${LIB_VERSION}" -PbuildInfo.build.number=$CI_JOB_ID -Pwith-libs="$(pwd)/libs" :ddprof-lib:jar assembleAll --exclude-task compileProfilerFuzzer --exclude-task compileSupportFuzzer --exclude-task sign --max-workers=1 --no-build-cache --stacktrace --info --no-watch-fs --no-daemon
fi

# Publish task (only when publishing to Maven Central)
Expand All @@ -47,5 +47,5 @@ if [ "$MODE" = "publish" ] || [ "$MODE" = "all" ]; then
echo "ERROR: GPG_PRIVATE_KEY is not set — run the create_key CI job first to provision the signing key in SSM (ci.java-profiler.signing.gpg_private_key)"
exit 1
fi
./gradlew -Pskip-native -Pskip-tests -Pddprof_version="${LIB_VERSION}" -PbuildInfo.build.number=$CI_JOB_ID -Pwith-libs="$(pwd)/libs" publishToSonatype closeAndReleaseSonatypeStagingRepository --exclude-task compileFuzzer --max-workers=1 --no-build-cache --stacktrace --info --no-watch-fs --no-daemon
./gradlew -Pskip-native -Pskip-tests -Pddprof_version="${LIB_VERSION}" -PbuildInfo.build.number=$CI_JOB_ID -Pwith-libs="$(pwd)/libs" publishToSonatype closeAndReleaseSonatypeStagingRepository --exclude-task compileProfilerFuzzer --exclude-task compileSupportFuzzer --max-workers=1 --no-build-cache --stacktrace --info --no-watch-fs --no-daemon
fi
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,28 @@ abstract class NativeBuildExtension @Inject constructor(
*/
abstract val includeDirectories: ListProperty<String>

/** Source directories that belong to the support library only. */
abstract val supportCppSourceDirs: ListProperty<String>

/**
* Source directories that belong to the profiler library only.
* When empty (the default), NativeBuildPlugin falls back to
* {@code cppSourceDirs - supportCppSourceDirs} at task-creation time.
* Set explicitly only when the profiler sources are not the complement
* of supportCppSourceDirs within cppSourceDirs.
*/
abstract val profilerCppSourceDirs: ListProperty<String>

/** When true, compile all sources into a single library (transitional mode). */
abstract val monolithicBuild: Property<Boolean>

init {
version.convention(project.version.toString())
cppSourceDirs.convention(listOf("src/main/cpp"))
includeDirectories.convention(emptyList())
supportCppSourceDirs.convention(emptyList())
profilerCppSourceDirs.convention(emptyList())
monolithicBuild.convention(false)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class NativeBuildPlugin : Plugin<Project> {
project.objects
)

// Step 3.2.2: read the monolithic Gradle property
val monolithic = project.hasProperty("monolithic")
extension.monolithicBuild.set(monolithic)

// Setup standard configurations after project evaluation
project.afterEvaluate {
setupStandardConfigurations(project, extension)
Expand Down Expand Up @@ -82,71 +86,118 @@ class NativeBuildPlugin : Plugin<Project> {
config: BuildConfiguration
) {
val configName = config.capitalizedName()

// Step 3.2.3: create two compile+link pairs when split mode is active
val linkTaskNames: List<String>
if (extension.monolithicBuild.get() || extension.supportCppSourceDirs.get().isEmpty()) {
linkTaskNames = listOf(
createCompileLinkPair(project, extension, config, extension.cppSourceDirs.get(), "")
)
} else {
val supportDirs = extension.supportCppSourceDirs.get()
val allDirs = extension.cppSourceDirs.get()
val explicitProfilerDirs = extension.profilerCppSourceDirs.get()
val profilerDirs = if (explicitProfilerDirs.isNotEmpty()) explicitProfilerDirs
else allDirs.filter { it !in supportDirs }
val supportLinkName = createCompileLinkPair(project, extension, config, supportDirs, "Support")
val profilerLinkName = createCompileLinkPair(
project, extension, config, profilerDirs, "Profiler",
linkAgainst = "libJavaSupport"
)
// Profiler link depends on support library being built first
project.tasks.named(profilerLinkName, NativeLinkTask::class.java) {
dependsOn(supportLinkName)
}
linkTaskNames = listOf(supportLinkName, profilerLinkName)
}

// Create assemble task depending on all link tasks for this config
project.tasks.register("assemble$configName") {
group = "build"
description = "Assembles ${config.name} configuration"
linkTaskNames.forEach { dependsOn(it) }
}

project.logger.debug("Created tasks for configuration: ${config.name}")
}

/**
* Creates a compile + link task pair for the given source directories and suffix.
*
* @param suffix "" for monolithic/default, "Support" or "Profiler" in split mode
* @param linkAgainst "libXxx" library name to add as a link-time dependency (with rpath)
* @return the name of the created link task
*/
private fun createCompileLinkPair(
project: Project,
extension: NativeBuildExtension,
config: BuildConfiguration,
sourceDirs: List<String>,
suffix: String,
linkAgainst: String? = null
): String {
val configName = config.capitalizedName()
val platform = config.platform.get()
val arch = config.architecture.get()

// Define paths
val objDir = project.file("build/obj/main/${config.name}")
val libBaseName = if (suffix == "Support") "JavaSupport" else "javaProfiler"
val libName = "lib$libBaseName.${PlatformUtils.sharedLibExtension()}"

val objSubDir = if (suffix.isEmpty()) "" else "/${suffix.lowercase()}"
val objDir = project.file("build/obj/main/${config.name}$objSubDir")
val libDir = project.file("build/lib/main/${config.name}/$platform/$arch")
val libName = "libjavaProfiler.${PlatformUtils.sharedLibExtension()}"
val outputLib = project.file("$libDir/$libName")

// Create compile task
val compileTask = project.tasks.register("compile$configName", NativeCompileTask::class.java) {
val compileTaskName = "compile${suffix}${configName}"
val compileTask = project.tasks.register(compileTaskName, NativeCompileTask::class.java) {
group = "build"
description = "Compiles C++ sources for ${config.name} configuration"
description = "Compiles C++ sources for ${config.name}${if (suffix.isNotEmpty()) " $suffix" else ""}"

// Find compiler
val compilerPath = findCompiler(project)
compiler.set(compilerPath)
compiler.set(findCompiler(project))
compilerArgs.set(config.compilerArgs.get())

// Set sources - default to src/main/cpp
val srcDirs = extension.cppSourceDirs.get()
sources.from(srcDirs.map { dir ->
sources.from(sourceDirs.map { dir ->
project.fileTree(dir) {
include("**/*.cpp", "**/*.cc", "**/*.c")
}
})

// Set includes - default + JNI
val includeList = extension.includeDirectories.get().toMutableList()
includeList.addAll(PlatformUtils.jniIncludePaths())
includes.from(includeList)

objectFileDir.set(objDir)
}

// Create link task
val linkTask = project.tasks.register("link$configName", NativeLinkTask::class.java) {
val linkTaskName = "link${suffix}${configName}"
project.tasks.register(linkTaskName, NativeLinkTask::class.java) {
group = "build"
description = "Links ${config.name} shared library"
description = "Links ${config.name}${if (suffix.isNotEmpty()) " $suffix" else ""} shared library"
dependsOn(compileTask)

val compilerPath = findCompiler(project)
linker.set(compilerPath)
linker.set(findCompiler(project))
linkerArgs.set(config.linkerArgs.get())
objectFiles.from(project.fileTree(objDir) {
include("*.o")
})
objectFiles.from(project.fileTree(objDir) { include("*.o") })
outputFile.set(outputLib)

// Enable debug symbol extraction for release builds
if (linkAgainst != null) {
val libFlag = linkAgainst.removePrefix("lib")
libraryPaths.add(libDir.absolutePath)
libraries.add(libFlag)
when (PlatformUtils.currentPlatform) {
Platform.LINUX -> runtimePaths.add("\$ORIGIN")
Platform.MACOS -> runtimePaths.add("@loader_path")
}
}

if (config.name == "release") {
extractDebugSymbols.set(true)
stripSymbols.set(true)
debugSymbolsDir.set(project.file("$libDir/debug"))
}
}

// Create assemble task
project.tasks.register("assemble$configName") {
group = "build"
description = "Assembles ${config.name} configuration"
dependsOn(linkTask)
}

project.logger.debug("Created tasks for configuration: ${config.name}")
return linkTaskName
}

private fun findCompiler(project: Project): String = PlatformUtils.findCompiler(project)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,15 @@ abstract class NativeLinkTask @Inject constructor(
@get:Optional
abstract val exportSymbols: ListProperty<String>

/**
* File containing explicit symbol names to export, one per line.
* Lines starting with '#' and blank lines are ignored.
* Merged with exportSymbols at link time.
*/
@get:InputFile
@get:Optional
abstract val exportSymbolsFile: RegularFileProperty

/**
* Symbol patterns to hide (make not visible).
* Applied after exportSymbols.
Expand Down Expand Up @@ -302,7 +311,7 @@ abstract class NativeLinkTask @Inject constructor(
}

// Add symbol visibility control if specified
if (exportSymbols.get().isNotEmpty() || hideSymbols.get().isNotEmpty()) {
if (exportSymbols.get().isNotEmpty() || exportSymbolsFile.isPresent || hideSymbols.get().isNotEmpty()) {
addAll(generateSymbolVisibilityFlags(outFile))
}

Expand Down Expand Up @@ -354,6 +363,13 @@ abstract class NativeLinkTask @Inject constructor(
logNormal("Successfully linked ${outFile.name} (${sizeKB}KB)")
}

private fun loadSymbolsFromFile(): List<String> =
if (exportSymbolsFile.isPresent)
exportSymbolsFile.get().asFile.readLines()
.map { it.trim() }
.filter { it.isNotBlank() && !it.startsWith("#") }
else emptyList()

/**
* Generate platform-specific symbol visibility flags.
* Returns linker flags to control symbol export/hiding.
Expand All @@ -375,25 +391,21 @@ abstract class NativeLinkTask @Inject constructor(
private fun generateLinuxVersionScript(outFile: java.io.File): List<String> {
val versionScript = java.io.File(temporaryDir, "${outFile.nameWithoutExtension}.ver")

val patternSymbols = exportSymbols.get()
val fileSymbols = loadSymbolsFromFile()

val scriptContent = buildString {
appendLine("{")
appendLine(" global:")

// Export specified symbols
exportSymbols.get().forEach { pattern ->
appendLine(" $pattern;")
}
patternSymbols.forEach { pattern -> appendLine(" $pattern;") }
fileSymbols.forEach { sym -> appendLine(" $sym;") }

// Consolidate all hidden symbols in a single local section
appendLine(" local:")

// Explicitly hide specified symbols (override exports)
hideSymbols.get().forEach { pattern ->
appendLine(" $pattern;")
}
hideSymbols.get().forEach { pattern -> appendLine(" $pattern;") }

// Hide everything else unless it was explicitly exported
if (exportSymbols.get().isNotEmpty() || hideSymbols.get().isNotEmpty()) {
if (patternSymbols.isNotEmpty() || fileSymbols.isNotEmpty() || hideSymbols.get().isNotEmpty()) {
appendLine(" *;")
}

Expand All @@ -408,49 +420,44 @@ abstract class NativeLinkTask @Inject constructor(

/**
* Generate macOS exported symbols list for symbol visibility control.
* macOS prepends an extra '_' to every C/C++ symbol name.
*/
private fun generateMacOSExportList(outFile: java.io.File): List<String> {
val exportList = java.io.File(temporaryDir, "${outFile.nameWithoutExtension}.exp")

val patternSymbols = exportSymbols.get()
val fileSymbols = loadSymbolsFromFile()

// Warn if wildcards are used - macOS doesn't support them
exportSymbols.get().forEach { pattern ->
patternSymbols.forEach { pattern ->
if (pattern.contains('*') || pattern.contains('?')) {
logger.warn("Symbol pattern '$pattern' contains wildcards which are not supported on macOS. " +
"Pattern will be treated as a literal symbol name. " +
"Consider using -fvisibility compiler flags instead, or list symbols explicitly.")
}
}

val allExportSymbols = patternSymbols + fileSymbols

// In Mach-O, every external symbol has a leading '_' — C++ mangled names like
// _ZN9VMStructs4initEP9CodeCache become __ZN9VMStructs4initEP9CodeCache.
// exported_symbols_list expects the full Mach-O name, so always prepend '_'.
val listContent = buildString {
// Export specified symbols (macOS needs leading underscore for C symbols)
exportSymbols.get().forEach { pattern ->
// Convert glob patterns to exact names or keep as-is
// macOS export list doesn't support wildcards like Linux version scripts
// For wildcards, we'd need to use -exported_symbols_list with all matching symbols
// For now, treat patterns as literal symbol names
val symbol = if (pattern.startsWith("_")) pattern else "_$pattern"
appendLine(symbol)
}
allExportSymbols.forEach { sym -> appendLine("_$sym") }
}

exportList.writeText(listContent)
logVerbose("Generated export list: ${exportList.name}")

val flags = mutableListOf<String>()

// Add export list
if (exportSymbols.get().isNotEmpty()) {
if (allExportSymbols.isNotEmpty()) {
flags.add("-Wl,-exported_symbols_list,${exportList.absolutePath}")
}

// For hiding, use -unexported_symbols_list if needed
if (hideSymbols.get().isNotEmpty()) {
val hideList = java.io.File(temporaryDir, "${outFile.nameWithoutExtension}.hide")
val hideContent = buildString {
hideSymbols.get().forEach { pattern ->
val symbol = if (pattern.startsWith("_")) pattern else "_$pattern"
appendLine(symbol)
}
hideSymbols.get().forEach { sym -> appendLine("_$sym") }
}
hideList.writeText(hideContent)
flags.add("-Wl,-unexported_symbols_list,${hideList.absolutePath}")
Expand Down
Loading