EditSession.cs 58.1 KB
Newer Older
1
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.
2 3 4 5 6

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
7
using System.Diagnostics.CodeAnalysis;
8 9 10 11 12 13
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Threading;
using System.Threading.Tasks;
14
using Microsoft.CodeAnalysis.Debugging;
15 16
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.ErrorReporting;
17
using Microsoft.CodeAnalysis.PooledObjects;
18
using Microsoft.CodeAnalysis.Text;
19 20
using Roslyn.Utilities;

21 22
#nullable enable

23 24
namespace Microsoft.CodeAnalysis.EditAndContinue
{
25
    internal sealed class EditSession : IDisposable
26
    {
27
        private readonly CancellationTokenSource _cancellationSource = new CancellationTokenSource();
28

29 30
        internal readonly DebuggingSession DebuggingSession;
        internal readonly EditSessionTelemetry Telemetry;
31

32
        private readonly ImmutableDictionary<ActiveMethodId, ImmutableArray<NonRemappableRegion>> _nonRemappableRegions;
33

34 35 36
        /// <summary>
        /// Lazily calculated map of base active statements.
        /// </summary>
37 38 39 40 41
        internal readonly AsyncLazy<ActiveStatementsMap> BaseActiveStatements;

        /// <summary>
        /// For each base active statement the exception regions around that statement. 
        /// </summary>
42
        internal ImmutableArray<ActiveStatementExceptionRegions> _lazyBaseActiveExceptionRegions;
43 44

        /// <summary>
45 46 47
        /// Results of changed documents analysis. 
        /// The work is triggered by an incremental analyzer on idle or explicitly when "continue" operation is executed.
        /// Contains analyses of the latest observed document versions.
48
        /// </summary>
49 50
        private readonly Dictionary<DocumentId, (Document Document, AsyncLazy<DocumentAnalysisResults> Results)> _analyses
            = new Dictionary<DocumentId, (Document, AsyncLazy<DocumentAnalysisResults>)>();
51
        private readonly object _analysesGuard = new object();
52

53 54 55 56 57 58 59 60 61 62 63
        /// <summary>
        /// Errors to be reported when a project is updated but the corresponding module does not support EnC.
        /// 
        /// The capability of a module to apply edits may change during edit session if the user attaches debugger to 
        /// an additional process that doesn't support EnC (or detaches from such process). The diagnostic reflects 
        /// the state of the module when queried for the first time. Before we actually apply an edit to the module 
        /// we need to query again instead of just reusing the diagnostic.
        /// </summary>
        private readonly Dictionary<Guid, ImmutableArray<LocationlessDiagnostic>> _moduleDiagnostics
             = new Dictionary<Guid, ImmutableArray<LocationlessDiagnostic>>();
        private readonly object _moduleDiagnosticsGuard = new object();
64

65 66 67 68 69 70 71
        /// <summary>
        /// A <see cref="DocumentId"/> is added whenever <see cref="EditAndContinueDiagnosticAnalyzer"/> reports 
        /// rude edits or module diagnostics. At the end of the session we ask the diagnostic analyzer to reanalyze 
        /// the documents to clean up the diagnostics.
        /// </summary>
        private readonly HashSet<DocumentId> _documentsWithReportedDiagnostics = new HashSet<DocumentId>();
        private readonly object _documentsWithReportedDiagnosticsGuard = new object();
72

73
        private bool _changesApplied;
D
dotnet-bot 已提交
74

75
        internal EditSession(DebuggingSession debuggingSession, EditSessionTelemetry telemetry)
76
        {
77 78
            DebuggingSession = debuggingSession;
            Telemetry = telemetry;
79
            _nonRemappableRegions = debuggingSession.NonRemappableRegions;
80 81 82 83

            BaseActiveStatements = new AsyncLazy<ActiveStatementsMap>(GetBaseActiveStatementsAsync, cacheResult: true);
        }

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
        internal CancellationToken CancellationToken => _cancellationSource.Token;
        internal void Cancel() => _cancellationSource.Cancel();

        public void Dispose()
        {
            _cancellationSource.Dispose();
        }

        internal void ModuleInstanceLoadedOrUnloaded(Guid mvid)
        {
            // invalidate diagnostic cache for the module:
            lock (_moduleDiagnosticsGuard)
            {
                _moduleDiagnostics.Remove(mvid);
            }
        }

        public ImmutableArray<LocationlessDiagnostic> GetModuleDiagnostics(Guid mvid, string projectDisplayName)
        {
            ImmutableArray<LocationlessDiagnostic> result;
            lock (_moduleDiagnosticsGuard)
            {
                if (_moduleDiagnostics.TryGetValue(mvid, out result))
                {
                    return result;
                }
            }

            var newResult = ImmutableArray<LocationlessDiagnostic>.Empty;
            if (!DebuggingSession.DebugeeModuleMetadataProvider.IsEditAndContinueAvailable(mvid, out var errorCode, out var localizedMessage))
            {
                var descriptor = EditAndContinueDiagnosticDescriptors.GetModuleDiagnosticDescriptor(errorCode);
                newResult = ImmutableArray.Create(new LocationlessDiagnostic(descriptor, new[] { projectDisplayName, localizedMessage }));
            }

            lock (_moduleDiagnosticsGuard)
            {
                if (!_moduleDiagnostics.TryGetValue(mvid, out result))
                {
                    _moduleDiagnostics.Add(mvid, result = newResult);
                }
            }

            return result;
        }

130 131 132 133
        private async Task<ActiveStatementsMap> GetBaseActiveStatementsAsync(CancellationToken cancellationToken)
        {
            try
            {
134 135
                // Last committed solution reflects the state of the source that is in sync with the binaries that are loaded in the debuggee.
                return CreateActiveStatementsMap(await DebuggingSession.ActiveStatementProvider.GetActiveStatementsAsync(cancellationToken).ConfigureAwait(false));
136 137 138 139
            }
            catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e))
            {
                return new ActiveStatementsMap(
D
dotnet-bot 已提交
140
                    SpecializedCollections.EmptyReadOnlyDictionary<DocumentId, ImmutableArray<ActiveStatement>>(),
141 142 143
                    SpecializedCollections.EmptyReadOnlyDictionary<ActiveInstructionId, ActiveStatement>());
            }
        }
D
dotnet-bot 已提交
144

