diff --git a/eng/Versions.props b/eng/Versions.props index d7fba73fd3d35..04b7536fee788 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -62,7 +62,7 @@ $(MicrosoftBuildPackagesVersion) 5.7.0 - 16.9.63 + 16.10.11-alpha true + 9 + + + + + + + + + + + + + + + + + + + + diff --git a/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs b/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs index 34a6dc8f009dc..bb36ed4e25064 100644 --- a/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs +++ b/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs @@ -5,16 +5,21 @@ #nullable disable using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using AnalyzerRunner; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Diagnosers; +using Microsoft.Build.Locator; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.NavigateTo; using Microsoft.CodeAnalysis.Storage; @@ -24,62 +29,78 @@ namespace IdeCoreBenchmarks [MemoryDiagnoser] public class NavigateToBenchmarks { - private readonly string _solutionPath; - - private MSBuildWorkspace _workspace; - - public NavigateToBenchmarks() - { - var roslynRoot = Environment.GetEnvironmentVariable(Program.RoslynRootPathEnvVariableName); - _solutionPath = Path.Combine(roslynRoot, @"C:\github\roslyn\Compilers.sln"); - - if (!File.Exists(_solutionPath)) - throw new ArgumentException("Couldn't find Roslyn.sln"); - - Console.Write("Found roslyn.sln"); - } - - [GlobalSetup] - public void Setup() - { - _workspace = AnalyzerRunnerHelper.CreateWorkspace(); - if (_workspace == null) - throw new ArgumentException("Couldn't create workspace"); - - _workspace.TryApplyChanges(_workspace.CurrentSolution.WithOptions(_workspace.Options - .WithChangedOption(StorageOptions.Database, StorageDatabase.SQLite))); - - Console.WriteLine("Opening roslyn"); - var start = DateTime.Now; - _ = _workspace.OpenSolutionAsync(_solutionPath, progress: null, CancellationToken.None).Result; - Console.WriteLine("Finished opening roslyn: " + (DateTime.Now - start)); - - var storageService = _workspace.Services.GetService(); - if (storageService == null) - throw new ArgumentException("Couldn't get storage service"); - - // Force a storage instance to be created. This makes it simple to go examine it prior to any operations we - // perform, including seeing how big the initial string table is. - using var storage = storageService.GetStorageAsync(_workspace.CurrentSolution, CancellationToken.None).AsTask().GetAwaiter().GetResult(); - } - - [GlobalCleanup] - public void Cleanup() - { - _workspace?.Dispose(); - _workspace = null; - } - [Benchmark] public async Task RunNavigateTo() { - var solution = _workspace.CurrentSolution; - // Search each project with an independent threadpool task. - var searchTasks = solution.Projects.Select( - p => Task.Run(() => SearchAsync(p, priorityDocuments: ImmutableArray.Empty), CancellationToken.None)).ToArray(); - - var result = await Task.WhenAll(searchTasks).ConfigureAwait(false); - var sum = result.Sum(); + try + { + // QueryVisualStudioInstances returns Visual Studio installations on .NET Framework, and .NET Core SDK + // installations on .NET Core. We use the one with the most recent version. + var msBuildInstance = MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(x => x.Version).First(); + + MSBuildLocator.RegisterInstance(msBuildInstance); + + var roslynRoot = Environment.GetEnvironmentVariable(Program.RoslynRootPathEnvVariableName); + var solutionPath = Path.Combine(roslynRoot, @"C:\github\roslyn\Roslyn.sln"); + + if (!File.Exists(solutionPath)) + throw new ArgumentException("Couldn't find Roslyn.sln"); + + Console.Write("Found Roslyn.sln: " + Process.GetCurrentProcess().Id); + + var assemblies = MSBuildMefHostServices.DefaultAssemblies + .Add(typeof(AnalyzerRunnerHelper).Assembly) + .Add(typeof(FindReferencesBenchmarks).Assembly); + var services = MefHostServices.Create(assemblies); + + var workspace = MSBuildWorkspace.Create(new Dictionary + { + // Use the latest language version to force the full set of available analyzers to run on the project. + { "LangVersion", "9.0" }, + }, services); + + if (workspace == null) + throw new ArgumentException("Couldn't create workspace"); + + workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options + .WithChangedOption(StorageOptions.Database, StorageDatabase.SQLite) + .WithChangedOption(StorageOptions.DatabaseMustSucceed, true))); + + Console.WriteLine("Opening roslyn. Attach to: " + Process.GetCurrentProcess().Id); + + var start = DateTime.Now; + var solution = workspace.OpenSolutionAsync(solutionPath, progress: null, CancellationToken.None).Result; + Console.WriteLine("Finished opening roslyn: " + (DateTime.Now - start)); + + // Force a storage instance to be created. This makes it simple to go examine it prior to any operations we + // perform, including seeing how big the initial string table is. + var storageService = workspace.Services.GetService(); + if (storageService == null) + throw new ArgumentException("Couldn't get storage service"); + + using (var storage = await storageService.GetStorageAsync(workspace.CurrentSolution, CancellationToken.None)) + { + Console.WriteLine(); + } + + Console.WriteLine("Starting navigate to"); + + start = DateTime.Now; + // Search each project with an independent threadpool task. + var searchTasks = solution.Projects.Select( + p => Task.Run(() => SearchAsync(p, priorityDocuments: ImmutableArray.Empty), CancellationToken.None)).ToArray(); + + var result = await Task.WhenAll(searchTasks).ConfigureAwait(false); + var sum = result.Sum(); + + start = DateTime.Now; + Console.WriteLine("Num results: " + (DateTime.Now - start)); + } + catch (ReflectionTypeLoadException ex) + { + foreach (var ex2 in ex.LoaderExceptions) + Console.WriteLine(ex2); + } } private async Task SearchAsync(Project project, ImmutableArray priorityDocuments) diff --git a/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj b/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj index fcdff9c634dcb..3f6bde5d966f7 100644 --- a/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj +++ b/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj @@ -52,6 +52,7 @@ + diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs index 4c54f7cca1358..d51cb83ca8fd7 100644 --- a/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs @@ -12,9 +12,11 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PersistentStorage; using Microsoft.CodeAnalysis.Storage; using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.VisualStudio.LanguageServices.UnitTests; using Roslyn.Test.Utilities; using Xunit; @@ -74,6 +76,9 @@ protected AbstractPersistentStorageTests() ThreadPool.SetMinThreads(Math.Max(workerThreads, NumThreads), completionPortThreads); } + internal abstract AbstractPersistentStorageService GetStorageService( + OptionSet options, IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector, string rootFolder); + public void Dispose() { // This should cause the service to release the cached connection it maintains for the primary workspace @@ -248,24 +253,6 @@ public async Task PersistentService_Document_SimultaneousWrites() Assert.True(value < NumThreads); } - private void DoSimultaneousWrites(Func write) - { - var barrier = new Barrier(NumThreads); - var countdown = new CountdownEvent(NumThreads); - for (var i = 0; i < NumThreads; i++) - { - ThreadPool.QueueUserWorkItem(s => - { - var id = (int)s; - barrier.SignalAndWait(); - write(id + "").Wait(); - countdown.Signal(); - }, i); - } - - countdown.Wait(); - } - [Theory] [CombinatorialData] public async Task PersistentService_Solution_SimultaneousReads(Size size, bool withChecksum) @@ -843,13 +830,45 @@ private void DoSimultaneousReads(Func> read, string expectedValue) Assert.Equal(new List(), exceptions); } + private void DoSimultaneousWrites(Func write) + { + var barrier = new Barrier(NumThreads); + var countdown = new CountdownEvent(NumThreads); + + var exceptions = new List(); + for (var i = 0; i < NumThreads; i++) + { + ThreadPool.QueueUserWorkItem(s => + { + var id = (int)s; + barrier.SignalAndWait(); + try + { + write(id + "").Wait(); + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + } + countdown.Signal(); + }, i); + } + + countdown.Wait(); + + Assert.Empty(exceptions); + } + protected Solution CreateOrOpenSolution(bool nullPaths = false) { var solutionFile = _persistentFolder.CreateOrOpenFile("Solution1.sln").WriteAllText(""); var info = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Create(), solutionFile.Path); - var workspace = new AdhocWorkspace(); + var workspace = new AdhocWorkspace(VisualStudioTestCompositions.LanguageServices.GetHostServices()); workspace.AddSolution(info); var solution = workspace.CurrentSolution; @@ -876,13 +895,15 @@ internal async Task GetStorageAsync( _storageService?.GetTestAccessor().Shutdown(); var locationService = new MockPersistentStorageLocationService(solution.Id, _persistentFolder.Path); - _storageService = GetStorageService((IMefHostExportProvider)solution.Workspace.Services.HostServices, locationService, faultInjector); + _storageService = GetStorageService( + solution.Options, (IMefHostExportProvider)solution.Workspace.Services.HostServices, + locationService, faultInjector, _persistentFolder.Path); var storage = await _storageService.GetStorageAsync(solution, checkBranchId: true, CancellationToken.None); // If we're injecting faults, we expect things to be strange if (faultInjector == null) { - Assert.NotEqual(NoOpPersistentStorage.Instance, storage); + Assert.NotEqual(NoOpPersistentStorage.TestAccessor.StorageInstance, storage); } return storage; @@ -895,13 +916,14 @@ internal async Task GetStorageFromKeyAsync( _storageService?.GetTestAccessor().Shutdown(); var locationService = new MockPersistentStorageLocationService(solutionKey.Id, _persistentFolder.Path); - _storageService = GetStorageService((IMefHostExportProvider)workspace.Services.HostServices, locationService, faultInjector); + _storageService = GetStorageService( + workspace.Options, (IMefHostExportProvider)workspace.Services.HostServices, locationService, faultInjector, _persistentFolder.Path); var storage = await _storageService.GetStorageAsync(workspace, solutionKey, checkBranchId: true, CancellationToken.None); // If we're injecting faults, we expect things to be strange if (faultInjector == null) { - Assert.NotEqual(NoOpPersistentStorage.Instance, storage); + Assert.NotEqual(NoOpPersistentStorage.TestAccessor.StorageInstance, storage); } return storage; @@ -927,8 +949,6 @@ public MockPersistentStorageLocationService(SolutionId solutionId, string storag => solutionKey.Id == _solutionId ? _storageLocation : null; } - internal abstract AbstractPersistentStorageService GetStorageService(IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector); - protected Stream EncodeString(string text) { var bytes = _encoding.GetBytes(text); @@ -940,14 +960,9 @@ private string ReadStringToEnd(Stream stream) { using (stream) { - var bytes = new byte[stream.Length]; - var count = 0; - while (count < stream.Length) - { - count = stream.Read(bytes, count, (int)stream.Length - count); - } - - return _encoding.GetString(bytes); + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + return _encoding.GetString(memoryStream.ToArray()); } } } diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/CloudCachePersistentStorageTests.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/CloudCachePersistentStorageTests.cs new file mode 100644 index 0000000000000..feb9fbd8b2fdd --- /dev/null +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/CloudCachePersistentStorageTests.cs @@ -0,0 +1,39 @@ +// 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; +using System.Linq; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Storage; +using Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks; + +namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices +{ + public class CloudCachePersistentStorageTests : AbstractPersistentStorageTests + { + internal override AbstractPersistentStorageService GetStorageService( + OptionSet options, IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector, string relativePathBase) + { + var threadingContext = exportProvider.GetExports().Single().Value; + return new MockCloudCachePersistentStorageService( + locationService, + relativePathBase, + cs => + { + if (cs is IAsyncDisposable asyncDisposable) + { + threadingContext.JoinableTaskFactory.Run( + () => asyncDisposable.DisposeAsync().AsTask()); + } + else if (cs is IDisposable disposable) + { + disposable.Dispose(); + } + }); + } + } +} diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/AuthorizationServiceMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/AuthorizationServiceMock.cs new file mode 100644 index 0000000000000..405d6af003f63 --- /dev/null +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/AuthorizationServiceMock.cs @@ -0,0 +1,36 @@ +// 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. + +// Copy of https://devdiv.visualstudio.com/DevDiv/_git/VS.CloudCache?path=%2Ftest%2FMicrosoft.VisualStudio.Cache.Tests%2FMocks&_a=contents&version=GBmain +// Try to keep in sync and avoid unnecessary changes here. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ServiceHub.Framework.Services; + +#pragma warning disable CS0067 // events that are never used + +namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks +{ + internal class AuthorizationServiceMock : IAuthorizationService + { + public event EventHandler? CredentialsChanged; + + public event EventHandler? AuthorizationChanged; + + internal bool Allow { get; set; } = true; + + public ValueTask CheckAuthorizationAsync(ProtectedOperation operation, CancellationToken cancellationToken = default) + { + return new ValueTask(this.Allow); + } + + public ValueTask> GetCredentialsAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/FileSystemServiceMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/FileSystemServiceMock.cs new file mode 100644 index 0000000000000..1e1481d0aef5d --- /dev/null +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/FileSystemServiceMock.cs @@ -0,0 +1,89 @@ +// 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. + +// Copy of https://devdiv.visualstudio.com/DevDiv/_git/VS.CloudCache?path=%2Ftest%2FMicrosoft.VisualStudio.Cache.Tests%2FMocks&_a=contents&version=GBmain +// Try to keep in sync and avoid unnecessary changes here. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.RpcContracts.FileSystem; + +namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks +{ + internal class FileSystemServiceMock : IFileSystem + { + public event EventHandler? DirectoryEntryChanged; + + public event EventHandler? RootEntriesChanged; + + public Task ConvertLocalFileNameToRemoteUriAsync(string fileName, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ConvertLocalFileNameToRemoteUriAsync(string fileName, string remoteScheme, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ConvertLocalUriToRemoteUriAsync(Uri localUri, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ConvertLocalUriToRemoteUriAsync(Uri localUri, string remoteScheme, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ConvertRemoteFileNameToRemoteUriAsync(string fileName, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ConvertRemoteFileNameToRemoteUriAsync(string fileName, string remoteScheme, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ConvertRemoteUriToLocalUriAsync(Uri remoteUri, CancellationToken cancellationToken) => Task.FromResult(remoteUri); + + public Task ConvertRemoteUriToRemoteFileNameAsync(Uri remoteUri, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task CopyAsync(Uri sourceUri, Uri destinationUri, bool overwrite, IProgress? progress, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task CreateDirectoryAsync(Uri uri, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task DeleteAsync(Uri uri, bool recursive, IProgress? progress, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task DownloadFileAsync(Uri remoteUri, IProgress? progress, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public IAsyncEnumerable EnumerateDirectoriesAsync(Uri uri, string searchPattern, SearchOption searchOption, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public IAsyncEnumerable EnumerateDirectoryEntriesAsync(Uri uri, string searchPattern, SearchOption searchOption, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public IAsyncEnumerable EnumerateFilesAsync(Uri uri, string searchPattern, SearchOption searchOption, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task GetDefaultRemoteUriSchemeAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task GetDisplayInfoAsync(Uri uri, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task GetDisplayInfoAsync(string fileName, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task GetInfoAsync(Uri uri, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task GetMonikerForFileSystemProviderAsync(string scheme, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task GetMonikerForRemoteFileSystemProviderAsync(string scheme, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> GetRootEntriesAsync(string scheme, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> GetRootEntriesAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> GetSupportedSchemesAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task MoveAsync(Uri oldUri, Uri newUri, bool overwrite, IProgress? progress, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ReadFileAsync(Uri uri, PipeWriter writer, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public ValueTask UnwatchAsync(WatchResult watchResult, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public ValueTask WatchDirectoryAsync(Uri uri, bool recursive, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public ValueTask WatchFileAsync(Uri uri, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task WriteFileAsync(Uri uri, PipeReader reader, bool overwrite, CancellationToken cancellationToken) => throw new NotImplementedException(); + + protected virtual void OnDirectoryEntryChanged(DirectoryEntryChangedEventArgs args) => this.DirectoryEntryChanged?.Invoke(this, args); + + protected virtual void OnRootEntriesChanged(RootEntriesChangedEventArgs args) => this.RootEntriesChanged?.Invoke(this, args); + } +} diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/MockCloudCachePersistentStorageService.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/MockCloudCachePersistentStorageService.cs new file mode 100644 index 0000000000000..894eb5b9f9f26 --- /dev/null +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/MockCloudCachePersistentStorageService.cs @@ -0,0 +1,61 @@ +// 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; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.ServiceHub.Framework; +using Microsoft.ServiceHub.Framework.Services; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Cache; +using Microsoft.VisualStudio.Cache.SQLite; +using Microsoft.VisualStudio.LanguageServices.Storage; +using Microsoft.VisualStudio.RpcContracts.Caching; + +namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks +{ + internal class MockCloudCachePersistentStorageService : AbstractCloudCachePersistentStorageService + { + private readonly string _relativePathBase; + private readonly Action _disposeCacheService; + + public MockCloudCachePersistentStorageService( + IPersistentStorageLocationService locationService, + string relativePathBase, + Action disposeCacheService) + : base(locationService) + { + _relativePathBase = relativePathBase; + _disposeCacheService = disposeCacheService; + } + + protected override void DisposeCacheService(ICacheService cacheService) + => _disposeCacheService(cacheService); + + protected override async ValueTask CreateCacheServiceAsync(CancellationToken cancellationToken) + { + // Directly access VS' CacheService through their library and not as a brokered service. Then create our + // wrapper CloudCacheService directly on that instance. + var authorizationServiceClient = new AuthorizationServiceClient(new AuthorizationServiceMock()); + var solutionService = new SolutionServiceMock(); + var fileSystem = new FileSystemServiceMock(); + var serviceBroker = new ServiceBrokerMock() + { + BrokeredServices = + { + { VisualStudioServices.VS2019_10.SolutionService.Moniker, solutionService }, + { VisualStudioServices.VS2019_10.FileSystem.Moniker, fileSystem }, + { FrameworkServices.Authorization.Moniker, new AuthorizationServiceMock() }, + }, + }; + + var someContext = new CacheContext { RelativePathBase = _relativePathBase }; + var pool = new SqliteConnectionPool(); + var activeContext = await pool.ActivateContextAsync(someContext, default); + var cacheService = new CacheService(activeContext, serviceBroker, authorizationServiceClient, pool); + return cacheService; + } + } +} diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/OptionServiceMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/OptionServiceMock.cs similarity index 97% rename from src/VisualStudio/CSharp/Test/PersistentStorage/OptionServiceMock.cs rename to src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/OptionServiceMock.cs index 0af88b3beef93..caa140bd3db5b 100644 --- a/src/VisualStudio/CSharp/Test/PersistentStorage/OptionServiceMock.cs +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/OptionServiceMock.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Options; -namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices +namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks { internal class OptionServiceMock : IOptionService { diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/ServiceBrokerMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/ServiceBrokerMock.cs new file mode 100644 index 0000000000000..4d271102005dd --- /dev/null +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/ServiceBrokerMock.cs @@ -0,0 +1,41 @@ +// 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. + +// Copy of https://devdiv.visualstudio.com/DevDiv/_git/VS.CloudCache?path=%2Ftest%2FMicrosoft.VisualStudio.Cache.Tests%2FMocks&_a=contents&version=GBmain +// Try to keep in sync and avoid unnecessary changes here. + +using System; +using System.Collections.Generic; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ServiceHub.Framework; + +namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks +{ + internal class ServiceBrokerMock : IServiceBroker + { + public event EventHandler? AvailabilityChanged; + + internal Dictionary BrokeredServices { get; } = new(); + + public ValueTask GetPipeAsync(ServiceMoniker serviceMoniker, ServiceActivationOptions options = default, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask GetProxyAsync(ServiceRpcDescriptor serviceDescriptor, ServiceActivationOptions options = default, CancellationToken cancellationToken = default) + where T : class + { + if (this.BrokeredServices.TryGetValue(serviceDescriptor.Moniker, out object? service)) + { + return new((T?)service); + } + + return default; + } + + internal void OnAvailabilityChanged(BrokeredServicesChangedEventArgs args) => this.AvailabilityChanged?.Invoke(this, args); + } +} diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/SolutionServiceMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/SolutionServiceMock.cs new file mode 100644 index 0000000000000..0819ce75e36df --- /dev/null +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/SolutionServiceMock.cs @@ -0,0 +1,104 @@ +// 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. + +// Copy of https://devdiv.visualstudio.com/DevDiv/_git/VS.CloudCache?path=%2Ftest%2FMicrosoft.VisualStudio.Cache.Tests%2FMocks&_a=contents&version=GBmain +// Try to keep in sync and avoid unnecessary changes here. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.VisualStudio.RpcContracts.Solution; +using Microsoft.VisualStudio.Threading; + +#pragma warning disable CS0067 // events that are never used + +namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks +{ + internal class SolutionServiceMock : ISolutionService + { + private readonly BroadcastObservable openContainerObservable = new BroadcastObservable(new OpenCodeContainersState()); + + public event EventHandler? ProjectsLoaded; + + public event EventHandler? ProjectsUnloaded; + + public event EventHandler? ProjectLoadProgressChanged; + + internal Uri? SolutionFilePath + { + get => this.openContainerObservable.Value.SolutionFilePath; + set => this.openContainerObservable.Value = this.openContainerObservable.Value with { SolutionFilePath = value }; + } + + public ValueTask AreProjectsLoadedAsync(Guid[] projectIds, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task SubscribeToOpenCodeContainersStateAsync(IObserver observer, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(this.openContainerObservable.Subscribe(observer)); + } + + public Task GetOpenCodeContainersStateAsync(CancellationToken cancellationToken) => Task.FromResult(this.openContainerObservable.Value); + + public Task CloseSolutionAndFolderAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + + public ValueTask> GetPropertyValuesAsync(IReadOnlyList propertyIds, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public ValueTask> GetSolutionTelemetryContextPropertyValuesAsync(IReadOnlyList propertyNames, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public ValueTask LoadProjectsAsync(Guid[] projectIds, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public ValueTask LoadProjectsWithResultAsync(Guid[] projectIds, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public ValueTask RemoveProjectsAsync(IReadOnlyList projectIds, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task RequestProjectEventsAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task SaveSolutionFilterFileAsync(string filterFileDirectory, string filterFileName, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public ValueTask UnloadProjectsAsync(Guid[] projectIds, ProjectUnloadReason unloadReason, CancellationToken cancellationToken) => throw new NotImplementedException(); + + internal void SimulateFolderChange(IReadOnlyList folderPaths) => this.openContainerObservable.Value = this.openContainerObservable.Value with { OpenFolderPaths = folderPaths }; + + private class BroadcastObservable : IObservable + { + private readonly BroadcastBlock sourceBlock = new(v => v); + private T value; + + internal BroadcastObservable(T initialValue) + { + this.sourceBlock.Post(this.value = initialValue); + } + + internal T Value + { + get => this.value; + set => this.sourceBlock.Post(this.value = value); + } + + public IDisposable Subscribe(IObserver observer) + { + var actionBlock = new ActionBlock(observer.OnNext); + actionBlock.Completion.ContinueWith( + static (t, s) => + { + var observer = (IObserver)s!; + if (t.Exception is object) + { + observer.OnError(t.Exception); + } + else + { + observer.OnCompleted(); + } + }, + observer, + TaskScheduler.Default).Forget(); + return this.sourceBlock.LinkTo(actionBlock, new DataflowLinkOptions { PropagateCompletion = true }); + } + } + } +} diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs index f05948bfd630a..06976afc94576 100644 --- a/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.SQLite.v2; using Microsoft.CodeAnalysis.Storage; using Xunit; @@ -21,8 +22,8 @@ namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices /// public class SQLiteV2PersistentStorageTests : AbstractPersistentStorageTests { - internal override AbstractPersistentStorageService GetStorageService(IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector) - => new SQLitePersistentStorageService(exportProvider.GetExports().Single().Value, locationService, faultInjector); + internal override AbstractPersistentStorageService GetStorageService(OptionSet options, IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector, string relativePathBase) + => new SQLitePersistentStorageService(options, exportProvider.GetExports().Single().Value, locationService, faultInjector); [Fact] public async Task TestCrashInNewConnection() diff --git a/src/VisualStudio/Core/Def/Storage/AbstractCloudCachePersistentStorageService.cs b/src/VisualStudio/Core/Def/Storage/AbstractCloudCachePersistentStorageService.cs new file mode 100644 index 0000000000000..db39f0747d521 --- /dev/null +++ b/src/VisualStudio/Core/Def/Storage/AbstractCloudCachePersistentStorageService.cs @@ -0,0 +1,54 @@ +// 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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.PersistentStorage; +using Microsoft.CodeAnalysis.Storage; +using Microsoft.VisualStudio.RpcContracts.Caching; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Storage +{ + internal abstract class AbstractCloudCachePersistentStorageService : AbstractPersistentStorageService + { + private const string StorageExtension = "CloudCache"; + + protected AbstractCloudCachePersistentStorageService( + IPersistentStorageLocationService locationService) + : base(locationService) + { + } + + protected abstract void DisposeCacheService(ICacheService cacheService); + protected abstract ValueTask CreateCacheServiceAsync(CancellationToken cancellationToken); + + protected sealed override string GetDatabaseFilePath(string workingFolderPath) + { + Contract.ThrowIfTrue(string.IsNullOrWhiteSpace(workingFolderPath)); + return Path.Combine(workingFolderPath, StorageExtension); + } + + protected sealed override bool ShouldDeleteDatabase(Exception exception) + { + // CloudCache owns the db, so we don't have to delete anything ourselves. + return false; + } + + protected sealed override async ValueTask TryOpenDatabaseAsync( + SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, CancellationToken cancellationToken) + { + var cacheService = await this.CreateCacheServiceAsync(cancellationToken).ConfigureAwait(false); + var relativePathBase = await cacheService.GetRelativePathBaseAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrEmpty(relativePathBase)) + return null; + + return new CloudCachePersistentStorage( + cacheService, solutionKey, workingFolderPath, relativePathBase, databaseFilePath, this.DisposeCacheService); + } + } +} diff --git a/src/VisualStudio/Core/Def/Storage/CloudCachePersistentStorage.cs b/src/VisualStudio/Core/Def/Storage/CloudCachePersistentStorage.cs new file mode 100644 index 0000000000000..d538d67739f67 --- /dev/null +++ b/src/VisualStudio/Core/Def/Storage/CloudCachePersistentStorage.cs @@ -0,0 +1,213 @@ +// 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; +using System.IO; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.PersistentStorage; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.VisualStudio.RpcContracts.Caching; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Storage +{ + /// + /// Implementation of Roslyn's sitting on top of the platform's cloud storage + /// system. + /// + internal class CloudCachePersistentStorage : AbstractPersistentStorage + { + private static readonly ObjectPool s_byteArrayPool = new(() => new byte[Checksum.HashSize]); + + /// + /// We do not need to store anything specific about the solution in this key as the platform cloud cache is + /// already keyed to the current solution. So this just allows us to store values considering that as the root. + /// + private static readonly CacheContainerKey s_solutionKey = new("Roslyn.Solution"); + + /// + /// Cache from project green nodes to the container keys we've computed for it (and the documents inside of it). + /// We can avoid computing these container keys when called repeatedly for the same projects/documents. + /// + private static readonly ConditionalWeakTable s_projectToContainerKeyCache = new(); + private readonly ConditionalWeakTable.CreateValueCallback _projectToContainerKeyCacheCallback; + + /// + /// Underlying cache service (owned by platform team) responsible for actual storage and retrieval of data. + /// + private readonly ICacheService _cacheService; + private readonly Action _disposeCacheService; + + public CloudCachePersistentStorage( + ICacheService cacheService, + SolutionKey solutionKey, + string workingFolderPath, + string relativePathBase, + string databaseFilePath, + Action disposeCacheService) + : base(workingFolderPath, relativePathBase, databaseFilePath) + { + _cacheService = cacheService; + _disposeCacheService = disposeCacheService; + _projectToContainerKeyCacheCallback = ps => new ProjectContainerKeyCache(relativePathBase, ProjectKey.ToProjectKey(solutionKey, ps)); + } + + public sealed override void Dispose() + => _disposeCacheService(_cacheService); + + public sealed override ValueTask DisposeAsync() + { + if (this._cacheService is IAsyncDisposable asyncDisposable) + { + return asyncDisposable.DisposeAsync(); + } + else if (this._cacheService is IDisposable disposable) + { + disposable.Dispose(); + return ValueTaskFactory.CompletedTask; + } + + return ValueTaskFactory.CompletedTask; + } + + /// + /// Maps our own roslyn key to the appropriate key to use for the cloud cache system. To avoid lots of + /// allocations we cache these (weakly) so if the same keys are used we can use the same platform keys. + /// + private CacheContainerKey? GetContainerKey(ProjectKey projectKey, Project? project) + { + return project != null + ? s_projectToContainerKeyCache.GetValue(project.State, _projectToContainerKeyCacheCallback).ProjectContainerKey + : ProjectContainerKeyCache.CreateProjectContainerKey(this.SolutionFilePath, projectKey); + } + + /// + /// Maps our own roslyn key to the appropriate key to use for the cloud cache system. To avoid lots of + /// allocations we cache these (weakly) so if the same keys are used we can use the same platform keys. + /// + private CacheContainerKey? GetContainerKey( + DocumentKey documentKey, Document? document) + { + return document != null + ? s_projectToContainerKeyCache.GetValue(document.Project.State, _projectToContainerKeyCacheCallback).GetDocumentContainerKey(document.State) + : ProjectContainerKeyCache.CreateDocumentContainerKey(this.SolutionFilePath, documentKey); + } + + public sealed override Task ChecksumMatchesAsync(string name, Checksum checksum, CancellationToken cancellationToken) + => ChecksumMatchesAsync(name, checksum, s_solutionKey, cancellationToken); + + protected sealed override Task ChecksumMatchesAsync(ProjectKey projectKey, Project? project, string name, Checksum checksum, CancellationToken cancellationToken) + => ChecksumMatchesAsync(name, checksum, GetContainerKey(projectKey, project), cancellationToken); + + protected sealed override Task ChecksumMatchesAsync(DocumentKey documentKey, Document? document, string name, Checksum checksum, CancellationToken cancellationToken) + => ChecksumMatchesAsync(name, checksum, GetContainerKey(documentKey, document), cancellationToken); + + private async Task ChecksumMatchesAsync(string name, Checksum checksum, CacheContainerKey? containerKey, CancellationToken cancellationToken) + { + // If we failed to get a container key (for example, because the client is referencing a file not under the + // solution folder) then we can't proceed. + if (containerKey == null) + return false; + + using var bytes = s_byteArrayPool.GetPooledObject(); + checksum.WriteTo(bytes.Object); + + return await _cacheService.CheckExistsAsync(new CacheItemKey(containerKey.Value, name) { Version = bytes.Object }, cancellationToken).ConfigureAwait(false); + } + + public sealed override Task ReadStreamAsync(string name, Checksum? checksum, CancellationToken cancellationToken) + => ReadStreamAsync(name, checksum, s_solutionKey, cancellationToken); + + protected sealed override Task ReadStreamAsync(ProjectKey projectKey, Project? project, string name, Checksum? checksum, CancellationToken cancellationToken) + => ReadStreamAsync(name, checksum, GetContainerKey(projectKey, project), cancellationToken); + + protected sealed override Task ReadStreamAsync(DocumentKey documentKey, Document? document, string name, Checksum? checksum, CancellationToken cancellationToken) + => ReadStreamAsync(name, checksum, GetContainerKey(documentKey, document), cancellationToken); + + private async Task ReadStreamAsync(string name, Checksum? checksum, CacheContainerKey? containerKey, CancellationToken cancellationToken) + { + // If we failed to get a container key (for example, because the client is referencing a file not under the + // solution folder) then we can't proceed. + if (containerKey == null) + return null; + + if (checksum == null) + { + return await ReadStreamAsync(new CacheItemKey(containerKey.Value, name), cancellationToken).ConfigureAwait(false); + } + else + { + using var bytes = s_byteArrayPool.GetPooledObject(); + checksum.WriteTo(bytes.Object); + + return await ReadStreamAsync(new CacheItemKey(containerKey.Value, name) { Version = bytes.Object }, cancellationToken).ConfigureAwait(false); + } + } + + private async Task ReadStreamAsync(CacheItemKey key, CancellationToken cancellationToken) + { + var pipe = new Pipe(); + var result = await _cacheService.TryGetItemAsync(key, pipe.Writer, cancellationToken).ConfigureAwait(false); + if (!result) + return null; + + // Clients will end up doing blocking reads on the synchronous stream we return from this. This can + // negatively impact our calls as that will cause sync blocking on the async work to fill the pipe. To + // alleviate that issue, we actually asynchronously read in the entire stream into memory inside the reader + // and then pass that out. This should not be a problem in practice as PipeReader internally intelligently + // uses and pools reasonable sized buffers, preventing us from exacerbating the GC or causing LOH + // allocations. + while (true) + { + var readResult = await pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + pipe.Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); + + if (readResult.IsCompleted) + break; + } + + return pipe.Reader.AsStream(); + } + + public sealed override Task WriteStreamAsync(string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) + => WriteStreamAsync(name, stream, checksum, s_solutionKey, cancellationToken); + + protected sealed override Task WriteStreamAsync(ProjectKey projectKey, Project? project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) + => WriteStreamAsync(name, stream, checksum, GetContainerKey(projectKey, project), cancellationToken); + + protected sealed override Task WriteStreamAsync(DocumentKey documentKey, Document? document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) + => WriteStreamAsync(name, stream, checksum, GetContainerKey(documentKey, document), cancellationToken); + + private async Task WriteStreamAsync(string name, Stream stream, Checksum? checksum, CacheContainerKey? containerKey, CancellationToken cancellationToken) + { + // If we failed to get a container key (for example, because the client is referencing a file not under the + // solution folder) then we can't proceed. + if (containerKey == null) + return false; + + if (checksum == null) + { + return await WriteStreamAsync(new CacheItemKey(containerKey.Value, name), stream, cancellationToken).ConfigureAwait(false); + } + else + { + using var bytes = s_byteArrayPool.GetPooledObject(); + checksum.WriteTo(bytes.Object); + + return await WriteStreamAsync(new CacheItemKey(containerKey.Value, name) { Version = bytes.Object }, stream, cancellationToken).ConfigureAwait(false); + } + } + + private async Task WriteStreamAsync(CacheItemKey key, Stream stream, CancellationToken cancellationToken) + { + await _cacheService.SetItemAsync(key, PipeReader.Create(stream), shareable: false, cancellationToken).ConfigureAwait(false); + return true; + } + } +} diff --git a/src/VisualStudio/Core/Def/Storage/ProjectContainerKeyCache.cs b/src/VisualStudio/Core/Def/Storage/ProjectContainerKeyCache.cs new file mode 100644 index 0000000000000..2d8bbc75d725d --- /dev/null +++ b/src/VisualStudio/Core/Def/Storage/ProjectContainerKeyCache.cs @@ -0,0 +1,110 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.PersistentStorage; +using Microsoft.VisualStudio.RpcContracts.Caching; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Storage +{ + /// + /// Cache of our own internal roslyn storage keys to the equivalent platform cloud cache keys. Cloud cache keys can + /// store a lot of date in them (like their 'dimensions' dictionary. We don't want to continually recreate these as + /// we read/write date to the db. + /// + internal class ProjectContainerKeyCache + { + private static readonly ImmutableSortedDictionary EmptyDimensions = ImmutableSortedDictionary.Create(StringComparer.Ordinal); + + /// + /// Container key explicitly for the project itself. + /// + public readonly CacheContainerKey? ProjectContainerKey; + + /// + /// Cache from document green nodes to the container keys we've computed for it. We can avoid computing these + /// container keys when called repeatedly for the same documents. + /// + /// + /// We can use a normal Dictionary here instead of a as + /// instances of are always owned in a context where the is alive. As that instance is alive, all s the project + /// points at will be held alive strongly too. + /// + private readonly Dictionary _documentToContainerKey = new(); + private readonly Func _documentToContainerKeyCallback; + + public ProjectContainerKeyCache(string relativePathBase, ProjectKey projectKey) + { + ProjectContainerKey = CreateProjectContainerKey(relativePathBase, projectKey); + + _documentToContainerKeyCallback = ds => CreateDocumentContainerKey(relativePathBase, DocumentKey.ToDocumentKey(projectKey, ds)); + } + + public CacheContainerKey? GetDocumentContainerKey(TextDocumentState state) + { + lock (_documentToContainerKey) + return _documentToContainerKey.GetOrAdd(state, _documentToContainerKeyCallback); + } + + public static CacheContainerKey? CreateProjectContainerKey( + string relativePathBase, ProjectKey projectKey) + { + // Creates a container key for this project. The container key is a mix of the project's name, relative + // file path (to the solution), and optional parse options. + + // If we don't have a valid solution path, we can't store anything. + if (string.IsNullOrEmpty(relativePathBase)) + return null; + + // We have to have a file path for this project + if (RoslynString.IsNullOrEmpty(projectKey.FilePath)) + return null; + + // The file path has to be relative to the base path the DB is associated with (either the solution-path or + // repo-path). + var relativePath = PathUtilities.GetRelativePath(relativePathBase, projectKey.FilePath!); + if (relativePath == projectKey.FilePath) + return null; + + var dimensions = EmptyDimensions + .Add($"{nameof(ProjectKey)}.{nameof(ProjectKey.Name)}", projectKey.Name) + .Add($"{nameof(ProjectKey)}.{nameof(ProjectKey.FilePath)}", relativePath) + .Add($"{nameof(ProjectKey)}.{nameof(ProjectKey.ParseOptionsChecksum)}", projectKey.ParseOptionsChecksum.ToString()); + + return new CacheContainerKey("Roslyn.Project", dimensions); + } + + public static CacheContainerKey? CreateDocumentContainerKey( + string relativePathBase, + DocumentKey documentKey) + { + // See if we can get a project key for this info. If not, we def can't get a doc key. + var projectContainerKey = CreateProjectContainerKey(relativePathBase, documentKey.Project); + if (projectContainerKey == null) + return null; + + // We have to have a file path for this document + if (string.IsNullOrEmpty(documentKey.FilePath)) + return null; + + // The file path has to be relative to the base path the DB is associated with (either the solution-path or + // repo-path). + var relativePath = PathUtilities.GetRelativePath(relativePathBase, documentKey.FilePath!); + if (relativePath == documentKey.FilePath) + return null; + + var dimensions = projectContainerKey.Value.Dimensions + .Add($"{nameof(DocumentKey)}.{nameof(DocumentKey.Name)}", documentKey.Name) + .Add($"{nameof(DocumentKey)}.{nameof(DocumentKey.FilePath)}", relativePath); + + return new CacheContainerKey("Roslyn.Document", dimensions); + } + } +} diff --git a/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageService.cs b/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageService.cs new file mode 100644 index 0000000000000..6ef9fa3714c54 --- /dev/null +++ b/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageService.cs @@ -0,0 +1,56 @@ +// 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; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host; +using Microsoft.VisualStudio.RpcContracts.Caching; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Storage +{ + internal class VisualStudioCloudCacheStorageService : AbstractCloudCachePersistentStorageService + { + private readonly IAsyncServiceProvider _serviceProvider; + private readonly IThreadingContext _threadingContext; + + public VisualStudioCloudCacheStorageService(IAsyncServiceProvider serviceProvider, IThreadingContext threadingContext, IPersistentStorageLocationService locationService) + : base(locationService) + { + _serviceProvider = serviceProvider; + _threadingContext = threadingContext; + } + + protected sealed override void DisposeCacheService(ICacheService cacheService) + { + if (cacheService is IAsyncDisposable asyncDisposable) + { + _threadingContext.JoinableTaskFactory.Run( + () => asyncDisposable.DisposeAsync().AsTask()); + } + else if (cacheService is IDisposable disposable) + { + disposable.Dispose(); + } + } + + protected sealed override async ValueTask CreateCacheServiceAsync(CancellationToken cancellationToken) + { + var serviceContainer = await _serviceProvider.GetServiceAsync().ConfigureAwait(false); + var serviceBroker = serviceContainer.GetFullAccessServiceBroker(); + +#pragma warning disable ISB001 // Dispose of proxies + // cache service will be disposed inside VisualStudioCloudCachePersistentStorage.Dispose + var cacheService = await serviceBroker.GetProxyAsync(VisualStudioServices.VS2019_10.CacheService, cancellationToken: cancellationToken).ConfigureAwait(false); +#pragma warning restore ISB001 // Dispose of proxies + + Contract.ThrowIfNull(cacheService); + return cacheService; + } + } +} diff --git a/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageServiceFactory.cs b/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageServiceFactory.cs new file mode 100644 index 0000000000000..da2e9afbae3de --- /dev/null +++ b/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageServiceFactory.cs @@ -0,0 +1,35 @@ +// 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; +using System.Composition; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Storage; +using Microsoft.CodeAnalysis.Storage.CloudCache; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.LanguageServices.Storage +{ + [ExportWorkspaceService(typeof(ICloudCacheStorageServiceFactory), ServiceLayer.Host), Shared] + internal class VisualStudioCloudCacheStorageServiceFactory : ICloudCacheStorageServiceFactory + { + private readonly IAsyncServiceProvider _serviceProvider; + private readonly IThreadingContext _threadingContext; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public VisualStudioCloudCacheStorageServiceFactory( + IThreadingContext threadingContext, + SVsServiceProvider serviceProvider) + { + _threadingContext = threadingContext; + _serviceProvider = (IAsyncServiceProvider)serviceProvider; + } + + public AbstractPersistentStorageService Create(IPersistentStorageLocationService locationService) + => new VisualStudioCloudCacheStorageService(_serviceProvider, _threadingContext, locationService); + } +} diff --git a/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs b/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs index 75d04b04128e8..854f5d100fac7 100644 --- a/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs +++ b/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs @@ -39,7 +39,7 @@ protected AbstractPersistentStorageService(IPersistentStorageLocationService loc /// to delete the database and retry opening one more time. If that fails again, the instance will be used. /// - protected abstract ValueTask TryOpenDatabaseAsync(SolutionKey solutionKey, string workingFolderPath, string databaseFilePath); + protected abstract ValueTask TryOpenDatabaseAsync(SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, CancellationToken cancellationToken); protected abstract bool ShouldDeleteDatabase(Exception exception); [Obsolete("Use GetStorageAsync instead")] @@ -62,9 +62,7 @@ public ValueTask GetStorageAsync( Workspace workspace, SolutionKey solutionKey, Solution? bulkLoadSnapshot, bool checkBranchId, CancellationToken cancellationToken) { if (!DatabaseSupported(solutionKey, checkBranchId)) - { - return new(NoOpPersistentStorage.Instance); - } + return new(NoOpPersistentStorage.GetOrThrow(workspace.Options)); return GetStorageWorkerAsync(workspace, solutionKey, bulkLoadSnapshot, cancellationToken); } @@ -84,7 +82,7 @@ internal async ValueTask GetStorageWorkerAsync( var workingFolder = TryGetWorkingFolder(workspace, solutionKey, bulkLoadSnapshot); if (workingFolder == null) - return NoOpPersistentStorage.Instance; + return NoOpPersistentStorage.GetOrThrow(workspace.Options); // If we already had some previous cached service, let's let it start cleaning up if (_currentPersistentStorage != null) @@ -101,7 +99,7 @@ internal async ValueTask GetStorageWorkerAsync( _currentPersistentStorageSolutionId = null; } - var storage = await CreatePersistentStorageAsync(solutionKey, workingFolder).ConfigureAwait(false); + var storage = await CreatePersistentStorageAsync(workspace, solutionKey, workingFolder, cancellationToken).ConfigureAwait(false); Contract.ThrowIfNull(storage); // Create and cache a new storage instance associated with this particular solution. @@ -145,23 +143,32 @@ private static bool DatabaseSupported(SolutionKey solution, bool checkBranchId) return true; } - private async ValueTask CreatePersistentStorageAsync(SolutionKey solutionKey, string workingFolderPath) + private async ValueTask CreatePersistentStorageAsync( + Workspace workspace, SolutionKey solutionKey, string workingFolderPath, CancellationToken cancellationToken) { // Attempt to create the database up to two times. The first time we may encounter // some sort of issue (like DB corruption). We'll then try to delete the DB and can // try to create it again. If we can't create it the second time, then there's nothing // we can do and we have to store things in memory. - return await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath).ConfigureAwait(false) ?? - await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath).ConfigureAwait(false) ?? - NoOpPersistentStorage.Instance; + var result = await TryCreatePersistentStorageAsync(workspace, solutionKey, workingFolderPath, cancellationToken).ConfigureAwait(false) ?? + await TryCreatePersistentStorageAsync(workspace, solutionKey, workingFolderPath, cancellationToken).ConfigureAwait(false); + + if (result != null) + return result; + + return NoOpPersistentStorage.GetOrThrow(workspace.Options); } - private async ValueTask TryCreatePersistentStorageAsync(SolutionKey solutionKey, string workingFolderPath) + private async ValueTask TryCreatePersistentStorageAsync( + Workspace workspace, + SolutionKey solutionKey, + string workingFolderPath, + CancellationToken cancellationToken) { var databaseFilePath = GetDatabaseFilePath(workingFolderPath); try { - return await TryOpenDatabaseAsync(solutionKey, workingFolderPath, databaseFilePath).ConfigureAwait(false); + return await TryOpenDatabaseAsync(solutionKey, workingFolderPath, databaseFilePath, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -175,6 +182,9 @@ await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath).ConfigureA IOUtilities.PerformIO(() => Directory.Delete(Path.GetDirectoryName(databaseFilePath)!, recursive: true)); } + if (workspace.Options.GetOption(StorageOptions.DatabaseMustSucceed)) + throw; + return null; } } diff --git a/src/Workspaces/Core/Portable/Storage/CloudCache/ICloudCacheStorageServiceFactory.cs b/src/Workspaces/Core/Portable/Storage/CloudCache/ICloudCacheStorageServiceFactory.cs new file mode 100644 index 0000000000000..a0a168a7dd385 --- /dev/null +++ b/src/Workspaces/Core/Portable/Storage/CloudCache/ICloudCacheStorageServiceFactory.cs @@ -0,0 +1,13 @@ +// 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 Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Storage.CloudCache +{ + internal interface ICloudCacheStorageServiceFactory : IWorkspaceService + { + AbstractPersistentStorageService Create(IPersistentStorageLocationService locationService); + } +} diff --git a/src/Workspaces/Core/Portable/Storage/DesktopPersistenceStorageServiceFactory.cs b/src/Workspaces/Core/Portable/Storage/DesktopPersistenceStorageServiceFactory.cs new file mode 100644 index 0000000000000..4c83680f15ffb --- /dev/null +++ b/src/Workspaces/Core/Portable/Storage/DesktopPersistenceStorageServiceFactory.cs @@ -0,0 +1,74 @@ +// 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; +using System.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; + +// When building for source-build, there is no sqlite dependency +#if !DOTNET_BUILD_FROM_SOURCE +using Microsoft.CodeAnalysis.SQLite.v2; +using Microsoft.CodeAnalysis.Storage.CloudCache; +#endif + +namespace Microsoft.CodeAnalysis.Storage +{ + [ExportWorkspaceServiceFactory(typeof(IPersistentStorageService), ServiceLayer.Desktop), Shared] + internal class DesktopPersistenceStorageServiceFactory : IWorkspaceServiceFactory + { +#if DOTNET_BUILD_FROM_SOURCE + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DesktopPersistenceStorageServiceFactory() + { + } + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + return NoOpPersistentStorageService.Instance; + } + +#else + + private readonly SQLiteConnectionPoolService _connectionPoolService; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DesktopPersistenceStorageServiceFactory(SQLiteConnectionPoolService connectionPoolService) + { + _connectionPoolService = connectionPoolService; + } + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + var optionService = workspaceServices.GetRequiredService(); + var database = optionService.GetOption(StorageOptions.Database); + var options = workspaceServices.Workspace.Options; + + var locationService = workspaceServices.GetService(); + if (locationService != null) + { + switch (database) + { + case StorageDatabase.SQLite: + return new SQLitePersistentStorageService(options, _connectionPoolService, locationService); + + case StorageDatabase.CloudCache: + var factory = workspaceServices.GetService(); + + return factory == null + ? NoOpPersistentStorageService.GetOrThrow(options) + : factory.Create(locationService); + } + } + + return NoOpPersistentStorageService.GetOrThrow(options); + } + +#endif + } +} diff --git a/src/Workspaces/Core/Portable/Storage/PersistenceStorageServiceFactory.cs b/src/Workspaces/Core/Portable/Storage/PersistenceStorageServiceFactory.cs deleted file mode 100644 index 7631e5fa81d25..0000000000000 --- a/src/Workspaces/Core/Portable/Storage/PersistenceStorageServiceFactory.cs +++ /dev/null @@ -1,57 +0,0 @@ -// 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; -using System.Composition; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Options; - -// When building for source-build, there is no sqlite dependency -#if !DOTNET_BUILD_FROM_SOURCE -using Microsoft.CodeAnalysis.SQLite.v2; -#endif - -namespace Microsoft.CodeAnalysis.Storage -{ - [ExportWorkspaceServiceFactory(typeof(IPersistentStorageService), ServiceLayer.Desktop), Shared] - internal class PersistenceStorageServiceFactory : IWorkspaceServiceFactory - { -#if !DOTNET_BUILD_FROM_SOURCE - private readonly SQLiteConnectionPoolService _connectionPoolService; -#endif - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public PersistenceStorageServiceFactory( -#if !DOTNET_BUILD_FROM_SOURCE - SQLiteConnectionPoolService connectionPoolService -#endif - ) - { -#if !DOTNET_BUILD_FROM_SOURCE - _connectionPoolService = connectionPoolService; -#endif - } - - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - { -#if !DOTNET_BUILD_FROM_SOURCE - var optionService = workspaceServices.GetRequiredService(); - var database = optionService.GetOption(StorageOptions.Database); - switch (database) - { - case StorageDatabase.SQLite: - var locationService = workspaceServices.GetService(); - if (locationService != null) - return new SQLite.v2.SQLitePersistentStorageService(_connectionPoolService, locationService); - - break; - } -#endif - - return NoOpPersistentStorageService.Instance; - } - } -} diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageConstants.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageConstants.cs index 77e5db7694bdc..788a8d516ed28 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageConstants.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageConstants.cs @@ -59,7 +59,7 @@ internal static class SQLitePersistentStorageConstants /// /// Inside the DB we have a table for data that we want associated with a . /// The data is keyed off of an integral value produced by combining the ID of the Project and - /// the ID of the name of the data (see . + /// the ID of the name of the data (see . /// /// This gives a very efficient integral key, and means that the we only have to store a /// single mapping from stream name to ID in the string table. @@ -76,7 +76,7 @@ internal static class SQLitePersistentStorageConstants /// /// Inside the DB we have a table for data that we want associated with a . /// The data is keyed off of an integral value produced by combining the ID of the Document and - /// the ID of the name of the data (see . + /// the ID of the name of the data (see . /// /// This gives a very efficient integral key, and means that the we only have to store a /// single mapping from stream name to ID in the string table. diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs index 7d6110b2c53a4..9baaf0089c3ba 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs @@ -4,8 +4,10 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PersistentStorage; using Roslyn.Utilities; @@ -17,19 +19,25 @@ internal class SQLitePersistentStorageService : AbstractSQLitePersistentStorageS private const string PersistentStorageFileName = "storage.ide"; private readonly SQLiteConnectionPoolService _connectionPoolService; + private readonly OptionSet _options; private readonly IPersistentStorageFaultInjector? _faultInjector; - public SQLitePersistentStorageService(SQLiteConnectionPoolService connectionPoolService, IPersistentStorageLocationService locationService) + public SQLitePersistentStorageService( + OptionSet options, + SQLiteConnectionPoolService connectionPoolService, + IPersistentStorageLocationService locationService) : base(locationService) { + _options = options; _connectionPoolService = connectionPoolService; } public SQLitePersistentStorageService( + OptionSet options, SQLiteConnectionPoolService connectionPoolService, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector) - : this(connectionPoolService, locationService) + : this(options, connectionPoolService, locationService) { _faultInjector = faultInjector; } @@ -41,7 +49,7 @@ protected override string GetDatabaseFilePath(string workingFolderPath) } protected override ValueTask TryOpenDatabaseAsync( - SolutionKey solutionKey, string workingFolderPath, string databaseFilePath) + SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, CancellationToken cancellationToken) { if (!TryInitializeLibraries()) { @@ -49,6 +57,9 @@ protected override string GetDatabaseFilePath(string workingFolderPath) return new((IChecksummedPersistentStorage?)null); } + if (solutionKey.FilePath == null) + return new(NoOpPersistentStorage.GetOrThrow(_options)); + return new(SQLitePersistentStorage.TryCreate( _connectionPoolService, workingFolderPath, diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_DocumentSerialization.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_DocumentSerialization.cs index 0910ec4d5c45d..f214a364f2c82 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_DocumentSerialization.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_DocumentSerialization.cs @@ -14,13 +14,13 @@ namespace Microsoft.CodeAnalysis.SQLite.v2 internal partial class SQLitePersistentStorage { - public override Task ChecksumMatchesAsync(DocumentKey documentKey, string name, Checksum checksum, CancellationToken cancellationToken) + protected override Task ChecksumMatchesAsync(DocumentKey documentKey, Document? document, string name, Checksum checksum, CancellationToken cancellationToken) => _documentAccessor.ChecksumMatchesAsync((documentKey, name), checksum, cancellationToken); - public override Task ReadStreamAsync(DocumentKey documentKey, string name, Checksum? checksum, CancellationToken cancellationToken) + protected override Task ReadStreamAsync(DocumentKey documentKey, Document? document, string name, Checksum? checksum, CancellationToken cancellationToken) => _documentAccessor.ReadStreamAsync((documentKey, name), checksum, cancellationToken); - public override Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) + protected override Task WriteStreamAsync(DocumentKey documentKey, Document? document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) => _documentAccessor.WriteStreamAsync((documentKey, name), stream, checksum, cancellationToken); /// diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_ProjectSerialization.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_ProjectSerialization.cs index 01d76ce05645b..d5248c430280d 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_ProjectSerialization.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_ProjectSerialization.cs @@ -14,13 +14,13 @@ namespace Microsoft.CodeAnalysis.SQLite.v2 internal partial class SQLitePersistentStorage { - public override Task ChecksumMatchesAsync(ProjectKey projectKey, string name, Checksum checksum, CancellationToken cancellationToken) + protected override Task ChecksumMatchesAsync(ProjectKey projectKey, Project? project, string name, Checksum checksum, CancellationToken cancellationToken) => _projectAccessor.ChecksumMatchesAsync((projectKey, name), checksum, cancellationToken); - public override Task ReadStreamAsync(ProjectKey projectKey, string name, Checksum? checksum, CancellationToken cancellationToken) + protected override Task ReadStreamAsync(ProjectKey projectKey, Project? project, string name, Checksum? checksum, CancellationToken cancellationToken) => _projectAccessor.ReadStreamAsync((projectKey, name), checksum, cancellationToken); - public override Task WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) + protected override Task WriteStreamAsync(ProjectKey projectKey, Project? project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) => _projectAccessor.WriteStreamAsync((projectKey, name), stream, checksum, cancellationToken); /// diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs index 96c6da04b2b98..d927f3422677b 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs @@ -15,7 +15,7 @@ internal partial class SQLitePersistentStorage { private readonly ConcurrentDictionary _stringToIdMap = new(); - private int? TryGetStringId(SqlConnection connection, string value) + private int? TryGetStringId(SqlConnection connection, string? value) { // Null strings are not supported at all. Just ignore these. Any read/writes // to null values will fail and will return 'false/null' to indicate failure diff --git a/src/Workspaces/Core/Portable/Storage/StorageDatabase.cs b/src/Workspaces/Core/Portable/Storage/StorageDatabase.cs index 4587658fe8c46..150f02a3469e8 100644 --- a/src/Workspaces/Core/Portable/Storage/StorageDatabase.cs +++ b/src/Workspaces/Core/Portable/Storage/StorageDatabase.cs @@ -8,5 +8,6 @@ internal enum StorageDatabase { None = 0, SQLite = 1, + CloudCache = 2, } } diff --git a/src/Workspaces/Core/Portable/Storage/StorageOptions.cs b/src/Workspaces/Core/Portable/Storage/StorageOptions.cs index 39b908bbfd548..02147490ba64e 100644 --- a/src/Workspaces/Core/Portable/Storage/StorageOptions.cs +++ b/src/Workspaces/Core/Portable/Storage/StorageOptions.cs @@ -17,8 +17,15 @@ internal static class StorageOptions public const string OptionName = "FeatureManager/Storage"; - public static readonly Option Database = new( - OptionName, nameof(Database), defaultValue: StorageDatabase.SQLite); + public static readonly Option Database = new(OptionName, nameof(Database), defaultValue: StorageDatabase.SQLite); + + /// + /// Option that can be set in certain scenarios (like tests) to indicate that the client expects the DB to + /// succeed at all work and that it should not ever gracefully fall over. Should not be set in normal host + /// environments, where it is completely reasonable for things to fail (for example, if a client asks for a key + /// that hasn't been stored yet). + /// + public static readonly Option DatabaseMustSucceed = new(OptionName, nameof(DatabaseMustSucceed), defaultValue: false); } [ExportOptionProvider, Shared] @@ -31,6 +38,7 @@ public RemoteHostOptionsProvider() } public ImmutableArray Options { get; } = ImmutableArray.Create( - StorageOptions.Database); + StorageOptions.Database, + StorageOptions.DatabaseMustSucceed); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs index 8fe74c72239b2..80e423375e559 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs @@ -41,24 +41,42 @@ protected AbstractPersistentStorage( public abstract Task ReadStreamAsync(string name, Checksum? checksum, CancellationToken cancellationToken); public abstract Task WriteStreamAsync(string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken); - public abstract Task ChecksumMatchesAsync(ProjectKey projectKey, string name, Checksum checksum, CancellationToken cancellationToken); - public abstract Task ChecksumMatchesAsync(DocumentKey documentKey, string name, Checksum checksum, CancellationToken cancellationToken); - public abstract Task ReadStreamAsync(ProjectKey projectKey, string name, Checksum? checksum, CancellationToken cancellationToken); - public abstract Task ReadStreamAsync(DocumentKey documentKey, string name, Checksum? checksum, CancellationToken cancellationToken); - public abstract Task WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken); - public abstract Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken); + protected abstract Task ChecksumMatchesAsync(ProjectKey projectKey, Project? project, string name, Checksum checksum, CancellationToken cancellationToken); + protected abstract Task ChecksumMatchesAsync(DocumentKey documentKey, Document? document, string name, Checksum checksum, CancellationToken cancellationToken); + protected abstract Task ReadStreamAsync(ProjectKey projectKey, Project? project, string name, Checksum? checksum, CancellationToken cancellationToken); + protected abstract Task ReadStreamAsync(DocumentKey documentKey, Document? document, string name, Checksum? checksum, CancellationToken cancellationToken); + protected abstract Task WriteStreamAsync(ProjectKey projectKey, Project? project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken); + protected abstract Task WriteStreamAsync(DocumentKey documentKey, Document? document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken); + + public Task ChecksumMatchesAsync(ProjectKey projectKey, string name, Checksum checksum, CancellationToken cancellationToken) + => ChecksumMatchesAsync(projectKey, project: null, name, checksum, cancellationToken); + + public Task ChecksumMatchesAsync(DocumentKey documentKey, string name, Checksum checksum, CancellationToken cancellationToken) + => ChecksumMatchesAsync(documentKey, document: null, name, checksum, cancellationToken); + + public Task ReadStreamAsync(ProjectKey projectKey, string name, Checksum? checksum, CancellationToken cancellationToken) + => ReadStreamAsync(projectKey, project: null, name, checksum, cancellationToken); + + public Task ReadStreamAsync(DocumentKey documentKey, string name, Checksum? checksum, CancellationToken cancellationToken) + => ReadStreamAsync(documentKey, document: null, name, checksum, cancellationToken); + + public Task WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) + => WriteStreamAsync(projectKey, project: null, name, stream, checksum, cancellationToken); + + public Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) + => WriteStreamAsync(documentKey, document: null, name, stream, checksum, cancellationToken); public Task ChecksumMatchesAsync(Project project, string name, Checksum checksum, CancellationToken cancellationToken) - => ChecksumMatchesAsync(ProjectKey.ToProjectKey(project), name, checksum, cancellationToken); + => ChecksumMatchesAsync(ProjectKey.ToProjectKey(project), project, name, checksum, cancellationToken); public Task ChecksumMatchesAsync(Document document, string name, Checksum checksum, CancellationToken cancellationToken) - => ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), name, checksum, cancellationToken); + => ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), document, name, checksum, cancellationToken); public Task ReadStreamAsync(Project project, string name, Checksum? checksum, CancellationToken cancellationToken) - => ReadStreamAsync(ProjectKey.ToProjectKey(project), name, checksum, cancellationToken); + => ReadStreamAsync(ProjectKey.ToProjectKey(project), project, name, checksum, cancellationToken); public Task ReadStreamAsync(Document document, string name, Checksum? checksum, CancellationToken cancellationToken) - => ReadStreamAsync(DocumentKey.ToDocumentKey(document), name, checksum, cancellationToken); + => ReadStreamAsync(DocumentKey.ToDocumentKey(document), document, name, checksum, cancellationToken); public Task ReadStreamAsync(string name, CancellationToken cancellationToken) => ReadStreamAsync(name, checksum: null, cancellationToken); @@ -70,10 +88,10 @@ public Task ChecksumMatchesAsync(Document document, string name, Checksum => ReadStreamAsync(document, name, checksum: null, cancellationToken); public Task WriteStreamAsync(Project project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) - => WriteStreamAsync(ProjectKey.ToProjectKey(project), name, stream, checksum, cancellationToken); + => WriteStreamAsync(ProjectKey.ToProjectKey(project), project, name, stream, checksum, cancellationToken); public Task WriteStreamAsync(Document document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) - => WriteStreamAsync(DocumentKey.ToDocumentKey(document), name, stream, checksum, cancellationToken); + => WriteStreamAsync(DocumentKey.ToDocumentKey(document), document, name, stream, checksum, cancellationToken); public Task WriteStreamAsync(string name, Stream stream, CancellationToken cancellationToken) => WriteStreamAsync(name, stream, checksum: null, cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/PersistentStorageServiceFactory.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DefaultPersistentStorageServiceFactory.cs similarity index 78% rename from src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/PersistentStorageServiceFactory.cs rename to src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DefaultPersistentStorageServiceFactory.cs index f9882b2c84b79..274c98d2c062f 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/PersistentStorageServiceFactory.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DefaultPersistentStorageServiceFactory.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Composition; using Microsoft.CodeAnalysis.Host.Mef; @@ -15,15 +13,15 @@ namespace Microsoft.CodeAnalysis.Host /// projects or documents across runtime sessions. /// [ExportWorkspaceServiceFactory(typeof(IPersistentStorageService), ServiceLayer.Default), Shared] - internal class PersistentStorageServiceFactory : IWorkspaceServiceFactory + internal class DefaultPersistentStorageServiceFactory : IWorkspaceServiceFactory { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public PersistentStorageServiceFactory() + public DefaultPersistentStorageServiceFactory() { } public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => NoOpPersistentStorageService.Instance; + => NoOpPersistentStorageService.GetOrThrow(workspaceServices.Workspace.Options); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DocumentKey.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DocumentKey.cs index 767182f21a196..0985abe979c93 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DocumentKey.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DocumentKey.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Host; @@ -20,10 +18,10 @@ internal readonly struct DocumentKey public readonly ProjectKey Project; public readonly DocumentId Id; - public readonly string FilePath; + public readonly string? FilePath; public readonly string Name; - public DocumentKey(ProjectKey project, DocumentId id, string filePath, string name) + public DocumentKey(ProjectKey project, DocumentId id, string? filePath, string name) { Project = project; Id = id; @@ -32,7 +30,10 @@ public DocumentKey(ProjectKey project, DocumentId id, string filePath, string na } public static DocumentKey ToDocumentKey(Document document) - => new(ProjectKey.ToProjectKey(document.Project), document.Id, document.FilePath, document.Name); + => ToDocumentKey(ProjectKey.ToProjectKey(document.Project), document.State); + + public static DocumentKey ToDocumentKey(ProjectKey projectKey, TextDocumentState state) + => new(projectKey, state.Id, state.FilePath, state.Name); public SerializableDocumentKey Dehydrate() => new(Project.Dehydrate(), Id, FilePath, Name); @@ -48,12 +49,12 @@ internal readonly struct SerializableDocumentKey public readonly DocumentId Id; [DataMember(Order = 2)] - public readonly string FilePath; + public readonly string? FilePath; [DataMember(Order = 3)] public readonly string Name; - public SerializableDocumentKey(SerializableProjectKey project, DocumentId id, string filePath, string name) + public SerializableDocumentKey(SerializableProjectKey project, DocumentId id, string? filePath, string name) { Project = project; Id = id; diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs index b418ca5a3890a..8feb549367738 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs @@ -60,27 +60,48 @@ internal interface IChecksummedPersistentStorage : IPersistentStorage Task ReadStreamAsync(DocumentKey document, string name, Checksum checksum = null, CancellationToken cancellationToken = default); /// - /// Reads the stream for the solution with the given . An optional - /// can be provided to store along with the data. This can be used along with ReadStreamAsync with future - /// reads to ensure the data is only read back if it matches that checksum. + /// Reads the stream for the solution with the given . An optional can be provided to store along with the data. This can be used along with ReadStreamAsync + /// with future reads to ensure the data is only read back if it matches that checksum. + /// + /// Returns if the data was successfully persisted to the storage subsystem. Subsequent + /// calls to read the same keys should succeed if called within the same session. + /// /// Task WriteStreamAsync(string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default); /// - /// Reads the stream for the with the given . An optional - /// can be provided to store along with the data. This can be used along with ReadStreamAsync with future - /// reads to ensure the data is only read back if it matches that checksum. + /// Reads the stream for the with the given . An optional + /// can be provided to store along with the data. This can be used along with + /// ReadStreamAsync with future reads to ensure the data is only read back if it matches that checksum. + /// + /// Returns if the data was successfully persisted to the storage subsystem. Subsequent + /// calls to read the same keys should succeed if called within the same session. + /// /// Task WriteStreamAsync(Project project, string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default); /// - /// Reads the stream for the with the given . An optional - /// can be provided to store along with the data. This can be used along with ReadStreamAsync with future - /// reads to ensure the data is only read back if it matches that checksum. + /// Reads the stream for the with the given . An optional + /// can be provided to store along with the data. This can be used along with + /// ReadStreamAsync with future reads to ensure the data is only read back if it matches that checksum. + /// + /// Returns if the data was successfully persisted to the storage subsystem. Subsequent + /// calls to read the same keys should succeed if called within the same session. + /// /// Task WriteStreamAsync(Document document, string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default); + /// + /// Returns if the data was successfully persisted to the storage subsystem. Subsequent + /// calls to read the same keys should succeed if called within the same session. + /// Task WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default); + + /// + /// Returns if the data was successfully persisted to the storage subsystem. Subsequent + /// calls to read the same keys should succeed if called within the same session. + /// Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs index c15c8d1601937..a602e12aceedd 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs @@ -20,8 +20,22 @@ public interface IPersistentStorage : IDisposable, IAsyncDisposable Task ReadStreamAsync(Project project, string name, CancellationToken cancellationToken = default); Task ReadStreamAsync(Document document, string name, CancellationToken cancellationToken = default); + /// + /// Returns if the data was successfully persisted to the storage subsystem. Subsequent + /// calls to read the same keys should succeed if called within the same session. + /// Task WriteStreamAsync(string name, Stream stream, CancellationToken cancellationToken = default); + + /// + /// Returns if the data was successfully persisted to the storage subsystem. Subsequent + /// calls to read the same keys should succeed if called within the same session. + /// Task WriteStreamAsync(Project project, string name, Stream stream, CancellationToken cancellationToken = default); + + /// + /// Returns if the data was successfully persisted to the storage subsystem. Subsequent + /// calls to read the same keys should succeed if called within the same session. + /// Task WriteStreamAsync(Document document, string name, Stream stream, CancellationToken cancellationToken = default); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs index d2bf20ad513a6..0f2784a222134 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs @@ -2,22 +2,30 @@ // 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; using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PersistentStorage; +using Microsoft.CodeAnalysis.Storage; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Host { internal class NoOpPersistentStorage : IChecksummedPersistentStorage { - public static readonly IChecksummedPersistentStorage Instance = new NoOpPersistentStorage(); + private static readonly IChecksummedPersistentStorage Instance = new NoOpPersistentStorage(); private NoOpPersistentStorage() { } + public static IChecksummedPersistentStorage GetOrThrow(OptionSet optionSet) + => optionSet.GetOption(StorageOptions.DatabaseMustSucceed) + ? throw new InvalidOperationException("Database was not supported") + : Instance; + public void Dispose() { } @@ -89,5 +97,10 @@ public Task WriteStreamAsync(ProjectKey projectKey, string name, Stream st public Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum checksum, CancellationToken cancellationToken) => SpecializedTasks.False; + + public struct TestAccessor + { + public static readonly IChecksummedPersistentStorage StorageInstance = Instance; + } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs index 4f00907cfeb2e..404d7cd1dccf6 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs @@ -2,33 +2,45 @@ // 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; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PersistentStorage; +using Microsoft.CodeAnalysis.Storage; namespace Microsoft.CodeAnalysis.Host { internal class NoOpPersistentStorageService : IChecksummedPersistentStorageService { +#if DOTNET_BUILD_FROM_SOURCE public static readonly IPersistentStorageService Instance = new NoOpPersistentStorageService(); +#else + private static readonly IPersistentStorageService Instance = new NoOpPersistentStorageService(); +#endif private NoOpPersistentStorageService() { } + public static IPersistentStorageService GetOrThrow(OptionSet optionSet) + => optionSet.GetOption(StorageOptions.DatabaseMustSucceed) + ? throw new InvalidOperationException("Database was not supported") + : Instance; + public IPersistentStorage GetStorage(Solution solution) - => NoOpPersistentStorage.Instance; + => NoOpPersistentStorage.GetOrThrow(solution.Options); public ValueTask GetStorageAsync(Solution solution, CancellationToken cancellationToken) - => new(NoOpPersistentStorage.Instance); + => new(NoOpPersistentStorage.GetOrThrow(solution.Options)); ValueTask IChecksummedPersistentStorageService.GetStorageAsync(Solution solution, CancellationToken cancellationToken) - => new(NoOpPersistentStorage.Instance); + => new(NoOpPersistentStorage.GetOrThrow(solution.Options)); ValueTask IChecksummedPersistentStorageService.GetStorageAsync(Solution solution, bool checkBranchId, CancellationToken cancellationToken) - => new(NoOpPersistentStorage.Instance); + => new(NoOpPersistentStorage.GetOrThrow(solution.Options)); ValueTask IChecksummedPersistentStorageService.GetStorageAsync(Workspace workspace, SolutionKey solutionKey, bool checkBranchId, CancellationToken cancellationToken) - => new(NoOpPersistentStorage.Instance); + => new(NoOpPersistentStorage.GetOrThrow(workspace.Options)); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/ProjectKey.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/ProjectKey.cs index 4b04854054ce9..895b901b442f9 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/ProjectKey.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/ProjectKey.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Host; @@ -20,11 +18,11 @@ internal readonly struct ProjectKey public readonly SolutionKey Solution; public readonly ProjectId Id; - public readonly string FilePath; + public readonly string? FilePath; public readonly string Name; public readonly Checksum ParseOptionsChecksum; - public ProjectKey(SolutionKey solution, ProjectId id, string filePath, string name, Checksum parseOptionsChecksum) + public ProjectKey(SolutionKey solution, ProjectId id, string? filePath, string name, Checksum parseOptionsChecksum) { Solution = solution; Id = id; @@ -37,7 +35,10 @@ public static ProjectKey ToProjectKey(Project project) => ToProjectKey(project.Solution.State, project.State); public static ProjectKey ToProjectKey(SolutionState solutionState, ProjectState projectState) - => new(SolutionKey.ToSolutionKey(solutionState), projectState.Id, projectState.FilePath, projectState.Name, projectState.GetParseOptionsChecksum()); + => ToProjectKey(SolutionKey.ToSolutionKey(solutionState), projectState); + + public static ProjectKey ToProjectKey(SolutionKey solutionKey, ProjectState projectState) + => new(solutionKey, projectState.Id, projectState.FilePath, projectState.Name, projectState.GetParseOptionsChecksum()); public SerializableProjectKey Dehydrate() => new(Solution.Dehydrate(), Id, FilePath, Name, ParseOptionsChecksum); @@ -53,7 +54,7 @@ internal readonly struct SerializableProjectKey public readonly ProjectId Id; [DataMember(Order = 2)] - public readonly string FilePath; + public readonly string? FilePath; [DataMember(Order = 3)] public readonly string Name; @@ -61,7 +62,7 @@ internal readonly struct SerializableProjectKey [DataMember(Order = 4)] public readonly Checksum ParseOptionsChecksum; - public SerializableProjectKey(SerializableSolutionKey solution, ProjectId id, string filePath, string name, Checksum parseOptionsChecksum) + public SerializableProjectKey(SerializableSolutionKey solution, ProjectId id, string? filePath, string name, Checksum parseOptionsChecksum) { Solution = solution; Id = id; diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs index 2b457d1b73c8b..ae17981a4ca1a 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Host; @@ -18,10 +16,10 @@ namespace Microsoft.CodeAnalysis.PersistentStorage internal readonly struct SolutionKey { public readonly SolutionId Id; - public readonly string FilePath; + public readonly string? FilePath; public readonly bool IsPrimaryBranch; - public SolutionKey(SolutionId id, string filePath, bool isPrimaryBranch) + public SolutionKey(SolutionId id, string? filePath, bool isPrimaryBranch) { Id = id; FilePath = filePath; @@ -45,12 +43,12 @@ internal readonly struct SerializableSolutionKey public readonly SolutionId Id; [DataMember(Order = 1)] - public readonly string FilePath; + public readonly string? FilePath; [DataMember(Order = 2)] public readonly bool IsPrimaryBranch; - public SerializableSolutionKey(SolutionId id, string filePath, bool isPrimaryBranch) + public SerializableSolutionKey(SolutionId id, string? filePath, bool isPrimaryBranch) { Id = id; FilePath = filePath; diff --git a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestPersistenceService.cs b/src/Workspaces/CoreTest/Host/WorkspaceServices/TestPersistenceService.cs index 5d4971387b802..c5b92c0c957f1 100644 --- a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestPersistenceService.cs +++ b/src/Workspaces/CoreTest/Host/WorkspaceServices/TestPersistenceService.cs @@ -21,9 +21,9 @@ public TestPersistenceService() } public IPersistentStorage GetStorage(Solution solution) - => NoOpPersistentStorage.Instance; + => NoOpPersistentStorage.GetOrThrow(solution.Options); public ValueTask GetStorageAsync(Solution solution, CancellationToken cancellationToken) - => new(NoOpPersistentStorage.Instance); + => new(NoOpPersistentStorage.GetOrThrow(solution.Options)); } } diff --git a/src/Workspaces/Remote/ServiceHub/Host/IGlobalServiceBroker.cs b/src/Workspaces/Remote/ServiceHub/Host/IGlobalServiceBroker.cs new file mode 100644 index 0000000000000..d75fc44e7567b --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Host/IGlobalServiceBroker.cs @@ -0,0 +1,50 @@ +// 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; +using System.Composition; +using System.Threading; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.ServiceHub.Framework; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Remote.Host +{ + internal interface IGlobalServiceBroker + { + IServiceBroker Instance { get; } + } + + /// + /// Hacky way to expose a to workspace services that expect there to be a global + /// singleton (like in visual studio). Effectively the first service that gets called into will record its + /// broker here for these services to use. + /// + // Note: this Export is only so MEF picks up the exported member internally. + [Export(typeof(IGlobalServiceBroker)), Shared] + internal class GlobalServiceBroker : IGlobalServiceBroker + { + private static IServiceBroker? s_instance; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public GlobalServiceBroker() + { + } + + public static void RegisterServiceBroker(IServiceBroker serviceBroker) + { + Interlocked.CompareExchange(ref s_instance, serviceBroker, null); + } + + public IServiceBroker Instance + { + get + { + Contract.ThrowIfNull(s_instance, "Global service broker not registered"); + return s_instance; + } + } + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Host/Storage/RemoteCloudCacheStorageServiceFactory.cs b/src/Workspaces/Remote/ServiceHub/Host/Storage/RemoteCloudCacheStorageServiceFactory.cs new file mode 100644 index 0000000000000..8b0ae8a90cebe --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Host/Storage/RemoteCloudCacheStorageServiceFactory.cs @@ -0,0 +1,72 @@ +// 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; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Remote.Host; +using Microsoft.CodeAnalysis.Storage; +using Microsoft.CodeAnalysis.Storage.CloudCache; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.LanguageServices.Storage; +using Microsoft.VisualStudio.RpcContracts.Caching; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Remote.Storage +{ + [ExportWorkspaceService(typeof(ICloudCacheStorageServiceFactory), WorkspaceKind.RemoteWorkspace), Shared] + internal class RemoteCloudCacheStorageServiceFactory : ICloudCacheStorageServiceFactory + { + private readonly IGlobalServiceBroker _globalServiceBroker; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RemoteCloudCacheStorageServiceFactory(IGlobalServiceBroker globalServiceBroker) + { + _globalServiceBroker = globalServiceBroker; + } + + public AbstractPersistentStorageService Create(IPersistentStorageLocationService locationService) + => new RemoteCloudCachePersistentStorageService(_globalServiceBroker, locationService); + + private class RemoteCloudCachePersistentStorageService : AbstractCloudCachePersistentStorageService + { + private readonly IGlobalServiceBroker _globalServiceBroker; + + public RemoteCloudCachePersistentStorageService(IGlobalServiceBroker globalServiceBroker, IPersistentStorageLocationService locationService) + : base(locationService) + { + _globalServiceBroker = globalServiceBroker; + } + + protected override void DisposeCacheService(ICacheService cacheService) + { + if (cacheService is IAsyncDisposable asyncDisposable) + { + asyncDisposable.DisposeAsync().AsTask().Wait(); + } + else if (cacheService is IDisposable disposable) + { + disposable.Dispose(); + } + } + + protected override async ValueTask CreateCacheServiceAsync(CancellationToken cancellationToken) + { + var serviceBroker = _globalServiceBroker.Instance; + +#pragma warning disable ISB001 // Dispose of proxies + // cache service will be disposed inside RemoteCloudCacheService.Dispose + var cacheService = await serviceBroker.GetProxyAsync(VisualStudioServices.VS2019_10.CacheService, cancellationToken: cancellationToken).ConfigureAwait(false); +#pragma warning restore ISB001 // Dispose of proxies + + Contract.ThrowIfNull(cacheService); + return cacheService; + } + } + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemotePersistentStorageLocationService.cs b/src/Workspaces/Remote/ServiceHub/Host/Storage/RemotePersistentStorageLocationService.cs similarity index 100% rename from src/Workspaces/Remote/ServiceHub/Host/RemotePersistentStorageLocationService.cs rename to src/Workspaces/Remote/ServiceHub/Host/Storage/RemotePersistentStorageLocationService.cs diff --git a/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj b/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj index 5e485c1cd4708..41c3b92b27149 100644 --- a/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj +++ b/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj @@ -28,6 +28,7 @@ + @@ -36,6 +37,9 @@ + + + diff --git a/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.FactoryBase.cs b/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.FactoryBase.cs index e6abd6cb72e64..8829144873748 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.FactoryBase.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.FactoryBase.cs @@ -8,6 +8,7 @@ using System.IO.Pipelines; using System.Runtime; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Remote.Host; using Microsoft.ServiceHub.Framework; using Microsoft.ServiceHub.Framework.Services; using Nerdbank.Streams; @@ -68,6 +69,10 @@ internal TService Create( ServiceActivationOptions serviceActivationOptions, IServiceBroker serviceBroker) { + // Register this service broker globally (if it's the first we encounter) so it can be used by other + // global services that need it. + GlobalServiceBroker.RegisterServiceBroker(serviceBroker); + var descriptor = ServiceDescriptors.Instance.GetServiceDescriptorForServiceFactory(typeof(TService)); var serviceHubTraceSource = (TraceSource)hostProvidedServices.GetService(typeof(TraceSource)); var serverConnection = descriptor.WithTraceSource(serviceHubTraceSource).ConstructRpcConnection(pipe);