Skip to content

Backport "Scala 2 forwardport: -Yprofile-trace" to 3.3 LTS #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
@@ -152,7 +152,10 @@ class Compiler {
List(new GenBCode) :: // Generate JVM bytecode
Nil

var runId: Int = 1
// TODO: Initially 0, so that the first nextRunId call would return InitialRunId == 1
// Changing the initial runId from 1 to 0 makes the scala2-library-bootstrap fail to compile,
// when the underlying issue is fixed, please update dotc.profiler.RealProfiler.chromeTrace logic
private var runId: Int = 1
def nextRunId: Int = {
runId += 1; runId
}
7 changes: 3 additions & 4 deletions compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
@@ -311,10 +311,9 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
if phaseWillRun then
Stats.trackTime(s"phase time ms/$phase") {
val start = System.currentTimeMillis
val profileBefore = profiler.beforePhase(phase)
try units = phase.runOn(units)
catch case _: InterruptedException => cancelInterrupted()
profiler.afterPhase(phase, profileBefore)
profiler.onPhase(phase):
try units = phase.runOn(units)
catch case _: InterruptedException => cancelInterrupted()
if (ctx.settings.Xprint.value.containsPhase(phase))
for (unit <- units)
lastPrintedTree =
13 changes: 5 additions & 8 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
@@ -174,7 +174,7 @@ private sealed trait WarningSettings:
name = "-Wunused",
helpArg = "warning",
descr = "Enable or disable specific `unused` warnings",
choices = List(
choices = List(
ChoiceWithHelp("nowarn", ""),
ChoiceWithHelp("all", ""),
ChoiceWithHelp(
@@ -422,14 +422,11 @@ private sealed trait YSettings:
val YfromTastyIgnoreList: Setting[List[String]] = MultiStringSetting("-Yfrom-tasty-ignore-list", "file", "List of `tasty` files in jar files that will not be loaded when using -from-tasty.")
val YnoExperimental: Setting[Boolean] = BooleanSetting("-Yno-experimental", "Disable experimental language features.")
val YlegacyLazyVals: Setting[Boolean] = BooleanSetting("-Ylegacy-lazy-vals", "Use legacy (pre 3.3.0) implementation of lazy vals.")

val YprofileEnabled: Setting[Boolean] = BooleanSetting("-Yprofile-enabled", "Enable profiling.")
val YprofileDestination: Setting[String] = StringSetting("-Yprofile-destination", "file", "Where to send profiling output - specify a file, default is to the console.", "")
//.withPostSetHook( _ => YprofileEnabled.value = true )
val YprofileExternalTool: Setting[List[String]] = PhasesSetting("-Yprofile-external-tool", "Enable profiling for a phase using an external tool hook. Generally only useful for a single phase.", "typer")
//.withPostSetHook( _ => YprofileEnabled.value = true )
val YprofileRunGcBetweenPhases: Setting[List[String]] = PhasesSetting("-Yprofile-run-gc", "Run a GC between phases - this allows heap size to be accurate at the expense of more time. Specify a list of phases, or *", "_")
//.withPostSetHook( _ => YprofileEnabled.value = true )
val YprofileDestination: Setting[String] = StringSetting("-Yprofile-destination", "file", "Where to send profiling output - specify a file, default is to the console.", "", depends = List(YprofileEnabled -> true))
val YprofileExternalTool: Setting[List[String]] = PhasesSetting("-Yprofile-external-tool", "Enable profiling for a phase using an external tool hook. Generally only useful for a single phase.", "typer", depends = List(YprofileEnabled -> true))
val YprofileRunGcBetweenPhases: Setting[List[String]] = PhasesSetting("-Yprofile-run-gc", "Run a GC between phases - this allows heap size to be accurate at the expense of more time. Specify a list of phases, or *", "_", depends = List(YprofileEnabled -> true))
val YprofileTrace: Setting[String] = StringSetting("-Yprofile-trace", "file", s"Capture trace of compilation in JSON Chrome Trace format to the specified file. This option requires ${YprofileEnabled.name}. The output file can be visualized using https://ui.perfetto.dev/.", "", depends = List(YprofileEnabled -> true))

// Experimental language features
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.")
25 changes: 19 additions & 6 deletions compiler/src/dotty/tools/dotc/config/Settings.scala
Original file line number Diff line number Diff line change
@@ -51,7 +51,20 @@ object Settings:

def warn(msg: String): Settings.ArgsSummary =
ArgsSummary(sstate, arguments.tail, errors, warnings :+ msg)
}

def deprecated(msg: String, extraArgs: List[String] = Nil): Settings.ArgsSummary =
ArgsSummary(sstate, extraArgs ++ arguments.tail, errors, warnings :+ msg)

@unshared
val settingCharacters = "[a-zA-Z0-9_\\-]*".r
def validateSettingString(name: String): Unit =
assert(settingCharacters.matches(name), s"Setting string $name contains invalid characters")

/** List of setting-value pairs that are required for another setting to be valid.
* For example, `s = Setting(..., depends = List(YprofileEnabled -> true))`
* means that `s` requires `YprofileEnabled` to be set to `true`.
*/
type SettingDependencies = List[(Setting[?], Any)]

case class Setting[T: ClassTag] private[Settings] (
name: String,
@@ -61,7 +74,7 @@ object Settings:
choices: Option[Seq[?]] = None,
prefix: String = "",
aliases: List[String] = Nil,
depends: List[(Setting[?], Any)] = Nil,
depends: SettingDependencies = Nil,
ignoreInvalidArgs: Boolean = false,
propertyClass: Option[Class[?]] = None)(private[Settings] val idx: Int) {

@@ -284,8 +297,8 @@ object Settings:
def BooleanSetting(name: String, descr: String, initialValue: Boolean = false, aliases: List[String] = Nil): Setting[Boolean] =
publish(Setting(name, descr, initialValue, aliases = aliases))

def StringSetting(name: String, helpArg: String, descr: String, default: String, aliases: List[String] = Nil): Setting[String] =
publish(Setting(name, descr, default, helpArg, aliases = aliases))
def StringSetting(name: String, helpArg: String, descr: String, default: String, aliases: List[String] = Nil, depends: SettingDependencies = Nil): Setting[String] =
publish(Setting(name, descr, default, helpArg, aliases = aliases, depends = depends))

def ChoiceSetting(name: String, helpArg: String, descr: String, choices: List[String], default: String, aliases: List[String] = Nil): Setting[String] =
publish(Setting(name, descr, default, helpArg, Some(choices), aliases = aliases))
@@ -314,8 +327,8 @@ object Settings:
def PathSetting(name: String, descr: String, default: String, aliases: List[String] = Nil): Setting[String] =
publish(Setting(name, descr, default, aliases = aliases))

def PhasesSetting(name: String, descr: String, default: String = "", aliases: List[String] = Nil): Setting[List[String]] =
publish(Setting(name, descr, if (default.isEmpty) Nil else List(default), aliases = aliases))
def PhasesSetting(name: String, descr: String, default: String = "", aliases: List[String] = Nil, depends: SettingDependencies = Nil): Setting[List[String]] =
publish(Setting(name, descr, if (default.isEmpty) Nil else List(default), aliases = aliases, depends = depends))

def PrefixSetting(name: String, pre: String, descr: String): Setting[List[String]] =
publish(Setting(name, descr, Nil, prefix = pre))
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
@@ -747,6 +747,7 @@ object Contexts {
.updated(settingsStateLoc, settingsGroup.defaultState)
.updated(notNullInfosLoc, Nil)
.updated(compilationUnitLoc, NoCompilationUnit)
.updated(profilerLoc, Profiler.NoOp)
c._searchHistory = new SearchRoot
c._gadtState = GadtState(GadtConstraint.empty)
c
6 changes: 5 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Phases.scala
Original file line number Diff line number Diff line change
@@ -330,7 +330,11 @@ object Phases {
/** @pre `isRunnable` returns true */
def runOn(units: List[CompilationUnit])(using runCtx: Context): List[CompilationUnit] =
val buf = List.newBuilder[CompilationUnit]
for unit <- units do

// Test that we are in a state where we need to check if the phase should be skipped for a java file,
// this prevents checking the expensive `unit.typedAsJava` unnecessarily.
val doCheckJava = skipIfJava && !isAfterLastJavaPhase
for unit <- units do ctx.profiler.onUnit(this, unit):
given unitCtx: Context = runCtx.fresh.setPhase(this.start).setCompilationUnit(unit).withRootImports
if ctx.run.enterUnit(unit) then
try
13 changes: 11 additions & 2 deletions compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala
Original file line number Diff line number Diff line change
@@ -7,7 +7,8 @@ import java.nio.channels.ClosedByInterruptException

import scala.util.control.NonFatal

import dotty.tools.io.{ ClassPath, ClassRepresentation, AbstractFile }
import dotty.tools.dotc.classpath.FileUtils.{hasTastyExtension, hasBetastyExtension}
import dotty.tools.io.{ ClassPath, ClassRepresentation, AbstractFile, NoAbstractFile }
import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions

import Contexts.*, Symbols.*, Flags.*, SymDenotations.*, Types.*, Scopes.*, Names.*
@@ -333,7 +334,15 @@ abstract class SymbolLoader extends LazyType { self =>
def description(using Context): String = s"proxy to ${self.description}"
}

override def complete(root: SymDenotation)(using Context): Unit = {
private inline def profileCompletion[T](root: SymDenotation)(inline body: T)(using Context): T = {
val sym = root.symbol
def associatedFile = root.symbol.associatedFile match
case file: AbstractFile => file
case _ => NoAbstractFile
ctx.profiler.onCompletion(sym, associatedFile)(body)
}

override def complete(root: SymDenotation)(using Context): Unit = profileCompletion(root) {
def signalError(ex: Exception): Unit = {
if (ctx.debug) ex.printStackTrace()
val msg = ex.getMessage()
190 changes: 190 additions & 0 deletions compiler/src/dotty/tools/dotc/profile/ChromeTrace.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Scala 2 compiler backport of https://github.com/scala/scala/pull/7364
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package dotty.tools.dotc.profile

import scala.language.unsafeNulls

import java.io.Closeable
import java.lang.management.ManagementFactory
import java.nio.file.{Files, Path}
import java.util
import java.util.concurrent.TimeUnit

import scala.collection.mutable

object ChromeTrace {
private object EventType {
final val Start = "B"
final val Instant = "I"
final val End = "E"
final val Complete = "X"

final val Counter = "C"

final val AsyncStart = "b"
final val AsyncInstant = "n"
final val AsyncEnd = "e"
}
}

/** Allows writing a subset of captrue traces based on https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#
* Can be visualized using https://ui.perfetto.dev/, Chrome's about://tracing (outdated) or the tooling in https://www.google.com.au/search?q=catapult+tracing&oq=catapult+tracing+&aqs=chrome..69i57.3974j0j4&sourceid=chrome&ie=UTF-8 */
final class ChromeTrace(f: Path) extends Closeable {
import ChromeTrace.EventType
private val traceWriter = FileUtils.newAsyncBufferedWriter(f)
private val context = mutable.Stack[JsonContext](TopContext)
private val tidCache = new ThreadLocal[String]() {
override def initialValue(): String = "%05d".format(Thread.currentThread().getId())
}
objStart()
fld("traceEvents")
context.push(ValueContext)
arrStart()
traceWriter.newLine()

private val pid = ManagementFactory.getRuntimeMXBean().getName().replaceAll("@.*", "")

override def close(): Unit = {
arrEnd()
objEnd()
context.pop()
tidCache.remove()
traceWriter.close()
}

def traceDurationEvent(name: String, startNanos: Long, durationNanos: Long, tid: String = this.tid(), pidSuffix: String = ""): Unit = {
val durationMicros = nanosToMicros(durationNanos)
val startMicros = nanosToMicros(startNanos)
objStart()
str("cat", "scalac")
str("name", name)
str("ph", EventType.Complete)
str("tid", tid)
writePid(pidSuffix)
lng("ts", startMicros)
lng("dur", durationMicros)
objEnd()
traceWriter.newLine()
}

private def writePid(pidSuffix: String) = {
if (pidSuffix == "")
str("pid", pid)
else
str2("pid", pid, "-", pidSuffix)
}

def traceCounterEvent(name: String, counterName: String, count: Long, processWide: Boolean): Unit = {
objStart()
str("cat", "scalac")
str("name", name)
str("ph", EventType.Counter)
str("tid", tid())
writePid(pidSuffix = if (processWide) "" else tid())
lng("ts", microTime())
fld("args")
objStart()
lng(counterName, count)
objEnd()
objEnd()
traceWriter.newLine()
}

def traceDurationEventStart(cat: String, name: String, colour: String = "", pidSuffix: String = tid()): Unit = traceDurationEventStartEnd(EventType.Start, cat, name, colour, pidSuffix)
def traceDurationEventEnd(cat: String, name: String, colour: String = "", pidSuffix: String = tid()): Unit = traceDurationEventStartEnd(EventType.End, cat, name, colour, pidSuffix)

private def traceDurationEventStartEnd(eventType: String, cat: String, name: String, colour: String, pidSuffix: String = ""): Unit = {
objStart()
str("cat", cat)
str("name", name)
str("ph", eventType)
writePid(pidSuffix)
str("tid", tid())
lng("ts", microTime())
if (colour != "") {
str("cname", colour)
}
objEnd()
traceWriter.newLine()
}

private def tid(): String = tidCache.get()

private def nanosToMicros(t: Long): Long = TimeUnit.NANOSECONDS.toMicros(t)

private def microTime(): Long = nanosToMicros(System.nanoTime())

private sealed abstract class JsonContext
private case class ArrayContext(var first: Boolean) extends JsonContext
private case class ObjectContext(var first: Boolean) extends JsonContext
private case object ValueContext extends JsonContext
private case object TopContext extends JsonContext

private def str(name: String, value: String): Unit = {
fld(name)
traceWriter.write("\"")
traceWriter.write(value) // This assumes no escaping is needed
traceWriter.write("\"")
}
private def str2(name: String, value: String, valueContinued1: String, valueContinued2: String): Unit = {
fld(name)
traceWriter.write("\"")
traceWriter.write(value) // This assumes no escaping is needed
traceWriter.write(valueContinued1) // This assumes no escaping is needed
traceWriter.write(valueContinued2) // This assumes no escaping is needed
traceWriter.write("\"")
}
private def lng(name: String, value: Long): Unit = {
fld(name)
traceWriter.write(String.valueOf(value))
traceWriter.write("")
}
private def objStart(): Unit = {
context.top match {
case ac @ ArrayContext(first) =>
if (first) ac.first = false
else traceWriter.write(",")
case _ =>
}
context.push(ObjectContext(true))
traceWriter.write("{")
}
private def objEnd(): Unit = {
traceWriter.write("}")
context.pop()
}
private def arrStart(): Unit = {
traceWriter.write("[")
context.push(ArrayContext(true))
}
private def arrEnd(): Unit = {
traceWriter.write("]")
context.pop()
}

private def fld(name: String) = {
val topContext = context.top
topContext match {
case oc @ ObjectContext(first) =>
if (first) oc.first = false
else traceWriter.write(",")
case context =>
throw new IllegalStateException("Wrong context: " + context)
}
traceWriter.write("\"")
traceWriter.write(name)
traceWriter.write("\"")
traceWriter.write(":")
}
}
204 changes: 204 additions & 0 deletions compiler/src/dotty/tools/dotc/profile/FileUtils.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Scala 2 compiler backport of https://github.com/scala/scala/pull/7364

/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package dotty.tools.dotc.profile

import scala.language.unsafeNulls

import java.io.{BufferedWriter, IOException, OutputStreamWriter, Writer}
import java.nio.CharBuffer
import java.nio.charset.{Charset, CharsetEncoder, StandardCharsets}
import java.nio.file.{Files, OpenOption, Path}
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean


import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Promise}
import scala.util.{Failure, Success}
import scala.annotation.internal.sharable

object FileUtils {
def newAsyncBufferedWriter(path: Path, charset: Charset = StandardCharsets.UTF_8.nn, options: Array[OpenOption] = NO_OPTIONS, threadsafe: Boolean = false): LineWriter = {
val encoder: CharsetEncoder = charset.newEncoder
val writer = new OutputStreamWriter(Files.newOutputStream(path, options: _*), encoder)
newAsyncBufferedWriter(new BufferedWriter(writer), threadsafe)
}
def newAsyncBufferedWriter(underlying: Writer, threadsafe: Boolean): LineWriter = {
val async = new AsyncBufferedWriter(underlying)
if (threadsafe) new ThreadsafeWriter(async) else async
}
private val NO_OPTIONS = new Array[OpenOption](0)

sealed abstract class LineWriter extends Writer {
def newLine(): Unit
}
private class ThreadsafeWriter(val underlying: AsyncBufferedWriter) extends LineWriter {
lock = underlying
override def write(c: Int): Unit =
lock.synchronized (underlying.write(c))

override def write(cbuf: Array[Char]): Unit =
lock.synchronized (underlying.write(cbuf))

override def write(cbuf: Array[Char], off: Int, len: Int): Unit =
lock.synchronized (underlying.write(cbuf, off, len))

override def write(str: String): Unit =
lock.synchronized (underlying.write(str))

override def write(str: String, off: Int, len: Int): Unit =
lock.synchronized (underlying.write(str, off, len))

override def flush(): Unit =
lock.synchronized (underlying.flush())

override def close(): Unit =
lock.synchronized (underlying.close())

override def newLine(): Unit =
lock.synchronized (underlying.newLine())

}

private object AsyncBufferedWriter {
@sharable private val Close = CharBuffer.allocate(0)
@sharable private val Flush = CharBuffer.allocate(0)
}
private class AsyncBufferedWriter(val underlying: Writer, bufferSize : Int = 4096) extends LineWriter {
private var current: CharBuffer = allocate
override def write(c: Int): Unit = super.write(c)
private def flushAsync(): Unit = {
background.ensureProcessed(current)
current = allocate
}
// allocate or reuse a CharArray which is guaranteed to have a backing array
private def allocate: CharBuffer = {
val reused = background.reuseBuffer
if (reused eq null) CharBuffer.allocate(bufferSize)
else {
//we don't care about race conditions
background.reuseBuffer = null
reused.clear()
reused
}
}

override def write(cbuf: Array[Char], initialOffset: Int, initialLength: Int): Unit = {
var offset = initialOffset
var length = initialLength
while (length > 0) {
val capacity = current.remaining()
if (length <= capacity) {
current.put(cbuf, offset, length)
length = 0
} else {
current.put(cbuf, offset, capacity)
flushAsync()
length -= capacity
offset += capacity
}
}
}

override def write(s: String, initialOffset: Int, initialLength: Int): Unit = {
var offset = initialOffset
var length = initialLength
while (length > 0) {
val capacity = current.remaining()
if (length <= capacity) {
current.put(s, offset, offset + length)
length = 0
} else {
current.put(s, offset, offset + capacity)
flushAsync()
length -= capacity
offset += capacity
}
}
}

def newLine(): Unit = write(scala.util.Properties.lineSeparator)

/** slightly breaks the flush contract in that the flush is not complete when the method returns */
override def flush(): Unit = {
flushAsync()
}

override def close(): Unit = {
background.ensureProcessed(current)
background.ensureProcessed(AsyncBufferedWriter.Close)
current = null
Await.result(background.asyncStatus.future, Duration.Inf)
underlying.close()
}
private object background extends Runnable{

import scala.concurrent.ExecutionContext.Implicits.global

private val pending = new LinkedBlockingQueue[CharBuffer]
//a failure detected will case an Failure, Success indicates a close
val asyncStatus = Promise[Unit]()
private val scheduled = new AtomicBoolean
@volatile var reuseBuffer: CharBuffer = _

def ensureProcessed(buffer: CharBuffer): Unit = {
if (asyncStatus.isCompleted) {
asyncStatus.future.value.get match {
case Success(()) => throw new IllegalStateException("closed")
case Failure(t) => throw new IOException("async failure", t)
}
}

//order is essential - add to the queue before the CAS
pending.add(buffer)
if (scheduled.compareAndSet(false, true)) {
global.execute(background)
}
}

def run(): Unit = {
try {
while (!pending.isEmpty) {
val next = pending.poll()
if (next eq AsyncBufferedWriter.Flush) {
underlying.flush()
} else if (next eq AsyncBufferedWriter.Close) {
underlying.flush()
underlying.close()
asyncStatus.trySuccess(())
} else {
val array = next.array()
next.flip()
underlying.write(array, next.arrayOffset() + next.position(), next.limit())
reuseBuffer = next
}
}
} catch {
case t: Throwable =>
asyncStatus.tryFailure(t)
throw t
}
finally scheduled.set(false)

//we are not scheduled any more
//as a last check ensure that we didnt race with an addition to the queue
//order is essential - queue is checked before CAS
if ((!pending.isEmpty) && scheduled.compareAndSet(false, true)) {
global.execute(background)
}
}
}
}
}
256 changes: 208 additions & 48 deletions compiler/src/dotty/tools/dotc/profile/Profiler.scala

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/profile/ThreadPoolFactory.scala
Original file line number Diff line number Diff line change
@@ -94,9 +94,9 @@ object ThreadPoolFactory {
val data = new ThreadProfileData
localData.set(data)

val profileStart = profiler.snapThread(0)
val profileStart = RealProfiler.snapThread(0)
try worker.run finally {
val snap = profiler.snapThread(data.idleNs)
val snap = RealProfiler.snapThread(data.idleNs)
val threadRange = ProfileRange(profileStart, snap, phase, shortId, data.taskCount, Thread.currentThread())
profiler.completeBackground(threadRange)
}
5 changes: 3 additions & 2 deletions compiler/src/dotty/tools/dotc/transform/Splicer.scala
Original file line number Diff line number Diff line change
@@ -47,13 +47,14 @@ object Splicer {
def splice(tree: Tree, splicePos: SrcPos, spliceExpansionPos: SrcPos, classLoader: ClassLoader)(using Context): Tree = tree match {
case Quote(quotedTree, Nil) => quotedTree
case _ =>
val macroOwner = newSymbol(ctx.owner, nme.MACROkw, Macro | Synthetic, defn.AnyType, coord = tree.span)
val owner = ctx.owner
val macroOwner = newSymbol(owner, nme.MACROkw, Macro | Synthetic, defn.AnyType, coord = tree.span)
try
val sliceContext = SpliceScope.contextWithNewSpliceScope(splicePos.sourcePos).withOwner(macroOwner)
inContext(sliceContext) {
val oldContextClassLoader = Thread.currentThread().getContextClassLoader
Thread.currentThread().setContextClassLoader(classLoader)
try {
try ctx.profiler.onMacroSplice(owner){
val interpreter = new SpliceInterpreter(splicePos, classLoader)

// Some parts of the macro are evaluated during the unpickling performed in quotedExprToTree
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
@@ -1040,7 +1040,7 @@ trait Implicits:
* it should be applied, EmptyTree otherwise.
* @param span The position where errors should be reported.
*/
def inferImplicit(pt: Type, argument: Tree, span: Span)(using Context): SearchResult =
def inferImplicit(pt: Type, argument: Tree, span: Span)(using Context): SearchResult = ctx.profiler.onImplicitSearch(pt):
trace(s"search implicit ${pt.show}, arg = ${argument.show}: ${argument.tpe.show}", implicits, show = true) {
record("inferImplicit")
assert(ctx.phase.allowsImplicitSearch,
21 changes: 17 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
@@ -2490,7 +2490,20 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
if filters == List(MessageFilter.None) then sup.markUsed()
ctx.run.nn.suppressions.addSuppression(sup)

def typedValDef(vdef: untpd.ValDef, sym: Symbol)(using Context): Tree = {
/** Run `typed` on `rhs` except if `rhs` is the right hand side of a deferred given,
* in which case the empty tree is returned.
*/
private inline def excludeDeferredGiven(
rhs: untpd.Tree, sym: Symbol)(
inline typed: untpd.Tree => Tree)(using Context): Tree =
rhs match
case rhs: RefTree
if rhs.name == nme.deferred && sym.isAllOf(DeferredGivenFlags, butNot = Param) =>
EmptyTree
case _ =>
typed(rhs)

def typedValDef(vdef: untpd.ValDef, sym: Symbol)(using Context): Tree = ctx.profiler.onTypedDef(sym) {
val ValDef(name, tpt, _) = vdef
completeAnnotations(vdef, sym)
if (sym.isOneOf(GivenOrImplicit)) checkImplicitConversionDefOK(sym)
@@ -2515,7 +2528,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
sym.owner.info.decls.openForMutations.unlink(sym)
EmptyTree

def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = if !sym.info.exists then retractDefDef(sym) else {
def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = if !sym.info.exists then retractDefDef(sym) else ctx.profiler.onTypedDef(sym) {

// TODO: - Remove this when `scala.language.experimental.erasedDefinitions` is no longer experimental.
// - Modify signature to `erased def erasedValue[T]: T`
@@ -2624,7 +2637,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
if !sym.is(Module) && !sym.isConstructor && sym.info.finalResultType.isErasedClass then
sym.setFlag(Erased)

def typedTypeDef(tdef: untpd.TypeDef, sym: Symbol)(using Context): Tree = {
def typedTypeDef(tdef: untpd.TypeDef, sym: Symbol)(using Context): Tree = ctx.profiler.onTypedDef(sym) {
val TypeDef(name, rhs) = tdef
completeAnnotations(tdef, sym)
val rhs1 = tdef.rhs match
@@ -2637,7 +2650,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
assignType(cpy.TypeDef(tdef)(name, rhs1), sym)
}

def typedClassDef(cdef: untpd.TypeDef, cls: ClassSymbol)(using Context): Tree = {
def typedClassDef(cdef: untpd.TypeDef, cls: ClassSymbol)(using Context): Tree = ctx.profiler.onTypedDef(cls) {
if (!cls.info.isInstanceOf[ClassInfo]) return EmptyTree.assertingErrorsReported

val TypeDef(name, impl @ Template(constr, _, self, _)) = cdef: @unchecked
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/TyperPhase.scala
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ class TyperPhase(addRootImports: Boolean = true) extends Phase {
def typeCheck(using Context)(using subphase: SubPhase): Boolean = monitor(subphase.name) {
val unit = ctx.compilationUnit
try
if !unit.suspended then
if !unit.suspended then ctx.profiler.onUnit(ctx.phase, unit):
unit.tpdTree = ctx.typer.typedExpr(unit.untpdTree)
typr.println("typed: " + unit.source)
record("retained untyped trees", unit.untpdTree.treeSize)
93 changes: 93 additions & 0 deletions compiler/test/dotty/tools/dotc/profile/ChromeTraceTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package dotty.tools.dotc.profile

import java.io.*

import org.junit.Assert.*
import org.junit.*
import java.nio.file.Files
import java.nio.charset.StandardCharsets
import java.util.concurrent.locks.LockSupport
import scala.concurrent.duration.*

class ChromeTraceTest:
private def testTraceOutputs(generator: ChromeTrace => Unit)(checkContent: PartialFunction[List[String], Unit]): Unit = {
val outfile = Files.createTempFile("trace-", ".json").nn
val tracer = new ChromeTrace(outfile)
try generator(tracer)
finally tracer.close()
val contentLines = scala.io.Source.fromFile(outfile.toFile().nn).getLines().toList
checkContent.applyOrElse(
contentLines,
content => fail(s"Invalid output lines: ${content.mkString(System.lineSeparator().nn)}")
)
}

@Test def traceCounterEvent(): Unit = testTraceOutputs{ tracer =>
tracer.traceCounterEvent("foo", "counter1", 42, processWide = true)
tracer.traceCounterEvent("bar", "counter2", 21, processWide = false)
}{
case """{"traceEvents":[""" ::
s"""{"cat":"scalac","name":"foo","ph":"C","tid":"${tid1}","pid":"${pid1}","ts":${ts1},"args":{"counter1":42}}""" ::
s""",{"cat":"scalac","name":"bar","ph":"C","tid":"${tid2}","pid":"${pid2}","ts":${ts2},"args":{"counter2":21}}""" ::
"]}" :: Nil =>
assertEquals(tid1, tid2)
assertTrue(tid1.toIntOption.isDefined)
assertNotEquals(pid1, pid2)
assertTrue(pid1.toIntOption.isDefined)
assertEquals(s"$pid1-$tid1", pid2)
assertTrue(ts1.toLong < ts2.toLong)
}

@Test def traceDurationEvent(): Unit = testTraceOutputs{ tracer =>
tracer.traceDurationEvent(name = "name1", startNanos = 1000L, durationNanos = 2500L, tid = "this-thread")
tracer.traceDurationEvent(name = "name2", startNanos = 1000L, durationNanos = 5000L, tid = "this-thread", pidSuffix = "pidSuffix")
}{
case """{"traceEvents":[""" ::
s"""{"cat":"scalac","name":"name1","ph":"X","tid":"this-thread","pid":"${pid1}","ts":1,"dur":2}""" ::
s""",{"cat":"scalac","name":"name2","ph":"X","tid":"this-thread","pid":"${pid2}","ts":1,"dur":5}""" ::
"]}" :: Nil =>
assertTrue(pid1.toIntOption.isDefined)
assertEquals(s"$pid1-pidSuffix", pid2)
}

@Test def traceDurationEvents(): Unit = {
val testStart = System.nanoTime()
testTraceOutputs{ tracer =>
tracer.traceDurationEventStart(cat = "test1", name = "event1")
LockSupport.parkNanos(2.millis.toNanos)
tracer.traceDurationEventStart(cat = "test2", name = "event2", colour = "RED", pidSuffix = "pid-suffix")
LockSupport.parkNanos(4.millis.toNanos)
tracer.traceDurationEventEnd(cat = "test2", name = "event2")
LockSupport.parkNanos(8.millis.toNanos)
tracer.traceDurationEventEnd(cat = "test1", name = "event1", colour = "RED", pidSuffix = "pid-suffix")
}{
case """{"traceEvents":[""" ::
s"""{"cat":"test1","name":"event1","ph":"B","pid":"${pid1}","tid":"${tid1}","ts":${ts1}}""" ::
s""",{"cat":"test2","name":"event2","ph":"B","pid":"${pid2}","tid":"${tid2}","ts":${ts2},"cname":"RED"}""" ::
s""",{"cat":"test2","name":"event2","ph":"E","pid":"${pid3}","tid":"${tid3}","ts":${ts3}}""" ::
s""",{"cat":"test1","name":"event1","ph":"E","pid":"${pid4}","tid":"${tid4}","ts":${ts4},"cname":"RED"}""" ::
"]}" :: Nil =>
val traceEnd = System.nanoTime()
assertTrue(tid1.toIntOption.isDefined)
assertEquals(pid1, pid3)
assertTrue(pid1.endsWith(s"-$tid1"))
assertEquals(pid2, pid4)
assertTrue(pid2.endsWith("-pid-suffix"))
List(tid1, tid2, tid3).foreach: tid =>
assertEquals(tid4, tid)
List(pid1, pid2, pid3, pid4).foreach: pid =>
assertTrue(pid.takeWhile(_ != '-').toIntOption.isDefined)

List(ts1, ts2, ts3, ts4).map(_.toLong) match {
case all @ List(ts1, ts2, ts3, ts4) =>
all.foreach: ts =>
// Timestamps are presented using Epoch microsecondos
assertTrue(ts >= testStart / 1000)
assertTrue(ts <= traceEnd / 1000)
assertTrue(ts2 >= ts1 + 2.millis.toMicros)
assertTrue(ts3 >= ts2 + 4.millis.toMicros)
assertTrue(ts4 >= ts3 + 8.millis.toMicros)
case _ => fail("unreachable")
}
}
}
91 changes: 91 additions & 0 deletions compiler/test/dotty/tools/dotc/profile/FileUtilsTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package dotty.tools.dotc.profile

import java.io.*

import org.junit.Assert.*
import org.junit.*

class FileUtilsTest {

@Test def writeIsSame(): Unit = {
val fileTest = File.createTempFile("FileUtilsTest", "t1").nn
val fileExpected = File.createTempFile("FileUtilsTest", "t2").nn

val sTest = FileUtils.newAsyncBufferedWriter(new FileWriter(fileTest), threadsafe = false)
val sExpected = new BufferedWriter(new FileWriter(fileExpected))

def writeBoth(s:String, asChars: Boolean) = {
if (asChars) {
sTest.write(s.toCharArray)
sExpected.write(s.toCharArray)
} else {
sTest.write(s)
sExpected.write(s)
}
}

for (i <- 1 to 2000) {
writeBoth(s"line $i text;", asChars = true)
writeBoth(s"line $i chars", asChars = false)
sTest.newLine()
sExpected.newLine()
}
sTest.close()
sExpected.close()

assertEquals(fileExpected.length(),fileTest.length())

val expIn = new BufferedReader(new FileReader(fileExpected))
val testIn = new BufferedReader(new FileReader(fileTest))

var exp = expIn.readLine()
while (exp ne null) {
val actual = testIn.readLine()
assertEquals(exp, actual)
exp = expIn.readLine()
}
expIn.close()
testIn.close()
fileTest.delete()
fileExpected.delete()
}

@Ignore
@Test def showPerformance(): Unit = {
//warmup
for (i <- 1 to 1000) {
writeIsSame()
}

val fileTest = File.createTempFile("FileUtilsTest", "t1").nn
val fileExpected = File.createTempFile("FileUtilsTest", "t2").nn

for (i <- 1 to 10) {
val sTest = FileUtils.newAsyncBufferedWriter(fileTest.toPath.nn)
val sExpected = new BufferedWriter(new FileWriter(fileExpected))

val t1 = System.nanoTime()
List.tabulate(10000) {i =>
sTest.write(s"line $i text;")
sTest.newLine()
}
val t2 = System.nanoTime()
sTest.close()
val t3 = System.nanoTime()
List.tabulate(10000) {i =>
sExpected.write(s"line $i text;")
sExpected.newLine()
}
val t4 = System.nanoTime()
sExpected.close()

println(s"async took ${t2 - t1} ns")
println(s"buffered took ${t4 - t3} ns")

fileTest.delete()
fileExpected.delete()
}
}

}

Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import dotty.tools.dotc.interactive.Interactive
import dotty.tools.dotc.interactive.Completion
import dotty.tools.dotc.interactive.InteractiveDriver
import dotty.tools.dotc.parsing.Tokens
import dotty.tools.dotc.profile.Profiler
import dotty.tools.dotc.util.SourceFile
import dotty.tools.pc.AutoImports.AutoImportEdits
import dotty.tools.pc.AutoImports.AutoImportsGenerator
@@ -75,7 +76,10 @@ class CompletionProvider(
val pos = driver.sourcePosition(params)
val (items, isIncomplete) = driver.compilationUnits.get(uri) match
case Some(unit) =>
val newctx = ctx.fresh.setCompilationUnit(unit).withPhase(Phases.typerPhase(using ctx))
val newctx = ctx.fresh
.setCompilationUnit(unit)
.setProfiler(Profiler()(using ctx))
.withPhase(Phases.typerPhase(using ctx))
val tpdPath0 = Interactive.pathTo(unit.tpdTree, pos.span)(using newctx)
val adjustedPath = Interactive.resolveTypedOrUntypedPath(tpdPath0, pos)(using newctx)