diff --git a/build.sbt b/build.sbt index 9d01da6..6b54f2c 100644 --- a/build.sbt +++ b/build.sbt @@ -15,7 +15,10 @@ inThisBuild( ) ) -addCommandAlias("compileSources", "core/Test/compile; taggingPlugin/compile; taggingPluginTests/compile; examples/compile; benchmarks/compiile;") +addCommandAlias( + "compileSources", + "core/Test/compile; taggingPlugin/compile; taggingPluginTests/compile; examples/compile; benchmarks/compiile;" +) addCommandAlias("testAll", "core/test; taggingPluginTests/test") addCommandAlias("check", "fixCheck; fmtCheck") diff --git a/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/BaseSpec.scala b/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/BaseSpec.scala new file mode 100644 index 0000000..5851e9a --- /dev/null +++ b/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/BaseSpec.scala @@ -0,0 +1,8 @@ +package zio.profiling + +import zio.test._ +import zio.{Runtime, ZLayer} + +abstract class BaseSpec extends ZIOSpecDefault { + override val bootstrap: ZLayer[Any, Any, TestEnvironment] = testEnvironment ++ Runtime.removeDefaultLoggers +} diff --git a/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/causal/PluginCausalProfilerSpec.scala b/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/causal/PluginCausalProfilerSpec.scala new file mode 100644 index 0000000..49db465 --- /dev/null +++ b/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/causal/PluginCausalProfilerSpec.scala @@ -0,0 +1,34 @@ +package zio.profiling.causal + +import zio._ +import zio.profiling.{BaseSpec, CostCenter, ProfilerExamples} +import zio.test.Assertion.isTrue +import zio.test._ + +object PluginSamplingProfilerSpec extends BaseSpec { + + def spec = suite("PluginCausalProfiler")( + test("Should correctly profile simple example program") { + val profiler = CausalProfiler( + iterations = 10, + warmUpPeriod = 0.seconds, + minExperimentDuration = 1.second, + experimentTargetSamples = 1 + ) + val program = (ProfilerExamples.zioProgram *> CausalProfiler.progressPoint("done")).forever + Live.live(profiler.profile(program)).map { result => + println(result) + + def isSlowEffect(location: CostCenter) = + location.hasParentMatching("zio\\.profiling\\.ProfilerExamples\\.slow\\(.*\\)".r) + def isFastEffect(location: CostCenter) = + location.hasParentMatching("zio\\.profiling\\.ProfilerExamples\\.fast\\(.*\\)".r) + + val hasSlow = result.experiments.exists(e => isSlowEffect(e.selected)) + val hasFast = result.experiments.exists(e => isFastEffect(e.selected)) + + assert(hasSlow)(isTrue) && assert(hasFast)(isTrue) + } + } + ) +} diff --git a/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/sampling/PluginSamplingProfilerSpec.scala b/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/sampling/PluginSamplingProfilerSpec.scala index 4b5c3bb..cafc65d 100644 --- a/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/sampling/PluginSamplingProfilerSpec.scala +++ b/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/sampling/PluginSamplingProfilerSpec.scala @@ -1,11 +1,11 @@ package zio.profiling.sampling -import zio.profiling.{CostCenter, ProfilerExamples} +import zio.Scope +import zio.profiling.{BaseSpec, CostCenter, ProfilerExamples} import zio.test.Assertion.{hasSize, isGreaterThanEqualTo} import zio.test._ -import zio.Scope -object PluginSamplingProfilerSpec extends ZIOSpecDefault { +object PluginSamplingProfilerSpec extends BaseSpec { def spec: Spec[Environment with TestEnvironment with Scope, Any] = suite("PluginSamplingProfiler")( test("Should correctly profile simple example program") { diff --git a/zio-profiling/src/main/scala/zio/profiling/CostCenter.scala b/zio-profiling/src/main/scala/zio/profiling/CostCenter.scala index 7af1f1e..5fd1b96 100644 --- a/zio-profiling/src/main/scala/zio/profiling/CostCenter.scala +++ b/zio-profiling/src/main/scala/zio/profiling/CostCenter.scala @@ -19,6 +19,13 @@ import scala.util.matching.Regex sealed trait CostCenter { self => import CostCenter._ + final def render: String = + self match { + case Root => "" + case Child(Root, name) => name + case Child(parent, name) => s"${parent.render};$name" + } + final def location: Option[String] = self match { case Root => None case Child(_, current) => Some(current) @@ -41,6 +48,11 @@ sealed trait CostCenter { self => Child(self, location) } + final def isChildOf(other: CostCenter): Boolean = self == other || (self match { + case Root => false + case Child(parent, current) => parent == other || parent.isChildOf(other) + }) + /** * Check whether this cost center has a parent with a given name. * @@ -60,7 +72,7 @@ sealed trait CostCenter { self => */ final def hasParentMatching(regex: Regex): Boolean = self match { case Root => false - case Child(parent, current) => regex.matches(current) || parent.hasParentMatching(regex) + case Child(parent, current) => regex.findFirstIn(current).nonEmpty || parent.hasParentMatching(regex) } } diff --git a/zio-profiling/src/main/scala/zio/profiling/causal/CausalProfiler.scala b/zio-profiling/src/main/scala/zio/profiling/causal/CausalProfiler.scala index 8e875c9..f4a342a 100644 --- a/zio-profiling/src/main/scala/zio/profiling/causal/CausalProfiler.scala +++ b/zio-profiling/src/main/scala/zio/profiling/causal/CausalProfiler.scala @@ -192,56 +192,55 @@ final case class CausalProfiler( val iterator = fibers.values().iterator() while (experiment == null && iterator.hasNext()) { - val fiber = iterator.next() - if (fiber.running) { - fiber.costCenter.location.filter(candidateSelector).foreach { candidate => - results match { - case previous :: _ => - // with a speedup of 100%, we expect the program to take twice as long - def compensateSpeedup(original: Int): Int = (original * (2 - previous.speedup)).toInt - - val minDelta = - if (previous.throughputData.isEmpty) 0 - else compensateSpeedup(previous.throughputData.map(_.delta).min) - - val nextDuration = - if (minDelta < experimentTargetSamples) { - previous.duration * 2 - } else if ( - minDelta >= experimentTargetSamples * 2 && previous.duration >= minExperimentDurationNanos * 2 - ) { - previous.duration / 2 - } else { - previous.duration - } - - experiment = new Experiment( - candidate, - now, - nextDuration, - selectSpeedUp(), - new ConcurrentHashMap[String, Int](), - new ConcurrentHashMap[String, Int](), - new ConcurrentHashMap[String, Int]() - ) - case Nil => - experiment = new Experiment( - candidate, - now, - minExperimentDurationNanos, - selectSpeedUp(), - new ConcurrentHashMap[String, Int](), - new ConcurrentHashMap[String, Int](), - new ConcurrentHashMap[String, Int]() - ) - } + val fiber = iterator.next() + val candidate = fiber.costCenter + if (fiber.running && !candidate.isRoot && candidate.location.fold(false)(candidateSelector)) { + results match { + case previous :: _ => + // with a speedup of 100%, we expect the program to take twice as long + def compensateSpeedup(original: Int): Int = (original * (2 - previous.speedup)).toInt + + val minDelta = + if (previous.throughputData.isEmpty) 0 + else compensateSpeedup(previous.throughputData.map(_.delta).min) + + val nextDuration = + if (minDelta < experimentTargetSamples) { + previous.duration * 2 + } else if ( + minDelta >= experimentTargetSamples * 2 && previous.duration >= minExperimentDurationNanos * 2 + ) { + previous.duration / 2 + } else { + previous.duration + } + + experiment = new Experiment( + candidate, + now, + nextDuration, + selectSpeedUp(), + new ConcurrentHashMap[String, Int](), + new ConcurrentHashMap[String, Int](), + new ConcurrentHashMap[String, Int]() + ) + case Nil => + experiment = new Experiment( + candidate, + now, + minExperimentDurationNanos, + selectSpeedUp(), + new ConcurrentHashMap[String, Int](), + new ConcurrentHashMap[String, Int](), + new ConcurrentHashMap[String, Int]() + ) } } } if (experiment != null) { samplingState = SamplingState.ExperimentInProgress(experiment, iteration, results) logMessage( - s"Starting experiment $iteration (costCenter: ${experiment.candidate}, speedUp: ${experiment.speedUp}, duration: ${experiment.duration}ns)" + s"Starting experiment $iteration (costCenter: ${experiment.candidate.render}, speedUp: ${experiment.speedUp}, duration: ${experiment.duration}ns)" ) } @@ -261,7 +260,7 @@ final case class CausalProfiler( val iterator = fibers.values.iterator() while (iterator.hasNext()) { val fiber = iterator.next() - if (fiber.running && fiber.costCenter.hasParent(experiment.candidate)) { + if (fiber.running && fiber.costCenter.isChildOf(experiment.candidate)) { val delayAmount = (experiment.speedUp * samplingPeriodNanos).toLong fiber.localDelay.addAndGet(delayAmount) globalDelay += delayAmount diff --git a/zio-profiling/src/main/scala/zio/profiling/causal/Experiment.scala b/zio-profiling/src/main/scala/zio/profiling/causal/Experiment.scala index 07e0d9c..0d35975 100644 --- a/zio-profiling/src/main/scala/zio/profiling/causal/Experiment.scala +++ b/zio-profiling/src/main/scala/zio/profiling/causal/Experiment.scala @@ -16,11 +16,13 @@ package zio.profiling.causal +import zio.profiling.CostCenter + import java.util.concurrent.ConcurrentHashMap import scala.jdk.CollectionConverters._ final private class Experiment( - val candidate: String, + val candidate: CostCenter, val startTime: Long, val duration: Long, val speedUp: Float, diff --git a/zio-profiling/src/main/scala/zio/profiling/causal/ExperimentResult.scala b/zio-profiling/src/main/scala/zio/profiling/causal/ExperimentResult.scala index b835f55..749e48c 100644 --- a/zio-profiling/src/main/scala/zio/profiling/causal/ExperimentResult.scala +++ b/zio-profiling/src/main/scala/zio/profiling/causal/ExperimentResult.scala @@ -16,8 +16,10 @@ package zio.profiling.causal +import zio.profiling.CostCenter + final case class ExperimentResult( - selected: String, + selected: CostCenter, speedup: Float, duration: Long, effectiveDuration: Long, @@ -26,8 +28,8 @@ final case class ExperimentResult( latencyData: List[LatencyData] ) { - lazy val render: List[String] = - s"experiment\tselected=$selected\tspeedup=$speedup\tduration=$effectiveDuration\tselected-samples=$selectedSamples" :: + def render: List[String] = + s"experiment\tselected=${selected.render}\tspeedup=$speedup\tduration=$effectiveDuration\tselected-samples=$selectedSamples" :: throughputData.map(_.render) ++ latencyData.map(_.render) } diff --git a/zio-profiling/src/main/scala/zio/profiling/sampling/ProfilingResult.scala b/zio-profiling/src/main/scala/zio/profiling/sampling/ProfilingResult.scala index 03fecc3..e46f7de 100644 --- a/zio-profiling/src/main/scala/zio/profiling/sampling/ProfilingResult.scala +++ b/zio-profiling/src/main/scala/zio/profiling/sampling/ProfilingResult.scala @@ -12,20 +12,8 @@ final case class ProfilingResult(entries: List[ProfilingResult.Entry]) { * * Example command: `flamegraph.pl profile.folded > profile.svg` */ - def stackCollapse: List[String] = { - def renderCostCenter(costCenter: CostCenter): String = { - import CostCenter._ - costCenter match { - case Root => "" - case Child(Root, name) => name - case Child(parent, name) => s"${renderCostCenter(parent)};$name" - } - } - - entries.map { entry => - s"${renderCostCenter(entry.costCenter)} ${entry.samples}" - } - } + def stackCollapse: List[String] = + entries.map(entry => s"${entry.costCenter.render} ${entry.samples}") /** * Convenience method to render the result using `stackCollapse` and write it to a file. diff --git a/zio-profiling/src/test/scala/zio/profiling/BaseSpec.scala b/zio-profiling/src/test/scala/zio/profiling/BaseSpec.scala index 50df260..5851e9a 100644 --- a/zio-profiling/src/test/scala/zio/profiling/BaseSpec.scala +++ b/zio-profiling/src/test/scala/zio/profiling/BaseSpec.scala @@ -1,5 +1,8 @@ package zio.profiling -import zio.test.ZIOSpecDefault +import zio.test._ +import zio.{Runtime, ZLayer} -abstract class BaseSpec extends ZIOSpecDefault +abstract class BaseSpec extends ZIOSpecDefault { + override val bootstrap: ZLayer[Any, Any, TestEnvironment] = testEnvironment ++ Runtime.removeDefaultLoggers +}