Skip to content

Commit a85da91

Browse files
committed
improvement: Add a debugging endpoint
1 parent 5457f2b commit a85da91

File tree

6 files changed

+301
-9
lines changed

6 files changed

+301
-9
lines changed

backend/src/main/scala/sbt/internal/inc/bloop/BloopZincCompiler.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import sbt.internal.inc.bloop.internal.BloopIncremental
2424
import sbt.internal.inc.bloop.internal.BloopLookup
2525
import sbt.internal.inc.bloop.internal.BloopStamps
2626
import sbt.util.InterfaceUtil
27-
import xsbti.AnalysisCallback
2827
import xsbti.VirtualFile
2928
import xsbti.compile._
3029

@@ -137,8 +136,7 @@ object BloopZincCompiler {
137136
val lookup = new BloopLookup(config, previousSetup, logger)
138137
val analysis = invalidateAnalysisFromSetup(config.currentSetup, previousSetup, setOfSources, prev, manager, logger)
139138

140-
// Scala needs the explicit type signature to infer the function type arguments
141-
val compile: (Set[VirtualFile], DependencyChanges, AnalysisCallback, ClassFileManager) => Task[Unit] = compiler.compile(_, _, _, _, cancelPromise)
139+
val compile = compiler.compile(_, _, _, _, cancelPromise)
142140
BloopIncremental
143141
.compile(
144142
setOfSources,

frontend/src/main/scala/bloop/bsp/BloopBspDefinitions.scala

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package bloop.bsp
22

3+
import ch.epfl.scala.bsp.BuildTargetIdentifier
34
import ch.epfl.scala.bsp.Uri
45

56
import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
@@ -41,4 +42,72 @@ object BloopBspDefinitions {
4142
StopClientCachingParams.codec,
4243
Endpoint.unitCodec
4344
)
45+
46+
// Incremental compilation debugging endpoint definitions
47+
final case class DebugIncrementalCompilationParams(
48+
targets: List[BuildTargetIdentifier]
49+
)
50+
51+
object DebugIncrementalCompilationParams {
52+
implicit val codec: JsonValueCodec[DebugIncrementalCompilationParams] =
53+
JsonCodecMaker.makeWithRequiredCollectionFields
54+
}
55+
56+
final case class IncrementalCompilationDebugInfo(
57+
target: BuildTargetIdentifier,
58+
analysisInfo: Option[AnalysisDebugInfo],
59+
allFileHashes: List[FileHashInfo],
60+
lastCompilationInfo: String,
61+
maybeFailedCompilation: String
62+
)
63+
64+
object IncrementalCompilationDebugInfo {
65+
implicit val codec: JsonValueCodec[IncrementalCompilationDebugInfo] =
66+
JsonCodecMaker.makeWithRequiredCollectionFields
67+
}
68+
69+
final case class AnalysisDebugInfo(
70+
lastModified: Long,
71+
sourceFiles: Int,
72+
classFiles: Int,
73+
internalDependencies: Int,
74+
externalDependencies: Int,
75+
location: Uri,
76+
excludedFiles: List[String]
77+
)
78+
79+
object AnalysisDebugInfo {
80+
implicit val codec: JsonValueCodec[AnalysisDebugInfo] =
81+
JsonCodecMaker.makeWithRequiredCollectionFields
82+
}
83+
84+
final case class FileHashInfo(
85+
uri: Uri,
86+
currentHash: Int,
87+
analysisHash: Option[Int],
88+
lastModified: Long,
89+
exists: Boolean
90+
)
91+
92+
object FileHashInfo {
93+
implicit val codec: JsonValueCodec[FileHashInfo] =
94+
JsonCodecMaker.makeWithRequiredCollectionFields
95+
}
96+
97+
final case class DebugIncrementalCompilationResult(
98+
debugInfos: List[IncrementalCompilationDebugInfo]
99+
)
100+
101+
object DebugIncrementalCompilationResult {
102+
implicit val codec: JsonValueCodec[DebugIncrementalCompilationResult] =
103+
JsonCodecMaker.makeWithRequiredCollectionFields
104+
}
105+
106+
object debugIncrementalCompilation
107+
extends Endpoint[DebugIncrementalCompilationParams, DebugIncrementalCompilationResult](
108+
"bloop/debugIncrementalCompilation"
109+
)(
110+
DebugIncrementalCompilationParams.codec,
111+
JsonCodecMaker.makeWithRequiredCollectionFields
112+
)
44113
}

frontend/src/main/scala/bloop/bsp/BloopBspServices.scala

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ import bloop.data.ClientInfo
4141
import bloop.data.ClientInfo.BspClientInfo
4242
import bloop.data.JdkConfig
4343
import bloop.data.Platform
44+
import bloop.data.Platform.Js
45+
import bloop.data.Platform.Jvm
46+
import bloop.data.Platform.Native
4447
import bloop.data.Project
4548
import bloop.data.WorkspaceSettings
4649
import bloop.engine.Aggregate
@@ -58,6 +61,7 @@ import bloop.engine.tasks.toolchains.ScalaNativeToolchain
5861
import bloop.exec.Forker
5962
import bloop.internal.build.BuildInfo
6063
import bloop.io.AbsolutePath
64+
import bloop.io.ByteHasher
6165
import bloop.io.Environment.lineSeparator
6266
import bloop.io.RelativePath
6367
import bloop.logging.BspServerLogger
@@ -70,22 +74,21 @@ import bloop.reporter.ReporterInputs
7074
import bloop.task.Task
7175
import bloop.testing.LoggingEventHandler
7276
import bloop.testing.TestInternals
77+
import bloop.util.JavaCompat._
7378
import bloop.util.JavaRuntime
7479

80+
import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
7581
import com.github.plokhotnyuk.jsoniter_scala.core._
82+
import com.github.plokhotnyuk.jsoniter_scala.core.readFromArray
7683
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
77-
import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
7884
import jsonrpc4s._
79-
import com.github.plokhotnyuk.jsoniter_scala.core.readFromArray
8085
import monix.execution.Cancelable
8186
import monix.execution.CancelablePromise
8287
import monix.execution.Scheduler
8388
import monix.execution.atomic.AtomicBoolean
8489
import monix.execution.atomic.AtomicInt
8590
import monix.reactive.subjects.BehaviorSubject
86-
import bloop.data.Platform.Js
87-
import bloop.data.Platform.Jvm
88-
import bloop.data.Platform.Native
91+
import sbt.internal.inc.PlainVirtualFileConverter
8992

9093
final class BloopBspServices(
9194
callSiteState: State,
@@ -150,6 +153,9 @@ final class BloopBspServices(
150153
.requestAsync(endpoints.BuildTarget.jvmTestEnvironment)(p => schedule(jvmTestEnvironment(p)))
151154
.requestAsync(endpoints.BuildTarget.jvmRunEnvironment)(p => schedule(jvmRunEnvironment(p)))
152155
.notificationAsync(BloopBspDefinitions.stopClientCaching)(p => stopClientCaching(p))
156+
.requestAsync(BloopBspDefinitions.debugIncrementalCompilation)(p =>
157+
schedule(debugIncrementalCompilation(p))
158+
)
153159

154160
// Internal state, initial value defaults to
155161
@volatile private var currentState: State = callSiteState
@@ -424,6 +430,138 @@ final class BloopBspServices(
424430
Task.eval { originToCompileStores.remove(params.originId); () }.executeAsync
425431
}
426432

433+
def debugIncrementalCompilation(
434+
params: BloopBspDefinitions.DebugIncrementalCompilationParams
435+
): BspEndpointResponse[BloopBspDefinitions.DebugIncrementalCompilationResult] = {
436+
def debugInfo(
437+
projects: Seq[ProjectMapping],
438+
state: State
439+
): BspResult[BloopBspDefinitions.DebugIncrementalCompilationResult] = {
440+
val debugInfos = projects.map {
441+
case (target, project) =>
442+
collectDebugInfo(target, project, state)
443+
}
444+
Task.sequence(debugInfos).map { debugInfos =>
445+
(state, Right(BloopBspDefinitions.DebugIncrementalCompilationResult(debugInfos.toList)))
446+
}
447+
}
448+
449+
ifInitialized(None) { (state: State, _: BspServerLogger) =>
450+
mapToProjects(params.targets, state) match {
451+
case Left(error) => Task.now((state, Left(Response.invalidRequest(error))))
452+
case Right(mappings) => debugInfo(mappings, state)
453+
}
454+
}
455+
}
456+
457+
private def collectDebugInfo(
458+
target: bsp.BuildTargetIdentifier,
459+
project: Project,
460+
state: State
461+
): Task[BloopBspDefinitions.IncrementalCompilationDebugInfo] = {
462+
val allSources = bloop.io.SourceHasher
463+
.findAndHashSourcesInProject(
464+
project,
465+
_ => Task.now(Nil),
466+
20,
467+
Promise[Unit](),
468+
ioScheduler,
469+
state.logger
470+
)
471+
.map(res => res.map(_.sortBy(_.source.id())))
472+
.executeOn(ioScheduler)
473+
allSources.map { allSources =>
474+
import bloop.bsp.BloopBspDefinitions._
475+
import java.nio.file.Files
476+
477+
val projectAnalysisFile = state.client
478+
.getUniqueClassesDirFor(project, forceGeneration = false)
479+
.resolve(s"../../${project.name}-analysis.bin")
480+
481+
val converter = PlainVirtualFileConverter.converter
482+
// Extract analysis info from successful compilation results
483+
val analysisInfo = state.results.lastSuccessfulResult(project) match {
484+
case Some(success) =>
485+
val maybeAnalysis = success.previous.analysis()
486+
val analysis = maybeAnalysis.toOption match {
487+
case Some(analysis: sbt.internal.inc.Analysis) => analysis
488+
case _ => sbt.internal.inc.Analysis.empty
489+
}
490+
val compilationInfo = analysis
491+
.readCompilations()
492+
.getAllCompilations
493+
.toList
494+
.map { compilation =>
495+
s" ${compilation.getStartTime} -> ${compilation.getOutput()}"
496+
}
497+
.mkString("\n")
498+
499+
val relations = analysis.relations
500+
val lastModifiedA =
501+
if (Files.exists(projectAnalysisFile.underlying))
502+
Files.getLastModifiedTime(projectAnalysisFile.underlying).toMillis()
503+
else 0L
504+
val changedSource = allSources match {
505+
case Left(_) => Nil
506+
case Right(value) =>
507+
value.filterNot { sourceHash =>
508+
success.sources.exists(_.source.id() == sourceHash.source.id())
509+
}
510+
}
511+
val hashes = success.sources.map { sourceHash =>
512+
val sourcePath = converter.toPath(sourceHash.source)
513+
val exists = Files.exists(sourcePath)
514+
val lastModified = if (exists) sourcePath.toFile.lastModified() else 0L
515+
val currentHash =
516+
if (exists) ByteHasher.hashFileContents(sourcePath.toFile)
517+
else 0
518+
519+
FileHashInfo(
520+
uri = bsp.Uri(sourcePath.toUri()),
521+
currentHash = currentHash,
522+
analysisHash = Some(sourceHash.hash),
523+
lastModified = lastModified,
524+
exists = exists
525+
)
526+
}
527+
val currentFailedResult = state.results.latestResult(project) match {
528+
case _: Compiler.Result.Success => ""
529+
case otherwise => otherwise.toString()
530+
}
531+
532+
val analysisInfo =
533+
AnalysisDebugInfo(
534+
lastModified = lastModifiedA,
535+
sourceFiles = analysis.readStamps.getAllSourceStamps.size(),
536+
classFiles = analysis.readStamps.getAllProductStamps.size(),
537+
internalDependencies = relations.allProducts.size,
538+
externalDependencies = relations.allLibraryDeps.size,
539+
location = bsp.Uri(projectAnalysisFile.toBspUri),
540+
excludedFiles = changedSource.map(_.source.toString())
541+
)
542+
Some(
543+
IncrementalCompilationDebugInfo(
544+
target = target,
545+
analysisInfo = Some(analysisInfo),
546+
allFileHashes = hashes.toList,
547+
lastCompilationInfo = compilationInfo,
548+
maybeFailedCompilation = currentFailedResult
549+
)
550+
)
551+
case _ => None
552+
}
553+
analysisInfo.getOrElse(
554+
IncrementalCompilationDebugInfo(
555+
target = target,
556+
analysisInfo = None,
557+
allFileHashes = Nil,
558+
lastCompilationInfo = "",
559+
maybeFailedCompilation = ""
560+
)
561+
)
562+
}
563+
}
564+
427565
def linkProjects(
428566
userProjects: Seq[ProjectMapping],
429567
state: State,

frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,18 @@ abstract class BspBaseSuite extends BaseSuite with BspClientTest {
518518
awaitForTask(jvmEnvironmentTask)
519519
}
520520

521+
def debugIncrementalCompilation(
522+
project: TestProject
523+
): (ManagedBspTestState, BloopBspDefinitions.DebugIncrementalCompilationResult) = {
524+
val debugTask = runAfterTargets(project) { target =>
525+
rpcRequest(
526+
BloopBspDefinitions.debugIncrementalCompilation,
527+
BloopBspDefinitions.DebugIncrementalCompilationParams(List(target))
528+
)
529+
}
530+
await(debugTask)
531+
}
532+
521533
def jvmTestEnvironment(
522534
project: TestProject,
523535
originId: Option[String]

frontend/src/test/scala/bloop/bsp/BspMetalsClientSpec.scala

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,81 @@ class BspMetalsClientSpec(
830830
}
831831
}
832832

833+
test("debugIncrementalCompilation-endpoint-succeeds") {
834+
TestUtil.withinWorkspace { workspace =>
835+
val sources = List(
836+
"""/Foo.scala
837+
|object Foo {
838+
| def main(args: Array[String]): Unit = {
839+
| println("Hello World")
840+
| }
841+
|}
842+
""".stripMargin
843+
)
844+
845+
val logger = new RecordingLogger(ansiCodesSupported = false)
846+
val A = TestProject(workspace, "a", sources)
847+
848+
loadBspState(workspace, List(A), logger) { state =>
849+
// First compile the project to generate incremental compilation data
850+
val compiled = state.compile(A)
851+
assertExitStatus(compiled, ExitStatus.Ok)
852+
853+
val (_, debugInfo) = compiled.debugIncrementalCompilation(A)
854+
855+
val debugInformation = debugInfo.debugInfos.head
856+
// Verify we get debug information
857+
assert(debugInformation.target == A.bspId)
858+
assert(debugInformation.lastCompilationInfo.nonEmpty)
859+
assert(debugInformation.analysisInfo.isDefined)
860+
assert(debugInformation.analysisInfo.get.classFiles == 2)
861+
assert(debugInformation.analysisInfo.get.sourceFiles == 1)
862+
assert(debugInformation.allFileHashes.size == 1)
863+
}
864+
}
865+
}
866+
867+
test("debugIncrementalCompilation-after-failure") {
868+
TestUtil.withinWorkspace { workspace =>
869+
val fooBefore =
870+
"""/Foo.scala
871+
|object Foo {
872+
| def main(args: Array[String]): Unit = {
873+
| println("Hello World")
874+
| }
875+
|}
876+
""".stripMargin
877+
val fooAfter =
878+
"""/Foo.scala
879+
|object Foo {
880+
| def main(args: Array[String]): Unit = {
881+
| val a: Int = String
882+
| }
883+
|}
884+
""".stripMargin
885+
886+
val logger = new RecordingLogger(ansiCodesSupported = false)
887+
val A = TestProject(workspace, "a", List(fooBefore))
888+
889+
loadBspState(workspace, List(A), logger) { state =>
890+
// First compile the project to generate incremental compilation data
891+
val compiled = state.compile(A)
892+
assertExitStatus(compiled, ExitStatus.Ok)
893+
assertIsFile(writeFile(A.srcFor("Foo.scala"), fooAfter))
894+
val compiled2 = state.compile(A)
895+
assertExitStatus(compiled2, ExitStatus.CompilationError)
896+
897+
val (_, debugInfo) = compiled.debugIncrementalCompilation(A)
898+
899+
val debugInformation = debugInfo.debugInfos.head
900+
// Verify we get debug information
901+
assert(debugInformation.maybeFailedCompilation.startsWith("Failed("))
902+
assert(debugInformation.analysisInfo.isDefined)
903+
assert(debugInformation.analysisInfo.get.excludedFiles.isEmpty)
904+
}
905+
}
906+
}
907+
833908
private val dummyFooScalaSources = List(
834909
"""/Foo.scala
835910
|class Foo

0 commit comments

Comments
 (0)