145
        private ActiveStatementsMap CreateActiveStatementsMap(ImmutableArray<ActiveStatementDebugInfo> debugInfos)
146 147 148 149
        {
            var byDocument = PooledDictionary<DocumentId, ArrayBuilder<ActiveStatement>>.GetInstance();
            var byInstruction = PooledDictionary<ActiveInstructionId, ActiveStatement>.GetInstance();

150
            bool supportsEditAndContinue(DocumentId documentId)
151
                => EditAndContinueWorkspaceService.SupportsEditAndContinue(DebuggingSession.LastCommittedSolution.GetProject(documentId.ProjectId)!);
152 153 154

            foreach (var debugInfo in debugInfos)
            {
155
                var documentName = debugInfo.DocumentNameOpt;
156 157 158 159 160 161
                if (documentName == null)
                {
                    // Ignore active statements that do not have a source location.
                    continue;
                }

162
                var documentIds = DebuggingSession.LastCommittedSolution.GetDocumentIdsWithFilePath(documentName);
163
                var firstDocumentId = documentIds.FirstOrDefault(supportsEditAndContinue);
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
                if (firstDocumentId == null)
                {
                    // Ignore active statements that don't belong to the solution or language that supports EnC service.
                    continue;
                }

                if (!byDocument.TryGetValue(firstDocumentId, out var primaryDocumentActiveStatements))
                {
                    byDocument.Add(firstDocumentId, primaryDocumentActiveStatements = ArrayBuilder<ActiveStatement>.GetInstance());
                }

                var activeStatement = new ActiveStatement(
                    ordinal: byInstruction.Count,
                    primaryDocumentOrdinal: primaryDocumentActiveStatements.Count,
                    documentIds: documentIds,
                    flags: debugInfo.Flags,
                    span: GetUpToDateSpan(debugInfo),
                    instructionId: debugInfo.InstructionId,
                    threadIds: debugInfo.ThreadIds);

                primaryDocumentActiveStatements.Add(activeStatement);

                // TODO: associate only those documents that are from a project with the right module id
                // https://github.com/dotnet/roslyn/issues/24320
188
                for (var i = 1; i < documentIds.Length; i++)
189 190
                {
                    var documentId = documentIds[i];
191
                    if (!supportsEditAndContinue(documentId))
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
                    {
                        continue;
                    }

                    if (!byDocument.TryGetValue(documentId, out var linkedDocumentActiveStatements))
                    {
                        byDocument.Add(documentId, linkedDocumentActiveStatements = ArrayBuilder<ActiveStatement>.GetInstance());
                    }

                    linkedDocumentActiveStatements.Add(activeStatement);
                }

                try
                {
                    byInstruction.Add(debugInfo.InstructionId, activeStatement);
                }
                catch (ArgumentException)
                {
                    throw new InvalidOperationException($"Multiple active statements with the same instruction id returned by " +
211
                        $"{DebuggingSession.ActiveStatementProvider.GetType()}.{nameof(IActiveStatementProvider.GetActiveStatementsAsync)}");
212 213 214
                }
            }

215
            return new ActiveStatementsMap(byDocument.ToMultiDictionaryAndFree(), byInstruction.ToDictionaryAndFree());
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
        }

        private LinePositionSpan GetUpToDateSpan(ActiveStatementDebugInfo activeStatementInfo)
        {
            if ((activeStatementInfo.Flags & ActiveStatementFlags.MethodUpToDate) != 0)
            {
                return activeStatementInfo.LinePositionSpan;
            }

            // Map active statement spans in non-remappable regions to the latest source locations.
            if (_nonRemappableRegions.TryGetValue(activeStatementInfo.InstructionId.MethodId, out var regionsInMethod))
            {
                foreach (var region in regionsInMethod)
                {
                    if (region.Span.Contains(activeStatementInfo.LinePositionSpan))
                    {
                        return activeStatementInfo.LinePositionSpan.AddLineDelta(region.LineDelta);
                    }
                }
            }

            // The active statement is in a method that's not up-to-date but the active span have not changed.
            // We only add changed spans to non-remappable regions map, so we won't find unchanged span there.
            // Return the original span.
            return activeStatementInfo.LinePositionSpan;
        }

243 244 245 246 247
        /// <summary>
        /// Calculates exception regions for all active statements.
        /// If an active statement is in a document that's out-of-sync returns default(<see cref="ActiveStatementExceptionRegions"/>) for that statement.
        /// </summary>
        internal async Task<ImmutableArray<ActiveStatementExceptionRegions>> GetBaseActiveExceptionRegionsAsync(CancellationToken cancellationToken)
248 249 250
        {
            try
            {
251 252 253 254 255
                if (!_lazyBaseActiveExceptionRegions.IsDefault)
                {
                    return _lazyBaseActiveExceptionRegions;
                }

256 257
                var baseActiveStatements = await BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
                var instructionMap = baseActiveStatements.InstructionMap;
258
                using var builderDisposer = ArrayBuilder<ActiveStatementExceptionRegions>.GetInstance(instructionMap.Count, out var builder);
259 260
                builder.Count = instructionMap.Count;

261 262
                bool hasOutOfSyncDocuments = false;

263 264
                foreach (var activeStatement in instructionMap.Values)
                {
265 266
                    bool isCovered;
                    ImmutableArray<LinePositionSpan> exceptionRegions;
267

268 269 270 271
                    // Can't calculate exception regions for active statements in out-of-sync documents.
                    var (document, _) = await DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(activeStatement.PrimaryDocumentId, cancellationToken).ConfigureAwait(false);
                    if (document != null)
                    {
272 273
                        Debug.Assert(document.SupportsSyntaxTree);

274 275
                        var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
                        var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
276
                        Contract.ThrowIfNull(syntaxRoot);
277

278 279
                        // The analyzer service have to be available as we only track active statements in projects that support EnC.
                        var analyzer = document.Project.LanguageServices.GetRequiredService<IEditAndContinueAnalyzer>();
280 281 282 283 284 285 286 287 288 289
                        exceptionRegions = analyzer.GetExceptionRegions(sourceText, syntaxRoot, activeStatement.Span, activeStatement.IsNonLeaf, out isCovered);
                    }
                    else
                    {
                        // Document is either out-of-sync, design-time-only or missing from the baseline.
                        // If it's missing or design-time-only it can't have active statements.
                        hasOutOfSyncDocuments = true;
                        isCovered = false;
                        exceptionRegions = default;
                    }
290 291 292 293

                    builder[activeStatement.Ordinal] = new ActiveStatementExceptionRegions(exceptionRegions, isCovered);
                }

294 295 296 297 298 299 300 301 302
                var result = builder.ToImmutable();

                // Only cache results if no active statements are in out-of-sync documents.
                if (!hasOutOfSyncDocuments)
                {
                    ImmutableInterlocked.InterlockedInitialize(ref _lazyBaseActiveExceptionRegions, result);
                }

                return result;
303 304 305 306 307
            }
            catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e))
            {
                return ImmutableArray<ActiveStatementExceptionRegions>.Empty;
            }
