From f65454700684993cb226634b11b1f7c0840f0a90 Mon Sep 17 00:00:00 2001 From: r1viollet Date: Fri, 27 Jun 2025 15:29:37 +0200 Subject: [PATCH 1/4] Split debug Add build steps to store split debug information for release builds --- .github/scripts/test_alpine_aarch64.sh | 2 +- .github/workflows/test_workflow.yml | 6 +-- ddprof-lib/build.gradle | 56 +++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.github/scripts/test_alpine_aarch64.sh b/.github/scripts/test_alpine_aarch64.sh index 4d107ca5a..af30d6208 100755 --- a/.github/scripts/test_alpine_aarch64.sh +++ b/.github/scripts/test_alpine_aarch64.sh @@ -29,6 +29,6 @@ JAVA_VERSION=$("${JAVA_TEST_HOME}/bin/java" -version 2>&1 | awk -F '"' '/version }') export JAVA_VERSION -apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar >/dev/null +apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar binutils >/dev/null ./gradlew -PCI -PkeepJFRs :ddprof-test:test${CONFIG} --no-daemon --parallel --build-cache --no-watch-fs \ No newline at end of file diff --git a/.github/workflows/test_workflow.yml b/.github/workflows/test_workflow.yml index e96121b7c..c9738218d 100644 --- a/.github/workflows/test_workflow.yml +++ b/.github/workflows/test_workflow.yml @@ -72,7 +72,7 @@ jobs: if: steps.set_enabled.outputs.enabled == 'true' run: | sudo apt-get update - sudo apt-get install -y curl zip unzip libgtest-dev libgmock-dev + sudo apt-get install -y curl zip unzip libgtest-dev libgmock-dev binutils if [[ ${{ matrix.java_version }} =~ "-zing" ]]; then sudo apt-get install -y g++-9 gcc-9 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 --slave /usr/bin/g++ g++ /usr/bin/g++-9 @@ -135,7 +135,7 @@ jobs: steps: - name: Setup OS run: | - apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar >/dev/null + apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar binutils >/dev/null - uses: actions/checkout@v3 - name: Cache Gradle Wrapper Binaries uses: actions/cache@v4 @@ -286,7 +286,7 @@ jobs: sudo apt update -y sudo apt remove -y g++ sudo apt autoremove -y - sudo apt install -y curl zip unzip clang make build-essential + sudo apt install -y curl zip unzip clang make build-essential binutils if [[ ${{ matrix.java_version }} =~ "-zing" ]]; then sudo apt -y install g++-9 gcc-9 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 --slave /usr/bin/g++ g++ /usr/bin/g++-9 diff --git a/ddprof-lib/build.gradle b/ddprof-lib/build.gradle index 9ceb5c4c2..dc625c0e8 100644 --- a/ddprof-lib/build.gradle +++ b/ddprof-lib/build.gradle @@ -366,11 +366,55 @@ tasks.whenTaskAdded { task -> outputs.file linkedFile } if (config.name == 'release') { - def stripTask = tasks.register('stripLibRelease', StripSymbols) { + def extractDebugTask = tasks.register('extractDebugLibRelease', Exec) { onlyIf { config.active } dependsOn linkTask + description = 'Extract debug symbols from release library' + workingDir project.buildDir + + def sourceFile = tasks.linkLibRelease.linkedFile.get() + def debugFile = file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug/libjavaProfiler.${os().isLinux() ? 'so' : 'dylib'}.debug") + + inputs.file sourceFile + outputs.file debugFile + + doFirst { + // Ensure debug directory exists + debugFile.parentFile.mkdirs() + } + + if (os().isLinux()) { + commandLine 'objcopy', '--only-keep-debug', sourceFile.asFile.absolutePath, debugFile.absolutePath + } else { + // For macOS, we'll use dsymutil instead + commandLine 'dsymutil', sourceFile.asFile.absolutePath, '-o', debugFile.absolutePath.replace('.debug', '.dSYM') + } + } + + def addDebugLinkTask = tasks.register('addDebugLinkLibRelease', Exec) { + onlyIf { + config.active && os().isLinux() + } + dependsOn extractDebugTask + description = 'Add debug link to the original library' + + def sourceFile = tasks.linkLibRelease.linkedFile.get() + def debugFile = file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug/libjavaProfiler.${os().isLinux() ? 'so' : 'dylib'}.debug") + + inputs.file sourceFile + inputs.file debugFile + outputs.file sourceFile // modifies the source file + + commandLine 'objcopy', '--add-gnu-debuglink=' + debugFile.absolutePath, sourceFile.asFile.absolutePath + } + + def stripTask = tasks.register('stripLibRelease', StripSymbols) { + onlyIf { + config.active + } + dependsOn addDebugLinkTask targetPlatform = tasks.linkLibRelease.targetPlatform toolChain = tasks.linkLibRelease.toolChain binaryFile = tasks.linkLibRelease.linkedFile.get() @@ -378,6 +422,16 @@ tasks.whenTaskAdded { task -> inputs.file binaryFile outputs.file outputFile } + + // Create a task to copy debug files to the final location + def copyDebugTask = tasks.register('copyReleaseDebugFiles', Copy) { + dependsOn extractDebugTask + from file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug") + into file(libraryTargetPath(config.name + '-debug')) + include '**/*.debug' + include '**/*.dSYM/**' + } + def copyTask = tasks.findByName("copyReleaseLibs") if (copyTask != null) { copyTask.dependsOn stripTask From 6ab90a7ef992901619a1028059fa7f1e61c6bd6a Mon Sep 17 00:00:00 2001 From: r1viollet Date: Tue, 1 Jul 2025 12:02:32 +0200 Subject: [PATCH 2/4] Split debug - Refactor - Refactor split debug tasks out of the build - Check if tooling is available to perform split debug operations --- ddprof-lib/build.gradle | 244 ++++++++++++++++++++++++++++------------ 1 file changed, 173 insertions(+), 71 deletions(-) diff --git a/ddprof-lib/build.gradle b/ddprof-lib/build.gradle index dc625c0e8..5612682eb 100644 --- a/ddprof-lib/build.gradle +++ b/ddprof-lib/build.gradle @@ -8,6 +8,178 @@ plugins { id 'de.undercouch.download' version '4.1.1' } +// Helper function to check if objcopy is available +def checkObjcopyAvailable() { + try { + def process = ['objcopy', '--version'].execute() + process.waitFor() + return process.exitValue() == 0 + } catch (Exception e) { + return false + } +} + +// Helper function to check if dsymutil is available (for macOS) +def checkDsymutilAvailable() { + try { + def process = ['dsymutil', '--version'].execute() + process.waitFor() + return process.exitValue() == 0 + } catch (Exception e) { + return false + } +} + +// Helper function to check if debug extraction should be skipped +def shouldSkipDebugExtraction() { + return project.hasProperty('skip-debug-extraction') +} + +// Helper function to get debug file path for a given config +def getDebugFilePath(config) { + def extension = os().isLinux() ? 'so' : 'dylib' + return file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug/libjavaProfiler.${extension}.debug") +} + +// Helper function to get stripped file path for a given config +def getStrippedFilePath(config) { + def extension = os().isLinux() ? 'so' : 'dylib' + return file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/stripped/libjavaProfiler.${extension}") +} + +// Helper function to create error message for missing tools +def getMissingToolErrorMessage(toolName, installInstructions) { + return """ + |${toolName} is not available but is required for split debug information. + | + |To fix this issue: + |${installInstructions} + | + |If you want to build without split debug info, set -Pskip-debug-extraction=true + """.stripMargin() +} + +// Helper function to create debug extraction task +def createDebugExtractionTask(config, linkTask) { + return tasks.register('extractDebugLibRelease', Exec) { + onlyIf { + config.active && !shouldSkipDebugExtraction() + } + dependsOn linkTask + description = 'Extract debug symbols from release library' + workingDir project.buildDir + + doFirst { + def sourceFile = linkTask.get().linkedFile.get().asFile + def debugFile = getDebugFilePath(config) + + // Check for required tools before proceeding + if (os().isLinux()) { + if (!checkObjcopyAvailable()) { + def installInstructions = """ + | - On Ubuntu/Debian: sudo apt-get install binutils + | - On RHEL/CentOS: sudo yum install binutils + | - On Alpine: apk add binutils""".stripMargin() + throw new GradleException(getMissingToolErrorMessage('objcopy', installInstructions)) + } + } else if (os().isMacOsX()) { + if (!checkDsymutilAvailable()) { + def installInstructions = """ + | dsymutil should be available with Xcode command line tools: + | xcode-select --install""".stripMargin() + throw new GradleException(getMissingToolErrorMessage('dsymutil', installInstructions)) + } + } + + // Ensure debug directory exists + debugFile.parentFile.mkdirs() + + // Set the command line based on platform + if (os().isLinux()) { + commandLine = ['objcopy', '--only-keep-debug', sourceFile.absolutePath, debugFile.absolutePath] + } else { + // For macOS, we'll use dsymutil instead + commandLine = ['dsymutil', sourceFile.absolutePath, '-o', debugFile.absolutePath.replace('.debug', '.dSYM')] + } + } + } +} + +// Helper function to create debug link task (Linux only) +def createDebugLinkTask(config, linkTask, extractDebugTask) { + return tasks.register('addDebugLinkLibRelease', Exec) { + onlyIf { + config.active && os().isLinux() && !shouldSkipDebugExtraction() + } + dependsOn extractDebugTask + description = 'Add debug link to the original library' + + doFirst { + def sourceFile = linkTask.get().linkedFile.get().asFile + def debugFile = getDebugFilePath(config) + + // Check for objcopy availability + if (!checkObjcopyAvailable()) { + def installInstructions = """ + | - On Ubuntu/Debian: sudo apt-get install binutils + | - On RHEL/CentOS: sudo yum install binutils + | - On Alpine: apk add binutils""".stripMargin() + throw new GradleException(getMissingToolErrorMessage('objcopy', installInstructions)) + } + + commandLine = ['objcopy', '--add-gnu-debuglink=' + debugFile.absolutePath, sourceFile.absolutePath] + } + } +} + +// Helper function to create debug file copy task +def createDebugCopyTask(config, extractDebugTask) { + return tasks.register('copyReleaseDebugFiles', Copy) { + onlyIf { + !shouldSkipDebugExtraction() + } + dependsOn extractDebugTask + from file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug") + into file(libraryTargetPath(config.name + '-debug')) + include '**/*.debug' + include '**/*.dSYM/**' + } +} + +// Main function to setup debug extraction for release builds +def setupDebugExtraction(config, linkTask) { + if (config.name == 'release') { + // Create all debug-related tasks + def extractDebugTask = createDebugExtractionTask(config, linkTask) + def addDebugLinkTask = createDebugLinkTask(config, linkTask, extractDebugTask) + + // Create the strip task and configure it properly + def stripTask = tasks.register('stripLibRelease', StripSymbols) { + onlyIf { + config.active + } + dependsOn addDebugLinkTask + } + + // Configure the strip task after registration + stripTask.configure { + targetPlatform = linkTask.get().targetPlatform + toolChain = linkTask.get().toolChain + binaryFile = linkTask.get().linkedFile.get().asFile + outputFile = getStrippedFilePath(config) + } + + def copyDebugTask = createDebugCopyTask(config, extractDebugTask) + + // Wire up the copy task to use stripped binaries + def copyTask = tasks.findByName("copyReleaseLibs") + if (copyTask != null) { + copyTask.dependsOn stripTask + copyTask.inputs.files stripTask.get().outputs.files + } + } +} + def libraryName = "ddprof" description = "Datadog Java Profiler Library" @@ -366,77 +538,7 @@ tasks.whenTaskAdded { task -> outputs.file linkedFile } if (config.name == 'release') { - def extractDebugTask = tasks.register('extractDebugLibRelease', Exec) { - onlyIf { - config.active - } - dependsOn linkTask - description = 'Extract debug symbols from release library' - workingDir project.buildDir - - def sourceFile = tasks.linkLibRelease.linkedFile.get() - def debugFile = file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug/libjavaProfiler.${os().isLinux() ? 'so' : 'dylib'}.debug") - - inputs.file sourceFile - outputs.file debugFile - - doFirst { - // Ensure debug directory exists - debugFile.parentFile.mkdirs() - } - - if (os().isLinux()) { - commandLine 'objcopy', '--only-keep-debug', sourceFile.asFile.absolutePath, debugFile.absolutePath - } else { - // For macOS, we'll use dsymutil instead - commandLine 'dsymutil', sourceFile.asFile.absolutePath, '-o', debugFile.absolutePath.replace('.debug', '.dSYM') - } - } - - def addDebugLinkTask = tasks.register('addDebugLinkLibRelease', Exec) { - onlyIf { - config.active && os().isLinux() - } - dependsOn extractDebugTask - description = 'Add debug link to the original library' - - def sourceFile = tasks.linkLibRelease.linkedFile.get() - def debugFile = file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug/libjavaProfiler.${os().isLinux() ? 'so' : 'dylib'}.debug") - - inputs.file sourceFile - inputs.file debugFile - outputs.file sourceFile // modifies the source file - - commandLine 'objcopy', '--add-gnu-debuglink=' + debugFile.absolutePath, sourceFile.asFile.absolutePath - } - - def stripTask = tasks.register('stripLibRelease', StripSymbols) { - onlyIf { - config.active - } - dependsOn addDebugLinkTask - targetPlatform = tasks.linkLibRelease.targetPlatform - toolChain = tasks.linkLibRelease.toolChain - binaryFile = tasks.linkLibRelease.linkedFile.get() - outputFile = file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/stripped/libjavaProfiler.${os().isLinux() ? 'so' : 'dylib'}") - inputs.file binaryFile - outputs.file outputFile - } - - // Create a task to copy debug files to the final location - def copyDebugTask = tasks.register('copyReleaseDebugFiles', Copy) { - dependsOn extractDebugTask - from file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug") - into file(libraryTargetPath(config.name + '-debug')) - include '**/*.debug' - include '**/*.dSYM/**' - } - - def copyTask = tasks.findByName("copyReleaseLibs") - if (copyTask != null) { - copyTask.dependsOn stripTask - copyTask.inputs.files stripTask.get().outputs.files - } + setupDebugExtraction(config, linkTask) } } } From c4dd50e780241dc634cbf03c645b3c7353a0899a Mon Sep 17 00:00:00 2001 From: r1viollet Date: Tue, 1 Jul 2025 15:14:54 +0200 Subject: [PATCH 3/4] Split debug - add an extra folder to the META-INF outputs --- README.md | 33 +++++++++++++++++++++++++++++++++ ddprof-lib/build.gradle | 13 +++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b8e24356b..93fa275dc 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,39 @@ The project includes both Java and C++ unit tests. You can run them using: ### Cross-JDK Testing `JAVA_TEST_HOME= ./gradlew testDebug` +## Release Builds and Debug Information + +### Split Debug Information +Release builds automatically generate split debug information to optimize deployment size while preserving debugging capabilities: + +- **Stripped libraries** (~1.2MB): Production-ready binaries with symbols removed for deployment +- **Debug symbol files** (~6.1MB): Separate `.debug` files containing full debugging information +- **Debug links**: Stripped libraries include `.gnu_debuglink` sections pointing to debug files + +### Build Artifacts Structure +``` +ddprof-lib/build/ +├── lib/main/release/linux/x64/ +│ ├── libjavaProfiler.so # Original library with debug symbols +│ ├── stripped/ +│ │ └── libjavaProfiler.so # Stripped library (83% smaller) +│ └── debug/ +│ └── libjavaProfiler.so.debug # Debug symbols only +├── native/release/ +│ └── META-INF/native-libs/linux-x64/ +│ └── libjavaProfiler.so # Final stripped library (deployed) +└── native/release-debug/ + └── META-INF/native-libs/linux-x64/ + └── libjavaProfiler.so.debug # Debug symbols package +``` + +### Build Options +- **Skip debug extraction**: `./gradlew buildRelease -Pskip-debug-extraction=true` +- **Debug extraction requires**: `objcopy` (Linux) or `dsymutil` (macOS) + - Ubuntu/Debian: `sudo apt-get install binutils` + - Alpine: `apk add binutils` + - macOS: Included with Xcode command line tools + ## Development ### Code Quality diff --git a/ddprof-lib/build.gradle b/ddprof-lib/build.gradle index 5612682eb..3cf338fee 100644 --- a/ddprof-lib/build.gradle +++ b/ddprof-lib/build.gradle @@ -63,7 +63,7 @@ def getMissingToolErrorMessage(toolName, installInstructions) { def createDebugExtractionTask(config, linkTask) { return tasks.register('extractDebugLibRelease', Exec) { onlyIf { - config.active && !shouldSkipDebugExtraction() + !shouldSkipDebugExtraction() } dependsOn linkTask description = 'Extract debug symbols from release library' @@ -109,7 +109,7 @@ def createDebugExtractionTask(config, linkTask) { def createDebugLinkTask(config, linkTask, extractDebugTask) { return tasks.register('addDebugLinkLibRelease', Exec) { onlyIf { - config.active && os().isLinux() && !shouldSkipDebugExtraction() + os().isLinux() && !shouldSkipDebugExtraction() } dependsOn extractDebugTask description = 'Add debug link to the original library' @@ -148,16 +148,14 @@ def createDebugCopyTask(config, extractDebugTask) { // Main function to setup debug extraction for release builds def setupDebugExtraction(config, linkTask) { - if (config.name == 'release') { + if (config.name == 'release' && config.active && !project.hasProperty('skip-native')) { // Create all debug-related tasks def extractDebugTask = createDebugExtractionTask(config, linkTask) def addDebugLinkTask = createDebugLinkTask(config, linkTask, extractDebugTask) // Create the strip task and configure it properly def stripTask = tasks.register('stripLibRelease', StripSymbols) { - onlyIf { - config.active - } + // No onlyIf needed here - setupDebugExtraction already handles the main conditions dependsOn addDebugLinkTask } @@ -176,6 +174,9 @@ def setupDebugExtraction(config, linkTask) { if (copyTask != null) { copyTask.dependsOn stripTask copyTask.inputs.files stripTask.get().outputs.files + + // Create an extra folder for the debug symbols + copyTask.dependsOn copyDebugTask } } } From 137e20329b774494a34f7c8a175130a0419af224 Mon Sep 17 00:00:00 2001 From: r1viollet Date: Wed, 2 Jul 2025 15:08:01 +0200 Subject: [PATCH 4/4] split debug - make binutils optional --- ddprof-lib/build.gradle | 43 ++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/ddprof-lib/build.gradle b/ddprof-lib/build.gradle index 3cf338fee..355ad5ef0 100644 --- a/ddprof-lib/build.gradle +++ b/ddprof-lib/build.gradle @@ -32,7 +32,21 @@ def checkDsymutilAvailable() { // Helper function to check if debug extraction should be skipped def shouldSkipDebugExtraction() { - return project.hasProperty('skip-debug-extraction') + // Skip if explicitly disabled + if (project.hasProperty('skip-debug-extraction')) { + return true + } + + // Skip if required tools are not available + if (os().isLinux() && !checkObjcopyAvailable()) { + return true + } + + if (os().isMacOsX() && !checkDsymutilAvailable()) { + return true + } + + return false } // Helper function to get debug file path for a given config @@ -73,24 +87,6 @@ def createDebugExtractionTask(config, linkTask) { def sourceFile = linkTask.get().linkedFile.get().asFile def debugFile = getDebugFilePath(config) - // Check for required tools before proceeding - if (os().isLinux()) { - if (!checkObjcopyAvailable()) { - def installInstructions = """ - | - On Ubuntu/Debian: sudo apt-get install binutils - | - On RHEL/CentOS: sudo yum install binutils - | - On Alpine: apk add binutils""".stripMargin() - throw new GradleException(getMissingToolErrorMessage('objcopy', installInstructions)) - } - } else if (os().isMacOsX()) { - if (!checkDsymutilAvailable()) { - def installInstructions = """ - | dsymutil should be available with Xcode command line tools: - | xcode-select --install""".stripMargin() - throw new GradleException(getMissingToolErrorMessage('dsymutil', installInstructions)) - } - } - // Ensure debug directory exists debugFile.parentFile.mkdirs() @@ -118,15 +114,6 @@ def createDebugLinkTask(config, linkTask, extractDebugTask) { def sourceFile = linkTask.get().linkedFile.get().asFile def debugFile = getDebugFilePath(config) - // Check for objcopy availability - if (!checkObjcopyAvailable()) { - def installInstructions = """ - | - On Ubuntu/Debian: sudo apt-get install binutils - | - On RHEL/CentOS: sudo yum install binutils - | - On Alpine: apk add binutils""".stripMargin() - throw new GradleException(getMissingToolErrorMessage('objcopy', installInstructions)) - } - commandLine = ['objcopy', '--add-gnu-debuglink=' + debugFile.absolutePath, sourceFile.absolutePath] } }