diff --git a/build.sbt b/build.sbt index 9ff7765..9d01da6 100644 --- a/build.sbt +++ b/build.sbt @@ -15,8 +15,8 @@ inThisBuild( ) ) -addCommandAlias("compileSources", "core/Test/compile; taggingPlugin/compile; examples/compile") -addCommandAlias("testAll", "core/test") +addCommandAlias("compileSources", "core/Test/compile; taggingPlugin/compile; taggingPluginTests/compile; examples/compile; benchmarks/compiile;") +addCommandAlias("testAll", "core/test; taggingPluginTests/test") addCommandAlias("check", "fixCheck; fmtCheck") addCommandAlias("fix", "scalafixAll") @@ -28,7 +28,7 @@ addCommandAlias("prepare", "fix; fmt") lazy val root = project .in(file(".")) .settings(publish / skip := true) - .aggregate(core, jmh, taggingPlugin, examples, benchmarks, docs) + .aggregate(core, jmh, taggingPlugin, taggingPluginTests, examples, benchmarks, docs) lazy val core = project .in(file("zio-profiling")) @@ -62,6 +62,20 @@ lazy val taggingPlugin = project pluginDefinitionSettings ) +lazy val taggingPluginTests = project + .in(file("zio-profiling-tagging-plugin-tests")) + .dependsOn(core, taggingPlugin % "plugin") + .settings( + stdSettings("zio-profiling-tagging-plugin-tests"), + publish / skip := true, + Compile / scalacOptions += s"-Xplugin:${(taggingPlugin / Compile / packageTask).value.getAbsolutePath}", + libraryDependencies ++= Seq( + "dev.zio" %% "zio-test" % zioVersion % Test, + "dev.zio" %% "zio-test-sbt" % zioVersion % Test + ), + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") + ) + lazy val examples = project .in(file("examples")) .dependsOn(core, taggingPlugin % "plugin") @@ -76,7 +90,7 @@ lazy val benchmarks = project .dependsOn(core) .enablePlugins(JmhPlugin) .settings( - stdSettings("examples"), + stdSettings("benchmarks"), publish / skip := true ) diff --git a/examples/src/main/scala/zio/profiling/examples/SamplingProfilerSimpleExample.scala b/examples/src/main/scala/zio/profiling/examples/SamplingProfilerSimpleExample.scala index 4ded4eb..5fe5aea 100644 --- a/examples/src/main/scala/zio/profiling/examples/SamplingProfilerSimpleExample.scala +++ b/examples/src/main/scala/zio/profiling/examples/SamplingProfilerSimpleExample.scala @@ -3,7 +3,7 @@ package zio.profiling.examples import zio._ import zio.profiling.sampling._ -object PresentationExample extends ZIOAppDefault { +object SamplingProfilerSimpleExample extends ZIOAppDefault { def run: URIO[Any, ExitCode] = { val fast = ZIO.succeed(Thread.sleep(400)) diff --git a/zio-profiling-tagging-plugin-tests/src/main/scala/zio/profiling/ProfilerExamples.scala b/zio-profiling-tagging-plugin-tests/src/main/scala/zio/profiling/ProfilerExamples.scala new file mode 100644 index 0000000..4690643 --- /dev/null +++ b/zio-profiling-tagging-plugin-tests/src/main/scala/zio/profiling/ProfilerExamples.scala @@ -0,0 +1,18 @@ +package zio.profiling + +import zio._ +import zio.stream.ZStream + +object ProfilerExamples { + val fast: ZIO[Any, Nothing, Unit] = ZIO.succeed(Thread.sleep(400)) + + val slow: ZIO[Any, Nothing, Unit] = ZIO.succeed(Thread.sleep(200)) <&> ZIO.succeed(Thread.sleep(600)) + + val zioProgram: ZIO[Any, Nothing, Unit] = fast <&> slow + + val fastStream: ZStream[Any, Nothing, Unit] = ZStream.fromZIO(fast) + + val slowStream: ZStream[Any, Nothing, Unit] = ZStream.fromZIO(slow) + + val zioStreamProgram: ZIO[Any, Nothing, Unit] = (fastStream <&> slowStream).runDrain +} 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 new file mode 100644 index 0000000..4b5c3bb --- /dev/null +++ b/zio-profiling-tagging-plugin-tests/src/test/scala/zio/profiling/sampling/PluginSamplingProfilerSpec.scala @@ -0,0 +1,41 @@ +package zio.profiling.sampling + +import zio.profiling.{CostCenter, ProfilerExamples} +import zio.test.Assertion.{hasSize, isGreaterThanEqualTo} +import zio.test._ +import zio.Scope + +object PluginSamplingProfilerSpec extends ZIOSpecDefault { + + def spec: Spec[Environment with TestEnvironment with Scope, Any] = suite("PluginSamplingProfiler")( + test("Should correctly profile simple example program") { + Live.live(SamplingProfiler().profile(ProfilerExamples.zioProgram)).map { result => + val sortedEntries = result.entries.sortBy(_.samples).reverse + + def isSlowEffect(location: CostCenter) = + location.hasParentMatching("zio\\.profiling\\.ProfilerExamples\\.slow\\(.*\\)".r) + def isFastEffect(location: CostCenter) = + location.hasParentMatching("zio\\.profiling\\.ProfilerExamples\\.fast\\(.*\\)".r) + + assert(sortedEntries)(hasSize(isGreaterThanEqualTo(2))) && + assertTrue(isSlowEffect(sortedEntries(0).costCenter)) && + assertTrue(isFastEffect(sortedEntries(1).costCenter)) + } + }, + test("Should correctly profile simple example streams program") { + Live.live(SamplingProfiler().profile(ProfilerExamples.zioStreamProgram)).map { result => + val sortedEntries = result.entries.sortBy(_.samples).reverse + + def isSlowEffect(location: CostCenter) = + location.hasParentMatching("zio\\.profiling\\.ProfilerExamples\\.slowStream\\(.*\\)".r) + def isFastEffect(location: CostCenter) = + location.hasParentMatching("zio\\.profiling\\.ProfilerExamples\\.fastStream\\(.*\\)".r) + + assert(sortedEntries)(hasSize(isGreaterThanEqualTo(2))) && + assertTrue(isSlowEffect(sortedEntries(0).costCenter)) && + assertTrue(isFastEffect(sortedEntries(1).costCenter)) + } + } + ) + +} diff --git a/zio-profiling-tagging-plugin/src/main/scala-3/zio/profiling/plugins/TaggingPhase.scala b/zio-profiling-tagging-plugin/src/main/scala-3/zio/profiling/plugins/TaggingPhase.scala index 9e987e3..114c5b1 100644 --- a/zio-profiling-tagging-plugin/src/main/scala-3/zio/profiling/plugins/TaggingPhase.scala +++ b/zio-profiling-tagging-plugin/src/main/scala-3/zio/profiling/plugins/TaggingPhase.scala @@ -14,7 +14,6 @@ import dotty.tools.dotc.report import dotty.tools.dotc.core.Types.TypeRef import dotty.tools.dotc.ast.tpd.{TreeOps, Literal} import dotty.tools.dotc.ast.untpd.Mod.Given.apply -import dotty.tools.dotc.core.Flags object TaggingPhase extends PluginPhase { @@ -23,12 +22,14 @@ object TaggingPhase extends PluginPhase { override val runsAfter = Set(Pickler.name) override val runsBefore = Set(Staging.name) - override def transformValDef(tree: tpd.ValDef)(using Context): tpd.Tree = tree match { - case ValDef(_, TaggableTypeTree(taggingTarget), rhs) if !tree.rhs.isEmpty => - val transformedRhs = tagEffectTree(descriptiveName(tree), tree.rhs, taggingTarget) - cpy.ValDef(tree)(rhs = transformedRhs) - case _ => - tree + override def transformValDef(tree: tpd.ValDef)(using Context): tpd.Tree = { + tree match { + case ValDef(_, TaggableTypeTree(taggingTarget), rhs) if !tree.rhs.isEmpty => + val transformedRhs = tagEffectTree(descriptiveName(tree), tree.rhs, taggingTarget) + cpy.ValDef(tree)(rhs = transformedRhs) + case _ => + tree + } } override def transformDefDef(tree: tpd.DefDef)(using Context): tpd.Tree = tree match { @@ -48,8 +49,8 @@ object TaggingPhase extends PluginPhase { } private def tagEffectTree(name: String, tree: tpd.Tree, taggingTarget: TaggingTarget)(using Context): tpd.Tree = { - val costcenterSym = requiredModule("_root_.zio.profiling.CostCenter") - val traceSym = requiredModule("_root_.zio.Trace") + val costcenterSym = requiredModule("zio.profiling.CostCenter") + val traceSym = requiredModule("zio.Trace") val emptyTraceSym = traceSym.requiredMethodRef("empty") taggingTarget match { @@ -79,9 +80,9 @@ object TaggingPhase extends PluginPhase { private case class ZStreamTaggingTarget(rType: Type, eType: Type, aType: Type) extends TaggingTarget private object TaggableTypeTree { - private def zioTypeRef(using Context): TypeRef = requiredClassRef("_root_.zio.ZIO") + private def zioTypeRef(using Context): TypeRef = requiredClassRef("zio.ZIO") - private def zStreamTypeRef(using Context): TypeRef = requiredClassRef("_root_.stream.ZStream") + private def zStreamTypeRef(using Context): TypeRef = requiredClassRef("stream.ZStream") def unapply(tp: Tree[Type])(using Context): Option[TaggingTarget] = tp.tpe.dealias match { diff --git a/zio-profiling/src/main/scala/zio/profiling/CostCenter.scala b/zio-profiling/src/main/scala/zio/profiling/CostCenter.scala index 35d8fe8..7af1f1e 100644 --- a/zio-profiling/src/main/scala/zio/profiling/CostCenter.scala +++ b/zio-profiling/src/main/scala/zio/profiling/CostCenter.scala @@ -3,6 +3,8 @@ package zio.profiling import zio._ import zio.stream.ZStream +import scala.util.matching.Regex + /** * A CostCenter allows grouping multiple source code locations into one unit for reporting and targeting purposes. * Instead of relying on a function call hierarchy to identify a location, zio-profiling relies on manual tagging. @@ -52,6 +54,14 @@ sealed trait CostCenter { self => case Root => false case Child(parent, current) => current == name || parent.hasParent(name) } + + /** + * Check whether this cost center has a parent with a name matching the given regex. + */ + final def hasParentMatching(regex: Regex): Boolean = self match { + case Root => false + case Child(parent, current) => regex.matches(current) || parent.hasParentMatching(regex) + } } object CostCenter {