Skip to content

Commit

Permalink
More consistent usage of cost center in causal profiler
Browse files Browse the repository at this point in the history
  • Loading branch information
mschuwalow committed Mar 11, 2024
1 parent 79a2867 commit ea82649
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 70 deletions.
5 changes: 4 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
)
}
Original file line number Diff line number Diff line change
@@ -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") {
Expand Down
14 changes: 13 additions & 1 deletion zio-profiling/src/main/scala/zio/profiling/CostCenter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
*
Expand All @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
)
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions zio-profiling/src/test/scala/zio/profiling/BaseSpec.scala
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit ea82649

Please sign in to comment.