308 309
        }

310
        private static async Task PopulateChangedAndAddedDocumentsAsync(CommittedSolution baseSolution, Project project, ArrayBuilder<Document> changedDocuments, ArrayBuilder<Document> addedDocuments, CancellationToken cancellationToken)
311
        {
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
            changedDocuments.Clear();
            addedDocuments.Clear();

            if (!EditAndContinueWorkspaceService.SupportsEditAndContinue(project))
            {
                return;
            }

            var baseProject = baseSolution.GetProject(project.Id);
            if (baseProject == project)
            {
                return;
            }

            // When debugging session is started some projects might not have been loaded to the workspace yet. 
            // We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied
            // and will result in source mismatch when the user steps into them.
            //
            // TODO (https://github.com/dotnet/roslyn/issues/1204):
            // hook up the debugger reported error, check that the project has not been loaded and report a better error.
            // Here, we assume these projects are not modified.
            if (baseProject == null)
            {
                EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not loaded", project.Id.DebugName, project.Id);
                return;
            }
338

339
            var changes = project.GetChanges(baseProject);
340
            foreach (var documentId in changes.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true))
341
            {
342
                var document = project.GetDocument(documentId)!;
343 344 345 346
                if (EditAndContinueWorkspaceService.IsDesignTimeOnlyDocument(document))
                {
                    continue;
                }
347

348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
                // Check if the currently observed document content has changed compared to the base document content.
                // This is an important optimization that aims to avoid IO while stepping in sources that have not changed.
                //
                // We may be comparing out-of-date committed document content but we only make a decision based on that content
                // if it matches the current content. If the current content is equal to baseline content that does not match
                // the debuggee then the workspace has not observed the change made to the file on disk since baseline was captured
                // (there had to be one as the content doesn't match). When we are about to apply changes it is ok to ignore this
                // document because the user does not see the change yet in the buffer (if the doc is open) and won't be confused
                // if it is not applied yet. The change will be applied later after it's observed by the workspace.
                var baseSource = await baseProject.GetDocument(documentId)!.GetTextAsync(cancellationToken).ConfigureAwait(false);
                var source = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
                if (baseSource.ContentEquals(source))
                {
                    continue;
                }

364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
                changedDocuments.Add(document);
            }

            foreach (var documentId in changes.GetAddedDocuments())
            {
                var document = project.GetDocument(documentId)!;
                if (EditAndContinueWorkspaceService.IsDesignTimeOnlyDocument(document))
                {
                    continue;
                }

                addedDocuments.Add(document);
            }
        }

        private async Task<(ImmutableArray<(Document Document, AsyncLazy<DocumentAnalysisResults> Results)>, ImmutableArray<Diagnostic> DocumentDiagnostics)> AnalyzeDocumentsAsync(
            ArrayBuilder<Document> changedDocuments, ArrayBuilder<Document> addedDocuments, CancellationToken cancellationToken)
        {
            var documentDiagnostics = ArrayBuilder<Diagnostic>.GetInstance();
            var builder = ArrayBuilder<(Document? Old, Document New)>.GetInstance();

            foreach (var document in changedDocuments)
            {
                var (oldDocument, oldDocumentState) = await DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(document.Id, cancellationToken, reloadOutOfSyncDocument: true).ConfigureAwait(false);
388 389 390 391
                switch (oldDocumentState)
                {
                    case CommittedSolution.DocumentState.DesignTimeOnly:
                        continue;
392

393
                    case CommittedSolution.DocumentState.Indeterminate:
394
                    case CommittedSolution.DocumentState.OutOfSync:
395 396 397
                        var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor((oldDocumentState == CommittedSolution.DocumentState.Indeterminate) ?
                            EditAndContinueErrorCode.UnableToReadSourceFileOrPdb : EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee);
                        documentDiagnostics.Add(Diagnostic.Create(descriptor, Location.Create(document.FilePath!, textSpan: default, lineSpan: default), new[] { document.FilePath }));
398
                        continue;
399

400
                    case CommittedSolution.DocumentState.MatchesBuildOutput:
401 402 403
                        // Include the document regardless of whether the module it was built into has been loaded or not.
                        // If the module has been built it might get loaded later during the debugging session,
                        // at which point we apply all changes that have been made to the project so far.
404
                        builder.Add((oldDocument, document));
405
                        break;
406 407 408

                    default:
                        throw ExceptionUtilities.UnexpectedValue(oldDocumentState);
409
                }
410
            }
411

412
            foreach (var document in addedDocuments)
413
            {
414
                builder.Add((null, document));
415
            }
416 417

            var result = ImmutableArray<(Document, AsyncLazy<DocumentAnalysisResults>)>.Empty;
418
            if (builder.Count != 0)
419
            {
420 421
                lock (_analysesGuard)
                {
422
                    result = builder.SelectAsArray(change => (change.New, GetDocumentAnalysisNoLock(change.Old, change.New)));
423
                }
424
            }
425

426 427
            builder.Free();
            return (result, documentDiagnostics.ToImmutableAndFree());
428 429
        }

430
        public AsyncLazy<DocumentAnalysisResults> GetDocumentAnalysis(Document? baseDocument, Document document)
431
        {
432
            lock (_analysesGuard)
433
            {
434
                return GetDocumentAnalysisNoLock(baseDocument, document);
435 436 437
            }
        }

438 439 440
        /// <summary>
        /// Returns a document analysis or kicks off a new one if one is not available for the specified document snapshot.
        /// </summary>
441
        /// <param name="baseDocument">Base document or null if the document did not exist in the baseline.</param>
442
        /// <param name="document">Document snapshot to analyze.</param>
443
        private AsyncLazy<DocumentAnalysisResults> GetDocumentAnalysisNoLock(Document? baseDocument, Document document)
