Skip to content
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

Update scala-debug-adpater to 4.x and add support for hot code replace #2276

Merged
merged 2 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
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
65 changes: 65 additions & 0 deletions backend/src/main/scala/bloop/ClientClassesObserver.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package bloop

import java.io.File
import java.util.concurrent.atomic.AtomicReference

import scala.jdk.CollectionConverters._

import bloop.io.AbsolutePath
import bloop.task.Task

import monix.reactive.Observable
import monix.reactive.subjects.PublishSubject
import sbt.internal.inc.PlainVirtualFileConverter
import xsbti.VirtualFileRef
import xsbti.compile.CompileAnalysis
import xsbti.compile.analysis.Stamp

/**
* Each time a new compile analysis is produced for a given client, it is given to
* the [[ClientClassObserver]] which computes the list of classes that changed or got created.
*
* A client can subscribe to the observer to get notified of classes to update.
* It is used by DAP to hot reload classes in the debuggee process.
*
* @param clientClassesDir the class directory for the client
*/
private[bloop] class ClientClassesObserver(val classesDir: AbsolutePath) {
private val converter = PlainVirtualFileConverter.converter
private val previousAnalysis: AtomicReference[CompileAnalysis] = new AtomicReference()
private val classesSubject: PublishSubject[Seq[String]] = PublishSubject()

def observable: Observable[Seq[String]] = classesSubject
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that worries me is that we will want to replace monix at some point. Do you know if there is a cats effect equivalent that we can use?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know for cats-effect but there is a reactivex equivalent and it already is a transitive dependency of Bloop brought by the scala-debug-adapter. Also I saw some other usages of monix obersvable/observers elsewhere in Bloop.


def nextAnalysis(analysis: CompileAnalysis): Task[Unit] = {
val prev = previousAnalysis.getAndSet(analysis)
if (prev != null && classesSubject.size > 0) {
Task {
val previousStamps = prev.readStamps.getAllProductStamps
analysis.readStamps.getAllProductStamps.asScala.iterator.collect {
case (vf, stamp) if isClassFile(vf) && isNewer(stamp, previousStamps.get(vf)) =>
getFullyQualifiedClassName(vf)
}.toSeq
}
.flatMap { classesToUpdate =>
Task.fromFuture(classesSubject.onNext(classesToUpdate)).map(_ => ())
}
} else Task.unit
}

private def isClassFile(vf: VirtualFileRef): Boolean = vf.id.endsWith(".class")

private def isNewer(current: Stamp, previous: Stamp): Boolean =
previous == null || {
val currentHash = current.getHash
val previousHash = previous.getHash
currentHash.isPresent &&
(!previousHash.isPresent || currentHash.get != previousHash.get)
}

private def getFullyQualifiedClassName(vf: VirtualFileRef): String = {
val path = converter.toPath(vf)
val relativePath = classesDir.underlying.relativize(path)
relativePath.toString.replace(File.separator, ".").stripSuffix(".class")
}
}
4 changes: 2 additions & 2 deletions backend/src/main/scala/bloop/CompileBackgroundTasks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import bloop.tracing.BraveTracer

