Skip to content

Split debug #233

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 7 commits into from
Jul 3, 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
2 changes: 1 addition & 1 deletion .github/scripts/test_alpine_aarch64.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions .github/workflows/test_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,39 @@ The project includes both Java and C++ unit tests. You can run them using:
### Cross-JDK Testing
`JAVA_TEST_HOME=<path to test JDK> ./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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not test on MacOS 😬
Feedback appreciated if this fails

- Ubuntu/Debian: `sudo apt-get install binutils`
- Alpine: `apk add binutils`
- macOS: Included with Xcode command line tools

## Development

### Code Quality
Expand Down
178 changes: 161 additions & 17 deletions ddprof-lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,166 @@ 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() {
// 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
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 {
!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)

// 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 {
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)

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' && 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) {
// No onlyIf needed here - setupDebugExtraction already handles the main conditions
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

// Create an extra folder for the debug symbols
copyTask.dependsOn copyDebugTask
}
}
}

def libraryName = "ddprof"

description = "Datadog Java Profiler Library"
Expand Down Expand Up @@ -366,23 +526,7 @@ tasks.whenTaskAdded { task ->
outputs.file linkedFile
}
if (config.name == 'release') {
def stripTask = tasks.register('stripLibRelease', StripSymbols) {
onlyIf {
config.active
}
dependsOn linkTask
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
}
def copyTask = tasks.findByName("copyReleaseLibs")
if (copyTask != null) {
copyTask.dependsOn stripTask
copyTask.inputs.files stripTask.get().outputs.files
}
setupDebugExtraction(config, linkTask)
}
}
}
Expand Down
Loading