Skip to content

Commit edcb336

Browse files
r1violletzhengyu123
authored andcommitted
Split debug (#233)
* Split debug Add build steps to store split debug information for release builds
1 parent 44531b0 commit edcb336

File tree

4 files changed

+198
-21
lines changed

4 files changed

+198
-21
lines changed

.github/scripts/test_alpine_aarch64.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ JAVA_VERSION=$("${JAVA_TEST_HOME}/bin/java" -version 2>&1 | awk -F '"' '/version
2929
}')
3030
export JAVA_VERSION
3131

32-
apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar >/dev/null
32+
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
3333

3434
./gradlew -PCI -PkeepJFRs :ddprof-test:test${CONFIG} --no-daemon --parallel --build-cache --no-watch-fs

.github/workflows/test_workflow.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
if: steps.set_enabled.outputs.enabled == 'true'
7373
run: |
7474
sudo apt-get update
75-
sudo apt-get install -y curl zip unzip libgtest-dev libgmock-dev
75+
sudo apt-get install -y curl zip unzip libgtest-dev libgmock-dev binutils
7676
if [[ ${{ matrix.java_version }} =~ "-zing" ]]; then
7777
sudo apt-get install -y g++-9 gcc-9
7878
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:
135135
steps:
136136
- name: Setup OS
137137
run: |
138-
apk update && apk add curl moreutils wget hexdump linux-headers bash make g++ clang git cppcheck jq cmake gtest-dev gmock tar >/dev/null
138+
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
139139
- uses: actions/checkout@v3
140140
- name: Cache Gradle Wrapper Binaries
141141
uses: actions/cache@v4
@@ -286,7 +286,7 @@ jobs:
286286
sudo apt update -y
287287
sudo apt remove -y g++
288288
sudo apt autoremove -y
289-
sudo apt install -y curl zip unzip clang make build-essential
289+
sudo apt install -y curl zip unzip clang make build-essential binutils
290290
if [[ ${{ matrix.java_version }} =~ "-zing" ]]; then
291291
sudo apt -y install g++-9 gcc-9
292292
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 --slave /usr/bin/g++ g++ /usr/bin/g++-9

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,39 @@ The project includes both Java and C++ unit tests. You can run them using:
6464
### Cross-JDK Testing
6565
`JAVA_TEST_HOME=<path to test JDK> ./gradlew testDebug`
6666

67+
## Release Builds and Debug Information
68+
69+
### Split Debug Information
70+
Release builds automatically generate split debug information to optimize deployment size while preserving debugging capabilities:
71+
72+
- **Stripped libraries** (~1.2MB): Production-ready binaries with symbols removed for deployment
73+
- **Debug symbol files** (~6.1MB): Separate `.debug` files containing full debugging information
74+
- **Debug links**: Stripped libraries include `.gnu_debuglink` sections pointing to debug files
75+
76+
### Build Artifacts Structure
77+
```
78+
ddprof-lib/build/
79+
├── lib/main/release/linux/x64/
80+
│ ├── libjavaProfiler.so # Original library with debug symbols
81+
│ ├── stripped/
82+
│ │ └── libjavaProfiler.so # Stripped library (83% smaller)
83+
│ └── debug/
84+
│ └── libjavaProfiler.so.debug # Debug symbols only
85+
├── native/release/
86+
│ └── META-INF/native-libs/linux-x64/
87+
│ └── libjavaProfiler.so # Final stripped library (deployed)
88+
└── native/release-debug/
89+
└── META-INF/native-libs/linux-x64/
90+
└── libjavaProfiler.so.debug # Debug symbols package
91+
```
92+
93+
### Build Options
94+
- **Skip debug extraction**: `./gradlew buildRelease -Pskip-debug-extraction=true`
95+
- **Debug extraction requires**: `objcopy` (Linux) or `dsymutil` (macOS)
96+
- Ubuntu/Debian: `sudo apt-get install binutils`
97+
- Alpine: `apk add binutils`
98+
- macOS: Included with Xcode command line tools
99+
67100
## Development
68101

69102
### Code Quality

ddprof-lib/build.gradle

Lines changed: 161 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,166 @@ plugins {
88
id 'de.undercouch.download' version '4.1.1'
99
}
1010

11+
// Helper function to check if objcopy is available
12+
def checkObjcopyAvailable() {
13+
try {
14+
def process = ['objcopy', '--version'].execute()
15+
process.waitFor()
16+
return process.exitValue() == 0
17+
} catch (Exception e) {
18+
return false
19+
}
20+
}
21+
22+
// Helper function to check if dsymutil is available (for macOS)
23+
def checkDsymutilAvailable() {
24+
try {
25+
def process = ['dsymutil', '--version'].execute()
26+
process.waitFor()
27+
return process.exitValue() == 0
28+
} catch (Exception e) {
29+
return false
30+
}
31+
}
32+
33+
// Helper function to check if debug extraction should be skipped
34+
def shouldSkipDebugExtraction() {
35+
// Skip if explicitly disabled
36+
if (project.hasProperty('skip-debug-extraction')) {
37+
return true
38+
}
39+
40+
// Skip if required tools are not available
41+
if (os().isLinux() && !checkObjcopyAvailable()) {
42+
return true
43+
}
44+
45+
if (os().isMacOsX() && !checkDsymutilAvailable()) {
46+
return true
47+
}
48+
49+
return false
50+
}
51+
52+
// Helper function to get debug file path for a given config
53+
def getDebugFilePath(config) {
54+
def extension = os().isLinux() ? 'so' : 'dylib'
55+
return file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug/libjavaProfiler.${extension}.debug")
56+
}
57+
58+
// Helper function to get stripped file path for a given config
59+
def getStrippedFilePath(config) {
60+
def extension = os().isLinux() ? 'so' : 'dylib'
61+
return file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/stripped/libjavaProfiler.${extension}")
62+
}
63+
64+
// Helper function to create error message for missing tools
65+
def getMissingToolErrorMessage(toolName, installInstructions) {
66+
return """
67+
|${toolName} is not available but is required for split debug information.
68+
|
69+
|To fix this issue:
70+
|${installInstructions}
71+
|
72+
|If you want to build without split debug info, set -Pskip-debug-extraction=true
73+
""".stripMargin()
74+
}
75+
76+
// Helper function to create debug extraction task
77+
def createDebugExtractionTask(config, linkTask) {
78+
return tasks.register('extractDebugLibRelease', Exec) {
79+
onlyIf {
80+
!shouldSkipDebugExtraction()
81+
}
82+
dependsOn linkTask
83+
description = 'Extract debug symbols from release library'
84+
workingDir project.buildDir
85+
86+
doFirst {
87+
def sourceFile = linkTask.get().linkedFile.get().asFile
88+
def debugFile = getDebugFilePath(config)
89+
90+
// Ensure debug directory exists
91+
debugFile.parentFile.mkdirs()
92+
93+
// Set the command line based on platform
94+
if (os().isLinux()) {
95+
commandLine = ['objcopy', '--only-keep-debug', sourceFile.absolutePath, debugFile.absolutePath]
96+
} else {
97+
// For macOS, we'll use dsymutil instead
98+
commandLine = ['dsymutil', sourceFile.absolutePath, '-o', debugFile.absolutePath.replace('.debug', '.dSYM')]
99+
}
100+
}
101+
}
102+
}
103+
104+
// Helper function to create debug link task (Linux only)
105+
def createDebugLinkTask(config, linkTask, extractDebugTask) {
106+
return tasks.register('addDebugLinkLibRelease', Exec) {
107+
onlyIf {
108+
os().isLinux() && !shouldSkipDebugExtraction()
109+
}
110+
dependsOn extractDebugTask
111+
description = 'Add debug link to the original library'
112+
113+
doFirst {
114+
def sourceFile = linkTask.get().linkedFile.get().asFile
115+
def debugFile = getDebugFilePath(config)
116+
117+
commandLine = ['objcopy', '--add-gnu-debuglink=' + debugFile.absolutePath, sourceFile.absolutePath]
118+
}
119+
}
120+
}
121+
122+
// Helper function to create debug file copy task
123+
def createDebugCopyTask(config, extractDebugTask) {
124+
return tasks.register('copyReleaseDebugFiles', Copy) {
125+
onlyIf {
126+
!shouldSkipDebugExtraction()
127+
}
128+
dependsOn extractDebugTask
129+
from file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/debug")
130+
into file(libraryTargetPath(config.name + '-debug'))
131+
include '**/*.debug'
132+
include '**/*.dSYM/**'
133+
}
134+
}
135+
136+
// Main function to setup debug extraction for release builds
137+
def setupDebugExtraction(config, linkTask) {
138+
if (config.name == 'release' && config.active && !project.hasProperty('skip-native')) {
139+
// Create all debug-related tasks
140+
def extractDebugTask = createDebugExtractionTask(config, linkTask)
141+
def addDebugLinkTask = createDebugLinkTask(config, linkTask, extractDebugTask)
142+
143+
// Create the strip task and configure it properly
144+
def stripTask = tasks.register('stripLibRelease', StripSymbols) {
145+
// No onlyIf needed here - setupDebugExtraction already handles the main conditions
146+
dependsOn addDebugLinkTask
147+
}
148+
149+
// Configure the strip task after registration
150+
stripTask.configure {
151+
targetPlatform = linkTask.get().targetPlatform
152+
toolChain = linkTask.get().toolChain
153+
binaryFile = linkTask.get().linkedFile.get().asFile
154+
outputFile = getStrippedFilePath(config)
155+
}
156+
157+
def copyDebugTask = createDebugCopyTask(config, extractDebugTask)
158+
159+
// Wire up the copy task to use stripped binaries
160+
def copyTask = tasks.findByName("copyReleaseLibs")
161+
if (copyTask != null) {
162+
copyTask.dependsOn stripTask
163+
copyTask.inputs.files stripTask.get().outputs.files
164+
165+
// Create an extra folder for the debug symbols
166+
copyTask.dependsOn copyDebugTask
167+
}
168+
}
169+
}
170+
11171
def libraryName = "ddprof"
12172

13173
description = "Datadog Java Profiler Library"
@@ -366,23 +526,7 @@ tasks.whenTaskAdded { task ->
366526
outputs.file linkedFile
367527
}
368528
if (config.name == 'release') {
369-
def stripTask = tasks.register('stripLibRelease', StripSymbols) {
370-
onlyIf {
371-
config.active
372-
}
373-
dependsOn linkTask
374-
targetPlatform = tasks.linkLibRelease.targetPlatform
375-
toolChain = tasks.linkLibRelease.toolChain
376-
binaryFile = tasks.linkLibRelease.linkedFile.get()
377-
outputFile = file("$buildDir/lib/main/${config.name}/${osIdentifier()}/${archIdentifier()}/stripped/libjavaProfiler.${os().isLinux() ? 'so' : 'dylib'}")
378-
inputs.file binaryFile
379-
outputs.file outputFile
380-
}
381-
def copyTask = tasks.findByName("copyReleaseLibs")
382-
if (copyTask != null) {
383-
copyTask.dependsOn stripTask
384-
copyTask.inputs.files stripTask.get().outputs.files
385-
}
529+
setupDebugExtraction(config, linkTask)
386530
}
387531
}
388532
}

0 commit comments

Comments
 (0)