From 17e564661e125f04102b25f81d9916f497a62fb5 Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Thu, 31 Oct 2024 21:15:12 -0700 Subject: [PATCH] [prelude][core] Frame and Log optimizations (#800) I noticed a major performance regression in `LoggingBench`. The main cause is a bug introduced by https://github.com/getkyo/kyo/pull/783 that makes the benchmark consider `Level.error` enabled when it isn't. I fixed the bug but also worked on a few optimizations to improve performance in case the log is enabled. Benchmark [results](https://jmh.morethan.io/?sources=https://gist.githubusercontent.com/fwbrasil/b7686518b8168f6fcba4ab2787d0ff41/raw/29e149f7bb688672cfdad844e308cde4df999e0e/jmh-result-baseline.json,https://gist.githubusercontent.com/fwbrasil/b7686518b8168f6fcba4ab2787d0ff41/raw/29e149f7bb688672cfdad844e308cde4df999e0e/jmh-result-candidate.json): ![image](https://github.com/user-attachments/assets/550bfe52-96dd-430f-a8b9-472f71d35b6e) --- .../kyo/internal/LogPlatformSpecific.scala | 30 ++--- .../shared/src/main/scala/kyo/Closed.scala | 2 +- .../shared/src/main/scala/kyo/Fiber.scala | 2 +- kyo-core/shared/src/main/scala/kyo/Log.scala | 26 ++-- .../shared/src/main/scala/kyo/Timeout.scala | 2 +- kyo-data/shared/src/main/scala/kyo/Ansi.scala | 15 +-- .../test/scala/kyo/kernel/SafepointTest.scala | 5 +- .../shared/src/main/scala/kyo/Batch.scala | 4 +- .../shared/src/main/scala/kyo/Check.scala | 10 +- .../shared/src/main/scala/kyo/Parse.scala | 13 +- .../src/main/scala/kyo/debug/Debug.scala | 12 +- .../src/main/scala/kyo/kernel/Frame.scala | 118 ++++++++++++------ .../src/main/scala/kyo/kernel/Trace.scala | 10 +- .../src/main/scala/kyo/kernel/package.scala | 3 +- .../src/test/scala/kyo/kernel/FrameTest.scala | 101 ++++++++++----- kyo-zio/shared/src/main/scala/kyo/ZIOs.scala | 2 +- 16 files changed, 210 insertions(+), 145 deletions(-) diff --git a/kyo-core/jvm/src/main/scala/kyo/internal/LogPlatformSpecific.scala b/kyo-core/jvm/src/main/scala/kyo/internal/LogPlatformSpecific.scala index 7964b1b77..e590420f0 100644 --- a/kyo-core/jvm/src/main/scala/kyo/internal/LogPlatformSpecific.scala +++ b/kyo-core/jvm/src/main/scala/kyo/internal/LogPlatformSpecific.scala @@ -17,42 +17,42 @@ object LogPlatformSpecific: def apply(name: String) = new SLF4J(org.slf4j.LoggerFactory.getLogger(name)) class SLF4J(logger: org.slf4j.Logger) extends Log.Unsafe: - def level = - if logger.isTraceEnabled() then Level.trace - else if logger.isDebugEnabled() then Level.debug - else if logger.isInfoEnabled() then Level.info + val level = + if logger.isErrorEnabled() then Level.error else if logger.isWarnEnabled() then Level.warn - else Level.error + else if logger.isInfoEnabled() then Level.info + else if logger.isDebugEnabled() then Level.debug + else Level.trace inline def trace(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.trace.enabled(level) then logger.trace(s"[${frame.parse.position}] $msg") + if Level.trace.enabled(level) then logger.trace(s"[${frame.position.show}] $msg") inline def trace(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.trace.enabled(level) then logger.trace(s"[${frame.parse.position}] $msg", t) + if Level.trace.enabled(level) then logger.trace(s"[${frame.position.show}] $msg", t) inline def debug(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.debug.enabled(level) then logger.debug(s"[${frame.parse.position}] $msg") + if Level.debug.enabled(level) then logger.debug(s"[${frame.position.show}] $msg") inline def debug(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.debug.enabled(level) then logger.debug(s"[${frame.parse.position}] $msg", t) + if Level.debug.enabled(level) then logger.debug(s"[${frame.position.show}] $msg", t) inline def info(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.info.enabled(level) then logger.info(s"[${frame.parse.position}] $msg") + if Level.info.enabled(level) then logger.info(s"[${frame.position.show}] $msg") inline def info(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.info.enabled(level) then logger.info(s"[${frame.parse.position}] $msg", t) + if Level.info.enabled(level) then logger.info(s"[${frame.position.show}] $msg", t) inline def warn(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.warn.enabled(level) then logger.warn(s"[${frame.parse.position}] $msg") + if Level.warn.enabled(level) then logger.warn(s"[${frame.position.show}] $msg") inline def warn(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.warn.enabled(level) then logger.warn(s"[${frame.parse.position}] $msg", t) + if Level.warn.enabled(level) then logger.warn(s"[${frame.position.show}] $msg", t) inline def error(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.error.enabled(level) then logger.error(s"[${frame.parse.position}] $msg") + if Level.error.enabled(level) then logger.error(s"[${frame.position.show}] $msg") inline def error(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if Level.error.enabled(level) then logger.error(s"[${frame.parse.position}] $msg", t) + if Level.error.enabled(level) then logger.error(s"[${frame.position.show}] $msg", t) end SLF4J end Unsafe end LogPlatformSpecific diff --git a/kyo-core/shared/src/main/scala/kyo/Closed.scala b/kyo-core/shared/src/main/scala/kyo/Closed.scala index 2c2d3e55f..43b5cdfc9 100644 --- a/kyo-core/shared/src/main/scala/kyo/Closed.scala +++ b/kyo-core/shared/src/main/scala/kyo/Closed.scala @@ -3,5 +3,5 @@ package kyo import scala.util.control.NoStackTrace case class Closed(message: String, createdAt: Frame, failedAt: Frame) - extends Exception(s"Resource created at ${createdAt.parse.position} is closed. Failure at ${failedAt.parse.position}: $message") + extends Exception(s"Resource created at ${createdAt.position.show} is closed. Failure at ${failedAt.position.show}: $message") with NoStackTrace diff --git a/kyo-core/shared/src/main/scala/kyo/Fiber.scala b/kyo-core/shared/src/main/scala/kyo/Fiber.scala index 3e7192870..67894e4af 100644 --- a/kyo-core/shared/src/main/scala/kyo/Fiber.scala +++ b/kyo-core/shared/src/main/scala/kyo/Fiber.scala @@ -248,7 +248,7 @@ object Fiber extends FiberPlatformSpecific: end extension case class Interrupted(at: Frame) - extends RuntimeException("Fiber interrupted at " + at.parse.position) + extends RuntimeException("Fiber interrupted at " + at.position.show) with NoStackTrace: override def getCause() = null end Interrupted diff --git a/kyo-core/shared/src/main/scala/kyo/Log.scala b/kyo-core/shared/src/main/scala/kyo/Log.scala index 8bd707c37..ed7c620a4 100644 --- a/kyo-core/shared/src/main/scala/kyo/Log.scala +++ b/kyo-core/shared/src/main/scala/kyo/Log.scala @@ -2,7 +2,7 @@ package kyo import kyo.internal.LogPlatformSpecific -final case class Log(unsafe: Log.Unsafe) extends AnyVal: +final case class Log(unsafe: Log.Unsafe): def level: Log.Level = unsafe.level inline def trace(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.trace(msg)) inline def trace(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.trace(msg, t)) @@ -19,8 +19,8 @@ end Log /** Logging utility object for Kyo applications. */ object Log extends LogPlatformSpecific: - final class Level private (private val priority: Int) extends AnyVal: - def enabled(minLevel: Level) = priority >= minLevel.priority + final case class Level private (private val priority: Int) extends AnyVal: + def enabled(maxLevel: Level) = maxLevel.priority <= priority object Level: val trace: Level = Level(10) @@ -109,62 +109,62 @@ object Log extends LogPlatformSpecific: using frame: Frame, allow: AllowUnsafe - ): Unit = if Level.trace.enabled(level) then println(s"TRACE $name -- [${frame.parse.position}] $msg") + ): Unit = if Level.trace.enabled(level) then println(s"TRACE $name -- [${frame.position.show}] $msg") inline def trace(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if Level.trace.enabled(level) then println(s"TRACE $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.trace.enabled(level) then println(s"TRACE $name -- [${frame.position.show}] $msg $t") inline def debug(msg: => String)( using frame: Frame, allow: AllowUnsafe ): Unit = - if Level.debug.enabled(level) then println(s"DEBUG $name -- [${frame.parse.position}] $msg") + if Level.debug.enabled(level) then println(s"DEBUG $name -- [${frame.position.show}] $msg") inline def debug(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if Level.debug.enabled(level) then println(s"DEBUG $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.debug.enabled(level) then println(s"DEBUG $name -- [${frame.position.show}] $msg $t") inline def info(msg: => String)( using frame: Frame, allow: AllowUnsafe - ): Unit = if Level.info.enabled(level) then println(s"INFO $name -- [${frame.parse.position}] $msg") + ): Unit = if Level.info.enabled(level) then println(s"INFO $name -- [${frame.position.show}] $msg") inline def info(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if Level.info.enabled(level) then println(s"INFO $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.info.enabled(level) then println(s"INFO $name -- [${frame.position.show}] $msg $t") inline def warn(msg: => String)( using frame: Frame, allow: AllowUnsafe - ): Unit = if Level.warn.enabled(level) then println(s"WARN $name -- [${frame.parse.position}] $msg") + ): Unit = if Level.warn.enabled(level) then println(s"WARN $name -- [${frame.position.show}] $msg") inline def warn(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if Level.warn.enabled(level) then println(s"WARN $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.warn.enabled(level) then println(s"WARN $name -- [${frame.position.show}] $msg $t") inline def error(msg: => String)( using frame: Frame, allow: AllowUnsafe - ): Unit = if Level.error.enabled(level) then println(s"ERROR $name -- [${frame.parse.position}] $msg") + ): Unit = if Level.error.enabled(level) then println(s"ERROR $name -- [${frame.position.show}] $msg") inline def error(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if Level.error.enabled(level) then println(s"ERROR $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.error.enabled(level) then println(s"ERROR $name -- [${frame.position.show}] $msg $t") end ConsoleLogger end Unsafe diff --git a/kyo-core/shared/src/main/scala/kyo/Timeout.scala b/kyo-core/shared/src/main/scala/kyo/Timeout.scala index bf24f1457..d92e9df81 100644 --- a/kyo-core/shared/src/main/scala/kyo/Timeout.scala +++ b/kyo-core/shared/src/main/scala/kyo/Timeout.scala @@ -3,5 +3,5 @@ package kyo import scala.util.control.NoStackTrace case class Timeout(frame: Frame) - extends Exception(frame.parse.position.toString) + extends Exception(frame.position.show) with NoStackTrace diff --git a/kyo-data/shared/src/main/scala/kyo/Ansi.scala b/kyo-data/shared/src/main/scala/kyo/Ansi.scala index 84eb34a16..2c9cc3f6d 100644 --- a/kyo-data/shared/src/main/scala/kyo/Ansi.scala +++ b/kyo-data/shared/src/main/scala/kyo/Ansi.scala @@ -67,18 +67,19 @@ object Ansi: */ def apply(header: String, code: String, trailer: String, startLine: Int = 1): String = try - val headerLines = if header.nonEmpty then header.split("\n") else Array.empty[String] - val codeLines = code.split("\n").dropWhile(_.isBlank).reverse.dropWhile(_.isBlank).reverse - val trailerLines = if trailer.nonEmpty then trailer.split("\n") else Array.empty[String] + val separatorLine = "─".repeat(30).dim + val headerLines = if header.nonEmpty then Array(separatorLine) ++ header.split("\n") else Array.empty[String] + val codeLines = code.split("\n").dropWhile(_.isBlank).reverse.dropWhile(_.isBlank).reverse + val trailerLines = if trailer.nonEmpty then trailer.split("\n") ++ Array(separatorLine) else Array.empty[String] - val allLines = headerLines ++ codeLines ++ trailerLines val toDrop = codeLines.filter(_.trim.nonEmpty).map(_.takeWhile(_ == ' ').length).minOption.getOrElse(0) - val lineNumberWidth = (startLine + codeLines.length - 1).toString.length + val lineNumberWidth = (startLine + codeLines.length).toString.length val separator = "│".dim + val allLines = headerLines ++ Array(separatorLine) ++ codeLines ++ Array(separatorLine) ++ trailerLines val processedLines = allLines.zipWithIndex.map { case (line, index) => - val isHeader = index < headerLines.length - val isTrailer = index >= (headerLines.length + codeLines.length) + val isHeader = index <= headerLines.length + val isTrailer = index > (headerLines.length + codeLines.length) val lineNumber = if isHeader || isTrailer then " ".repeat(lineNumberWidth) diff --git a/kyo-prelude/jvm/src/test/scala/kyo/kernel/SafepointTest.scala b/kyo-prelude/jvm/src/test/scala/kyo/kernel/SafepointTest.scala index e5d118303..eea1cfb35 100644 --- a/kyo-prelude/jvm/src/test/scala/kyo/kernel/SafepointTest.scala +++ b/kyo-prelude/jvm/src/test/scala/kyo/kernel/SafepointTest.scala @@ -281,8 +281,7 @@ class SafepointTest extends Test: val logs = ArrayBuffer.empty[String] def enter(frame: Frame, value: Any): Boolean = - val parsed = frame.parse - logs += s"Entering ${parsed.methodName} with value: $value" + logs += s"Entering ${frame.methodName} with value: $value" true end enter @@ -365,7 +364,7 @@ class SafepointTest extends Test: } assert(interceptor.log.size == 3) - assert(interceptor.log.exists(_._1.parse.methodName == "computation")) + assert(interceptor.log.exists(_._1.methodName == "computation")) } } diff --git a/kyo-prelude/shared/src/main/scala/kyo/Batch.scala b/kyo-prelude/shared/src/main/scala/kyo/Batch.scala index 87cbd0c31..8dbbabb31 100644 --- a/kyo-prelude/shared/src/main/scala/kyo/Batch.scala +++ b/kyo-prelude/shared/src/main/scala/kyo/Batch.scala @@ -51,7 +51,7 @@ object Batch: f(input).map { output => require( input.size == output.size, - s"Source created at ${frame.parse.position} returned a different number of elements than input: ${input.size} != ${output.size}" + s"Source created at ${frame.position.show} returned a different number of elements than input: ${input.size} != ${output.size}" ) ((a: A) => output(a): B < S) } @@ -69,7 +69,7 @@ object Batch: f(input).map { output => require( input.size == output.size, - s"Source created at ${frame.parse.position} returned a different number of elements than input: ${input.size} != ${output.size}" + s"Source created at ${frame.position.show} returned a different number of elements than input: ${input.size} != ${output.size}" ) input.zip(output).toMap } diff --git a/kyo-prelude/shared/src/main/scala/kyo/Check.scala b/kyo-prelude/shared/src/main/scala/kyo/Check.scala index 8a8138a68..c3d97c6a5 100644 --- a/kyo-prelude/shared/src/main/scala/kyo/Check.scala +++ b/kyo-prelude/shared/src/main/scala/kyo/Check.scala @@ -14,15 +14,7 @@ import kyo.kernel.* * The [[Frame]] where the check failure occurred */ final class CheckFailed(val message: String, val frame: Frame) extends AssertionError(message): - override def getMessage() = - Seq( - "", - "──────────────────────────────".dim, - "Check failed! ".red.bold + message, - "──────────────────────────────".dim, - frame.parse.show, - "──────────────────────────────".dim - ).mkString("\n") + override def getMessage() = frame.render("Check failed! ".red.bold + message) end CheckFailed /** Represents a check effect for validating conditions. diff --git a/kyo-prelude/shared/src/main/scala/kyo/Parse.scala b/kyo-prelude/shared/src/main/scala/kyo/Parse.scala index 34a66fe5d..66f8651d9 100644 --- a/kyo-prelude/shared/src/main/scala/kyo/Parse.scala +++ b/kyo-prelude/shared/src/main/scala/kyo/Parse.scala @@ -458,16 +458,5 @@ end Parse case class ParseFailed(frame: Frame, states: Seq[Parse.State], message: String) extends Exception with Serializable: - override def getMessage() = - Seq( - "\n".dim, - "──────────────────────────────".dim, - "Parse failed! ".red.bold + message, - "──────────────────────────────".dim, - frame.parse.show, - "──────────────────────────────".dim, - pprint(states).plainText, - "──────────────────────────────".dim - ).mkString("\n") - end getMessage + override def getMessage() = frame.render("Parse failed! ".red.bold + message, states) end ParseFailed diff --git a/kyo-prelude/shared/src/main/scala/kyo/debug/Debug.scala b/kyo-prelude/shared/src/main/scala/kyo/debug/Debug.scala index c4282bf4e..574e0198a 100644 --- a/kyo-prelude/shared/src/main/scala/kyo/debug/Debug.scala +++ b/kyo-prelude/shared/src/main/scala/kyo/debug/Debug.scala @@ -24,12 +24,12 @@ object Debug: def apply[A, S](v: => A < S)(using frame: Frame): A < S = Effect.catching { v.map { value => - println(frame.show) + println(frame.render) printValue(value) value } } { ex => - println(frame.show) + println(frame.render) printValue(ex) throw ex } @@ -48,7 +48,7 @@ object Debug: val interceptor = new Safepoint.Interceptor: def enter(frame: Frame, value: Any): Boolean = printValue(value) - println(frame.parse.show) + println(frame.render) true end enter def addFinalizer(f: () => Unit): Unit = () @@ -77,7 +77,7 @@ object Debug: def values(params: Param[?]*)(using frame: Frame): Unit = val tuples = LinkedHashMap(params.map(p => (p.code, p.value))*) val string = pprint(tuples).render.replaceFirst("LinkedHashMap", "Params") - println(frame.parse.show) + println(frame.render) println(string) end values @@ -104,7 +104,7 @@ object Debug: end Param private def printValue(value: Any) = - println("──────────────────────────────".dim) + println("───────────────────────────────────".dim) val rendered = pprint(value).render val truncated = if rendered.length > maxValueLength then @@ -112,6 +112,6 @@ object Debug: else rendered println(truncated) - println("──────────────────────────────".dim) + println("───────────────────────────────────".dim) end printValue end Debug diff --git a/kyo-prelude/shared/src/main/scala/kyo/kernel/Frame.scala b/kyo-prelude/shared/src/main/scala/kyo/kernel/Frame.scala index 47925ae44..49d3f84bc 100644 --- a/kyo-prelude/shared/src/main/scala/kyo/kernel/Frame.scala +++ b/kyo-prelude/shared/src/main/scala/kyo/kernel/Frame.scala @@ -9,45 +9,88 @@ opaque type Frame <: AnyRef = String object Frame: + private val version = '0' + private val snippetShortMaxChars = 50 given CanEqual[Frame, Frame] = CanEqual.derived - case class Parsed( - declaringClass: String, - methodName: String, - position: Position, - snippetShort: String, - snippetLong: String - ) derives CanEqual: - - def show: String = - Ansi.highlight(s"// $position $declaringClass $methodName", snippetLong, s"", position.lineNumber) - - override def toString = s"Frame($declaringClass, $methodName, $position, $snippetShort)" - end Parsed - - case class Position( - fileName: String, - lineNumber: Int, - columnNumber: Int - ) derives CanEqual: - override def toString = s"$fileName:$lineNumber:$columnNumber" + opaque type Position = String + + object Position: + extension (self: Position) + def fileName: String = + self.substring(1, self.indexOf(':')) + + def lineNumber: Int = + val start = self.indexOf(':') + 1 + val end = self.indexOf(':', start) + self.substring(start, end).toInt + end lineNumber + + def columnNumber: Int = + val firstColon = self.indexOf(':') + val start = self.indexOf(':', firstColon + 1) + 1 + val end = self.indexOf('|', start) + self.substring(start, end).toInt + end columnNumber + + def show: String = + self.substring(1, self.indexOf('|')) + end extension end Position - extension (t: Frame) - def parse: Parsed = - val arr = t.split('£') - Parsed( - arr(0), - arr(1), - Position(arr(2), arr(3).toInt, arr(4).toInt), - arr(5).split("📍")(0).reverse.take(snippetShortMaxChars).takeWhile(_ != '\n').trim.reverse, - arr(5) + extension (self: Frame) + private def findNextSeparator(from: Int): Int = + self.indexOf('|', from) + + private def parseSection(start: Int, end: Int): String = + if end < 0 then self.substring(start) + else self.substring(start, end) + + def position: Position = self + + def className: String = + val start = findNextSeparator(1) + 1 + parseSection(start, findNextSeparator(start)) + + def methodName: String = + val firstSep = findNextSeparator(1) + val start = findNextSeparator(firstSep + 1) + 1 + parseSection(start, findNextSeparator(start)) + end methodName + + def snippet: String = + val firstSep = findNextSeparator(1) + val secondSep = findNextSeparator(firstSep + 1) + val start = findNextSeparator(secondSep + 1) + 1 + self.substring(start) + end snippet + + def snippetShort: String = snippet.split("📍")(0).reverse.take(snippetShortMaxChars).takeWhile(_ != '\n').trim.reverse + + def show: String = s"Frame(${Position.show(position)}, $className, $methodName, $snippetShort)" + + def render: String = + Ansi.highlight(s"// ${Position.show(position)} $className $methodName", snippet.toString(), s"", Position.lineNumber(self)) + + def render(details: Any*): String = + val detailsString = + details.size match + case 0 => "" + case 1 => pprint(details(0)).render + case _ => + details.map { + case v: String => v + case v => pprint(v).render + }.mkString("\n\n") + Ansi.highlight( + s"// ${Position.show(position)} $className $methodName", + snippet.toString(), + detailsString, + Position.lineNumber(self) ) - end parse - - def show: String = parse.show + end render end extension implicit inline def derive: Frame = ${ frameImpl(false) } @@ -90,17 +133,17 @@ object Frame: val snippetLines = lines.slice(startLine - 1, endLine + 2).filter(_.exists(_ != ' ')) val toDrop = snippetLines.map(_.takeWhile(_ == ' ').size).minOption.getOrElse(0) val snippet = if internal then "" else snippetLines.map(_.drop(toDrop)).mkString("\n") - val cls = findEnclosing(_.isClassDef).map(show).getOrElse("?") - val method = findEnclosing(_.isDefDef).map(show).getOrElse("?") + val cls = findEnclosing(_.isClassDef).map(render).getOrElse("?") + val method = findEnclosing(_.isDefDef).map(render).getOrElse("?") - Expr(s"$cls£$method£${pos.sourceFile.name}£${startLine + 1}£${startColumn + 1}£$snippet") + Expr(s"$version${pos.sourceFile.name}:${startLine + 1}:${startColumn + 1}|$cls|$method|$snippet") end frameImpl - private def show(using Quotes)(symbol: quotes.reflect.Symbol): String = + private def render(using Quotes)(symbol: quotes.reflect.Symbol): String = if symbol.isClassDef then symbol.fullName else if symbol.isDefDef then symbol.name else "" - end show + end render private def findEnclosing(using Quotes)(predicate: quotes.reflect.Symbol => Boolean): Maybe[quotes.reflect.Symbol] = import quotes.reflect.* @@ -113,4 +156,5 @@ object Frame: findSymbol(Symbol.spliceOwner) end findEnclosing + end Frame diff --git a/kyo-prelude/shared/src/main/scala/kyo/kernel/Trace.scala b/kyo-prelude/shared/src/main/scala/kyo/kernel/Trace.scala index ff244f1e8..fa9bc8d32 100644 --- a/kyo-prelude/shared/src/main/scala/kyo/kernel/Trace.scala +++ b/kyo-prelude/shared/src/main/scala/kyo/kernel/Trace.scala @@ -85,11 +85,11 @@ object Trace: if index < maxTraceFrames then 0 else index & (maxTraceFrames - 1) - val parsed = new Array[Frame.Parsed](size) + val ordered = new Array[Frame](size) @tailrec def parse(idx: Int, maxSnippetSize: Int): Int = if idx < size then - val curr = frames((start + idx) & (maxTraceFrames - 1)).parse - parsed(idx) = curr + val curr = frames((start + idx) & (maxTraceFrames - 1)) + ordered(idx) = curr val snippetSize = curr.snippetShort.size parse(idx + 1, if snippetSize > maxSnippetSize then snippetSize else maxSnippetSize) else @@ -97,14 +97,14 @@ object Trace: val toPad = parse(0, 0) val elements = - parsed.foldLeft(List.empty[Frame.Parsed]) { + ordered.foldLeft(List.empty[Frame]) { case (acc, curr) => acc match case `curr` :: tail => acc case _ => curr :: acc }.map { frame => StackTraceElement( - frame.snippetShort.reverse.padTo(toPad, ' ').reverse + " @ " + frame.declaringClass, + frame.snippetShort.reverse.padTo(toPad, ' ').reverse + " @ " + frame.className, frame.methodName, frame.position.fileName, frame.position.lineNumber diff --git a/kyo-prelude/shared/src/main/scala/kyo/kernel/package.scala b/kyo-prelude/shared/src/main/scala/kyo/kernel/package.scala index a091f32e3..141884349 100644 --- a/kyo-prelude/shared/src/main/scala/kyo/kernel/package.scala +++ b/kyo-prelude/shared/src/main/scala/kyo/kernel/package.scala @@ -32,8 +32,7 @@ package object kernel: def apply(v: O[A], context: Context)(using Safepoint): B < S final override def toString = - val parsed = frame.parse - s"Kyo(${tag.show}, Input($input), ${parsed.position}, ${parsed.snippetShort})" + s"Kyo(${tag.show}, Input($input), ${frame.position.show}, ${frame.snippetShort})" end KyoSuspend abstract class KyoContinue[I[_], O[_], E <: ArrowEffect[I, O], A, B, S](kyo: KyoSuspend[I, O, E, A, ?, ?]) diff --git a/kyo-prelude/shared/src/test/scala/kyo/kernel/FrameTest.scala b/kyo-prelude/shared/src/test/scala/kyo/kernel/FrameTest.scala index 55fb2ed8b..0e6213bb0 100644 --- a/kyo-prelude/shared/src/test/scala/kyo/kernel/FrameTest.scala +++ b/kyo-prelude/shared/src/test/scala/kyo/kernel/FrameTest.scala @@ -13,44 +13,85 @@ class FrameTest extends Test: x / x } - "parse.toString" in { - assert(test1.parse.toString == "Frame(kyo.kernel.FrameTest, test1, FrameTest.scala:9:28, def test1 = test(1 + 2))") - assert(test2.parse.toString == "Frame(kyo.kernel.FrameTest, test2, FrameTest.scala:14:6, })") - } + val internal = Frame.internal "show" in { - import kyo.Ansi.* - assert(test1.show.stripAnsi == - """| │ // FrameTest.scala:9:28 kyo.kernel.FrameTest test1 - |9 │ def test1 = test(1 + 2)📍""".stripMargin) - assert(test2.show.stripAnsi == - """| │ // FrameTest.scala:14:6 kyo.kernel.FrameTest test2 - |14 │ x / x - |15 │ }📍""".stripMargin) + assert(test1.show == "Frame(FrameTest.scala:9:28, kyo.kernel.FrameTest, test1, def test1 = test(1 + 2))") + assert(test2.show == "Frame(FrameTest.scala:14:6, kyo.kernel.FrameTest, test2, })") + } + + "render" - { + "no details" in { + import kyo.Ansi.* + assert(test1.render.stripAnsi == + """| │ ────────────────────────────── + | │ // FrameTest.scala:9:28 kyo.kernel.FrameTest test1 + | │ ────────────────────────────── + |10 │ def test1 = test(1 + 2)📍 + | │ ──────────────────────────────""".stripMargin) + assert(test2.render.stripAnsi == + """| │ ────────────────────────────── + | │ // FrameTest.scala:14:6 kyo.kernel.FrameTest test2 + | │ ────────────────────────────── + |15 │ x / x + |16 │ }📍 + | │ ──────────────────────────────""".stripMargin) + } + + "with details" in { + import kyo.Ansi.* + assert(test1.render(3).stripAnsi == + """| │ ────────────────────────────── + | │ // FrameTest.scala:9:28 kyo.kernel.FrameTest test1 + | │ ────────────────────────────── + |10 │ def test1 = test(1 + 2)📍 + | │ ────────────────────────────── + | │ 3 + | │ ──────────────────────────────""".stripMargin) + + assert(test1.render(1, "hello", true).stripAnsi == + """| │ ────────────────────────────── + | │ // FrameTest.scala:9:28 kyo.kernel.FrameTest test1 + | │ ────────────────────────────── + |10 │ def test1 = test(1 + 2)📍 + | │ ────────────────────────────── + | │ 1 + | │ + | │ hello + | │ + | │ true + | │ ──────────────────────────────""".stripMargin) + + case class Person(name: String, age: Int) + assert(test1.render(Person("Alice", 30)).stripAnsi == + """| │ ────────────────────────────── + | │ // FrameTest.scala:9:28 kyo.kernel.FrameTest test1 + | │ ────────────────────────────── + |10 │ def test1 = test(1 + 2)📍 + | │ ────────────────────────────── + | │ Person(name = "Alice", age = 30) + | │ ──────────────────────────────""".stripMargin) + } } "parse" in { - val parsed = test1.parse - assert(parsed.declaringClass == "kyo.kernel.FrameTest") - assert(parsed.methodName == "test1") - assert(parsed.position.fileName == "FrameTest.scala") - assert(parsed.position.lineNumber == 9) - assert(parsed.position.columnNumber == 28) - assert(parsed.snippetShort == "def test1 = test(1 + 2)") - assert(parsed.snippetLong == "def test1 = test(1 + 2)📍") + assert(test1.className == "kyo.kernel.FrameTest") + assert(test1.methodName == "test1") + assert(test1.position.fileName == "FrameTest.scala") + assert(test1.position.lineNumber == 9) + assert(test1.position.columnNumber == 28) + assert(test1.snippet == "def test1 = test(1 + 2)📍") + assert(test1.snippetShort == "def test1 = test(1 + 2)") } "internal" in { - val internal = Frame.internal - val parsed = internal.parse - assert(parsed.declaringClass == "kyo.kernel.FrameTest") - assert(parsed.methodName == "?") - assert(parsed.position.fileName == "FrameTest.scala") - assert(parsed.position.lineNumber == 44) - assert(parsed.position.columnNumber == 24) - assert(parsed.snippetShort == "") - assert(parsed.snippetLong == "") - assert(parsed.toString == "Frame(kyo.kernel.FrameTest, ?, FrameTest.scala:44:24, )") + assert(internal.className == "kyo.kernel.FrameTest") + assert(internal.methodName == "?") + assert(internal.position.fileName == "FrameTest.scala") + assert(internal.position.lineNumber == 16) + assert(internal.position.columnNumber == 20) + assert(internal.snippet == "") + assert(internal.snippetShort == "") } end FrameTest diff --git a/kyo-zio/shared/src/main/scala/kyo/ZIOs.scala b/kyo-zio/shared/src/main/scala/kyo/ZIOs.scala index be21a5e5a..301f35e19 100644 --- a/kyo-zio/shared/src/main/scala/kyo/ZIOs.scala +++ b/kyo-zio/shared/src/main/scala/kyo/ZIOs.scala @@ -103,7 +103,7 @@ object ZIOs: case Both(left, right) => loop(left).orElse(loop(right)) case Stackless(e, trace) => loop(e) case _: Empty.type => Maybe.empty - loop(cause).getOrElse(Result.Panic(new Exception("Unexpected zio.Cause.Empty at " + frame.parse.position))) + loop(cause).getOrElse(Result.Panic(new Exception("Unexpected zio.Cause.Empty at " + frame.position.show))) end toError end extension