Skip to content

Commit 93f778f

Browse files
committed
added JaCoCo-based dynamic method coverage (JacocoDC), as alternative to JavaCallGraphDCG; made JacocoDC the implementation for the dcg command
1 parent 225971a commit 93f778f

File tree

11 files changed

+718
-66
lines changed

11 files changed

+718
-66
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
*.zip
1111
!src/main/resources/jcg_agent.jar.zip
12+
!src/main/resources/jacocoagent.jar.zip
13+
!src/main/resources/jacococli.jar.zip
1214

1315
## macOS
1416
# General

src/main/kotlin/ch/uzh/ifi/seal/bencher/analysis/CodeTransformations.kt

+14-1
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,18 @@ fun descriptorToParamList(desc: String): Option<List<String>> {
111111
return Option.empty()
112112
}
113113

114-
return Option.Some(desc.substring(1, paramEnd).split(";").filter { !it.isBlank() }.map { it.sourceCode })
114+
return Option.Some(desc.substring(1, paramEnd)
115+
.split(";")
116+
.filter { !it.isBlank() }
117+
.map { it.sourceCode })
118+
}
119+
120+
fun descriptorToReturnType(desc: String): Option<String> {
121+
if (desc.isEmpty()) {
122+
return Option.empty()
123+
}
124+
125+
val paramEnd = desc.indexOf(')')
126+
val returnString = desc.substring(paramEnd+1)
127+
return Option.Some(returnString.sourceCode)
115128
}

src/main/kotlin/ch/uzh/ifi/seal/bencher/analysis/callgraph/dyn/AbstractDynamicCoverage.kt