444
        {
C
CyrusNajmabadi 已提交
445
            if (_analyses.TryGetValue(document.Id, out var analysis) && analysis.Document == document)
446 447 448 449
            {
                return analysis.Results;
            }

450
            var analyzer = document.Project.LanguageServices.GetRequiredService<IEditAndContinueAnalyzer>();
451 452 453 454 455 456

            var lazyResults = new AsyncLazy<DocumentAnalysisResults>(
                asynchronousComputeFunction: async cancellationToken =>
                {
                    try
                    {
457 458 459 460 461 462
                        var baseActiveStatements = await BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
                        if (!baseActiveStatements.DocumentMap.TryGetValue(document.Id, out var documentBaseActiveStatements))
                        {
                            documentBaseActiveStatements = ImmutableArray<ActiveStatement>.Empty;
                        }

463
                        var trackingService = DebuggingSession.Workspace.Services.GetService<IActiveStatementTrackingService>();
464

465
                        return await analyzer.AnalyzeDocumentAsync(baseDocument, documentBaseActiveStatements, document, trackingService, cancellationToken).ConfigureAwait(false);
466
                    }
B
beep boop 已提交
467
                    catch (Exception e) when (FatalError.ReportUnlessCanceled(e))
468 469 470
                    {
                        throw ExceptionUtilities.Unreachable;
                    }
B
beep boop 已提交
471
                },
472 473
                cacheResult: true);

474 475
            // TODO: this will replace potentially running analysis with another one.
            // Consider cancelling the replaced one.
476
            _analyses[document.Id] = (document, lazyResults);
477 478 479
            return lazyResults;
        }

480
        internal ImmutableArray<DocumentId> GetDocumentsWithReportedDiagnostics()
481
        {
482
            lock (_documentsWithReportedDiagnosticsGuard)
483
            {
484
                return ImmutableArray.CreateRange(_documentsWithReportedDiagnostics);
485 486 487
            }
        }

488
        internal void TrackDocumentWithReportedDiagnostics(DocumentId documentId)
489
        {
490
            lock (_documentsWithReportedDiagnosticsGuard)
491
            {
492 493 494
                _documentsWithReportedDiagnostics.Add(documentId);
            }
        }
495

496
        /// <summary>
497 498
        /// Determines whether projects contain any changes that might need to be applied.
        /// Checks only projects containing a given <paramref name="sourceFilePath"/> or all projects of the solution if <paramref name="sourceFilePath"/> is null.
499 500
        /// Invoked by the debugger on every step. It is critical for stepping performance that this method returns as fast as possible in absence of changes.
        /// </summary>
501
        public async Task<bool> HasChangesAsync(Solution solution, string? sourceFilePath, CancellationToken cancellationToken)
502 503 504 505
        {
            try
            {
                if (_changesApplied)
506
                {
507
                    return false;
508 509
                }

510 511
                var baseSolution = DebuggingSession.LastCommittedSolution;
                if (baseSolution.HasNoChanges(solution))
512
                {
513
                    return false;
514 515
                }

516 517
                var projects = (sourceFilePath == null) ? solution.Projects :
                    from documentId in solution.GetDocumentIdsWithFilePath(sourceFilePath)
518
                    select solution.GetDocument(documentId)!.Project;
519

520 521 522
                using var changedDocumentsDisposer = ArrayBuilder<Document>.GetInstance(out var changedDocuments);
                using var addedDocumentsDisposer = ArrayBuilder<Document>.GetInstance(out var addedDocuments);

523
                foreach (var project in projects)
524
                {
525 526
                    await PopulateChangedAndAddedDocumentsAsync(baseSolution, project, changedDocuments, addedDocuments, cancellationToken).ConfigureAwait(false);
                    if (changedDocuments.IsEmpty() && addedDocuments.IsEmpty())
527 528 529
                    {
                        continue;
                    }
530

531 532 533
                    // Check MVID before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID.
                    var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(project.Id, cancellationToken).ConfigureAwait(false);
                    if (mvidReadError != null)
534
                    {
535 536 537 538
                        // Can't read MVID. This might be an intermittent failure, so don't report it here.
                        // Report the project as containing changes, so that we proceed to EmitSolutionUpdateAsync where we report the error if it still persists.
                        EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not built", project.Id.DebugName, project.Id);
                        return true;
539
                    }
540

541
                    if (mvid == Guid.Empty)
542
                    {
543 544
                        // Project not built. We ignore any changes made in its sources.
                        EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not built", project.Id.DebugName, project.Id);
545 546
                        continue;
                    }
547

548 549
                    var (changedDocumentAnalyses, documentDiagnostics) = await AnalyzeDocumentsAsync(changedDocuments, addedDocuments, cancellationToken).ConfigureAwait(false);
                    if (documentDiagnostics.Any())
550 551
                    {
                        EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: out-of-sync documents present (diagnostic: '{2}')",
552
                            project.Id.DebugName, project.Id, documentDiagnostics[0]);
553

554 555 556 557
                        // Although we do not apply changes in out-of-sync/indeterminate documents we report that changes are present,
                        // so that the debugger triggers emit of updates. There we check if these documents are still in a bad state and report warnings
                        // that any changes in such documents are not applied.
                        return true;
558 559
                    }

560
                    var projectSummary = await GetProjectAnalysisSymmaryAsync(changedDocumentAnalyses, cancellationToken).ConfigureAwait(false);
561
                    if (projectSummary != ProjectAnalysisSummary.NoChanges)
562
                    {
563 564
                        EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: {2}", project.Id.DebugName, project.Id, projectSummary);
                        return true;
565
                    }
566
                }
567

568
                return false;
569 570 571 572 573 574 575 576
            }
            catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceledAndPropagate(e))
            {
                throw ExceptionUtilities.Unreachable;
            }
        }

        private async Task<ProjectAnalysisSummary> GetProjectAnalysisSymmaryAsync(
577
            ImmutableArray<(Document Document, AsyncLazy<DocumentAnalysisResults> Results)> documentAnalyses,
578 579 580 581 582 583 584 585 586 587 588 589 590
            CancellationToken cancellationToken)
        {
            bool hasChanges = false;
            bool hasSignificantValidChanges = false;

            foreach (var analysis in documentAnalyses)
            {
                var result = await analysis.Results.GetValueAsync(cancellationToken).ConfigureAwait(false);

                // skip documents that actually were not changed:
                if (!result.HasChanges)
                {
                    continue;
591 592
                }

593 594
                // rude edit detection wasn't completed due to errors in compilation:
                if (result.HasChangesAndCompilationErrors)
595
                {
596
                    return ProjectAnalysisSummary.CompilationErrors;
597 598
                }

599 600
                // rude edits detected:
                if (!result.RudeEditErrors.IsEmpty)
601 602 603 604
                {
                    return ProjectAnalysisSummary.RudeEdits;
                }

605 606
                hasChanges = true;
                hasSignificantValidChanges |= result.HasSignificantValidChanges;
607
            }
608 609

            if (!hasChanges)
610
            {
611 612
                // we get here if a document is closed and reopen without any actual change:
                return ProjectAnalysisSummary.NoChanges;
613
            }
614 615 616 617 618 619 620

            if (!hasSignificantValidChanges)
            {
                return ProjectAnalysisSummary.ValidInsignificantChanges;
            }

            return ProjectAnalysisSummary.ValidChanges;
B
beep boop 已提交
621
        }
