@@ -82,7 +82,7 @@ internal sealed class DebuggingSession : IDisposable
8282 /// read lock is acquired before every operation that may access a baseline module/symbol reader
8383 /// and write lock when the baseline readers are being disposed.
8484 /// </summary>
85- private readonly ReaderWriterLockSlim _baselineAccessLock = new ( ) ;
85+ private readonly ReaderWriterLockSlim _baselineContentAccessLock = new ( ) ;
8686 private bool _isDisposed ;
8787
8888 internal EditSession EditSession { get ; private set ; }
@@ -168,7 +168,7 @@ public void Dispose()
168168 _cancellationSource . Dispose ( ) ;
169169
170170 // Wait for all operations on baseline to finish before we dispose the readers.
171- _baselineAccessLock . EnterWriteLock ( ) ;
171+ _baselineContentAccessLock . EnterWriteLock ( ) ;
172172
173173 lock ( _projectEmitBaselinesGuard )
174174 {
@@ -179,8 +179,8 @@ public void Dispose()
179179 }
180180 }
181181
182- _baselineAccessLock . ExitWriteLock ( ) ;
183- _baselineAccessLock . Dispose ( ) ;
182+ _baselineContentAccessLock . ExitWriteLock ( ) ;
183+ _baselineContentAccessLock . Dispose ( ) ;
184184
185185 if ( Interlocked . Exchange ( ref _pendingUpdate , null ) != null )
186186 {
@@ -312,7 +312,7 @@ internal ImmutableList<ProjectBaseline> GetOrCreateEmitBaselines(
312312 ArrayBuilder < Diagnostic > diagnostics ,
313313 out ReaderWriterLockSlim baselineAccessLock )
314314 {
315- baselineAccessLock = _baselineAccessLock ;
315+ baselineAccessLock = _baselineContentAccessLock ;
316316
317317 ImmutableList < ProjectBaseline > ? existingBaselines ;
318318 lock ( _projectEmitBaselinesGuard )
@@ -521,53 +521,74 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
521521 // Make sure the solution snapshot has all source-generated documents up-to-date.
522522 solution = solution . WithUpToDateSourceGeneratorDocuments ( solution . ProjectIds ) ;
523523
524- var solutionUpdate = await EditSession . EmitSolutionUpdateAsync ( solution , activeStatementSpanProvider , updateId , cancellationToken ) . ConfigureAwait ( false ) ;
524+ var solutionUpdate = await EditSession . EmitSolutionUpdateAsync ( solution , activeStatementSpanProvider , updateId , runningProjects , cancellationToken ) . ConfigureAwait ( false ) ;
525+
526+ var allowPartialUpdates = runningProjects . Any ( p => p . Value . AllowPartialUpdate ) ;
525527
526528 solutionUpdate . Log ( SessionLog , updateId ) ;
527529 _lastModuleUpdatesLog = solutionUpdate . ModuleUpdates . Updates ;
528530
529531 switch ( solutionUpdate . ModuleUpdates . Status )
530532 {
531533 case ModuleUpdateStatus . Ready :
532- // We have updates to be applied. The debugger will call Commit/Discard on the solution
534+ Contract . ThrowIfTrue ( solutionUpdate . ModuleUpdates . Updates . IsEmpty && solutionUpdate . ProjectsToRebuild . IsEmpty ) ;
535+
536+ // We have updates to be applied or processes to restart. The debugger will call Commit/Discard on the solution
533537 // based on whether the updates will be applied successfully or not.
534- StorePendingUpdate ( new PendingSolutionUpdate (
535- solution ,
536- solutionUpdate . ProjectsToStale ,
537- solutionUpdate . ProjectBaselines ,
538- solutionUpdate . ModuleUpdates . Updates ,
539- solutionUpdate . NonRemappableRegions ) ) ;
538+
539+ if ( allowPartialUpdates )
540+ {
541+ StorePendingUpdate ( new PendingSolutionUpdate (
542+ solution ,
543+ solutionUpdate . ProjectsToStale ,
544+ solutionUpdate . ProjectsToRebuild ,
545+ solutionUpdate . ProjectBaselines ,
546+ solutionUpdate . ModuleUpdates . Updates ,
547+ solutionUpdate . NonRemappableRegions ) ) ;
548+ }
549+ else if ( solutionUpdate . ProjectsToRebuild . IsEmpty )
550+ {
551+ // no rude edits
552+
553+ StorePendingUpdate ( new PendingSolutionUpdate (
554+ solution ,
555+ solutionUpdate . ProjectsToStale ,
556+ // if partial updates are not allowed we don't treat rebuild as part of solution update:
557+ projectsToRebuild : [ ] ,
558+ solutionUpdate . ProjectBaselines ,
559+ solutionUpdate . ModuleUpdates . Updates ,
560+ solutionUpdate . NonRemappableRegions ) ) ;
561+ }
540562
541563 break ;
542564
543565 case ModuleUpdateStatus . None :
544566 Contract . ThrowIfFalse ( solutionUpdate . ModuleUpdates . Updates . IsEmpty ) ;
545567 Contract . ThrowIfFalse ( solutionUpdate . NonRemappableRegions . IsEmpty ) ;
546568
569+ // Insignificant changes should not cause rebuilds/restarts:
570+ Contract . ThrowIfFalse ( solutionUpdate . ProjectsToRestart . IsEmpty ) ;
571+ Contract . ThrowIfFalse ( solutionUpdate . ProjectsToRebuild . IsEmpty ) ;
572+
547573 // No significant changes have been made.
548574 // Commit the solution to apply any insignificant changes that do not generate updates.
549575 LastCommittedSolution . CommitChanges ( solution , projectsToStale : solutionUpdate . ProjectsToStale , projectsToUnstale : [ ] ) ;
550576 break ;
551577 }
552578
553- EmitSolutionUpdateResults . GetProjectsToRebuildAndRestart (
554- solution ,
555- solutionUpdate . ModuleUpdates ,
556- solutionUpdate . Diagnostics ,
557- runningProjects ,
558- out var projectsToRestart ,
559- out var projectsToRebuild ) ;
560-
561579 // Note that we may return empty deltas if all updates have been deferred.
562580 // The debugger will still call commit or discard on the update batch.
563581 return new EmitSolutionUpdateResults ( )
564582 {
565583 Solution = solution ,
566- ModuleUpdates = solutionUpdate . ModuleUpdates ,
584+ // If partial updates are disabled the debugger does not expect module updates when rude edits are reported:
585+ ModuleUpdates = allowPartialUpdates || solutionUpdate . ProjectsToRebuild . IsEmpty
586+ ? solutionUpdate . ModuleUpdates
587+ : new ModuleUpdates ( solutionUpdate . ModuleUpdates . Status , [ ] ) ,
567588 Diagnostics = solutionUpdate . Diagnostics ,
568589 SyntaxError = solutionUpdate . SyntaxError ,
569- ProjectsToRestart = projectsToRestart ,
570- ProjectsToRebuild = projectsToRebuild
590+ ProjectsToRestart = solutionUpdate . ProjectsToRestart ,
591+ ProjectsToRebuild = solutionUpdate . ProjectsToRebuild
571592 } ;
572593 }
573594
@@ -576,6 +597,7 @@ public void CommitSolutionUpdate()
576597 ThrowIfDisposed ( ) ;
577598
578599 ImmutableDictionary < ManagedMethodId , ImmutableArray < NonRemappableRegion > > ? newNonRemappableRegions = null ;
600+ using var _ = PooledHashSet < ProjectId > . GetInstance ( out var projectsToRebuildTransitive ) ;
579601
580602 var pendingUpdate = RetrievePendingUpdate ( ) ;
581603 if ( pendingUpdate is PendingSolutionUpdate pendingSolutionUpdate )
@@ -591,18 +613,44 @@ from region in moduleRegions.Regions
591613 if ( newNonRemappableRegions . IsEmpty )
592614 newNonRemappableRegions = null ;
593615
594- LastCommittedSolution . CommitChanges ( pendingSolutionUpdate . Solution , projectsToStale : pendingSolutionUpdate . ProjectsToStale , projectsToUnstale : [ ] ) ;
616+ var solution = pendingSolutionUpdate . Solution ;
617+
618+ // Once the project is rebuilt all its dependencies are going to be up-to-date.
619+ var dependencyGraph = solution . GetProjectDependencyGraph ( ) ;
620+ foreach ( var projectId in pendingSolutionUpdate . ProjectsToRebuild )
621+ {
622+ projectsToRebuildTransitive . Add ( projectId ) ;
623+ projectsToRebuildTransitive . AddRange ( dependencyGraph . GetProjectsThatThisProjectTransitivelyDependsOn ( projectId ) ) ;
624+ }
625+
626+ // Unstale all projects that will be up-to-date after rebuild.
627+ LastCommittedSolution . CommitChanges ( solution , projectsToStale : pendingSolutionUpdate . ProjectsToStale , projectsToUnstale : projectsToRebuildTransitive ) ;
628+
629+ foreach ( var projectId in projectsToRebuildTransitive )
630+ {
631+ _editSessionTelemetry . LogUpdatedBaseline ( solution . GetRequiredProject ( projectId ) . State . ProjectInfo . Attributes . TelemetryId ) ;
632+ }
595633 }
596634
597635 // update baselines:
636+
637+ // Wait for all operations on baseline content to finish before we dispose the readers.
638+ _baselineContentAccessLock . EnterWriteLock ( ) ;
639+
598640 lock ( _projectEmitBaselinesGuard )
599641 {
600642 foreach ( var updatedBaseline in pendingUpdate . ProjectBaselines )
601643 {
602644 _projectBaselines [ updatedBaseline . ProjectId ] = [ .. _projectBaselines [ updatedBaseline . ProjectId ] . Select ( existingBaseline => existingBaseline . ModuleId == updatedBaseline . ModuleId ? updatedBaseline : existingBaseline ) ] ;
603645 }
646+
647+ // Discard any open baseline readers for projects that need to be rebuilt,
648+ // so that the build can overwrite the underlying files.
649+ DiscardProjectBaselinesNoLock ( projectsToRebuildTransitive ) ;
604650 }
605651
652+ _baselineContentAccessLock . ExitWriteLock ( ) ;
653+
606654 _editSessionTelemetry . LogCommitted ( ) ;
607655
608656 // Restart edit session with no active statements (switching to run mode).
@@ -615,6 +663,28 @@ public void DiscardSolutionUpdate()
615663 _ = RetrievePendingUpdate ( ) ;
616664 }
617665
666+ private void DiscardProjectBaselinesNoLock ( IEnumerable < ProjectId > projects )
667+ {
668+ foreach ( var projectId in projects )
669+ {
670+ if ( _projectBaselines . TryGetValue ( projectId , out var projectBaselines ) )
671+ {
672+ // remove all versions of modules associated with the project:
673+ _projectBaselines . Remove ( projectId ) ;
674+
675+ foreach ( var projectBaseline in projectBaselines )
676+ {
677+ var ( metadata , pdb ) = _initialBaselineModuleReaders [ projectBaseline . ModuleId ] ;
678+ metadata . Dispose ( ) ;
679+ pdb . Dispose ( ) ;
680+
681+ _initialBaselineModuleReaders . Remove ( projectBaseline . ModuleId ) ;
682+ }
683+ }
684+ }
685+ }
686+
687+ // TODO: remove once the debugger implements https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2459003
618688 public void UpdateBaselines ( Solution solution , ImmutableArray < ProjectId > rebuiltProjects )
619689 {
620690 ThrowIfDisposed ( ) ;
@@ -625,30 +695,15 @@ public void UpdateBaselines(Solution solution, ImmutableArray<ProjectId> rebuilt
625695 LastCommittedSolution . CommitChanges ( solution , projectsToStale : [ ] , projectsToUnstale : rebuiltProjects ) ;
626696
627697 // Wait for all operations on baseline to finish before we dispose the readers.
628- _baselineAccessLock . EnterWriteLock ( ) ;
698+
699+ _baselineContentAccessLock . EnterWriteLock ( ) ;
629700
630701 lock ( _projectEmitBaselinesGuard )
631702 {
632- foreach ( var projectId in rebuiltProjects )
633- {
634- if ( _projectBaselines . TryGetValue ( projectId , out var projectBaselines ) )
635- {
636- // remove all versions of modules associated with the project:
637- _projectBaselines . Remove ( projectId ) ;
638-
639- foreach ( var projectBaseline in projectBaselines )
640- {
641- var ( metadata , pdb ) = _initialBaselineModuleReaders [ projectBaseline . ModuleId ] ;
642- metadata . Dispose ( ) ;
643- pdb . Dispose ( ) ;
644-
645- _initialBaselineModuleReaders . Remove ( projectBaseline . ModuleId ) ;
646- }
647- }
648- }
703+ DiscardProjectBaselinesNoLock ( rebuiltProjects ) ;
649704 }
650705
651- _baselineAccessLock . ExitWriteLock ( ) ;
706+ _baselineContentAccessLock . ExitWriteLock ( ) ;
652707
653708 foreach ( var projectId in rebuiltProjects )
654709 {
0 commit comments