Skip to content

Commit 474c9d1

Browse files
authored
Merge pull request #5 from odd/extract-regex
2 parents 9236ae5 + 0f7d822 commit 474c9d1

File tree

2 files changed

+88
-42
lines changed

2 files changed

+88
-42
lines changed

core/src/main/scala/com/lightbend/benchdb/Main.scala

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,16 @@ object Main extends Logging {
6262
val runs = Opts.options[String]("run", short = "r", help = "IDs of the test runs to include (or 'last').").map(_.toList).orElse(Opts(Nil))
6363
val benchs = Opts.options[String]("benchmark", short = "b", help = "Glob patterns of benchmark names to include.").map(_.toList).orElse(Opts(Nil))
6464
val extract = Opts.options[String]("extract", help = "Extractor pattern to generate parameters from names.").map(_.toList).orElse(Opts(Nil))
65+
val regex = Opts.flag("regex", short = "re", help = "Interpret extract pattern as a Java regular expression.").orFalse
6566
val scorePrecision = Opts.option[Int]("score-precision", help = "Precision of score and error in tables (default: 3)").withDefault(3)
6667
val queryResultsCommand = Command[GlobalOptions => Unit](name = "results", header =
6768
"Query the database for test results and print them.") {
6869
val pivot = Opts.options[String]("pivot", help = "Parameter names to pivot in table output.").map(_.toList).orElse(Opts(Nil))
6970
val raw = Opts.flag("raw", "Print raw JSON data instead of a table.").orFalse
70-
(runs, benchs, extract, scorePrecision, pivot, raw).mapN { case (runs, benchs, extract, sp, pivot, raw) =>
71+
(runs, benchs, extract, regex, scorePrecision, pivot, raw).mapN { case (runs, benchs, extract, regex, sp, pivot, raw) =>
7172
{ go =>
7273
if(raw && pivot.nonEmpty) logger.error("Cannot pivot in raw output mode.")
73-
else queryResults(go, runs, benchs, extract, sp, pivot, raw)
74+
else queryResults(go, runs, benchs, extract, regex, sp, pivot, raw)
7475
}
7576
}
7677
}
@@ -79,8 +80,8 @@ object Main extends Logging {
7980
val template = Opts.option[Path]("template", "HTML template containing file.").orNone
8081
val out = Opts.option[Path]("out", short = "o", help = "Output file to generate, or '-' for stdout.").orNone
8182
val pivot = Opts.options[String]("pivot", help = "Parameter names to combine in a chart.").map(_.toList).orElse(Opts(Nil))
82-
(runs, benchs, extract, scorePrecision, pivot, template, out).mapN { case (runs, benchs, extract, sp, pivot, template, out) =>
83-
createChart(_, runs, benchs, extract, sp, pivot, template, out, args)
83+
(runs, benchs, extract, regex, scorePrecision, pivot, template, out).mapN { case (runs, benchs, extract, regex, sp, pivot, template, out) =>
84+
createChart(_, runs, benchs, extract, regex, sp, pivot, template, out, args)
8485
}
8586
}
8687

@@ -168,12 +169,12 @@ object Main extends Logging {
168169
}
169170
}
170171

171-
def queryResults(go: GlobalOptions, runs: Seq[String], benchs: Seq[String], extract: Seq[String], scorePrecision: Int, pivot: Seq[String], raw: Boolean): Unit = try {
172+
def queryResults(go: GlobalOptions, runs: Seq[String], benchs: Seq[String], extract: Seq[String], regex: Boolean, scorePrecision: Int, pivot: Seq[String], raw: Boolean): Unit = try {
172173
new Global(go).use { g =>
173174
val multi = runs.size > 1
174175
val allRs = g.dao.run(g.dao.checkVersion andThen g.dao.queryResults(runs))
175176
.map { case (rr, runId) => RunResult.fromDb(rr, runId, multi) }
176-
val rs = RunResult.extract(extract, RunResult.filterByName(benchs, allRs)).toSeq
177+
val rs = RunResult.extract(extract, regex, RunResult.filterByName(benchs, allRs)).toSeq
177178
if(raw) {
178179
print("[")
179180
rs.zipWithIndex.foreach { case (r, idx) =>
@@ -258,12 +259,12 @@ object Main extends Logging {
258259
case ex: PatternSyntaxException => logger.error(ex.toString)
259260
}
260261

261-
def createChart(go: GlobalOptions, runs: Seq[String], benchs: Seq[String], extract: Seq[String], scorePrecision: Int, pivot: Seq[String], template: Option[Path], out: Option[Path], cmdLine: Array[String]): Unit = {
262+
def createChart(go: GlobalOptions, runs: Seq[String], benchs: Seq[String], extract: Seq[String], regex: Boolean, scorePrecision: Int, pivot: Seq[String], template: Option[Path], out: Option[Path], cmdLine: Array[String]): Unit = {
262263
new Global(go).use { g =>
263264
val multi = runs.size > 1
264265
val allRs = g.dao.run(g.dao.checkVersion andThen g.dao.queryResults(runs))
265266
.map { case (rr, runId) => RunResult.fromDb(rr, runId, multi) }
266-
val rs = RunResult.extract(extract, RunResult.filterByName(benchs, allRs)).toSeq
267+
val rs = RunResult.extract(extract, regex, RunResult.filterByName(benchs, allRs)).toSeq
267268
val allParamNames = rs.flatMap(_.params.keys).distinct.toVector
268269
val pivotSet = pivot.toSet
269270
val paramNames = allParamNames.filterNot(pivotSet.contains)

core/src/main/scala/com/lightbend/benchdb/RunResult.scala

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package com.lightbend.benchdb
22

3+
import java.lang.reflect.{Field, Method}
34
import java.util.regex.{Pattern, PatternSyntaxException}
4-
55
import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions}
6-
76
import scala.collection.JavaConverters._
87
import scala.collection.mutable.ArrayBuffer
98

@@ -87,43 +86,66 @@ object RunResult extends Logging {
8786
Pattern.compile(b.toString)
8887
}
8988

90-
private def compileExtractorPattern(expr: String): (Pattern, ArrayBuffer[String]) = {
91-
val b = new StringBuilder
92-
var i = 0
93-
var inGroup = false
94-
val groups = new ArrayBuffer[String]
95-
while(i < expr.length) {
96-
val c = expr.charAt(i)
97-
i += 1
98-
c match {
99-
case '(' if inGroup => throw new PatternSyntaxException("Capture groups must not be nested", expr, i-1)
100-
case ')' if !inGroup => throw new PatternSyntaxException("Unexpected end of capture group", expr, i-1)
101-
case '(' =>
102-
b.append('(')
103-
inGroup = true
104-
var nextEq = expr.indexOf('=', i)
105-
val nextClose = expr.indexOf(')', i)
106-
if(nextClose == -1) throw new PatternSyntaxException("Capture group not closed", expr, i)
107-
if(nextEq == -1 || nextEq > nextClose) groups += ""
108-
else {
109-
val n = expr.substring(i, nextEq)
110-
groups += n
111-
i = nextEq+1
112-
}
113-
case ')' =>
114-
b.append(')')
115-
inGroup = false
116-
case '*' => b.append(".*")
117-
case c => b.append(Pattern.quote(String.valueOf(c)))
89+
private def compileExtractorPattern(expr: String, regex: Boolean): (Pattern, ArrayBuffer[String]) = {
90+
if (regex) {
91+
val patternLiteral = expr.replaceAll("\\(([^)=]+)=", "(?<$1>")
92+
val pattern = Pattern.compile(patternLiteral)
93+
val namedGroups = pattern.namedGroups.toSeq.sortBy(_._2)
94+
val groups = ArrayBuffer.empty[String]
95+
var i = 1
96+
for ((name, n) <- namedGroups) {
97+
while (i < n) {
98+
groups.append("")
99+
i += 1
100+
}
101+
groups.append(name)
102+
i += 1
103+
}
104+
val m = pattern.groupCount - 1
105+
while (i < m) {
106+
groups.append("")
107+
i += 1
108+
}
109+
logger.debug(s"Compiled regex extractor '$expr' to $groups, $namedGroups")
110+
(pattern, groups)
111+
} else {
112+
val b = new StringBuilder
113+
var i = 0
114+
var inGroup = false
115+
val groups = new ArrayBuffer[String]
116+
while(i < expr.length) {
117+
val c = expr.charAt(i)
118+
i += 1
119+
c match {
120+
case '(' if inGroup => throw new PatternSyntaxException("Capture groups must not be nested", expr, i-1)
121+
case ')' if !inGroup => throw new PatternSyntaxException("Unexpected end of capture group", expr, i-1)
122+
case '(' =>
123+
b.append('(')
124+
inGroup = true
125+
var nextEq = expr.indexOf('=', i)
126+
val nextClose = expr.indexOf(')', i)
127+
if(nextClose == -1) throw new PatternSyntaxException("Capture group not closed", expr, i)
128+
if(nextEq == -1 || nextEq > nextClose) groups += ""
129+
else {
130+
val n = expr.substring(i, nextEq)
131+
groups += n
132+
i = nextEq+1
133+
}
134+
case ')' =>
135+
b.append(')')
136+
inGroup = false
137+
case '*' => b.append(".*")
138+
case c => b.append(Pattern.quote(String.valueOf(c)))
139+
}
118140
}
141+
logger.debug(s"Compiled extractor '$expr' to $b, $groups")
142+
(Pattern.compile(b.toString), groups)
119143
}
120-
logger.debug(s"Compiled extractor '$expr' to $b, $groups")
121-
(Pattern.compile(b.toString), groups)
122144
}
123145

124-
def extract(extractors: Seq[String], rs: Iterable[RunResult]): Iterable[RunResult] = {
146+
def extract(extractors: Seq[String], regex: Boolean, rs: Iterable[RunResult]): Iterable[RunResult] = {
125147
if(extractors.isEmpty) rs else {
126-
val patterns = extractors.map(compileExtractorPattern).filter(_._2.nonEmpty)
148+
val patterns = extractors.map(extractor => compileExtractorPattern(extractor, regex)).filter(_._2.nonEmpty)
127149
rs.map { r =>
128150
val name = r.name
129151
val matcherOpt = patterns.iterator.map(p => (p._1.matcher(name), p._2)).find(_._1.matches)
@@ -217,4 +239,27 @@ object RunResult extends Logging {
217239
0
218240
}
219241
}
242+
243+
object RichPattern {
244+
lazy val namedGroupsMethod: Method = {
245+
val m = classOf[Pattern].getDeclaredMethod("namedGroups")
246+
m.setAccessible(true)
247+
m
248+
}
249+
lazy val capturingGroupCount: Field = {
250+
val f = classOf[Pattern].getDeclaredField("capturingGroupCount")
251+
f.setAccessible(true)
252+
f
253+
}
254+
}
255+
implicit class RichPattern(val pattern: Pattern) extends AnyVal {
256+
import RichPattern._
257+
import collection.JavaConverters._
258+
def namedGroups: Map[String, Int] = {
259+
Map.empty ++ namedGroupsMethod.invoke(pattern).asInstanceOf[java.util.Map[String, Int]].asScala
260+
}
261+
def groupCount: Int = {
262+
capturingGroupCount.get(pattern).asInstanceOf[Int]
263+
}
264+
}
220265
}

0 commit comments

Comments
 (0)