Skip to content

Commit aa99ce7

Browse files
authored
Merge branch 'master' into ts/43260_profiler_crash_after_missconfig
2 parents c66fecc + 5cf20d7 commit aa99ce7

19 files changed

Lines changed: 480 additions & 101 deletions

File tree

.github/workflows/actions.yml

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Build
22

33
on:
44
push:
5-
branches: '**'
5+
branches: master
66
tags: 'v*'
77
pull_request:
88
branches: '**'
@@ -47,6 +47,14 @@ jobs:
4747
-Psigning.keyId=4FB80B8E \
4848
-PsonatypeUsername=${{ secrets.SONATYPE_USER }} \
4949
-PsonatypePassword=${{ secrets.SONATYPE_PASSWORD }}
50+
- name: Push Docker Image to DockerHub
51+
if: startsWith(github.ref, 'refs/tags/v')
52+
run: |
53+
./gradlew :agent:pushOciImage :agent:pushLegacyOciImage --registry=dockerHub
54+
./gradlew :agent:pushOciImage :agent:pushLegacyOciImage --registry=dockerHub -PociImageTag=latest
55+
env:
56+
ORG_GRADLE_PROJECT_dockerHubUsername: ${{ secrets.DOCKERHUB_USER }}
57+
ORG_GRADLE_PROJECT_dockerHubPassword: ${{ secrets.DOCKERHUB_TOKEN }}
5058
- name: Upload coverage to Teamscale
5159
if: always() && github.event_name == 'push'
5260
uses: cqse/teamscale-upload-action@v9.2.1
@@ -83,32 +91,3 @@ jobs:
8391
format: 'JACOCO'
8492
message: 'Coverage Windows'
8593
files: '**/jacocoTestReport.xml'
86-
87-
docker:
88-
runs-on: ubuntu-latest
89-
steps:
90-
- name: Set up QEMU
91-
uses: docker/setup-qemu-action@v4
92-
- name: Set up Docker Buildx
93-
uses: docker/setup-buildx-action@v4
94-
- name: Docker meta
95-
id: meta
96-
uses: docker/metadata-action@v6
97-
with:
98-
images: cqse/teamscale-jacoco-agent
99-
- name: Login to DockerHub
100-
if: github.event_name != 'pull_request'
101-
uses: docker/login-action@v4
102-
with:
103-
username: ${{ secrets.DOCKERHUB_USER }}
104-
password: ${{ secrets.DOCKERHUB_TOKEN }}
105-
- name: Build and push
106-
uses: docker/build-push-action@v7
107-
with:
108-
file: 'agent/src/docker/Dockerfile'
109-
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
110-
tags: ${{ steps.meta.outputs.tags }}
111-
labels: ${{ steps.meta.outputs.labels }}
112-
build-args: |
113-
GITHUB_REF=${{ github.ref }}
114-

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ We use [semantic versioning](http://semver.org/):
66

77
# Next version
88
- [fix] _agent_: The profiled application no longer crashes when the profiler configuration is invalid (e.g., missing `teamscale-user`). Instead, the application starts normally without coverage collection.
9-
- [feature] _teamscale-gradle-plugin_: Annotated tasks with `@DisableCachingByDefault` where caching can't be applied
9+
10+
# 36.5.0
11+
- [feature] _agent_: Renamed the docker image to `cqse/teamscale-java-profiler` and added support for the `linux/arm64` platform
12+
- [fix] _teamscale-gradle-plugin_: Coverage aggregation report and upload were skipped when tests failed with `--continue`
13+
14+
# 36.4.1
15+
- [fix] _agent_: Fixed `IllegalStateException: Can't add different class with same name` when application servers like JBoss perform a reload without full restart or when duplicate class files exist across multiple archives
16+
- [fix] _teamscale-gradle-plugin_: Annotated tasks with `@DisableCachingByDefault` where caching can't be applied
1017

1118
# 36.4.0
1219
- [feature] _maven-plugin_: Auto-detect commit from CI/CD environment variables (Jenkins, GitHub Actions, GitLab CI, Azure DevOps, etc.)

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Entry point: `PreMain.premain()` in `agent/src/main/java/com/teamscale/jacoco/ag
5151
4. Classes are instrumented at load time with coverage probes
5252

5353
**Coverage modes:**
54-
- **Interval-based** (`Agent` class) - Periodically dumps coverage (default every 10 min)
54+
- **Interval-based** (`Agent` class) - Periodically dumps coverage (default every 480 min)
5555
- **Test-wise** (`TestwiseCoverageAgent` class) - Per-test coverage via HTTP endpoints (`/test/start`, `/test/end`)
5656

5757
**Key classes:**

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,86 @@
1111
* [Teamscale Gradle Plugin](https://docs.teamscale.com/reference/integrations/gradle-plugin/)
1212
* [Teamscale Maven Plugin](https://docs.teamscale.com/reference/integrations/maven-plugin/)
1313

14+
## Architecture
15+
16+
The profiler is a JVM agent that uses [JaCoCo](https://www.jacoco.org/) under the hood for bytecode instrumentation and coverage recording.
17+
The diagram below shows the data flow from JVM startup to coverage upload.
18+
19+
```
20+
┌──────────────────────────────────────────────────────────────────────────┐
21+
│ JVM │
22+
│ │
23+
│ -javaagent:teamscale-jacoco-agent.jar=... │
24+
│ │ │
25+
│ ▼ │
26+
│ ┌──────────┐ ┌─────────────────────┐ ┌──────────────────────┐ │
27+
│ │ PreMain │────▶│ JaCoCoPreMain │────▶│ LenientCoverage- │ │
28+
│ │ │ │ │ │ Transformer │ │
29+
│ │ Parses │ │ Creates JaCoCo │ │ │ │
30+
│ │ options, │ │ runtime, registers │ │ Registered with JVM │ │
31+
│ │ logging │ │ class transformer │ │ via addTransformer() │ │
32+
│ └──────────┘ └─────────────────────┘ └──────────┬───────────┘ │
33+
│ │ │ │
34+
│ │ ┌──────────────────────────────────────────────────┐ │
35+
│ │ │ Class Loading │ │
36+
│ │ │ │ │
37+
│ │ │ For every class loaded by the JVM: │ │
38+
│ │ │ 1. Transformer receives original bytecode │ │
39+
│ │ │ 2. JaCoCo injects boolean[] probes at branches │ │
40+
│ │ │ and lines │ │
41+
│ │ │ 3. Modified bytecode returned to JVM │ │
42+
│ │ │ │ │
43+
│ │ │ Instrumentation happens ONLY at class load │ │
44+
│ │ │ time. Already-loaded classes are never │ │
45+
│ │ │ retransformed. │ │
46+
│ │ └──────────────────────────────────────────────────┘ │
47+
│ │ │ │
48+
│ │ ┌──────────────────────────────────────────────────┐ │
49+
│ │ │ Runtime │ │
50+
│ │ │ │ │
51+
│ │ │ Probes fire during normal code execution. │ │
52+
│ │ │ Each probe sets a flag in a per-class │ │
53+
│ │ │ boolean[], tracking which lines/branches ran. │ │
54+
│ │ │ Data accumulates in JaCoCo runtime memory. │ │
55+
│ │ └──────────────────────────────────────────────────┘ │
56+
│ │ │
57+
│ ▼ │
58+
│ ┌─────────────────────────────────────────────────────────────────┐ │
59+
│ │ Agent (Normal mode) OR TestwiseCoverageAgent │ │
60+
│ │ │ │
61+
│ │ HTTP server (Jetty + Jersey) for control: │ │
62+
│ │ POST /dump ─── trigger coverage dump │ │
63+
│ │ POST /test/start, /test/end ─── per-test coverage │ │
64+
│ └─────────────────────────────────────────────────────────────────┘ │
65+
│ │
66+
└──────────────────────────────────────────────────────────────────────────┘
67+
68+
│ Periodically (default: every 480 min) or on HTTP request
69+
70+
┌─────────────────────────────────────────────────────────────────────────┐
71+
│ Coverage Dump Pipeline │
72+
│ │
73+
│ 1. JacocoRuntimeController.dumpAndReset() │
74+
│ Retrieves execution data from JaCoCo runtime and resets probes. │
75+
│ │
76+
│ 2. JaCoCoXmlReportGenerator.convertSingleDumpToReport() │
77+
│ Reads class files (from auto-created dump dir or user-specified │
78+
│ class-dir), matches them with execution data by CRC64 class ID, │
79+
│ and produces a JaCoCo XML coverage report. │
80+
│ │
81+
│ 3. IUploader.upload() │
82+
│ Sends the XML report to the configured destination. │
83+
│ │
84+
└────────────────────────────┬────────────────────────────────────────────┘
85+
86+
┌──────────────┼──────────────┐
87+
▼ ▼ ▼
88+
┌──────────┐ ┌───────────┐ ┌────────────┐
89+
│Teamscale │ │Artifactory│ │Local disk │
90+
│ Server │ │ / Azure │ │ │
91+
└──────────┘ └───────────┘ └────────────┘
92+
```
93+
1494
## Development
1595

1696
Before starting development, please enable the pre-commit hook by running:

agent/build.gradle.kts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import io.github.sgtsilvio.gradle.oci.dsl.OciImageDefinition
2+
13
plugins {
24
com.teamscale.`java-convention`
35
application
@@ -9,6 +11,7 @@ plugins {
911
com.teamscale.coverage
1012
com.teamscale.publish
1113
com.teamscale.`logger-patch`
14+
alias(libs.plugins.oci)
1215
}
1316

1417
evaluationDependsOn(":installer")
@@ -101,3 +104,57 @@ distributions {
101104
tasks.shadowDistZip {
102105
archiveFileName = "teamscale-jacoco-agent.zip"
103106
}
107+
108+
// The OCI plugin adds a project-level repository ('dockerHubOciRegistry') for resolving
109+
// OCI image dependencies, which overrides the settings-level dependencyResolutionManagement
110+
// repositories. We need to re-declare mavenCentral() here so regular dependencies can still
111+
// be resolved. See: https://github.com/SgtSilvio/gradle-oci/issues/125
112+
repositories {
113+
mavenCentral()
114+
}
115+
116+
oci {
117+
registries {
118+
dockerHub {
119+
optionalCredentials()
120+
}
121+
}
122+
123+
val ociImageTag = providers.gradleProperty("ociImageTag").orElse(appVersion)
124+
val configureImage: Action<OciImageDefinition> = Action {
125+
imageTag = ociImageTag
126+
allPlatforms {
127+
dependencies {
128+
runtime("library:alpine:latest")
129+
}
130+
config {
131+
entryPoint = listOf("/entrypoint.sh")
132+
}
133+
layer("agent") {
134+
contents {
135+
into("agent") {
136+
from(tasks.shadowJar)
137+
}
138+
}
139+
}
140+
layer("entrypoint") {
141+
contents {
142+
from("src/docker/entrypoint.sh") {
143+
filePermissions = "755".toInt(8)
144+
}
145+
}
146+
}
147+
}
148+
specificPlatform(platform("linux", "amd64"))
149+
specificPlatform(platform("linux", "arm64"))
150+
}
151+
152+
imageDefinitions.register("main") {
153+
imageName = "cqse/teamscale-java-profiler"
154+
configureImage.execute(this)
155+
}
156+
imageDefinitions.register("legacy") {
157+
imageName = "cqse/teamscale-jacoco-agent"
158+
configureImage.execute(this)
159+
}
160+
}

agent/src/docker/Dockerfile

Lines changed: 0 additions & 35 deletions
This file was deleted.

agent/src/docker/entrypoint.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
# When /transfer exists (shared volume), copy the agent JAR there and exit.
3+
# This supports the Kubernetes init container pattern where this image provides
4+
# the agent JAR to the application container via a shared volume.
5+
if [ -e /transfer ]; then
6+
cp -r /agent/teamscale-jacoco-agent.jar /transfer
7+
exit 0
8+
fi
9+
# Otherwise, keep the container alive indefinitely as a sidecar.
10+
trap : TERM INT
11+
sleep infinity & wait

agent/src/main/java/com/teamscale/jacoco/agent/Agent.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@
4040
*/
4141
public class Agent extends AgentBase {
4242

43-
/** Converts binary data to XML. */
44-
private final JaCoCoXmlReportGenerator generator;
45-
4643
/** Regular dump task. */
4744
private Timer timer;
4845

@@ -57,9 +54,6 @@ public Agent(AgentOptions options, Instrumentation instrumentation)
5754
uploader = options.createUploader(instrumentation);
5855
logger.info("Upload method: {}", uploader.describe());
5956
retryUnsuccessfulUploads(options, uploader);
60-
generator = new JaCoCoXmlReportGenerator(options.getClassDirectoriesOrZips(),
61-
options.getLocationIncludeFilter(), options.getDuplicateClassFileBehavior(),
62-
options.shouldIgnoreUncoveredClasses(), wrap(logger));
6357

6458
if (options.shouldDumpInIntervals()) {
6559
timer = new Timer(this::dumpReport, Duration.ofMinutes(options.getDumpIntervalInMinutes()));
@@ -165,9 +159,11 @@ private static void deleteDirectoryIfEmpty(Path directory) throws IOException {
165159
/**
166160
* Dumps the current execution data, converts it, writes it to the output directory defined in {@link #options} and
167161
* uploads it if an uploader is configured. Logs any errors, never throws an exception.
162+
* <p>
163+
* Synchronized because this can be triggered concurrently by the timer and by the HTTP /dump endpoint.
168164
*/
169165
@Override
170-
public void dumpReport() {
166+
public synchronized void dumpReport() {
171167
logger.debug("Starting dump");
172168

173169
try {
@@ -188,6 +184,10 @@ private void dumpReportUnsafe() {
188184
return;
189185
}
190186

187+
JaCoCoXmlReportGenerator generator = new JaCoCoXmlReportGenerator(
188+
options.getClassDirectoriesOrZips(), options.getLocationIncludeFilter(),
189+
options.getDuplicateClassFileBehavior(), options.shouldIgnoreUncoveredClasses(), wrap(logger));
190+
191191
try (Benchmark ignored = new Benchmark("Generating the XML report")) {
192192
File outputFile = options.createNewFileInOutputDirectory("jacoco", "xml");
193193
CoverageFile coverageFile = generator.convertSingleDumpToReport(dump, outputFile);

agent/src/main/java/com/teamscale/jacoco/agent/options/JacocoAgentOptionsBuilder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ public String createJacocoAgentOptions() throws AgentOptionParseException, IOExc
3535
// Don't dump class files in testwise mode when coverage is written to an exec file
3636
boolean needsClassFiles = agentOptions.mode == EMode.NORMAL || agentOptions.testwiseCoverageMode != ETestwiseCoverageMode.EXEC_FILE;
3737
if (agentOptions.classDirectoriesOrZips.isEmpty() && needsClassFiles) {
38-
Path tempDir = createTemporaryDumpDirectory();
39-
tempDir.toFile().deleteOnExit();
40-
builder.append(",classdumpdir=").append(tempDir.toAbsolutePath());
38+
Path classDumpDirectory = createTemporaryDumpDirectory();
39+
classDumpDirectory.toFile().deleteOnExit();
40+
builder.append(",classdumpdir=").append(classDumpDirectory.toAbsolutePath());
4141

42-
agentOptions.classDirectoriesOrZips = Collections.singletonList(tempDir.toFile());
42+
agentOptions.classDirectoriesOrZips = Collections.singletonList(classDumpDirectory.toFile());
4343
}
4444

4545
agentOptions.additionalJacocoOptions

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44

55
group = "com.teamscale"
66

7-
val appVersion by extra("36.4.0")
7+
val appVersion by extra("36.5.0")
88

99
val snapshotVersion = appVersion + if (VersionUtils.isTaggedRelease()) "" else "-SNAPSHOT"
1010

0 commit comments

Comments
 (0)