+69-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import org.apache.logging.log4j.LogManager
1212
import org.apache.logging.log4j.Logger
1313
import org.funktionale.either.Either
1414
import java.io.File
15+
import java.io.FileReader
16+
import java.io.IOException
17+
import java.io.Reader
1518
import java.nio.file.Files
1619
import java.nio.file.Path
1720
import java.time.Duration
@@ -107,7 +110,7 @@ abstract class AbstractDynamicCoverage(
107110
log.debug("Param bench $b: ${i + 1}/$total; '$cs'")
108111

109112
val l = logTimesParam(b, i, total, "CG for parameterized benchmark")
110-
val ers = exec(cs, tmpDir, b)
113+
val ers = exec(cs, jar, tmpDir, b)
111114
return try {
112115
if (ers.isLeft()) {
113116
log.error("Could not retrieve DCG for $b with '$cs': ${ers.left().get()}")
@@ -140,7 +143,7 @@ abstract class AbstractDynamicCoverage(
140143
}
141144
}
142145

143-
private fun exec(cmd: String, dir: File, b: Benchmark): Either<String, Reachabilities> {
146+
private fun exec(cmd: String, jar: Path, dir: File, b: Benchmark): Either<String, Reachabilities> {
144147
val (ok, out, err) = cmd.runCommand(dir, timeOut)
145148
if (!ok) {
146149
return Either.left("Execution of '$cmd' did not finish within $timeOut")
@@ -154,7 +157,64 @@ abstract class AbstractDynamicCoverage(
154157
log.debug("Process err: $err")
155158
}
156159

157-
return parseReachabilities(dir, b)
160+
return reachabilities(jar, dir, b)
161+
}
162+
163+
private fun reachabilities(jar: Path, dir: File, b: Benchmark): Either<String, Reachabilities> {
164+
val resultFileName = resultFileName(b)
165+
val fn = "$dir${File.separator}$resultFileName"
166+
val f = File(fn)
167+
168+
if (!f.isFile) {
169+
return Either.left("Not a file: $fn")
170+
}
171+
172+
if (!f.exists()) {
173+
return Either.left("File does not exist: $fn")
174+
}
175+
176+
val ecv = transformResultFile(jar, dir, b, f)
177+
if (ecv.isLeft()) {
178+
return Either.left("Could not get coverage file: ${ecv.left()}")
179+
}
180+
181+
val fr = FileReader(ecv.right().get())
182+
183+
try {
184+
val errs = parseReachabilityResults(fr, b)
185+
if (errs.isLeft()) {
186+
return Either.left(errs.left().get())
187+
}
188+
189+
val rrss = mutableSetOf<Method>()
190+
191+
val rrs = errs.right().get()
192+
val srrs = rrs.toSortedSet(ReachabilityResultComparator)
193+
.filter {
194+
val m = it.to
195+
if (rrss.contains(m)) {
196+
false
197+
} else {
198+
rrss.add(m)
199+
true
200+
}
201+
}.toSet()
202+
203+
log.info("CG for $b has ${srrs.size} reachable nodes (from ${rrs.size} traces)")
204+
205+
val rs = Reachabilities(
206+
start = b,
207+
reachabilities = srrs
208+
)
209+
210+
return Either.right(rs)
211+
} finally {
212+
try {
213+
fr.close()
214+
} catch (e: IOException) {
215+
log.warn("Could not close file output stream of '$fn'")
216+
}
217+
}
158218
}
159219

160220
private fun cmdStr(jar: Path, b: Benchmark): String =
@@ -171,7 +231,12 @@ abstract class AbstractDynamicCoverage(
171231

172232
protected abstract fun jvmArgs(b: Benchmark): String
173233

174-
protected abstract fun parseReachabilities(dir: File, b: Benchmark): Either<String, Reachabilities>
234+
protected abstract fun resultFileName(b: Benchmark): String
235+
236+
protected abstract fun transformResultFile(jar: Path, dir: File, b: Benchmark, resultFile: File): Either<String, File>
237+
238+
protected abstract fun parseReachabilityResults(r: Reader, b: Benchmark): Either<String, Set<ReachabilityResult>>
239+
175240

176241
companion object {
177242
val log: Logger = LogManager.getLogger(AbstractDynamicCoverage::class.java.canonicalName)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package ch.uzh.ifi.seal.bencher.analysis.callgraph.dyn
2+
3+
import ch.uzh.ifi.seal.bencher.*
4+
import ch.uzh.ifi.seal.bencher.analysis.*
5+
import ch.uzh.ifi.seal.bencher.analysis.callgraph.CGExecutor
6+
import ch.uzh.ifi.seal.bencher.analysis.callgraph.CGInclusions
7+
import ch.uzh.ifi.seal.bencher.analysis.callgraph.IncludeAll
8+
import ch.uzh.ifi.seal.bencher.analysis.callgraph.IncludeOnly
9+
import ch.uzh.ifi.seal.bencher.analysis.callgraph.reachability.RF
10+
import ch.uzh.ifi.seal.bencher.analysis.callgraph.reachability.ReachabilityResult
11+
import ch.uzh.ifi.seal.bencher.analysis.callgraph.reachability.Reachable
12+
import ch.uzh.ifi.seal.bencher.analysis.finder.MethodFinder
13+
import org.apache.logging.log4j.LogManager
14+
import org.apache.logging.log4j.Logger
15+
import org.funktionale.either.Either
16+
import java.io.*
17+
import java.nio.file.Path
18+
import java.nio.file.Paths
19+
import java.time.Duration
20+
import javax.xml.stream.XMLInputFactory
21+
import javax.xml.stream.XMLStreamConstants
22+
23+
class JacocoDC(
24+
benchmarkFinder: MethodFinder<Benchmark>,
25+
oneCoverageForParameterizedBenchmarks: Boolean = true,
26+
private val inclusion: CGInclusions = IncludeAll,
27+
timeOut: Duration = Duration.ofMinutes(10)
28+
) : AbstractDynamicCoverage(
29+
benchmarkFinder = benchmarkFinder,
30+
oneCoverageForParameterizedBenchmarks = oneCoverageForParameterizedBenchmarks,
31+
timeOut = timeOut
32+
), CGExecutor {
33+
34+
private val inclusionsString = inclusions(inclusion)
35+
36+
override fun resultFileName(b: Benchmark): String = fileName(b, execFileExt)
37+
38+
override fun transformResultFile(jar: Path, dir: File, b: Benchmark, resultFile: File): Either<String, File> {
39+
val fn = reportFileName
40+
val cmd = String.format(reportCmd, cliJar, resultFile.absolutePath, jar.toString(), fn)
41+
val (ok, out, err) = cmd.runCommand(dir, cliTimeout)
42+
43+
if (!ok) {
44+
return Either.left("Execution of '$cmd' did not finish within $cliTimeout")
45+
}
46+
47+
if (out != null && out.isNotBlank()) {
48+
log.debug("Process out: $out")
49+
}
50+
51+
if (err != null && err.isNotBlank()) {
52+
log.debug("Process err: $err")
53+
}
54+
55+
val fp = Paths.get(dir.absolutePath, fn)
56+
return Either.right(fp.toFile())
57+
}
58+
59+
private fun fileName(b: Benchmark, ext: String): String {
60+
val sb = StringBuilder()
61+
val sep = "__"
62+
63+
sb.append(b.clazz.replace(".", "_"))
64+
sb.append(sep)
65+
sb.append(b.name)
66+
67+
if (b.jmhParams.isNotEmpty()) {
68+
sb.append(sep)
69+
sb.append(b.jmhParams.joinToString("_") { (k, v) -> "$k=$v" })
70+
}
71+
72+
sb.append(".")
73+
sb.append(ext)
74+
75+
return sb.toString()
76+
}
77+
78+
override fun parseReachabilityResults(r: Reader, b: Benchmark): Either<String, Set<ReachabilityResult>> {
79+
val from = b.toPlainMethod()
80+
81+
val xmlFac = XMLInputFactory.newInstance()
82+
val sr = xmlFac.createXMLStreamReader(r)
83+
84+
var exclusions: Set<String> = setOf(
85+
jmhGeneratedClassPrefix(b)
86+
)
87+
88+
var rs = mutableSetOf<ReachabilityResult>()
89+
90+
var className = ""
91+
var methodName = ""
92+
var desc = ""
93+
94+
var state = 0
95+
96+
while (sr.hasNext()) {
97+
when (sr.next()) {
98+
XMLStreamConstants.START_ELEMENT -> {
99+
when (sr.localName) {
100+
xmlTagClass -> {
101+
if (state == 0) {
102+
val cn = sr.getAttributeValue(null, xmlAttrName)
103+
104+
if(!excluded(exclusions, cn)) {
105+
state++
106+
className = cn
107+
}
108+
}
109+
}
110+
xmlTagMethod -> {
111+
if (state == 1) {
112+
state++
113+
methodName = sr.getAttributeValue(null, xmlAttrName)
114+
desc = sr.getAttributeValue(null, xmlAttrDesc)
115+
}
116+
}
117+
xmlTagCounter -> {
118+
if (state == 2) {
119+
state++
120+
val type = sr.getAttributeValue(null, xmlAttrType)
121+
if (type == xmlAttrTypeMethod && state == 3) {
122+
val covered = sr.getAttributeValue(null, xmlAttrCovered)
123+
if (covered == "1") {
124+
rs.add(reachabilitResult(from, className, methodName, desc))
125+
}
126+
}
127+
}
128+
}
129+
}
130+
131+
}
132+
XMLStreamConstants.END_ELEMENT -> {
133+
when (sr.localName) {
134+
xmlTagClass -> {
135+
if (state == 1) {
136+
state--
137+
className = ""
138+
}
139+
}
140+
xmlTagMethod -> {
141+
if (state == 2) {
142+
state--
143+
methodName = ""
144+
desc = ""
145+
}
146+
}
147+
xmlTagCounter -> {
148+
if (state == 3) {
149+
state--
150+
}
151+
}
152+
}
153+
}
154+
}
155+
}
156+
157+
return Either.right(rs)
158+
}
159+
160+
private fun reachabilitResult(from: Method, c: String, m: String, d: String): Reachable {
161+
val params: List<String> = descriptorToParamList(d).let { o ->
162+
if (o.isDefined()) {
163+
o.get()
164+
} else {
165+
listOf()
166+
}
167+
}
168+
169+
val ret: String = descriptorToReturnType(d).let { o ->
170+
if (o.isDefined()) {
171+
o.get()
172+
} else {
173+
SourceCodeConstants.void
174+
}
175+
}
176+
177+
return RF.reachable(
178+
from = from,
179+
to = MF.plainMethod(
180+
clazz = c.replaceSlashesWithDots,
181+
name = m,
182+
params = params,
183+
returnType = ret
184+
),
185+
level = defaultStackDepth
186+
)
187+
}
188+
189+
private fun jmhGeneratedClassPrefix(b: Benchmark): String {
190+
val outerClassName = b.clazz
191+
.substringAfterLast(".") // remove fully-qualified package path
192+
.substringBefore("$") // remove (potential) subclasses
193+
.replaceSlashesWithDots
194+
return "generated/$outerClassName"
195+
}
196+
197+
private fun excluded(exclusions: Set<String>, className: String): Boolean =
198+
exclusions
199+
.map { className.contains(it) }
200+
.fold(false) { acc, contained -> acc || contained }
201+
202+
override fun jvmArgs(b: Benchmark): String =
203+
String.format(jvmArgs, agentJar, inclusionsString, fileName(b, execFileExt))
204+
205+
private fun inclusions(i: CGInclusions): String =
206+
when (i) {
207+
is IncludeAll -> ".*"
208+
is IncludeOnly -> i.includes.joinToString(separator = ",") { "$it.*" }
209+
}
210+
211+
212+
companion object {
213+
val log: Logger = LogManager.getLogger(JacocoDC::class.java.canonicalName)
214+
215+
private const val execFileExt = "exec"
216+
private const val reportFileName = "jacoco_report.xml"
217+
218+
// JVM arguments
219+
// 1. Jacoco agent jar path (e.g., agentJar)
220+
// 2. Jacoco inclusions (e.g., inclusionsString)
221+
// 3. Jacoco (binary) execution file
222+
private const val jvmArgs = "-javaagent:%s=includes=%s,destfile=%s"
223+
224+
private val agentJar = "jacocoagent.jar.zip".fileResource().absolutePath
225+
226+
// Command to generate Jacoco report
227+
// 1. Jacoco CLI jar (cliJar)
228+
// 2. Jacoco execution file (e.g., execFile)
229+
// 3. Benchmark class files (e.g., parameter `jar` from method `parseReachabilities`)
230+
// 4. Report XML file name (e.g., reportFileName)
231+
private const val reportCmd = "java -jar %s report %s --classfiles %s --xml %s"
232+
233+
private val cliTimeout = Duration.ofMinutes(1)
234+
235+
private val cliJar = "jacococli.jar.zip".fileResource().absolutePath
236+
237+
private const val defaultStackDepth = -1
238+
239+
private const val xmlTagClass = "class"
240+
private const val xmlTagMethod = "method"
241+
private const val xmlTagCounter = "counter"
242+
private const val xmlAttrName = "name"
243+
private const val xmlAttrDesc = "desc"
244+
private const val xmlAttrType = "type"
245+
private const val xmlAttrTypeMethod = "METHOD"
246+
private const val xmlAttrCovered = "covered"
247+
}
248+
}

0 commit comments

Comments
 (0)