622

623
        private static async Task<ProjectChanges> GetProjectChangesAsync(ImmutableArray<(Document Document, AsyncLazy<DocumentAnalysisResults> Results)> changedDocumentAnalyses, CancellationToken cancellationToken)
624 625 626
        {
            try
            {
627 628
                var allEdits = ArrayBuilder<SemanticEdit>.GetInstance();
                var allLineEdits = ArrayBuilder<(DocumentId, ImmutableArray<LineChange>)>.GetInstance();
629
                var activeStatementsInChangedDocuments = ArrayBuilder<(DocumentId, ImmutableArray<ActiveStatement>, ImmutableArray<ImmutableArray<LinePositionSpan>>)>.GetInstance();
630
                var allAddedSymbols = ArrayBuilder<ISymbol>.GetInstance();
631

632
                foreach (var (document, asyncResult) in changedDocumentAnalyses)
633
                {
634
                    var result = await asyncResult.GetValueAsync(cancellationToken).ConfigureAwait(false);
635

636 637 638 639 640
                    if (!result.HasSignificantValidChanges)
                    {
                        continue;
                    }

641 642 643 644
                    // we shouldn't be asking for deltas in presence of errors:
                    Debug.Assert(!result.HasChangesAndErrors);

                    allEdits.AddRange(result.SemanticEdits);
645 646 647 648 649 650 651 652 653 654 655 656

                    if (!result.HasChangesAndErrors)
                    {
                        foreach (var edit in result.SemanticEdits)
                        {
                            if (edit.Kind == SemanticEditKind.Insert)
                            {
                                allAddedSymbols.Add(edit.NewSymbol);
                            }
                        }
                    }

657 658
                    if (result.LineEdits.Length > 0)
                    {
659
                        allLineEdits.Add((document.Id, result.LineEdits));
660 661 662 663
                    }

                    if (result.ActiveStatements.Length > 0)
                    {
664
                        activeStatementsInChangedDocuments.Add((document.Id, result.ActiveStatements, result.ExceptionRegions));
665 666 667
                    }
                }

668 669 670 671 672 673 674 675
                var allAddedSymbolResult = allAddedSymbols.ToImmutableHashSet();
                allAddedSymbols.Free();

                return new ProjectChanges(
                    allEdits.ToImmutableAndFree(),
                    allLineEdits.ToImmutableAndFree(),
                    allAddedSymbolResult,
                    activeStatementsInChangedDocuments.ToImmutableAndFree());
676
            }
677
            catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceledAndPropagate(e))
678 679 680
            {
                throw ExceptionUtilities.Unreachable;
            }
B
beep boop 已提交
681
        }
682

683 684 685 686 687 688
        internal ImmutableArray<LocationlessDiagnostic> GetDebugeeStateDiagnostics()
        {
            return ImmutableArray<LocationlessDiagnostic>.Empty;
        }

        public async Task<SolutionUpdate> EmitSolutionUpdateAsync(Solution solution, CancellationToken cancellationToken)
