diff --git a/docs/api-guide/debug.md b/docs/api-guide/debug.md index 63bcbe565..abf3efe15 100644 --- a/docs/api-guide/debug.md +++ b/docs/api-guide/debug.md @@ -12,7 +12,7 @@ of what parsers take the most time to execute. The combinators themselves are all contained within `parsley.debug.DebugCombinators`. @:callout(info) -*The Scaladoc for this page can be found at [`parsley.debug`](@:api(parsley.debug$)) and [`parsley.debug.DebugCombinators`](@:api(parsley.debug$$DebugCombinators))* +*The Scaladoc for this page can be found at [`parsley.debug`](@:api(parsley.debug)) and [`parsley.debug.DebugCombinators`](@:api(parsley.debug.DebugCombinators))* @:@ ## Debugging Problematic Parsers (`debug`) diff --git a/parsley-debug/shared/src/main/scala/parsley/debugger/combinator.scala b/parsley-debug/shared/src/main/scala/parsley/debugger/combinator.scala index 4cda67009..3fa031ce8 100644 --- a/parsley-debug/shared/src/main/scala/parsley/debugger/combinator.scala +++ b/parsley-debug/shared/src/main/scala/parsley/debugger/combinator.scala @@ -18,32 +18,6 @@ import parsley.internal.deepembedding.backend.debugger.{CheckDivergence, Debuggi * @since 4.5.0 */ object combinator { - /** Returns true for any function type, which we ideally don't store as a string */ - val DefaultStringRules: PartialFunction[Any, Boolean] = { - case _ : Function1[_, _] => true - case _ : Function2[_, _, _] => true - case _ : Function3[_, _, _, _] => true - case _ : Function4[_, _, _, _, _] => true - case _ : Function5[_, _, _, _, _, _] => true - case _ : Function6[_, _, _, _, _, _, _] => true - case _ : Function7[_, _, _, _, _, _, _, _] => true - case _ : Function8[_, _, _, _, _, _, _, _, _] => true - case _ : Function9[_, _, _, _, _, _, _, _, _, _] => true - case _ : Function10[_, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function11[_, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function12[_, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function13[_, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function14[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function15[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function16[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function17[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function18[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function19[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function20[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function21[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - case _ : Function22[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true - } - /** Shorthand representation of a pair of a tree extraction function and a debugged parser. */ private [parsley] type DebuggedPair[+A] = (() => DebugTree, Parsley[A]) @@ -167,8 +141,7 @@ object combinator { //private def attachReusable[A](parser: Parsley[A]): () => DebuggedPair[A] = attachReusable[A](parser, defaultRules) // TODO: fix docs by incorporating some of the base stuff above - /** Attach a debugger and an explicitly-available frontend in which the debug tree should be - * proessed with. + /** Attach a debugger to be rendered via a given view. This view will render whenever the parser produces debug content. * * You would normally obtain a [[parsley.debugger.DebugView]] from its * respective package as either a static object or an instance object depending on whether the @@ -207,9 +180,7 @@ object combinator { atomic(attached <* renderer) <|> (renderer *> empty) } - /** Attach a debugger and an explicitly-available frontend in which the debug tree should be - * processed with. This frontend will also be called automatically with any debug trees produced - * by the parser. + /** Attach a debugger to be rendered via a given view. This view will render whenever the parser produces debug content. * * This assumes the default rules of converting only lambdas and closures into strings when * storing in the output debug tree. @@ -306,4 +277,30 @@ object combinator { def named(name: String): Parsley[A] = combinator.named(par, name) } // $COVERAGE-ON$ + + /** Returns true for any function type, which we ideally don't store as a string */ + val DefaultStringRules: PartialFunction[Any, Boolean] = { + case _ : Function1[_, _] => true + case _ : Function2[_, _, _] => true + case _ : Function3[_, _, _, _] => true + case _ : Function4[_, _, _, _, _] => true + case _ : Function5[_, _, _, _, _, _] => true + case _ : Function6[_, _, _, _, _, _, _] => true + case _ : Function7[_, _, _, _, _, _, _, _] => true + case _ : Function8[_, _, _, _, _, _, _, _, _] => true + case _ : Function9[_, _, _, _, _, _, _, _, _, _] => true + case _ : Function10[_, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function11[_, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function12[_, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function13[_, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function14[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function15[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function16[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function17[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function18[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function19[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function20[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function21[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + case _ : Function22[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => true + } } diff --git a/parsley-debug/shared/src/main/scala/parsley/internal/deepembedding/backend/debugger/Debugged.scala b/parsley-debug/shared/src/main/scala/parsley/internal/deepembedding/backend/debugger/Debugged.scala index 5e3eb8d12..6c20a5691 100644 --- a/parsley-debug/shared/src/main/scala/parsley/internal/deepembedding/backend/debugger/Debugged.scala +++ b/parsley-debug/shared/src/main/scala/parsley/internal/deepembedding/backend/debugger/Debugged.scala @@ -13,7 +13,7 @@ import parsley.internal.deepembedding.backend.{CodeGenState, StrictParsley, Unar import parsley.internal.deepembedding.backend.StrictParsley.InstrBuffer import parsley.internal.deepembedding.frontend.LazyParsley import parsley.internal.machine.instructions.{Label, Pop} -import parsley.internal.machine.instructions.debugger.{AddAttemptAndLeave, DropSnapshot, EnterParser, TakeSnapshot} +import parsley.internal.machine.instructions.debug.{AddAttemptAndLeave, DropSnapshot, EnterParser, TakeSnapshot} // TODO: rename to TagFactory? this file can then be called Tags // this should probably be split out into its own file as well diff --git a/parsley-debug/shared/src/main/scala/parsley/internal/machine/instructions/debugger/DebuggerInstrs.scala b/parsley-debug/shared/src/main/scala/parsley/internal/machine/instructions/debug/DebugInstrs.scala similarity index 98% rename from parsley-debug/shared/src/main/scala/parsley/internal/machine/instructions/debugger/DebuggerInstrs.scala rename to parsley-debug/shared/src/main/scala/parsley/internal/machine/instructions/debug/DebugInstrs.scala index dfc87781e..4123d305f 100644 --- a/parsley-debug/shared/src/main/scala/parsley/internal/machine/instructions/debugger/DebuggerInstrs.scala +++ b/parsley-debug/shared/src/main/scala/parsley/internal/machine/instructions/debug/DebugInstrs.scala @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: BSD-3-Clause */ -package parsley.internal.machine.instructions.debugger +package parsley.internal.machine.instructions.debug import parsley.debugger.ParseAttempt import parsley.debugger.internal.{DebugContext, DivergenceContext} diff --git a/parsley/shared/src/main/scala/parsley/debug/Breakpoint.scala b/parsley/shared/src/main/scala/parsley/debug/Breakpoint.scala new file mode 100644 index 000000000..0068c5d97 --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/debug/Breakpoint.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Parsley Contributors + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.debug + +/** Base trait for breakpoints. + * + * @group break + */ +sealed trait Breakpoint +/** Indicates that no breaking should occur. + * + * @group break + */ +case object NoBreak extends Breakpoint +/** Break on entry to the combinator, require user input to advance. + * + * @group break + */ +case object EntryBreak extends Breakpoint +/** Break on exit to the combinator, require user input to advance. + * + * @group break + */ +case object ExitBreak extends Breakpoint +/** Break on both entry and exit to the combinator, require user input to advance in both cases. + * + * @group break + */ +case object FullBreak extends Breakpoint diff --git a/parsley/shared/src/main/scala/parsley/debug/Profiler.scala b/parsley/shared/src/main/scala/parsley/debug/Profiler.scala new file mode 100644 index 000000000..821ce6988 --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/debug/Profiler.scala @@ -0,0 +1,169 @@ +/* + * Copyright 2020 Parsley Contributors + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.debug + +import scala.annotation.tailrec +import scala.collection.mutable + +/** This class is used to store the profile data for a specific group of sub-parsers. + * + * It records the start and end timestamps of the parsers that interact with it. It is possible + * to use multiple different profilers if you want to establish the cumulative time for a sub-parser + * instead of the self-time. + * + * This class is mutable, so care must be taken to call `reset()` between runs, unless you want to + * accumulate the data. + * + * @since 4.4.0 + */ +class Profiler { + private val entries = mutable.Map.empty[String, mutable.Buffer[Long]] + private val exits = mutable.Map.empty[String, mutable.Buffer[Long]] + private var lastTime: Long = 0 + private var lastTimeCount: Long = 0 + + // $COVERAGE-OFF$ + /** Prints a summary of the data sampled by this profiler. + * + * After the run(s) of the parser are complete, this method can be used to + * generate the summary of the sampled data. It will print a table where the + * total "self-time", number of invocations and average "self-time" are displayed + * for each profiled sub-parser. + * + * * '''self-time''': this is the amount of time spend in a specific parser, removing + * the times from within the child parsers. + * + * @note to measure cumulative time of a parser, consider using a separate `Profiler` + * object for it instead. + * @since 4.4.0 + */ + def summary(): Unit = { + val (selfTotals, invocations) = process + render(selfTotals, invocations) + } + // $COVERAGE-ON$ + + /** Clears the data within this profiler. + * @since 4.4.0 + */ + def reset(): Unit = { + // can't clear the maps, because the instructions may have already captured the lists + for ((_, timings) <- entries) timings.clear() + for ((_, timings) <- exits) timings.clear() + lastTime = 0 + lastTimeCount = 0 + } + + private [parsley] def entriesFor(name: String): mutable.Buffer[Long] = entries.getOrElseUpdate(name, mutable.ListBuffer.empty) + private [parsley] def exitsFor(name: String): mutable.Buffer[Long] = exits.getOrElseUpdate(name, mutable.ListBuffer.empty) + private [parsley] def monotone(n: Long) = { + if (n == lastTime) { + lastTimeCount += 1 + n + lastTimeCount + } + else { + lastTime = n + lastTimeCount = 0 + n + } + } + + private [parsley] def process: (Map[String, Long], Map[String, Int]) = { + val allEntries = collapse(entries).sortBy(_._2) + val allExits = collapse(exits).sortBy(_._2) + + require((allEntries ::: allExits).toSet.size == (allExits.length + allExits.length), + "recorded times must all be monotonically increasing") + + val selfTotals = mutable.Map.empty[String, Long] + val invocations = mutable.Map.empty[String, Int] + + @tailrec + def go(entries: List[(String, Long)], exits: List[(String, Long)], stack: List[((String, Long), Long)], cum: Long): Unit = { + (entries, exits, stack) match { + case (Nil, Nil, Nil) => + // final unwinding or stuff to clear on the stack (cum here is for the children) + case (ens, (n2, t2)::exs, ((n1, t1), oldCum)::stack) if ens.headOption.forall(t2 < _._2) => + assert(n1 == n2, "unwinding should result in matching values") + add(invocations, n1)(1) + add(selfTotals, n1)(t2 - t1 - cum) + go(ens, exs, stack, oldCum + t2 - t1) + // in this case, the scope closes quickly (cum here is for your siblings) + case ((n1, t1)::ens, (n2, t2)::exs, stack) if ens.headOption.forall(t2 < _._2) && n1 == n2 => + assert(ens.nonEmpty || n1 == n2, "unwinding should result in matching values") + add(invocations, n1)(1) + add(selfTotals, n1)(t2 - t1) + go(ens, exs, stack, cum + t2 - t1) + // the next one opens first, or the entry and exit don't match + // in either case, this isn't our exit, push ourselves onto the stack (cum here is for your siblings) + case (nt::ens, exs@(_ :: _), stack) => go(ens, exs, (nt, cum)::stack, 0) + // $COVERAGE-OFF$ + case (Nil, Nil, _::_) + | (Nil, _::_, Nil) + | (_ ::_, Nil, _) => assert(false, "something has gone very wrong") + case (Nil, _::_, _::_) => ??? // deadcode from case 2 + // $COVERAGE-ON$ + } + } + + //println(allEntries.map { case (name, t) => (name, t - allEntries.head._2) }) + //println(allExits.map { case (name, t) => (name, t - allEntries.head._2) }) + go(allEntries, allExits, Nil, 0) + (selfTotals.toMap, invocations.toMap) + } + + private def collapse(timings: Iterable[(String, Iterable[Long])]): List[(String, Long)] = timings.flatMap { + case (name, times) => times.map(t => (name, t)) + }.toList + + private def add[A: Numeric](m: mutable.Map[String, A], name: String)(n: A): Unit = m.get(name) match { + case Some(x) => m(name) = implicitly[Numeric[A]].plus(x, n) + case None => m(name) = n + } + + // $COVERAGE-OFF$ + private def render(selfTimes: Map[String, Long], invocations: Map[String, Int]): Unit = { + val combined = selfTimes.map { + case (name, selfTime) => + val invokes = invocations(name) + (name, (f"${selfTime/1000.0}%.1fμs", invocations(name), f"${selfTime/invokes/1000.0}%.3fμs")) + } + val head1 = "name" + val head2 = "self time" + val head3 = "num calls" + val head4 = "average self time" + + val (names, data) = combined.unzip + val (selfs, invokes, avs) = data.unzip3 + + val col1Width = (head1.length :: names.map(_.length).toList).max + val col2Width = (head2.length :: selfs.map(_.length).toList).max + val col3Width = (head3.length :: invokes.map(digits(_)).toList).max + val col4Width = (head4.length :: avs.map(_.length).toList).max + + val header = List(pad(head1, col1Width), tab(col1Width), + pad(head2, col2Width), tab(col2Width), + pad(head3, col3Width), tab(col3Width), + pad(head4, col4Width)).mkString + val hline = header.map(_ => '-') + + println(header) + println(hline) + for ((name, (selfTime, invokes, avSelfTime)) <- combined) { + println(List(pad(name, col1Width), tab(col1Width), + prePad(selfTime, col2Width), tab(col2Width), + prePad(invokes.toString, col3Width), tab(col3Width), + prePad(avSelfTime, col4Width)).mkString) + } + println(hline) + } + + private def pad(str: String, n: Int) = str + " " * (n - str.length) + private def prePad(str: String, n: Int) = " " * (n - str.length) + str + private def digits[A: Numeric](n: A): Int = Math.log10(implicitly[Numeric[A]].toDouble(n)).toInt + 1 + private def tab(n: Int) = " " * (4 - n % 4) + // $COVERAGE-ON$ +} diff --git a/parsley/shared/src/main/scala/parsley/debug.scala b/parsley/shared/src/main/scala/parsley/debug/package.scala similarity index 59% rename from parsley/shared/src/main/scala/parsley/debug.scala rename to parsley/shared/src/main/scala/parsley/debug/package.scala index 866866ff7..a096bf229 100644 --- a/parsley/shared/src/main/scala/parsley/debug.scala +++ b/parsley/shared/src/main/scala/parsley/debug/package.scala @@ -5,9 +5,6 @@ */ package parsley -import scala.annotation.tailrec -import scala.collection.mutable - import parsley.errors.ErrorBuilder import parsley.state.Ref @@ -16,10 +13,10 @@ import parsley.internal.deepembedding.frontend /** This module contains the very useful debugging combinator, as well as breakpoints. * * @groupprio comb 0 - * @groupname comb Debug Combinator Extension Methods + * @groupname comb (Vanilla) Debug Combinator Extension Methods * @groupdesc comb * These are the debugging combinators, which are enabled by bringing these implicit classes - * into scope. + * into scope. These are part of base parsley. * * @groupprio break 10 * @groupname break Breakpoints @@ -32,34 +29,8 @@ import parsley.internal.deepembedding.frontend * @groupdesc ctrl * These methods can control how the debug mechanism functions in a general way. */ -object debug { +package object debug { // $COVERAGE-OFF$ - /** Base trait for breakpoints. - * - * @group break - */ - sealed trait Breakpoint - /** Indicates that no breaking should occur. - * - * @group break - */ - case object NoBreak extends Breakpoint - /** Break on entry to the combinator, require user input to advance. - * - * @group break - */ - case object EntryBreak extends Breakpoint - /** Break on exit to the combinator, require user input to advance. - * - * @group break - */ - case object ExitBreak extends Breakpoint - /** Break on both entry and exit to the combinator, require user input to advance in both cases. - * - * @group break - */ - case object FullBreak extends Breakpoint - private [parsley] var renderAscii = false /** This method can be used to disable the colored debug output for terminals that don't support it. * @@ -292,164 +263,4 @@ object debug { def profile(name: String)(implicit profiler: Profiler): Parsley[A] = new Parsley(new frontend.Profile[A](con(p).internal, name, profiler)) } // $COVERAGE-ON$ - - /** This class is used to store the profile data for a specific group of sub-parsers. - * - * It records the start and end timestamps of the parsers that interact with it. It is possible - * to use multiple different profilers if you want to establish the cumulative time for a sub-parser - * instead of the self-time. - * - * This class is mutable, so care must be taken to call `reset()` between runs, unless you want to - * accumulate the data. - * - * @since 4.4.0 - */ - class Profiler { - private val entries = mutable.Map.empty[String, mutable.Buffer[Long]] - private val exits = mutable.Map.empty[String, mutable.Buffer[Long]] - private var lastTime: Long = 0 - private var lastTimeCount: Long = 0 - - // $COVERAGE-OFF$ - /** Prints a summary of the data sampled by this profiler. - * - * After the run(s) of the parser are complete, this method can be used to - * generate the summary of the sampled data. It will print a table where the - * total "self-time", number of invocations and average "self-time" are displayed - * for each profiled sub-parser. - * - * * '''self-time''': this is the amount of time spend in a specific parser, removing - * the times from within the child parsers. - * - * @note to measure cumulative time of a parser, consider using a separate `Profiler` - * object for it instead. - * @since 4.4.0 - */ - def summary(): Unit = { - val (selfTotals, invocations) = process - render(selfTotals, invocations) - } - // $COVERAGE-ON$ - - /** Clears the data within this profiler. - * @since 4.4.0 - */ - def reset(): Unit = { - // can't clear the maps, because the instructions may have already captured the lists - for ((_, timings) <- entries) timings.clear() - for ((_, timings) <- exits) timings.clear() - lastTime = 0 - lastTimeCount = 0 - } - - private [parsley] def entriesFor(name: String): mutable.Buffer[Long] = entries.getOrElseUpdate(name, mutable.ListBuffer.empty) - private [parsley] def exitsFor(name: String): mutable.Buffer[Long] = exits.getOrElseUpdate(name, mutable.ListBuffer.empty) - private [parsley] def monotone(n: Long) = { - if (n == lastTime) { - lastTimeCount += 1 - n + lastTimeCount - } - else { - lastTime = n - lastTimeCount = 0 - n - } - } - - private [parsley] def process: (Map[String, Long], Map[String, Int]) = { - val allEntries = collapse(entries).sortBy(_._2) - val allExits = collapse(exits).sortBy(_._2) - - require((allEntries ::: allExits).toSet.size == (allExits.length + allExits.length), - "recorded times must all be monotonically increasing") - - val selfTotals = mutable.Map.empty[String, Long] - val invocations = mutable.Map.empty[String, Int] - - @tailrec - def go(entries: List[(String, Long)], exits: List[(String, Long)], stack: List[((String, Long), Long)], cum: Long): Unit = { - (entries, exits, stack) match { - case (Nil, Nil, Nil) => - // final unwinding or stuff to clear on the stack (cum here is for the children) - case (ens, (n2, t2)::exs, ((n1, t1), oldCum)::stack) if ens.headOption.forall(t2 < _._2) => - assert(n1 == n2, "unwinding should result in matching values") - add(invocations, n1)(1) - add(selfTotals, n1)(t2 - t1 - cum) - go(ens, exs, stack, oldCum + t2 - t1) - // in this case, the scope closes quickly (cum here is for your siblings) - case ((n1, t1)::ens, (n2, t2)::exs, stack) if ens.headOption.forall(t2 < _._2) && n1 == n2 => - assert(ens.nonEmpty || n1 == n2, "unwinding should result in matching values") - add(invocations, n1)(1) - add(selfTotals, n1)(t2 - t1) - go(ens, exs, stack, cum + t2 - t1) - // the next one opens first, or the entry and exit don't match - // in either case, this isn't our exit, push ourselves onto the stack (cum here is for your siblings) - case (nt::ens, exs@(_ :: _), stack) => go(ens, exs, (nt, cum)::stack, 0) - // $COVERAGE-OFF$ - case (Nil, Nil, _::_) - | (Nil, _::_, Nil) - | (_ ::_, Nil, _) => assert(false, "something has gone very wrong") - case (Nil, _::_, _::_) => ??? // deadcode from case 2 - // $COVERAGE-ON$ - } - } - - //println(allEntries.map { case (name, t) => (name, t - allEntries.head._2) }) - //println(allExits.map { case (name, t) => (name, t - allEntries.head._2) }) - go(allEntries, allExits, Nil, 0) - (selfTotals.toMap, invocations.toMap) - } - - private def collapse(timings: Iterable[(String, Iterable[Long])]): List[(String, Long)] = timings.flatMap { - case (name, times) => times.map(t => (name, t)) - }.toList - - private def add[A: Numeric](m: mutable.Map[String, A], name: String)(n: A): Unit = m.get(name) match { - case Some(x) => m(name) = implicitly[Numeric[A]].plus(x, n) - case None => m(name) = n - } - - // $COVERAGE-OFF$ - private def render(selfTimes: Map[String, Long], invocations: Map[String, Int]): Unit = { - val combined = selfTimes.map { - case (name, selfTime) => - val invokes = invocations(name) - (name, (f"${selfTime/1000.0}%.1fμs", invocations(name), f"${selfTime/invokes/1000.0}%.3fμs")) - } - val head1 = "name" - val head2 = "self time" - val head3 = "num calls" - val head4 = "average self time" - - val (names, data) = combined.unzip - val (selfs, invokes, avs) = data.unzip3 - - val col1Width = (head1.length :: names.map(_.length).toList).max - val col2Width = (head2.length :: selfs.map(_.length).toList).max - val col3Width = (head3.length :: invokes.map(digits(_)).toList).max - val col4Width = (head4.length :: avs.map(_.length).toList).max - - val header = List(pad(head1, col1Width), tab(col1Width), - pad(head2, col2Width), tab(col2Width), - pad(head3, col3Width), tab(col3Width), - pad(head4, col4Width)).mkString - val hline = header.map(_ => '-') - - println(header) - println(hline) - for ((name, (selfTime, invokes, avSelfTime)) <- combined) { - println(List(pad(name, col1Width), tab(col1Width), - prePad(selfTime, col2Width), tab(col2Width), - prePad(invokes.toString, col3Width), tab(col3Width), - prePad(avSelfTime, col4Width)).mkString) - } - println(hline) - } - - private def pad(str: String, n: Int) = str + " " * (n - str.length) - private def prePad(str: String, n: Int) = " " * (n - str.length) + str - private def digits[A: Numeric](n: A): Int = Math.log10(implicitly[Numeric[A]].toDouble(n)).toInt + 1 - private def tab(n: Int) = " " * (4 - n % 4) - // $COVERAGE-ON$ - } }