abstract class CompileBackgroundTasks {
def trigger(
clientClassesDir: AbsolutePath,
clientClassesObserver: ClientClassesObserver,
clientReporter: Reporter,
clientTracer: BraveTracer,
clientLogger: Logger
Expand All @@ -20,7 +20,7 @@ object CompileBackgroundTasks {
val empty: CompileBackgroundTasks = {
new CompileBackgroundTasks {
def trigger(
clientClassesDir: AbsolutePath,
clientClassesObserver: ClientClassesObserver,
clientReporter: Reporter,
clientTracer: BraveTracer,
clientLogger: Logger
Expand Down
63 changes: 42 additions & 21 deletions backend/src/main/scala/bloop/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -452,11 +452,12 @@ object Compiler {

val backgroundTasks = new CompileBackgroundTasks {
def trigger(
clientClassesDir: AbsolutePath,
clientClassesObserver: ClientClassesObserver,
clientReporter: Reporter,
clientTracer: BraveTracer,
clientLogger: Logger
): Task[Unit] = Task.defer {
val clientClassesDir = clientClassesObserver.classesDir
clientLogger.debug(s"Triggering background tasks for $clientClassesDir")
val updateClientState =
updateExternalClassesDirWithReadOnly(clientClassesDir, clientTracer, clientLogger)
Expand All @@ -472,10 +473,20 @@ object Compiler {
}

val deleteNewClassesDir = Task(BloopPaths.delete(AbsolutePath(newClassesDir)))
val allTasks = List(deleteNewClassesDir, updateClientState, writeAnalysisIfMissing)
val publishClientAnalysis = Task {
rebaseAnalysisClassFiles(
analysis,
readOnlyClassesDir,
clientClassesDir.underlying,
sourcesWithFatal
)
}
.flatMap(clientClassesObserver.nextAnalysis)
Task
.gatherUnordered(allTasks)
.map(_ => ())
.gatherUnordered(
List(deleteNewClassesDir, updateClientState, writeAnalysisIfMissing)
)
.flatMap(_ => publishClientAnalysis)
.onErrorHandleWith(err => {
clientLogger.debug("Caught error in background tasks"); clientLogger.trace(err);
Task.raiseError(err)
Expand All @@ -495,14 +506,12 @@ object Compiler {
)
} else {
val allGeneratedProducts = allGeneratedRelativeClassFilePaths.toMap
val analysisForFutureCompilationRuns = {
rebaseAnalysisClassFiles(
analysis,
readOnlyClassesDir,
newClassesDir,
sourcesWithFatal
)
}
val analysisForFutureCompilationRuns = rebaseAnalysisClassFiles(
analysis,
readOnlyClassesDir,
newClassesDir,
sourcesWithFatal
)

val resultForFutureCompilationRuns = {
resultForDependentCompilationsInSameRun.withAnalysis(
Expand All @@ -517,12 +526,12 @@ object Compiler {
// Schedule the tasks to run concurrently after the compilation end
val backgroundTasksExecution = new CompileBackgroundTasks {
def trigger(
clientClassesDir: AbsolutePath,
clientClassesObserver: ClientClassesObserver,
clientReporter: Reporter,
clientTracer: BraveTracer,
clientLogger: Logger
): Task[Unit] = {
val clientClassesDirPath = clientClassesDir.toString
val clientClassesDir = clientClassesObserver.classesDir
val successBackgroundTasks =
backgroundTasksWhenNewSuccessfulAnalysis
.map(f => f(clientClassesDir, clientReporter, clientTracer))
Expand All @@ -543,15 +552,26 @@ object Compiler {
val syntax = path.syntax
if (syntax.startsWith(readOnlyClassesDirPath)) {
val rebasedFile = AbsolutePath(
syntax.replace(readOnlyClassesDirPath, clientClassesDirPath)
syntax.replace(readOnlyClassesDirPath, clientClassesDir.toString)
)
if (rebasedFile.exists) {
Files.delete(rebasedFile.underlying)
}
}
}
}
Task.gatherUnordered(List(firstTask, secondTask)).map(_ => ())

val publishClientAnalysis = Task {
rebaseAnalysisClassFiles(
analysis,
newClassesDir,
clientClassesDir.underlying,
sourcesWithFatal
)
}.flatMap(clientClassesObserver.nextAnalysis)
Task
.gatherUnordered(List(firstTask, secondTask))
.flatMap(_ => publishClientAnalysis)
}

allClientSyncTasks.doOnFinish(_ => Task(clientReporter.reportEndCompilation()))
Expand Down Expand Up @@ -691,11 +711,12 @@ object Compiler {
): CompileBackgroundTasks = {
new CompileBackgroundTasks {
def trigger(
clientClassesDir: AbsolutePath,
clientClassesObserver: ClientClassesObserver,
clientReporter: Reporter,
tracer: BraveTracer,
clientLogger: Logger
): Task[Unit] = {
val clientClassesDir = clientClassesObserver.classesDir
val backgroundTasks = tasks.map(f => f(clientClassesDir, clientReporter, tracer))
Task.gatherUnordered(backgroundTasks).memoize.map(_ => ())
}
Expand Down Expand Up @@ -783,19 +804,19 @@ object Compiler {
*/
def rebaseAnalysisClassFiles(
analysis0: CompileAnalysis,
readOnlyClassesDir: Path,
newClassesDir: Path,
origin: Path,
target: Path,
sourceFilesWithFatalWarnings: scala.collection.Set[File]
): Analysis = {
// Cast to the only internal analysis that we support
val analysis = analysis0.asInstanceOf[Analysis]
def rebase(file: VirtualFileRef): VirtualFileRef = {

val filePath = converter.toPath(file).toAbsolutePath()
if (!filePath.startsWith(readOnlyClassesDir)) file
if (!filePath.startsWith(origin)) file
else {
// Hash for class file is the same because the copy duplicates metadata
val path = newClassesDir.resolve(readOnlyClassesDir.relativize(filePath))
val path = target.resolve(origin.relativize(filePath))
converter.toVirtualFile(path)
}
}
Expand Down
7 changes: 2 additions & 5 deletions frontend/src/main/scala/bloop/bsp/BloopBspServices.scala
Original file line number Diff line number Diff line change
Expand Up @@ -603,10 +603,7 @@ final class BloopBspServices(
params: bsp.DebugSessionParams
): BspEndpointResponse[bsp.DebugSessionAddress] = {

def inferDebuggee(
projects: Seq[Project],
state: State
): BspResponse[Debuggee] = {
def inferDebuggee(projects: Seq[Project], state: State): BspResponse[Debuggee] = {
def convert[A: JsonValueCodec](
f: A => Either[String, Debuggee]
): Either[Response.Error, Debuggee] = {
Expand Down Expand Up @@ -647,7 +644,7 @@ final class BloopBspServices(
BloopDebuggeeRunner.forTestSuite(projects, testClasses, state, ioScheduler)
})
case Some(bsp.DebugSessionParamsDataKind.ScalaAttachRemote) =>
Right(BloopDebuggeeRunner.forAttachRemote(state, ioScheduler, projects))
Right(BloopDebuggeeRunner.forAttachRemote(projects, state, ioScheduler))
case dataKind => Left(Response.invalidRequest(s"Unsupported data kind: $dataKind"))
}
}
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/main/scala/bloop/dap/BloopDebugToolsResolver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,12 @@ class BloopDebugToolsResolver(logger: Logger) extends DebugToolsResolver {
}
}

override def resolveStepFilter(scalaVersion: ScalaVersion): Try[ClassLoader] = {
getOrTryUpdate(stepFilterCache, scalaVersion) {
val stepFilterModule = s"${BuildInfo.scala3StepFilterName}_${scalaVersion.binaryVersion}"
val stepFilter = Artifact(BuildInfo.organization, stepFilterModule, BuildInfo.version)
val tastyCore = Artifact("org.scala-lang", "tasty-core_3", scalaVersion.value)
override def resolveDecoder(scalaVersion: ScalaVersion): Try[ClassLoader] = {
getOrTryUpdate(decoderCache, scalaVersion) {
val decoderModule = s"${BuildInfo.decoderName}_${scalaVersion.binaryVersion}"
val artifact = Artifact(BuildInfo.organization, decoderModule, BuildInfo.version)
DependencyResolution
.resolveWithErrors(List(stepFilter, tastyCore), logger)
.resolveWithErrors(List(artifact), logger)
.map(jars => toClassLoader(jars, true))
.toTry
}
Expand All @@ -66,5 +65,5 @@ class BloopDebugToolsResolver(logger: Logger) extends DebugToolsResolver {

object BloopDebugToolsResolver {
private val expressionCompilerCache: mutable.Map[ScalaVersion, ClassLoader] = mutable.Map.empty
private val stepFilterCache: mutable.Map[ScalaVersion, ClassLoader] = mutable.Map.empty
private val decoderCache: mutable.Map[ScalaVersion, ClassLoader] = mutable.Map.empty
}
Loading
Loading