689 690 691
        {
            try
            {
692 693 694 695 696 697
                using var deltasDisposer = ArrayBuilder<Deltas>.GetInstance(out var deltas);
                using var emitBaselinesDisposer = ArrayBuilder<(ProjectId, EmitBaseline)>.GetInstance(out var emitBaselines);
                using var readersDisposer = ArrayBuilder<IDisposable>.GetInstance(out var readers);
                using var diagnosticsDisposer = ArrayBuilder<(ProjectId, ImmutableArray<Diagnostic>)>.GetInstance(out var diagnostics);
                using var changedDocumentsDisposer = ArrayBuilder<Document>.GetInstance(out var changedDocuments);
                using var addedDocumentsDisposer = ArrayBuilder<Document>.GetInstance(out var addedDocuments);
698

699 700 701
                var baseSolution = DebuggingSession.LastCommittedSolution;

                bool isBlocked = false;
702 703
                foreach (var project in solution.Projects)
                {
704 705
                    await PopulateChangedAndAddedDocumentsAsync(baseSolution, project, changedDocuments, addedDocuments, cancellationToken).ConfigureAwait(false);
                    if (changedDocuments.IsEmpty() && addedDocuments.IsEmpty())
706 707 708 709
                    {
                        continue;
                    }

710 711
                    var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(project.Id, cancellationToken).ConfigureAwait(false);
                    if (mvidReadError != null)
712
                    {
713 714 715 716 717 718 719
                        // The error hasn't been reported by GetDocumentDiagnosticsAsync since it might have been intermittent.
                        // The MVID is required for emit so we consider the error permanent and report it here.
                        // Bail before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID.
                        diagnostics.Add((project.Id, ImmutableArray.Create(mvidReadError)));

                        Telemetry.LogProjectAnalysisSummary(ProjectAnalysisSummary.ValidChanges, ImmutableArray.Create(mvidReadError.Descriptor.Id));
                        isBlocked = true;
720 721 722
                        continue;
                    }

723
                    if (mvid == Guid.Empty)
724 725 726 727 728
                    {
                        EditAndContinueWorkspaceService.Log.Write("Emitting update of '{0}' [0x{1:X8}]: project not built", project.Id.DebugName, project.Id);
                        continue;
                    }

729 730 731 732 733 734 735 736 737 738 739 740 741 742
                    // Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync.
                    // Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by 
                    // incoming events updating the content of out-of-sync documents.
                    // 
                    // If in past we concluded that a document is out-of-sync, attempt to check one more time before we block apply.
                    // The source file content might have been updated since the last time we checked.
                    //
                    // TODO (investigate): https://github.com/dotnet/roslyn/issues/38866
                    // It is possible that the result of Rude Edit semantic analysis of an unchanged document will change if there
                    // another document is updated. If we encounter a significant case of this we should consider caching such a result per project,
                    // rather then per document. Also, we might be observing an older semantics if the document that is causing the change is out-of-sync --
                    // e.g. the binary was built with an overload C.M(object), but a generator updated class C to also contain C.M(string),
                    // which change we have not observed yet. Then call-sites of C.M in a changed document observed by the analysis will be seen as C.M(object) 
                    // instead of the true C.M(string).
743 744
                    var (changedDocumentAnalyses, documentDiagnostics) = await AnalyzeDocumentsAsync(changedDocuments, addedDocuments, cancellationToken).ConfigureAwait(false);
                    if (documentDiagnostics.Any())
745
                    {
746 747 748 749 750
                        // The diagnostic hasn't been reported by GetDocumentDiagnosticsAsync since out-of-sync documents are likely to be synchronized
                        // before the changes are attempted to be applied. If we still have any out-of-sync documents we report warnings and ignore changes in them.
                        // If in future the file is updated so that its content matches the PDB checksum, the document transitions to a matching state, 
                        // and we consider any further changes to it for application.
                        diagnostics.Add((project.Id, documentDiagnostics));
751 752
                    }

753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777
                    var projectSummary = await GetProjectAnalysisSymmaryAsync(changedDocumentAnalyses, cancellationToken).ConfigureAwait(false);

                    if (projectSummary != ProjectAnalysisSummary.ValidChanges)
                    {
                        Telemetry.LogProjectAnalysisSummary(projectSummary, ImmutableArray<string>.Empty);

                        if (projectSummary == ProjectAnalysisSummary.CompilationErrors || projectSummary == ProjectAnalysisSummary.RudeEdits)
                        {
                            isBlocked = true;
                        }

                        continue;
                    }

                    var moduleDiagnostics = GetModuleDiagnostics(mvid, project.Name);
                    if (!moduleDiagnostics.IsEmpty)
                    {
                        Telemetry.LogProjectAnalysisSummary(projectSummary, moduleDiagnostics.SelectAsArray(d => d.Descriptor.Id));
                        isBlocked = true;
                        continue;
                    }

                    var projectChanges = await GetProjectChangesAsync(changedDocumentAnalyses, cancellationToken).ConfigureAwait(false);
                    var currentCompilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
                    var baseActiveStatements = await BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
778

779 780 781
                    // project must support compilations since it supports EnC
                    Contract.ThrowIfNull(currentCompilation);

782 783 784 785
                    // Exception regions of active statements in changed documents are calculated (non-default),
                    // since we already checked that no changed document is out-of-sync above.
                    var baseActiveExceptionRegions = await GetBaseActiveExceptionRegionsAsync(cancellationToken).ConfigureAwait(false);

786
                    var lineEdits = projectChanges.LineChanges.SelectAsArray((lineChange, p) => (p.GetDocument(lineChange.DocumentId)!.FilePath, lineChange.Changes), project);
787 788 789 790 791

                    // Dispatch to a background thread - the compiler reads symbols and ISymUnmanagedReader requires MTA thread.
                    // We also don't want to block the UI thread - emit might perform IO.
                    if (Thread.CurrentThread.GetApartmentState() != ApartmentState.MTA)
                    {
792 793 794 795 796 797 798 799 800 801 802
                        await Task.Factory.StartNew(() =>
                        {
                            try
                            {
                                Emit();
                            }
                            catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceledAndPropagate(e))
                            {
                                throw ExceptionUtilities.Unreachable;
                            }
                        }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default).ConfigureAwait(false);
803 804 805 806 807 808 809 810 811 812 813 814
                    }
                    else
                    {
                        Emit();
                    }

                    void Emit()
                    {
                        Debug.Assert(Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA, "SymReader requires MTA");

                        var baseline = DebuggingSession.GetOrCreateEmitBaseline(project.Id, mvid);

815 816 817
                        // The metadata blob is guaranteed to not be disposed while "continue" operation is being executed.
                        // If it is disposed it means it had been disposed when "continue" operation started.
                        if (baseline == null || baseline.OriginalMetadata.IsDisposed)
818
                        {
819 820 821 822
                            // If we have no baseline the module has not been loaded yet.
                            // We need to create the baseline from compiler outputs.
                            var outputs = DebuggingSession.CompilationOutputsProvider.GetCompilationOutputs(project.Id);
                            if (CreateInitialBaselineForDeferredModuleUpdate(outputs, out var createBaselineDiagnostics, out baseline, out var debugInfoReaderProvider, out var metadataReaderProvider))
823
                            {
824 825
                                readers.Add(metadataReaderProvider);
                                readers.Add(debugInfoReaderProvider);
826 827 828
                            }
                            else
                            {
829 830
                                // Report diagnosics even when the module is never going to be loaded (e.g. in multi-targeting scenario, where only one framework being debugged).
                                // This is consistent with reporting compilation errors - the IDE reports them for all TFMs regardless of what framework the app is running on.
831 832
                                diagnostics.Add((project.Id, createBaselineDiagnostics));
                                Telemetry.LogProjectAnalysisSummary(projectSummary, createBaselineDiagnostics);
833
                                isBlocked = true;
834
                                return;
835
                            }
836 837 838 839 840 841 842 843 844
                        }

                        EditAndContinueWorkspaceService.Log.Write("Emitting update of '{0}' [0x{1:X8}]", project.Id.DebugName, project.Id);

                        using var pdbStream = SerializableBytes.CreateWritableStream();
                        using var metadataStream = SerializableBytes.CreateWritableStream();
                        using var ilStream = SerializableBytes.CreateWritableStream();

                        var updatedMethods = ImmutableArray.CreateBuilder<MethodDefinitionHandle>();
845

846 847
                        // TODO: ! should not be required (https://github.com/dotnet/roslyn/issues/38548)
                        var emitResult = currentCompilation!.EmitDifference(
848 849
                            baseline,
                            projectChanges.SemanticEdits,
850
                            projectChanges.AddedSymbols.Contains,
851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882
                            metadataStream,
                            ilStream,
                            pdbStream,
                            updatedMethods,
                            cancellationToken);

                        if (emitResult.Success)
                        {
                            var updatedMethodTokens = updatedMethods.SelectAsArray(h => MetadataTokens.GetToken(h));

                            // Determine all active statements whose span changed and exception region span deltas.
                            GetActiveStatementAndExceptionRegionSpans(
                                mvid,
                                baseActiveStatements,
                                baseActiveExceptionRegions,
                                updatedMethodTokens,
                                _nonRemappableRegions,
                                projectChanges.NewActiveStatements,
                                out var activeStatementsInUpdatedMethods,
                                out var nonRemappableRegions);

                            deltas.Add(new Deltas(
                                mvid,
                                ilStream.ToImmutableArray(),
                                metadataStream.ToImmutableArray(),
                                pdbStream.ToImmutableArray(),
                                updatedMethodTokens,
                                lineEdits,
                                nonRemappableRegions,
                                activeStatementsInUpdatedMethods));

                            emitBaselines.Add((project.Id, emitResult.Baseline));
883
                        }
884
                        else
885
                        {
886 887
                            // error
                            isBlocked = true;
888
                        }
889 890 891 892 893 894 895 896 897 898

                        // TODO: https://github.com/dotnet/roslyn/issues/36061
                        // We should only report diagnostics from emit phase.
                        // Syntax and semantic diagnostics are already reported by the diagnostic analyzer.
                        // Currently we do not have means to distinguish between diagnostics reported from compilation and emit phases.
                        // Querying diagnostics of the entire compilation or just the updated files migth be slow.
                        // In fact, it is desirable to allow emitting deltas for symbols affected by the change while allowing untouched
                        // method bodies to have errors.
                        diagnostics.Add((project.Id, emitResult.Diagnostics));
                        Telemetry.LogProjectAnalysisSummary(projectSummary, emitResult.Diagnostics);
899 900 901 902
                    }
                }

                if (isBlocked)
903
                {
904
                    foreach (var reader in readers)
905
                    {
906
                        reader.Dispose();
907 908
                    }

909
                    return SolutionUpdate.Blocked(diagnostics.ToImmutable());
910
                }
911 912 913

                return new SolutionUpdate(
                    (deltas.Count > 0) ? SolutionUpdateStatus.Ready : SolutionUpdateStatus.None,
914 915 916 917
                    deltas.ToImmutable(),
                    readers.ToImmutable(),
                    emitBaselines.ToImmutable(),
                    diagnostics.ToImmutable());
918
            }
