Skip to content

Commit

Permalink
refactor!: parsley.debug is now a package, not object
Browse files Browse the repository at this point in the history
  • Loading branch information
j-mie6 committed Dec 28, 2024
1 parent 88d8d09 commit a0e4267
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 226 deletions.
2 changes: 1 addition & 1 deletion docs/api-guide/debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
32 changes: 32 additions & 0 deletions parsley/shared/src/main/scala/parsley/debug/Breakpoint.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2020 Parsley Contributors <https://github.com/j-mie6/Parsley/graphs/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
169 changes: 169 additions & 0 deletions parsley/shared/src/main/scala/parsley/debug/Profiler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright 2020 Parsley Contributors <https://github.com/j-mie6/Parsley/graphs/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$
}
Loading

0 comments on commit a0e4267

Please sign in to comment.