|
| 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