919
            catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceledAndPropagate(e))
920 921 922 923 924
            {
                throw ExceptionUtilities.Unreachable;
            }
        }

925 926 927
        private static unsafe bool CreateInitialBaselineForDeferredModuleUpdate(
            CompilationOutputs compilationOutputs,
            out ImmutableArray<Diagnostic> diagnostics,
928 929 930
            [NotNullWhen(true)] out EmitBaseline? baseline,
            [NotNullWhen(true)] out DebugInformationReaderProvider? debugInfoReaderProvider,
            [NotNullWhen(true)] out MetadataReaderProvider? metadataReaderProvider)
931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993
        {
            // Since the module has not been loaded to the debuggee the debugger does not have its metadata or symbols available yet.
            // Read the metadata and symbols from the disk. Close the files as soon as we are done emitting the delta to minimize 
            // the time when they are being locked. Since we need to use the baseline that is produced by delta emit for the subsequent
            // delta emit we need to keep the module metadata and symbol info backing the symbols of the baseline alive in memory. 
            // Alternatively, we could drop the data once we are done with emitting the delta and re-emit the baseline again 
            // when we need it next time and the module is loaded.

            diagnostics = default;
            baseline = null;
            debugInfoReaderProvider = null;
            metadataReaderProvider = null;

            bool success = false;
            string fileBeingRead = compilationOutputs.PdbDisplayPath;
            try
            {
                debugInfoReaderProvider = compilationOutputs.OpenPdb();
                if (debugInfoReaderProvider == null)
                {
                    throw new FileNotFoundException();
                }

                var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader();

                fileBeingRead = compilationOutputs.AssemblyDisplayPath;

                metadataReaderProvider = compilationOutputs.OpenAssemblyMetadata(prefetch: true);
                if (metadataReaderProvider == null)
                {
                    throw new FileNotFoundException();
                }

                var metadataReader = metadataReaderProvider.GetMetadataReader();
                var moduleMetadata = ModuleMetadata.CreateFromMetadata((IntPtr)metadataReader.MetadataPointer, metadataReader.MetadataLength);

                baseline = EmitBaseline.CreateInitialBaseline(
                    moduleMetadata,
                    debugInfoReader.GetDebugInfo,
                    debugInfoReader.GetLocalSignature,
                    debugInfoReader.IsPortable);

                success = true;
            }
            catch (Exception e)
            {
                var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile);
                diagnostics = ImmutableArray.Create(Diagnostic.Create(descriptor, Location.None, new[] { fileBeingRead, e.Message }));
            }
            finally
            {
                if (!success)
                {
                    debugInfoReaderProvider?.Dispose();
                    metadataReaderProvider?.Dispose();
                }
            }

            return success;
        }

        // internal for testing
        internal static void GetActiveStatementAndExceptionRegionSpans(
994
            Guid moduleId,
D
dotnet-bot 已提交
995
            ActiveStatementsMap baseActiveStatements,
996
            ImmutableArray<ActiveStatementExceptionRegions> baseActiveExceptionRegions,
997
            ImmutableArray<int> updatedMethodTokens,
998 999 1000 1001 1002 1003 1004 1005
            ImmutableDictionary<ActiveMethodId, ImmutableArray<NonRemappableRegion>> previousNonRemappableRegions,
            ImmutableArray<(DocumentId DocumentId, ImmutableArray<ActiveStatement> ActiveStatements, ImmutableArray<ImmutableArray<LinePositionSpan>> ExceptionRegions)> newActiveStatementsInChangedDocuments,
            out ImmutableArray<(Guid ThreadId, ActiveInstructionId OldInstructionId, LinePositionSpan NewSpan)> activeStatementsInUpdatedMethods,
            out ImmutableArray<(ActiveMethodId Method, NonRemappableRegion Region)> nonRemappableRegions)
        {
            var changedNonRemappableSpans = PooledDictionary<(int MethodToken, int MethodVersion, LinePositionSpan BaseSpan), LinePositionSpan>.GetInstance();
            var activeStatementsInUpdatedMethodsBuilder = ArrayBuilder<(Guid, ActiveInstructionId, LinePositionSpan)>.GetInstance();
            var nonRemappableRegionsBuilder = ArrayBuilder<(ActiveMethodId Method, NonRemappableRegion Region)>.GetInstance();
1006

1007 1008 1009 1010 1011 1012 1013
            // Process active statements and their exception regions in changed documents of this project/module:
            foreach (var (documentId, newActiveStatements, newExceptionRegions) in newActiveStatementsInChangedDocuments)
            {
                var oldActiveStatements = baseActiveStatements.DocumentMap[documentId];
                Debug.Assert(oldActiveStatements.Length == newActiveStatements.Length);
                Debug.Assert(newActiveStatements.Length == newExceptionRegions.Length);

1014
                for (var i = 0; i < newActiveStatements.Length; i++)
1015 1016 1017 1018 1019 1020 1021
                {
                    var oldActiveStatement = oldActiveStatements[i];
                    var newActiveStatement = newActiveStatements[i];
                    var oldInstructionId = oldActiveStatement.InstructionId;
                    var methodToken = oldInstructionId.MethodId.Token;
                    var methodVersion = oldInstructionId.MethodId.Version;

1022
                    var isMethodUpdated = updatedMethodTokens.Contains(methodToken);
1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038
                    if (isMethodUpdated)
                    {
                        foreach (var threadId in oldActiveStatement.ThreadIds)
                        {
                            activeStatementsInUpdatedMethodsBuilder.Add((threadId, oldInstructionId, newActiveStatement.Span));
                        }
                    }

                    void AddNonRemappableRegion(LinePositionSpan oldSpan, LinePositionSpan newSpan, bool isExceptionRegion)
                    {
                        if (oldActiveStatement.IsMethodUpToDate)
                        {
                            // Start tracking non-remappable regions for active statements in methods that were up-to-date 
                            // when break state was entered and now being updated (regardless of whether the active span changed or not).
                            if (isMethodUpdated)
                            {
1039
                                var lineDelta = oldSpan.GetLineDelta(newSpan: newSpan);
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056
                                nonRemappableRegionsBuilder.Add((oldInstructionId.MethodId, new NonRemappableRegion(oldSpan, lineDelta, isExceptionRegion)));
                            }

                            // If the method has been up-to-date and it is not updated now then either the active statement span has not changed,
                            // or the entire method containing it moved. In neither case do we need to start tracking non-remapable region
                            // for the active statement since movement of whole method bodies (if any) is handled only on PDB level without 
                            // triggering any remapping on the IL level.
                        }
                        else if (oldSpan != newSpan)
                        {
                            // The method is not up-to-date hence we maintain non-remapable span map for it that needs to be updated.
                            changedNonRemappableSpans[(methodToken, methodVersion, oldSpan)] = newSpan;
                        }
                    }

                    AddNonRemappableRegion(oldActiveStatement.Span, newActiveStatement.Span, isExceptionRegion: false);

1057 1058 1059 1060
                    // The spans of the exception regions are known (non-default) for active statements in changed documents
                    // as we ensured earlier that all changed documents are in-sync. The outer loop only enumerates active 
                    // statements of changed documents, so the corresponding exception regions are initialized.

1061
                    var j = 0;
1062 1063 1064 1065
                    foreach (var oldSpan in baseActiveExceptionRegions[oldActiveStatement.Ordinal].Spans)
                    {
                        AddNonRemappableRegion(oldSpan, newExceptionRegions[oldActiveStatement.PrimaryDocumentOrdinal][j++], isExceptionRegion: true);
                    }
1066 1067
                }
            }
1068 1069 1070 1071 1072 1073

            activeStatementsInUpdatedMethods = activeStatementsInUpdatedMethodsBuilder.ToImmutableAndFree();

            // Gather all active method instances contained in this project/module that are not up-to-date:
            var unremappedActiveMethods = PooledHashSet<ActiveMethodId>.GetInstance();
            foreach (var (instruction, baseActiveStatement) in baseActiveStatements.InstructionMap)
1074
            {
1075 1076 1077 1078
                if (moduleId == instruction.MethodId.ModuleId && !baseActiveStatement.IsMethodUpToDate)
                {
                    unremappedActiveMethods.Add(instruction.MethodId);
                }
1079
            }
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118

            if (unremappedActiveMethods.Count > 0)
            {
                foreach (var (methodInstance, regionsInMethod) in previousNonRemappableRegions)
                {
                    // Skip non-remappable regions that belong to method instances that are from a different module 
                    // or no longer active (all active statements in these method instances have been remapped to newer versions).
                    if (!unremappedActiveMethods.Contains(methodInstance))
                    {
                        continue;
                    }

                    foreach (var region in regionsInMethod)
                    {
                        // We have calculated changes against a base snapshot (last break state):
                        var baseSpan = region.Span.AddLineDelta(region.LineDelta);

                        NonRemappableRegion newRegion;
                        if (changedNonRemappableSpans.TryGetValue((methodInstance.Token, methodInstance.Version, baseSpan), out var newSpan))
                        {
                            // all spans must be of the same size:
                            Debug.Assert(newSpan.End.Line - newSpan.Start.Line == baseSpan.End.Line - baseSpan.Start.Line);
                            Debug.Assert(region.Span.End.Line - region.Span.Start.Line == baseSpan.End.Line - baseSpan.Start.Line);

                            newRegion = region.WithLineDelta(region.Span.GetLineDelta(newSpan: newSpan));
                        }
                        else
                        {
                            newRegion = region;
                        }

                        nonRemappableRegionsBuilder.Add((methodInstance, newRegion));
                    }
                }
            }

            nonRemappableRegions = nonRemappableRegionsBuilder.ToImmutableAndFree();
            changedNonRemappableSpans.Free();
            unremappedActiveMethods.Free();
B
beep boop 已提交
1119
        }
1120

1121
        internal void ChangesApplied()
1122
        {
1123 1124
            Debug.Assert(!_changesApplied);
            _changesApplied = true;
1125
        }
1126 1127
    }
}