File: Services\AssetSynchronization\RemoteAssetSynchronizationService.cs
Web Access
Project: src\src\Workspaces\Remote\ServiceHub\Microsoft.CodeAnalysis.Remote.ServiceHub.csproj (Microsoft.CodeAnalysis.Remote.ServiceHub)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using RoslynLogger = Microsoft.CodeAnalysis.Internal.Log.Logger;
 
namespace Microsoft.CodeAnalysis.Remote;
 
/// <summary>
/// This service is used by the SolutionChecksumUpdater to proactively update the solution snapshot in the
/// out-of-process workspace. We do this to limit the amount of time required to synchronize a solution over after
/// an edit once a feature is asking for a snapshot.
/// </summary>
internal sealed class RemoteAssetSynchronizationService(in BrokeredServiceBase.ServiceConstructionArguments arguments)
    : BrokeredServiceBase(in arguments), IRemoteAssetSynchronizationService
{
    internal sealed class Factory : FactoryBase<IRemoteAssetSynchronizationService>
    {
        protected override IRemoteAssetSynchronizationService CreateService(in ServiceConstructionArguments arguments)
            => new RemoteAssetSynchronizationService(in arguments);
    }
 
    public ValueTask SynchronizePrimaryWorkspaceAsync(Checksum solutionChecksum, CancellationToken cancellationToken)
    {
        return RunServiceAsync(async cancellationToken =>
        {
            using (RoslynLogger.LogBlock(FunctionId.RemoteHostService_SynchronizePrimaryWorkspaceAsync, Checksum.GetChecksumLogInfo, solutionChecksum, cancellationToken))
            {
                var workspace = GetWorkspace();
                var assetProvider = workspace.CreateAssetProvider(solutionChecksum, WorkspaceManager.SolutionAssetCache, SolutionAssetSource);
                await workspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, cancellationToken).ConfigureAwait(false);
            }
        }, cancellationToken);
    }
 
    public ValueTask SynchronizeActiveDocumentAsync(DocumentId? documentId, CancellationToken cancellationToken)
    {
        var documentTrackingService = GetWorkspace().Services.GetRequiredService<IDocumentTrackingService>() as RemoteDocumentTrackingService;
        documentTrackingService?.SetActiveDocument(documentId);
        return ValueTaskFactory.CompletedTask;
    }
 
    public ValueTask SynchronizeTextChangesAsync(
        ImmutableArray<(DocumentId documentId, Checksum baseTextChecksum, ImmutableArray<TextChange> textChanges, Checksum newTextChecksum)> changes,
        CancellationToken cancellationToken)
    {
        return RunServiceAsync(async cancellationToken =>
        {
            var workspace = GetWorkspace();
 
            using (RoslynLogger.LogBlock(FunctionId.RemoteHostService_SynchronizeTextAsync, cancellationToken))
            {
                foreach (var (documentId, baseTextChecksum, textChanges, newTextChecksum) in changes)
                {
                    // Try to get the text associated with baseTextChecksum
                    var text = await TryGetSourceTextAsync(WorkspaceManager, workspace, documentId, baseTextChecksum, cancellationToken).ConfigureAwait(false);
                    if (text == null)
                    {
                        // it won't bring in base text if it is not there already.
                        // text needed will be pulled in when there is request
                        continue;
                    }
 
                    // Now attempt to manually apply the edit, producing the new forked text.  Store that directly in
                    // the asset cache so that future calls to retrieve it can do so quickly, without synchronizing over
                    // the entire document.
                    var newText = text.WithChanges(textChanges);
                    var newSerializableText = new SerializableSourceText(newText, newTextChecksum);
 
                    WorkspaceManager.SolutionAssetCache.GetOrAdd(newSerializableText.ContentChecksum, newSerializableText);
                }
            }
 
            return;
 
            async static Task<SourceText?> TryGetSourceTextAsync(
                RemoteWorkspaceManager workspaceManager,
                Workspace workspace,
                DocumentId documentId,
                Checksum baseTextChecksum,
                CancellationToken cancellationToken)
            {
                // check the cheap and fast one first.
                // see if the cache has the source text
                if (workspaceManager.SolutionAssetCache.TryGetAsset<SerializableSourceText>(baseTextChecksum, out var serializableSourceText))
                {
                    return await serializableSourceText.GetTextAsync(cancellationToken).ConfigureAwait(false);
                }
 
                // do slower one
                // check whether existing solution has it
                var document = workspace.CurrentSolution.GetDocument(documentId);
                if (document == null)
                {
                    return null;
                }
 
                // check checksum whether it is there.
                // since we lazily synchronize whole solution (SynchronizePrimaryWorkspaceAsync) when things are idle,
                // soon or later this will get hit even if text changes got out of sync due to issues in VS side
                // such as file is first opened and there is no SourceText in memory yet.
                if (!document.State.TryGetStateChecksums(out var state) ||
                    !state.Text.Equals(baseTextChecksum))
                {
                    return null;
                }
 
                return await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
            }
        }, cancellationToken);
    }
}