diff --git a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCommitter.cs b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCommitter.cs index 260f69db544da..dcc3fe3db63ed 100644 --- a/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCommitter.cs +++ b/src/EditorFeatures/Core/RenameTracking/RenameTrackingTaggerProvider.RenameTrackingCommitter.cs @@ -46,7 +46,9 @@ public RenameTrackingCommitter( _refactorNotifyServices = refactorNotifyServices; _undoHistoryRegistry = undoHistoryRegistry; _displayText = displayText; - _renameSymbolResultGetter = AsyncLazy.Create(c => RenameSymbolWorkerAsync(c)); + _renameSymbolResultGetter = AsyncLazy.Create( + static (self, c) => self.RenameSymbolWorkerAsync(c), + arg: this); } /// diff --git a/src/Features/Core/Portable/AddImport/SearchScopes/SourceSymbolsProjectSearchScope.cs b/src/Features/Core/Portable/AddImport/SearchScopes/SourceSymbolsProjectSearchScope.cs index ba04099888007..456f38ab8eaea 100644 --- a/src/Features/Core/Portable/AddImport/SearchScopes/SourceSymbolsProjectSearchScope.cs +++ b/src/Features/Core/Portable/AddImport/SearchScopes/SourceSymbolsProjectSearchScope.cs @@ -48,11 +48,12 @@ protected override async Task> FindDeclarationsAsync( return declarations; static AsyncLazy CreateLazyAssembly(Project project) - => new(async c => + => AsyncLazy.Create(static async (project, c) => { var compilation = await project.GetRequiredCompilationAsync(c).ConfigureAwait(false); - return compilation.Assembly; - }); + return (IAssemblySymbol?)compilation.Assembly; + }, + arg: project); } } } diff --git a/src/Features/Core/Portable/Completion/SharedSyntaxContextsWithSpeculativeModel.cs b/src/Features/Core/Portable/Completion/SharedSyntaxContextsWithSpeculativeModel.cs index ac417ecd79574..8e23beb338c7f 100644 --- a/src/Features/Core/Portable/Completion/SharedSyntaxContextsWithSpeculativeModel.cs +++ b/src/Features/Core/Portable/Completion/SharedSyntaxContextsWithSpeculativeModel.cs @@ -43,8 +43,8 @@ public Task GetSyntaxContextAsync(Document document, Cancellation // Extract a local function to avoid creating a closure for code path of cache hit. static AsyncLazy GetLazySyntaxContextWithSpeculativeModel(Document document, SharedSyntaxContextsWithSpeculativeModel self) { - return self._cache.GetOrAdd(document, d => AsyncLazy.Create(cancellationToken - => Utilities.CreateSyntaxContextWithExistingSpeculativeModelAsync(d, self._position, cancellationToken))); + return self._cache.GetOrAdd(document, d => AsyncLazy.Create(static (arg, cancellationToken) + => Utilities.CreateSyntaxContextWithExistingSpeculativeModelAsync(arg.d, arg._position, cancellationToken), (d, self._position))); } } } diff --git a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs index 38d837127a930..8fe77a1502baa 100644 --- a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs +++ b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs @@ -82,8 +82,9 @@ static async Task HasDesignerCategoryTypeAsync( } var asyncLazy = s_metadataIdToDesignerAttributeInfo.GetValue( - metadataId, _ => AsyncLazy.Create(cancellationToken => - ComputeHasDesignerCategoryTypeAsync(solutionServices, solutionKey, peReference, cancellationToken))); + metadataId, _ => AsyncLazy.Create(static (arg, cancellationToken) => + ComputeHasDesignerCategoryTypeAsync(arg.solutionServices, arg.solutionKey, arg.peReference, cancellationToken), + arg: (solutionServices, solutionKey, peReference))); return await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false); } @@ -124,7 +125,9 @@ public async ValueTask ProcessPriorityDocumentAsync( using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) { - var lazyProjectVersion = AsyncLazy.Create(frozenProject.GetSemanticVersionAsync); + var lazyProjectVersion = AsyncLazy.Create(static (frozenProject, c) => + frozenProject.GetSemanticVersionAsync(c), + arg: frozenProject); await ScanForDesignerCategoryUsageAsync( frozenProject, frozenDocument, callback, lazyProjectVersion, cancellationToken).ConfigureAwait(false); @@ -170,7 +173,9 @@ private async Task ProcessProjectAsync( // The top level project version for this project. We only care if anything top level changes here. // Downstream impact will already happen due to us keying off of the references a project has (which will // change if anything it depends on changes). - var lazyProjectVersion = AsyncLazy.Create(project.GetSemanticVersionAsync); + var lazyProjectVersion = AsyncLazy.Create(static (project, c) => + project.GetSemanticVersionAsync(c), + arg: project); await ScanForDesignerCategoryUsageAsync( project, specificDocument: null, callback, lazyProjectVersion, cancellationToken).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs index 547610c7cea89..fbe2a833404d6 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs @@ -183,18 +183,19 @@ private AsyncLazy GetDocumentAnalysisNoLock(Project bas } var lazyResults = AsyncLazy.Create( - asynchronousComputeFunction: async cancellationToken => + static async (arg, cancellationToken) => { try { - var analyzer = document.Project.Services.GetRequiredService(); - return await analyzer.AnalyzeDocumentAsync(baseProject, _baseActiveStatements, document, activeStatementSpans, _capabilities, cancellationToken).ConfigureAwait(false); + var analyzer = arg.document.Project.Services.GetRequiredService(); + return await analyzer.AnalyzeDocumentAsync(arg.baseProject, arg.self._baseActiveStatements, arg.document, arg.activeStatementSpans, arg.self._capabilities, cancellationToken).ConfigureAwait(false); } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { throw ExceptionUtilities.Unreachable(); } - }); + }, + arg: (self: this, document, baseProject, activeStatementSpans)); // Previous results for this document id are discarded as they are no longer relevant. // The only relevant analysis is for the latest base and document snapshots. diff --git a/src/Features/Core/Portable/EditAndContinue/EditSession.cs b/src/Features/Core/Portable/EditAndContinue/EditSession.cs index 89ed4e4c87244..fc558099aebaf 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditSession.cs @@ -108,10 +108,14 @@ internal EditSession( telemetry.SetBreakState(inBreakState); BaseActiveStatements = lazyActiveStatementMap ?? (inBreakState - ? AsyncLazy.Create(GetBaseActiveStatementsAsync) - : new AsyncLazy(ActiveStatementsMap.Empty)); - - Capabilities = AsyncLazy.Create(GetCapabilitiesAsync); + ? AsyncLazy.Create(static (self, cancellationToken) => + self.GetBaseActiveStatementsAsync(cancellationToken), + arg: this) + : AsyncLazy.Create(ActiveStatementsMap.Empty)); + + Capabilities = AsyncLazy.Create(static (self, cancellationToken) => + self.GetCapabilitiesAsync(cancellationToken), + arg: this); Analyses = new EditAndContinueDocumentAnalysesCache(BaseActiveStatements, Capabilities); } diff --git a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs index a4dfc2e8fc822..3a0fd67c501d9 100644 --- a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs +++ b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs @@ -211,8 +211,9 @@ await ProcessIndexAsync( // match on disk anymore. var asyncLazy = cachedIndexMap.GetOrAdd( (storageService, documentKey, stringTable), - static t => AsyncLazy.Create( - c => TopLevelSyntaxTreeIndex.LoadAsync(t.service, t.documentKey, checksum: null, t.stringTable, c))); + static t => AsyncLazy.Create(static (t, c) => + TopLevelSyntaxTreeIndex.LoadAsync(t.service, t.documentKey, checksum: null, t.stringTable, c), + arg: t)); return asyncLazy.GetValueAsync(cancellationToken); } } diff --git a/src/Interactive/Host/Interactive/Core/InteractiveHost.LazyRemoteService.cs b/src/Interactive/Host/Interactive/Core/InteractiveHost.LazyRemoteService.cs index f1d15a0399e95..c8b9005c0f668 100644 --- a/src/Interactive/Host/Interactive/Core/InteractiveHost.LazyRemoteService.cs +++ b/src/Interactive/Host/Interactive/Core/InteractiveHost.LazyRemoteService.cs @@ -34,7 +34,7 @@ private sealed class LazyRemoteService public LazyRemoteService(InteractiveHost host, InteractiveHostOptions options, int instanceId, bool skipInitialization) { - _lazyInitializedService = AsyncLazy.Create(TryStartAndInitializeProcessAsync); + _lazyInitializedService = AsyncLazy.Create(static (self, cancellationToken) => self.TryStartAndInitializeProcessAsync(cancellationToken), this); _cancellationSource = new CancellationTokenSource(); InstanceId = instanceId; Options = options; diff --git a/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj b/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj index 1f981252b1675..c4cd1fa0ad9ae 100644 --- a/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj +++ b/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj @@ -37,10 +37,12 @@ + + diff --git a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeTracker.cs b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeTracker.cs index c9c67c996ecc7..7963103802bbb 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeTracker.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeTracker.cs @@ -20,7 +20,7 @@ internal sealed class FileChangeTracker : IVsFreeThreadedFileChangeEvents2, IDis { internal const _VSFILECHANGEFLAGS DefaultFileChangeFlags = _VSFILECHANGEFLAGS.VSFILECHG_Time | _VSFILECHANGEFLAGS.VSFILECHG_Add | _VSFILECHANGEFLAGS.VSFILECHG_Del | _VSFILECHANGEFLAGS.VSFILECHG_Size; - private static readonly AsyncLazy s_none = new(value: null); + private static readonly AsyncLazy s_none = AsyncLazy.Create(value: (uint?)null); private readonly IVsFileChangeEx _fileChangeService; private readonly string _filePath; @@ -107,30 +107,34 @@ public Task StartFileChangeListeningAsync() Contract.ThrowIfTrue(_fileChangeCookie != s_none); - _fileChangeCookie = new AsyncLazy(async cancellationToken => - { - try + _fileChangeCookie = AsyncLazy.Create( + static async (self, cancellationToken) => { - // TODO: Should we pass in cancellationToken here insead of CancellationToken.None? - return await ((IVsAsyncFileChangeEx2)_fileChangeService).AdviseFileChangeAsync(_filePath, _fileChangeFlags, this, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception e) when (ReportException(e)) + try + { + // TODO: Should we pass in cancellationToken here instead of CancellationToken.None? + uint? result = await ((IVsAsyncFileChangeEx2)self._fileChangeService).AdviseFileChangeAsync(self._filePath, self._fileChangeFlags, self, CancellationToken.None).ConfigureAwait(false); + return result; + } + catch (Exception e) when (ReportException(e)) + { + return null; + } + }, + static (self, cancellationToken) => { - return null; - } - }, cancellationToken => - { - try - { - Marshal.ThrowExceptionForHR( - _fileChangeService.AdviseFileChange(_filePath, (uint)_fileChangeFlags, this, out var newCookie)); - return newCookie; - } - catch (Exception e) when (ReportException(e)) - { - return null; - } - }); + try + { + Marshal.ThrowExceptionForHR( + self._fileChangeService.AdviseFileChange(self._filePath, (uint)self._fileChangeFlags, self, out var newCookie)); + return newCookie; + } + catch (Exception e) when (ReportException(e)) + { + return null; + } + }, + arg: this); lock (s_lastBackgroundTaskGate) { diff --git a/src/Workspaces/Core/Portable/FindSymbols/Declarations/DeclarationFinder_AllDeclarations.cs b/src/Workspaces/Core/Portable/FindSymbols/Declarations/DeclarationFinder_AllDeclarations.cs index ea50232d0d10d..37aba259d93a3 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/Declarations/DeclarationFinder_AllDeclarations.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/Declarations/DeclarationFinder_AllDeclarations.cs @@ -63,7 +63,9 @@ internal static async Task> FindAllDeclarationsWithNorma // Lazily produce the compilation. We don't want to incur any costs (especially source generators) if there // are no results for this query in this project. - var lazyCompilation = AsyncLazy.Create(project.GetRequiredCompilationAsync); + var lazyCompilation = AsyncLazy.Create(static (project, cancellationToken) => + project.GetRequiredCompilationAsync(cancellationToken), + arg: project); if (project.SupportsCompilation) { @@ -114,12 +116,13 @@ async Task SearchMetadataReferencesAsync() { using var _ = ArrayBuilder.GetInstance(out var buffer); - var lazyAssembly = AsyncLazy.Create(async cancellationToken => - { - var compilation = await lazyCompilation.GetValueAsync(cancellationToken).ConfigureAwait(false); - var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(peReference) as IAssemblySymbol; - return assemblySymbol; - }); + var lazyAssembly = AsyncLazy.Create(static async (arg, cancellationToken) => + { + var compilation = await arg.lazyCompilation.GetValueAsync(cancellationToken).ConfigureAwait(false); + var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(arg.peReference) as IAssemblySymbol; + return assemblySymbol; + }, + arg: (lazyCompilation, peReference)); await AddMetadataDeclarationsWithNormalQueryAsync( project, lazyAssembly, peReference, diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder_ProjectIndex.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder_ProjectIndex.cs index 57466793e2d62..083c3d0937a87 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder_ProjectIndex.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder_ProjectIndex.cs @@ -35,8 +35,9 @@ public static Task GetIndexAsync( if (!s_projectToIndex.TryGetValue(project.State, out var lazyIndex)) { lazyIndex = s_projectToIndex.GetValue( - project.State, p => new AsyncLazy( - c => CreateIndexAsync(project, c))); + project.State, p => AsyncLazy.Create( + static (project, c) => CreateIndexAsync(project, c), + project)); } return lazyIndex.GetValueAsync(cancellationToken); diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs index 19bf32975812a..fe3a5ac849b02 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs @@ -123,7 +123,7 @@ public Task> FindAsync( Contract.ThrowIfTrue(query.Kind == SearchKind.Custom, "Custom queries are not supported in this API"); return this.FindAsync( - query, new AsyncLazy(assembly), filter, cancellationToken); + query, AsyncLazy.Create((IAssemblySymbol?)assembly), filter, cancellationToken); } public async Task> FindAsync( diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Metadata.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Metadata.cs index 44981b28d5e2f..f314281d828e6 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Metadata.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Metadata.cs @@ -158,8 +158,9 @@ static async Task GetInfoForMetadataReferenceSlowAsync( // CreateMetadataSymbolTreeInfoAsync var asyncLazy = s_peReferenceToInfo.GetValue( reference, - id => AsyncLazy.Create( - c => CreateMetadataSymbolTreeInfoAsync(services, solutionKey, reference, checksum, c))); + id => AsyncLazy.Create(static (arg, c) => + CreateMetadataSymbolTreeInfoAsync(arg.services, arg.solutionKey, arg.reference, arg.checksum, c), + arg: (services, solutionKey, reference, checksum))); return await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false); } @@ -177,14 +178,15 @@ static async Task CreateMetadataSymbolTreeInfoAsync( var asyncLazy = s_metadataIdToSymbolTreeInfo.GetValue( metadataId, - metadataId => AsyncLazy.Create( - cancellationToken => LoadOrCreateAsync( - services, - solutionKey, - checksum, - createAsync: checksum => new ValueTask(new MetadataInfoCreator(checksum, GetMetadataNoThrow(reference)).Create()), - keySuffix: GetMetadataKeySuffix(reference), - cancellationToken))); + metadataId => AsyncLazy.Create(static (arg, cancellationToken) => + LoadOrCreateAsync( + arg.services, + arg.solutionKey, + arg.checksum, + createAsync: checksum => new ValueTask(new MetadataInfoCreator(checksum, GetMetadataNoThrow(arg.reference)).Create()), + keySuffix: GetMetadataKeySuffix(arg.reference), + cancellationToken), + arg: (services, solutionKey, checksum, reference))); var metadataIdSymbolTreeInfo = await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs index 7d8203ac803c1..a0fef76868bc6 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs @@ -72,7 +72,10 @@ public static Task GetInfoForSourceAssemblyAsync( public static Task GetSourceSymbolsChecksumAsync(Project project, CancellationToken cancellationToken) { var lazy = s_projectToSourceChecksum.GetValue( - project.State, static p => AsyncLazy.Create(c => ComputeSourceSymbolsChecksumAsync(p, c))); + project.State, + static p => AsyncLazy.Create( + static (p, c) => ComputeSourceSymbolsChecksumAsync(p, c), + arg: p)); return lazy.GetValueAsync(cancellationToken); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigDocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigDocumentState.cs index cfae9a533fcad..da0d0ef11002a 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigDocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigDocumentState.cs @@ -38,9 +38,10 @@ public AnalyzerConfigDocumentState( private AsyncLazy CreateAnalyzerConfigValueSource() { - return new AsyncLazy( - asynchronousComputeFunction: async cancellationToken => AnalyzerConfig.Parse(await GetTextAsync(cancellationToken).ConfigureAwait(false), FilePath), - synchronousComputeFunction: cancellationToken => AnalyzerConfig.Parse(GetTextSynchronously(cancellationToken), FilePath)); + return AsyncLazy.Create( + asynchronousComputeFunction: static async (self, cancellationToken) => AnalyzerConfig.Parse(await self.GetTextAsync(cancellationToken).ConfigureAwait(false), self.FilePath), + synchronousComputeFunction: static (self, cancellationToken) => AnalyzerConfig.Parse(self.GetTextSynchronously(cancellationToken), self.FilePath), + arg: this); } public AnalyzerConfig GetAnalyzerConfig(CancellationToken cancellationToken) => _analyzerConfigValueSource.GetValue(cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs index b69a16a93eb50..43f4eca13e51e 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs @@ -556,11 +556,12 @@ public Task GetOptionsAsync(CancellationToken cancellationTok private void InitializeCachedOptions(OptionSet solutionOptions) { - var newAsyncLazy = AsyncLazy.Create(async cancellationToken => + var newAsyncLazy = AsyncLazy.Create(static async (arg, cancellationToken) => { - var options = await GetAnalyzerConfigOptionsAsync(cancellationToken).ConfigureAwait(false); - return new DocumentOptionSet(options, solutionOptions, Project.Language); - }); + var options = await arg.self.GetAnalyzerConfigOptionsAsync(cancellationToken).ConfigureAwait(false); + return new DocumentOptionSet(options, arg.solutionOptions, arg.self.Project.Language); + }, + arg: (self: this, solutionOptions)); Interlocked.CompareExchange(ref _cachedOptions, newAsyncLazy, comparand: null); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs index c536e7a2437b4..9c92c71478ee4 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs @@ -105,9 +105,10 @@ protected static AsyncLazy CreateLazyFullyParsedTree( LanguageServices languageServices, PreservationMode mode = PreservationMode.PreserveValue) { - return new AsyncLazy( - c => FullyParseTreeAsync(newTextSource, loadTextOptions, filePath, options, languageServices, mode, c), - c => FullyParseTree(newTextSource, loadTextOptions, filePath, options, languageServices, mode, c)); + return AsyncLazy.Create( + static (arg, c) => FullyParseTreeAsync(arg.newTextSource, arg.loadTextOptions, arg.filePath, arg.options, arg.languageServices, arg.mode, c), + static (arg, c) => FullyParseTree(arg.newTextSource, arg.loadTextOptions, arg.filePath, arg.options, arg.languageServices, arg.mode, c), + arg: (newTextSource, loadTextOptions, filePath, options, languageServices, mode)); } private static async Task FullyParseTreeAsync( @@ -167,9 +168,10 @@ private static AsyncLazy CreateLazyIncrementallyParsedTree( ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions) { - return new AsyncLazy( - c => IncrementallyParseTreeAsync(oldTreeSource, newTextSource, loadTextOptions, c), - c => IncrementallyParseTree(oldTreeSource, newTextSource, loadTextOptions, c)); + return AsyncLazy.Create( + static (arg, c) => IncrementallyParseTreeAsync(arg.oldTreeSource, arg.newTextSource, arg.loadTextOptions, c), + static (arg, c) => IncrementallyParseTree(arg.oldTreeSource, arg.newTextSource, arg.loadTextOptions, c), + arg: (oldTreeSource, newTextSource, loadTextOptions)); } private static async Task IncrementallyParseTreeAsync( @@ -543,7 +545,10 @@ internal DocumentState UpdateTree(SyntaxNode newRoot, PreservationMode mode) // its okay to use a strong cached AsyncLazy here because the compiler layer SyntaxTree will also keep the text alive once its built. var lazyTextAndVersion = new TreeTextSource( - new AsyncLazy(tree.GetTextAsync, tree.GetText), + AsyncLazy.Create( + static (tree, c) => tree.GetTextAsync(c), + static (tree, c) => tree.GetText(c), + arg: tree), textVersion); return (lazyTextAndVersion, new TreeAndVersion(tree, treeVersion)); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs index 9d6fcbde84e32..9353d1f140a87 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs @@ -77,9 +77,10 @@ static AsyncLazy GetReuseTreeSource( AsyncLazy siblingTreeSource, bool forceEvenIfTreesWouldDiffer) { - return new AsyncLazy( - cancellationToken => TryReuseSiblingTreeAsync(filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer, cancellationToken), - cancellationToken => TryReuseSiblingTree(filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer, cancellationToken)); + return AsyncLazy.Create( + static (arg, cancellationToken) => TryReuseSiblingTreeAsync(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.treeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), + static (arg, cancellationToken) => TryReuseSiblingTree(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.treeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), + arg: (filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer)); } static bool TryReuseSiblingRoot( diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index 5352bcfa18e35..6577677f41562 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -88,7 +88,7 @@ private ProjectState( // the info has changed by DocumentState. _projectInfo = ClearAllDocumentsFromProjectInfo(projectInfo); - _lazyChecksums = AsyncLazy.Create(ComputeChecksumsAsync); + _lazyChecksums = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeChecksumsAsync(cancellationToken), arg: this); } public ProjectState(LanguageServices languageServices, ProjectInfo projectInfo) @@ -119,8 +119,8 @@ public ProjectState(LanguageServices languageServices, ProjectInfo projectInfo) DocumentStates = new TextDocumentStates(projectInfoFixed.Documents, info => CreateDocument(info, parseOptions, loadTextOptions)); AdditionalDocumentStates = new TextDocumentStates(projectInfoFixed.AdditionalDocuments, info => new AdditionalDocumentState(languageServices.SolutionServices, info, loadTextOptions)); - _lazyLatestDocumentVersion = AsyncLazy.Create(c => ComputeLatestDocumentVersionAsync(DocumentStates, AdditionalDocumentStates, c)); - _lazyLatestDocumentTopLevelChangeVersion = AsyncLazy.Create(c => ComputeLatestDocumentTopLevelChangeVersionAsync(DocumentStates, AdditionalDocumentStates, c)); + _lazyLatestDocumentVersion = AsyncLazy.Create(static (self, c) => ComputeLatestDocumentVersionAsync(self.DocumentStates, self.AdditionalDocumentStates, c), arg: this); + _lazyLatestDocumentTopLevelChangeVersion = AsyncLazy.Create(static (self, c) => ComputeLatestDocumentTopLevelChangeVersionAsync(self.DocumentStates, self.AdditionalDocumentStates, c), arg: this); // ownership of information on document has moved to project state. clear out documentInfo the state is // holding on. otherwise, these information will be held onto unnecessarily by projectInfo even after @@ -128,7 +128,7 @@ public ProjectState(LanguageServices languageServices, ProjectInfo projectInfo) // we hold onto the info so that we don't need to duplicate all information info already has in the state _projectInfo = ClearAllDocumentsFromProjectInfo(projectInfoFixed); - _lazyChecksums = AsyncLazy.Create(ComputeChecksumsAsync); + _lazyChecksums = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeChecksumsAsync(cancellationToken), arg: this); } private static ProjectInfo ClearAllDocumentsFromProjectInfo(ProjectInfo projectInfo) @@ -195,11 +195,15 @@ private AsyncLazy CreateLazyLatestDocumentTopLevelChangeVersion( { if (_lazyLatestDocumentTopLevelChangeVersion.TryGetValue(out var oldVersion)) { - return AsyncLazy.Create(c => ComputeTopLevelChangeTextVersionAsync(oldVersion, newDocument, c)); + return AsyncLazy.Create(static (arg, c) => + ComputeTopLevelChangeTextVersionAsync(arg.oldVersion, arg.newDocument, c), + arg: (oldVersion, newDocument)); } else { - return AsyncLazy.Create(c => ComputeLatestDocumentTopLevelChangeVersionAsync(newDocumentStates, newAdditionalDocumentStates, c)); + return AsyncLazy.Create(static (arg, c) => + ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, c), + arg: (newDocumentStates, newAdditionalDocumentStates)); } } @@ -463,8 +467,8 @@ public override bool Equals(object? obj) private static AsyncLazy ComputeAnalyzerConfigOptionsValueSource(TextDocumentStates analyzerConfigDocumentStates) { - return new AsyncLazy( - asynchronousComputeFunction: async cancellationToken => + return AsyncLazy.Create( + asynchronousComputeFunction: static async (analyzerConfigDocumentStates, cancellationToken) => { var tasks = analyzerConfigDocumentStates.States.Values.Select(a => a.GetAnalyzerConfigAsync(cancellationToken)); var analyzerConfigs = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -473,11 +477,12 @@ private static AsyncLazy ComputeAnalyzerConfigOption return new AnalyzerConfigOptionsCache(AnalyzerConfigSet.Create(analyzerConfigs)); }, - synchronousComputeFunction: cancellationToken => + synchronousComputeFunction: static (analyzerConfigDocumentStates, cancellationToken) => { var analyzerConfigs = analyzerConfigDocumentStates.SelectAsArray(a => a.GetAnalyzerConfig(cancellationToken)); return new AnalyzerConfigOptionsCache(AnalyzerConfigSet.Create(analyzerConfigs)); - }); + }, + arg: analyzerConfigDocumentStates); } private readonly struct AnalyzerConfigOptionsCache(AnalyzerConfigSet configSet) @@ -954,13 +959,19 @@ private void GetLatestDependentVersions( } dependentDocumentVersion = recalculateDocumentVersion - ? AsyncLazy.Create(c => ComputeLatestDocumentVersionAsync(newDocumentStates, newAdditionalDocumentStates, c)) + ? AsyncLazy.Create(static (arg, c) => + ComputeLatestDocumentVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, c), + arg: (newDocumentStates, newAdditionalDocumentStates)) : contentChanged - ? AsyncLazy.Create(newDocument.GetTextVersionAsync) + ? AsyncLazy.Create(static (newDocument, c) => + newDocument.GetTextVersionAsync(c), + arg: newDocument) : _lazyLatestDocumentVersion; dependentSemanticVersion = recalculateSemanticVersion - ? AsyncLazy.Create(c => ComputeLatestDocumentTopLevelChangeVersionAsync(newDocumentStates, newAdditionalDocumentStates, c)) + ? AsyncLazy.Create(static (arg, c) => + ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, c), + arg: (newDocumentStates, newAdditionalDocumentStates)) : contentChanged ? CreateLazyLatestDocumentTopLevelChangeVersion(newDocument, newDocumentStates, newAdditionalDocumentStates) : _lazyLatestDocumentTopLevelChangeVersion; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index 2320f5c8595f2..e7d1aeca91fcf 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -48,7 +48,10 @@ private Solution( _projectIdToProjectMap = []; _compilationState = compilationState; - _cachedFrozenSolution = cachedFrozenSolution ?? AsyncLazy.Create(synchronousComputeFunction: ComputeFrozenSolution); + _cachedFrozenSolution = cachedFrozenSolution ?? + AsyncLazy.Create(synchronousComputeFunction: static (self, c) => + self.ComputeFrozenSolution(c), + this); } internal Solution( @@ -1510,7 +1513,9 @@ AsyncLazy GetLazySolution() } static AsyncLazy CreateLazyFrozenSolution(SolutionCompilationState compilationState, DocumentId documentId) - => AsyncLazy.Create(synchronousComputeFunction: cancellationToken => ComputeFrozenSolution(compilationState, documentId, cancellationToken)); + => AsyncLazy.Create(synchronousComputeFunction: static (arg, cancellationToken) => + ComputeFrozenSolution(arg.compilationState, arg.documentId, cancellationToken), + arg: (compilationState, documentId)); static Solution ComputeFrozenSolution(SolutionCompilationState compilationState, DocumentId documentId, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs index b8cf9b3df1074..d89f2c1958087 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs @@ -974,11 +974,13 @@ public Task GetDependentVersionAsync( { if (_lazyDependentVersion == null) { - // temp. local to avoid a closure allocation for the fast path // note: solution is captured here, but it will go away once GetValueAsync executes. - var compilationStateCapture = compilationState; - Interlocked.CompareExchange(ref _lazyDependentVersion, AsyncLazy.Create( - c => ComputeDependentVersionAsync(compilationStateCapture, c)), null); + Interlocked.CompareExchange( + ref _lazyDependentVersion, + AsyncLazy.Create(static (arg, c) => + arg.self.ComputeDependentVersionAsync(arg.compilationState, c), + arg: (self: this, compilationState)), + null); } return _lazyDependentVersion.GetValueAsync(cancellationToken); @@ -1011,11 +1013,13 @@ public Task GetDependentSemanticVersionAsync( { if (_lazyDependentSemanticVersion == null) { - // temp. local to avoid a closure allocation for the fast path // note: solution is captured here, but it will go away once GetValueAsync executes. - var compilationStateCapture = compilationState; - Interlocked.CompareExchange(ref _lazyDependentSemanticVersion, AsyncLazy.Create( - c => ComputeDependentSemanticVersionAsync(compilationStateCapture, c)), null); + Interlocked.CompareExchange( + ref _lazyDependentSemanticVersion, + AsyncLazy.Create(static (arg, c) => + arg.self.ComputeDependentSemanticVersionAsync(arg.compilationState, c), + arg: (self: this, compilationState)) + , null); } return _lazyDependentSemanticVersion.GetValueAsync(cancellationToken); @@ -1047,9 +1051,13 @@ public Task GetDependentChecksumAsync( { if (_lazyDependentChecksum == null) { - var tmp = compilationState.SolutionState; // temp. local to avoid a closure allocation for the fast path // note: solution is captured here, but it will go away once GetValueAsync executes. - Interlocked.CompareExchange(ref _lazyDependentChecksum, AsyncLazy.Create(c => ComputeDependentChecksumAsync(tmp, c)), null); + Interlocked.CompareExchange( + ref _lazyDependentChecksum, + AsyncLazy.Create(static (arg, c) => + arg.self.ComputeDependentChecksumAsync(arg.SolutionState, c), + arg: (self: this, compilationState.SolutionState)), + null); } return _lazyDependentChecksum.GetValueAsync(cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs index d15a4637db46d..5c9e50eafe760 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs @@ -129,7 +129,12 @@ public Task GetDependentChecksumAsync(SolutionCompilationState compila { var tmp = compilationState; // temp. local to avoid a closure allocation for the fast path // note: solution is captured here, but it will go away once GetValueAsync executes. - Interlocked.CompareExchange(ref _lazyDependentChecksum, AsyncLazy.Create(c => ComputeDependentChecksumAsync(tmp, c)), null); + Interlocked.CompareExchange( + ref _lazyDependentChecksum, + AsyncLazy.Create(static (arg, c) => + arg.self.ComputeDependentChecksumAsync(arg.tmp, c), + arg: (self: this, tmp)), + null); } return _lazyDependentChecksum.GetValueAsync(cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs index 2f260a249e4a4..85b47c5e21a74 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs @@ -197,8 +197,9 @@ public readonly SkeletonReferenceCache Clone() // concurrent requests asynchronously wait for that work to be done. var lazy = s_compilationToSkeletonSet.GetValue(compilation, - compilation => AsyncLazy.Create( - cancellationToken => Task.FromResult(CreateSkeletonSet(services, compilation, cancellationToken)))); + compilation => AsyncLazy.Create(static (arg, cancellationToken) => + Task.FromResult(CreateSkeletonSet(arg.services, arg.compilation, cancellationToken)), + arg: (services, compilation))); return await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs index 42ffca0acaa59..47ebbecf525a9 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs @@ -69,8 +69,13 @@ private SolutionCompilationState( FrozenSourceGeneratedDocumentStates = frozenSourceGeneratedDocumentStates; // when solution state is changed, we recalculate its checksum - _lazyChecksums = AsyncLazy.Create(c => ComputeChecksumsAsync(projectId: null, c)); - _cachedFrozenSnapshot = cachedFrozenSnapshot ?? AsyncLazy.Create(synchronousComputeFunction: ComputeFrozenSnapshot); + _lazyChecksums = AsyncLazy.Create(static (self, c) => + self.ComputeChecksumsAsync(projectId: null, c), + arg: this); + _cachedFrozenSnapshot = cachedFrozenSnapshot ?? + AsyncLazy.Create(synchronousComputeFunction: static (self, c) => + self.ComputeFrozenSnapshot(c), + arg: this); CheckInvariants(); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_Checksum.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_Checksum.cs index 0a884218c21d0..dd7d83c0b8e4b 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_Checksum.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_Checksum.cs @@ -69,17 +69,16 @@ public async Task GetStateChecksumsAsync( { if (!_lazyProjectChecksums.TryGetValue(projectId, out checksums)) { - checksums = Compute(projectId); + checksums = AsyncLazy.Create(static (arg, c) => + arg.self.ComputeChecksumsAsync(arg.projectId, c), + arg: (self: this, projectId)); + _lazyProjectChecksums.Add(projectId, checksums); } } var collection = await checksums.GetValueAsync(cancellationToken).ConfigureAwait(false); return collection; - - // Extracted as a local function to prevent delegate allocations when not needed. - AsyncLazy Compute(ProjectId projectId) - => AsyncLazy.Create(c => ComputeChecksumsAsync(projectId, c)); } /// Gets the checksum for only the requested project (and any project it depends on) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index 82970d6f12a59..3e6200977dde5 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -74,7 +74,9 @@ private SolutionState( _lazyAnalyzers = lazyAnalyzers ?? CreateLazyHostDiagnosticAnalyzers(analyzerReferences); // when solution state is changed, we recalculate its checksum - _lazyChecksums = AsyncLazy.Create(c => ComputeChecksumsAsync(projectConeId: null, c)); + _lazyChecksums = AsyncLazy.Create(static (self, c) => + self.ComputeChecksumsAsync(projectConeId: null, c), + arg: this); CheckInvariants(); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState_Checksum.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState_Checksum.cs index 689f50f249a6c..50e95e08fa857 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState_Checksum.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState_Checksum.cs @@ -89,7 +89,9 @@ public async Task GetStateChecksumsAsync( // Extracted as a local function to prevent delegate allocations when not needed. AsyncLazy Compute(ProjectId projectConeId) { - return AsyncLazy.Create(c => ComputeChecksumsAsync(projectConeId, c)); + return AsyncLazy.Create(static (arg, c) => + arg.self.ComputeChecksumsAsync(arg.projectConeId, c), + arg: (self: this, projectConeId)); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs index 40d745f3ba545..33a799e996aba 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs @@ -52,7 +52,7 @@ protected TextDocumentState( // a new AsyncLazy to compute the checksum though, and that's because there's no practical way for // the newly created TextDocumentState to have the same checksum as a previous TextDocumentState: // if we're creating a new state, it's because something changed, and we'll have to create a new checksum. - _lazyChecksums = AsyncLazy.Create(ComputeChecksumsAsync); + _lazyChecksums = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeChecksumsAsync(cancellationToken), arg: this); } public TextDocumentState(SolutionServices solutionServices, DocumentInfo info, LoadTextOptions loadTextOptions) diff --git a/src/Workspaces/CoreTest/UtilityTest/AsyncLazyTests.cs b/src/Workspaces/CoreTest/UtilityTest/AsyncLazyTests.cs index 836e141ebc277..c8f23e41194a1 100644 --- a/src/Workspaces/CoreTest/UtilityTest/AsyncLazyTests.cs +++ b/src/Workspaces/CoreTest/UtilityTest/AsyncLazyTests.cs @@ -22,7 +22,7 @@ public void GetValueAsyncReturnsCompletedTaskIfAsyncComputationCompletesImmediat // Note, this test may pass even if GetValueAsync posted a task to the threadpool, since the // current thread may context switch out and allow the threadpool to complete the task before // we check the state. However, a failure here definitely indicates a bug in AsyncLazy. - var lazy = AsyncLazy.Create(c => Task.FromResult(5)); + var lazy = AsyncLazy.Create(static c => Task.FromResult(5)); var t = lazy.GetValueAsync(CancellationToken.None); Assert.Equal(TaskStatus.RanToCompletion, t.Status); Assert.Equal(5, t.Result); @@ -40,26 +40,27 @@ public void SynchronousContinuationsDoNotRunWithinGetValueCall(TaskStatus expect var requestCancellationTokenSource = new CancellationTokenSource(); // First, create an async lazy that will only ever do synchronous computations. - var lazy = new AsyncLazy( - asynchronousComputeFunction: c => { throw new Exception("We should not get an asynchronous computation."); }, - synchronousComputeFunction: c => + var lazy = AsyncLazy.Create( + asynchronousComputeFunction: static (arg, c) => { throw new Exception("We should not get an asynchronous computation."); }, + synchronousComputeFunction: static (arg, c) => { // Notify that the synchronous computation started - synchronousComputationStartedEvent.Set(); + arg.synchronousComputationStartedEvent.Set(); // And now wait when we should finish - synchronousComputationShouldCompleteEvent.WaitOne(); + arg.synchronousComputationShouldCompleteEvent.WaitOne(); c.ThrowIfCancellationRequested(); - if (expectedTaskStatus == TaskStatus.Faulted) + if (arg.expectedTaskStatus == TaskStatus.Faulted) { // We want to see what happens if this underlying task faults, so let's fault! throw new Exception("Task blew up!"); } return 42; - }); + }, + arg: (synchronousComputationStartedEvent, synchronousComputationShouldCompleteEvent, expectedTaskStatus)); // Second, start a synchronous request. While we are in the GetValue, we will record which thread is being occupied by the request Thread? synchronousRequestThread = null; @@ -144,11 +145,11 @@ private static void GetValueOrGetValueAsyncThrowsCorrectExceptionDuringCancellat var computeFunctionRunning = new ManualResetEvent(initialState: false); AsyncLazy lazy; - Func? synchronousComputation = null; + Func? synchronousComputation = null; if (includeSynchronousComputation) { - synchronousComputation = c => + synchronousComputation = (arg, c) => { computeFunctionRunning.Set(); while (true) @@ -158,14 +159,17 @@ private static void GetValueOrGetValueAsyncThrowsCorrectExceptionDuringCancellat }; } - lazy = new AsyncLazy(c => - { - computeFunctionRunning.Set(); - while (true) + lazy = AsyncLazy.Create( + static (computeFunctionRunning, c) => { - c.ThrowIfCancellationRequested(); - } - }, synchronousComputeFunction: synchronousComputation); + computeFunctionRunning.Set(); + while (true) + { + c.ThrowIfCancellationRequested(); + } + }, + synchronousComputeFunction: synchronousComputation!, + arg: computeFunctionRunning); var cancellationTokenSource = new CancellationTokenSource(); @@ -192,14 +196,14 @@ public void GetValueAsyncThatIsCancelledReturnsTaskCancelledWithCorrectToken() { var cancellationTokenSource = new CancellationTokenSource(); - var lazy = AsyncLazy.Create(c => Task.Run((Func)(() => + var lazy = AsyncLazy.Create(static (cancellationTokenSource, c) => Task.Run((Func)(() => { cancellationTokenSource.Cancel(); while (true) { c.ThrowIfCancellationRequested(); } - }), c)); + }), c), arg: cancellationTokenSource); var task = lazy.GetValueAsync(cancellationTokenSource.Token); @@ -237,9 +241,10 @@ private static void CancellationDuringInlinedComputationFromGetValueOrGetValueAs return createdObject; }; - var lazy = new AsyncLazy( - c => Task.FromResult(synchronousComputation(c)), - includeSynchronousComputation ? synchronousComputation : null); + var lazy = AsyncLazy.Create( + static (synchronousComputation, c) => Task.FromResult(synchronousComputation(c)), + includeSynchronousComputation ? static (synchronousComputation, c) => synchronousComputation(c) : null!, + arg: synchronousComputation); var thrownException = Assert.Throws(() => { @@ -259,7 +264,7 @@ private static void CancellationDuringInlinedComputationFromGetValueOrGetValueAs [Fact] public void SynchronousRequestShouldCacheValueWithAsynchronousComputeFunction() { - var lazy = new AsyncLazy(c => Task.FromResult(new object())); + var lazy = AsyncLazy.Create(static c => Task.FromResult(new object())); var firstRequestResult = lazy.GetValue(CancellationToken.None); var secondRequestResult = lazy.GetValue(CancellationToken.None); @@ -285,8 +290,8 @@ public async Task AwaitingProducesCorrectException(bool producerAsync, bool cons }; var lazy = producerAsync - ? new AsyncLazy(asynchronousComputeFunction) - : new AsyncLazy(asynchronousComputeFunction, synchronousComputeFunction); + ? AsyncLazy.Create(asynchronousComputeFunction) + : AsyncLazy.Create(asynchronousComputeFunction, synchronousComputeFunction); var actual = consumerAsync ? await Assert.ThrowsAsync(async () => await lazy.GetValueAsync(CancellationToken.None)) @@ -307,19 +312,20 @@ public async Task CancelledAndReranAsynchronousComputationDoesNotBreakSynchronou // We don't want the async path to run sooner than we expect, so we'll set it once ready Func>? asynchronousComputation = null; - var lazy = new AsyncLazy( - asynchronousComputeFunction: ct => + var lazy = AsyncLazy.Create( + asynchronousComputeFunction: static (arg, ct) => { - AssertEx.NotNull(asynchronousComputation, $"The asynchronous computation was not expected to be running."); - return asynchronousComputation(ct); + AssertEx.NotNull(arg.asynchronousComputation, $"The asynchronous computation was not expected to be running."); + return arg.asynchronousComputation(ct); }, - synchronousComputeFunction: _ => + synchronousComputeFunction: static (arg, ct) => { // Let the test know we've started, and we'll continue once asked - synchronousComputationStartedEvent.Set(); - synchronousComputationShouldCompleteEvent.WaitOne(); + arg.synchronousComputationStartedEvent.Set(); + arg.synchronousComputationShouldCompleteEvent.WaitOne(); return "Returned from synchronous computation: " + Guid.NewGuid(); - }); + }, + arg: (asynchronousComputation, synchronousComputationStartedEvent, synchronousComputationShouldCompleteEvent)); // Step 1: start the synchronous operation and wait for it to be running var synchronousRequest = Task.Run(() => lazy.GetValue(CancellationToken.None)); @@ -364,28 +370,29 @@ public async Task AsynchronousResultThatWasCancelledDoesNotBreakSynchronousReque var asynchronousRequestCancellationToken = new CancellationTokenSource(); - var lazy = new AsyncLazy( - asynchronousComputeFunction: ct => + var lazy = AsyncLazy.Create( + asynchronousComputeFunction: static (arg, ct) => { - asynchronousRequestCancellationToken.Cancel(); + arg.asynchronousRequestCancellationToken.Cancel(); // Now wait until the cancellation is sent to this underlying computation while (!ct.IsCancellationRequested) Thread.Yield(); // Now we're ready to complete, so this is when we want to pause - asynchronousComputationReadyToComplete.Set(); - asynchronousComputationShouldCompleteEvent.WaitOne(); + arg.asynchronousComputationReadyToComplete.Set(); + arg.asynchronousComputationShouldCompleteEvent.WaitOne(); return Task.FromResult("Returned from asynchronous computation: " + Guid.NewGuid()); }, - synchronousComputeFunction: _ => + synchronousComputeFunction: static (arg, _) => { // Let the test know we've started, and we'll continue once asked - synchronousComputationStartedEvent.Set(); - synchronousComputationShouldCompleteEvent.WaitOne(); + arg.synchronousComputationStartedEvent.Set(); + arg.synchronousComputationShouldCompleteEvent.WaitOne(); return "Returned from synchronous computation: " + Guid.NewGuid(); - }); + }, + arg: (asynchronousRequestCancellationToken, asynchronousComputationReadyToComplete, asynchronousComputationShouldCompleteEvent, synchronousComputationStartedEvent, synchronousComputationShouldCompleteEvent)); // Steps 1 and 2: start asynchronous computation and wait until it's running; this will cancel itself once it's started var asynchronousRequest = Task.Run(() => lazy.GetValueAsync(asynchronousRequestCancellationToken.Token)); diff --git a/src/Workspaces/Remote/ServiceHub/Services/ClientOptionsProvider.cs b/src/Workspaces/Remote/ServiceHub/Services/ClientOptionsProvider.cs index 707deb56b06a8..1f18c201cd072 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/ClientOptionsProvider.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/ClientOptionsProvider.cs @@ -18,10 +18,11 @@ internal sealed class ClientOptionsProvider(RemoteCallback< public async ValueTask GetOptionsAsync(LanguageServices languageServices, CancellationToken cancellationToken) { - var lazyOptions = ImmutableInterlocked.GetOrAdd(ref _cache, languageServices.Language, _ => AsyncLazy.Create(GetRemoteOptionsAsync)); + var lazyOptions = ImmutableInterlocked.GetOrAdd(ref _cache, languageServices.Language, _ => AsyncLazy.Create( + static (arg, cancellationToken) => arg.self.GetRemoteOptionsAsync(arg.languageServices, cancellationToken), arg: (self: this, languageServices))); return await lazyOptions.GetValueAsync(cancellationToken).ConfigureAwait(false); - - Task GetRemoteOptionsAsync(CancellationToken cancellationToken) - => callback.InvokeAsync((callback, cancellationToken) => callback.GetOptionsAsync(callbackId, languageServices.Language, cancellationToken), cancellationToken).AsTask(); } + + private Task GetRemoteOptionsAsync(LanguageServices languageServices, CancellationToken cancellationToken) + => callback.InvokeAsync((callback, cancellationToken) => callback.GetOptionsAsync(callbackId, languageServices.Language, cancellationToken), cancellationToken).AsTask(); } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index 1eca87d241e15..320b61bf7b2f4 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -483,6 +483,7 @@ + diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncLazy.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncLazy.cs new file mode 100644 index 0000000000000..43cfec9304f24 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncLazy.cs @@ -0,0 +1,46 @@ +// 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; + +namespace Roslyn.Utilities; + +internal static class AsyncLazy +{ + public static AsyncLazy Create(Func> asynchronousComputeFunction, Func? synchronousComputeFunction, TArg arg) + => AsyncLazy.Create(asynchronousComputeFunction, synchronousComputeFunction, arg); + + public static AsyncLazy Create(Func> asynchronousComputeFunction, TArg arg) + => Create( + asynchronousComputeFunction, + synchronousComputeFunction: null, + arg); + + public static AsyncLazy Create(Func synchronousComputeFunction, TArg arg) + => Create( + asynchronousComputeFunction: static (outerArg, cancellationToken) => Task.FromResult(outerArg.synchronousComputeFunction(outerArg.arg, cancellationToken)), + synchronousComputeFunction: static (outerArg, cancellationToken) => outerArg.synchronousComputeFunction(outerArg.arg, cancellationToken), + (synchronousComputeFunction, arg)); + + public static AsyncLazy Create(Func> asynchronousComputeFunction) + => Create( + asynchronousComputeFunction: static (asynchronousComputeFunction, cancellationToken) => asynchronousComputeFunction(cancellationToken), + arg: asynchronousComputeFunction); + + public static AsyncLazy Create(Func synchronousComputeFunction) + => Create( + synchronousComputeFunction: static (synchronousComputeFunction, cancellationToken) => synchronousComputeFunction(cancellationToken), + arg: synchronousComputeFunction); + + public static AsyncLazy Create(Func> asynchronousComputeFunction, Func synchronousComputeFunction) + => Create( + asynchronousComputeFunction: static (arg, cancellationToken) => arg.asynchronousComputeFunction(cancellationToken), + synchronousComputeFunction: static (arg, cancellationToken) => arg.synchronousComputeFunction(cancellationToken), + arg: (asynchronousComputeFunction, synchronousComputeFunction)); + + public static AsyncLazy Create(T value) + => AsyncLazy.Create(value); +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncLazy`1.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncLazy`1.cs index 4b725dd93333f..43feb207f76a5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncLazy`1.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AsyncLazy`1.cs @@ -11,559 +11,584 @@ namespace Roslyn.Utilities; -internal static class AsyncLazy +internal abstract class AsyncLazy { - public static AsyncLazy Create(Func> asynchronousComputeFunction) - => new(asynchronousComputeFunction); - - public static AsyncLazy Create(Func synchronousComputeFunction) - => new(cancellationToken => Task.FromResult(synchronousComputeFunction(cancellationToken)), synchronousComputeFunction); - - public static AsyncLazy Create(T value) - => new(value); -} + public abstract bool TryGetValue([MaybeNullWhen(false)] out T result); + public abstract T GetValue(CancellationToken cancellationToken); + public abstract Task GetValueAsync(CancellationToken cancellationToken); + + public static AsyncLazy Create( + Func> asynchronousComputeFunction, + Func? synchronousComputeFunction, + TData data) + { + return AsyncLazyImpl.CreateImpl(asynchronousComputeFunction, synchronousComputeFunction, data); + } -/// -/// Represents a value that can be retrieved synchronously or asynchronously by many clients. -/// The value will be computed on-demand the moment the first client asks for it. While being -/// computed, more clients can request the value. As long as there are outstanding clients the -/// underlying computation will proceed. If all outstanding clients cancel their request then -/// the underlying value computation will be cancelled as well. -/// -/// Creators of an can specify whether the result of the computation is -/// cached for future requests or not. Choosing to not cache means the computation functions are kept -/// alive, whereas caching means the value (but not functions) are kept alive once complete. -/// -internal sealed class AsyncLazy -{ - /// - /// The underlying function that starts an asynchronous computation of the resulting value. - /// Null'ed out once we've computed the result and we've been asked to cache it. Otherwise, - /// it is kept around in case the value needs to be computed again. - /// - private Func>? _asynchronousComputeFunction; + public static AsyncLazy Create(T value) + => AsyncLazyImpl.CreateImpl(value); /// - /// The underlying function that starts a synchronous computation of the resulting value. - /// Null'ed out once we've computed the result and we've been asked to cache it, or if we - /// didn't get any synchronous function given to us in the first place. + /// Represents a value that can be retrieved synchronously or asynchronously by many clients. + /// The value will be computed on-demand the moment the first client asks for it. While being + /// computed, more clients can request the value. As long as there are outstanding clients the + /// underlying computation will proceed. If all outstanding clients cancel their request then + /// the underlying value computation will be cancelled as well. + /// + /// Creators of an can specify whether the result of the computation is + /// cached for future requests or not. Choosing to not cache means the computation functions are kept + /// alive, whereas caching means the value (but not functions) are kept alive once complete. /// - private Func? _synchronousComputeFunction; + private sealed class AsyncLazyImpl : AsyncLazy + { + /// + /// The underlying function that starts an asynchronous computation of the resulting value. + /// Null'ed out once we've computed the result and we've been asked to cache it. Otherwise, + /// it is kept around in case the value needs to be computed again. + /// + private Func>? _asynchronousComputeFunction; - /// - /// The Task that holds the cached result. - /// - private Task? _cachedResult; + /// + /// The underlying function that starts a synchronous computation of the resulting value. + /// Null'ed out once we've computed the result and we've been asked to cache it, or if we + /// didn't get any synchronous function given to us in the first place. + /// + private Func? _synchronousComputeFunction; - /// - /// Mutex used to protect reading and writing to all mutable objects and fields. Traces - /// indicate that there's negligible contention on this lock, hence we can save some memory - /// by using a single lock for all AsyncLazy instances. Only trivial and non-reentrant work - /// should be done while holding the lock. - /// - private static readonly NonReentrantLock s_gate = new(useThisInstanceForSynchronization: true); + /// + /// The Task that holds the cached result. + /// + private Task? _cachedResult; - /// - /// The hash set of all currently outstanding asynchronous requests. Null if there are no requests, - /// and will never be empty. - /// - private HashSet? _requests; + /// + /// Mutex used to protect reading and writing to all mutable objects and fields. Traces + /// indicate that there's negligible contention on this lock, hence we can save some memory + /// by using a single lock for all AsyncLazy instances. Only trivial and non-reentrant work + /// should be done while holding the lock. + /// + private static readonly NonReentrantLock s_gate = new(useThisInstanceForSynchronization: true); - /// - /// If an asynchronous request is active, the CancellationTokenSource that allows for - /// cancelling the underlying computation. - /// - private CancellationTokenSource? _asynchronousComputationCancellationSource; + /// + /// The hash set of all currently outstanding asynchronous requests. Null if there are no requests, + /// and will never be empty. + /// + private HashSet? _requests; - /// - /// Whether a computation is active or queued on any thread, whether synchronous or - /// asynchronous. - /// - private bool _computationActive; + /// + /// If an asynchronous request is active, the CancellationTokenSource that allows for + /// cancelling the underlying computation. + /// + private CancellationTokenSource? _asynchronousComputationCancellationSource; - /// - /// Creates an AsyncLazy that always returns the value, analogous to . - /// - public AsyncLazy(T value) - => _cachedResult = Task.FromResult(value); + /// + /// Whether a computation is active or queued on any thread, whether synchronous or + /// asynchronous. + /// + private bool _computationActive; - public AsyncLazy(Func> asynchronousComputeFunction) - : this(asynchronousComputeFunction, synchronousComputeFunction: null) - { - } + private TData _data; - /// - /// Creates an AsyncLazy that supports both asynchronous computation and inline synchronous - /// computation. - /// - /// A function called to start the asynchronous - /// computation. This function should be cheap and non-blocking. - /// A function to do the work synchronously, which - /// is allowed to block. This function should not be implemented by a simple Wait on the - /// asynchronous value. If that's all you are doing, just don't pass a synchronous function - /// in the first place. - public AsyncLazy(Func> asynchronousComputeFunction, Func? synchronousComputeFunction) - { - Contract.ThrowIfNull(asynchronousComputeFunction); - _asynchronousComputeFunction = asynchronousComputeFunction; - _synchronousComputeFunction = synchronousComputeFunction; - } + /// + /// Creates an AsyncLazy that always returns the value, analogous to . + /// + private AsyncLazyImpl(T value) + { + _cachedResult = Task.FromResult(value); + _data = default!; + } - #region Lock Wrapper for Invariant Checking + /// + /// Creates an AsyncLazy that supports both asynchronous computation and inline synchronous + /// computation. + /// + /// A function called to start the asynchronous + /// computation. This function should be cheap and non-blocking. + /// A function to do the work synchronously, which + /// is allowed to block. This function should not be implemented by a simple Wait on the + /// asynchronous value. If that's all you are doing, just don't pass a synchronous function + /// in the first place. + private AsyncLazyImpl( + Func> asynchronousComputeFunction, + Func? synchronousComputeFunction, + TData data) + { + Contract.ThrowIfNull(asynchronousComputeFunction); + _asynchronousComputeFunction = asynchronousComputeFunction; + _synchronousComputeFunction = synchronousComputeFunction; + _data = data; + } - /// - /// Takes the lock for this object and if acquired validates the invariants of this class. - /// - private WaitThatValidatesInvariants TakeLock(CancellationToken cancellationToken) - { - s_gate.Wait(cancellationToken); - AssertInvariants_NoLock(); - return new WaitThatValidatesInvariants(this); - } + public static AsyncLazy CreateImpl(T value) + => new AsyncLazyImpl(value); - private readonly struct WaitThatValidatesInvariants(AsyncLazy asyncLazy) : IDisposable - { - public void Dispose() + public static AsyncLazy CreateImpl( + Func> asynchronousComputeFunction, + Func? synchronousComputeFunction, + TData data) { - asyncLazy.AssertInvariants_NoLock(); - s_gate.Release(); + return new AsyncLazyImpl(asynchronousComputeFunction, synchronousComputeFunction, data); } - } - private void AssertInvariants_NoLock() - { - // Invariant #1: thou shalt never have an asynchronous computation running without it - // being considered a computation - Contract.ThrowIfTrue(_asynchronousComputationCancellationSource != null && - !_computationActive); - - // Invariant #2: thou shalt never waste memory holding onto empty HashSets - Contract.ThrowIfTrue(_requests != null && - _requests.Count == 0); - - // Invariant #3: thou shalt never have an request if there is not - // something trying to compute it - Contract.ThrowIfTrue(_requests != null && - !_computationActive); - - // Invariant #4: thou shalt never have a cached value and any computation function - Contract.ThrowIfTrue(_cachedResult != null && - (_synchronousComputeFunction != null || _asynchronousComputeFunction != null)); - - // Invariant #5: thou shalt never have a synchronous computation function but not an - // asynchronous one - Contract.ThrowIfTrue(_asynchronousComputeFunction == null && _synchronousComputeFunction != null); - } - - #endregion + #region Lock Wrapper for Invariant Checking - public bool TryGetValue([MaybeNullWhen(false)] out T result) - { - // No need to lock here since this is only a fast check to - // see if the result is already computed. - if (_cachedResult != null) + /// + /// Takes the lock for this object and if acquired validates the invariants of this class. + /// + private WaitThatValidatesInvariants TakeLock(CancellationToken cancellationToken) { - result = _cachedResult.Result; - return true; + s_gate.Wait(cancellationToken); + AssertInvariants_NoLock(); + return new WaitThatValidatesInvariants(this); } - result = default; - return false; - } - - public T GetValue(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + private readonly struct WaitThatValidatesInvariants(AsyncLazyImpl asyncLazy) : IDisposable + { + public void Dispose() + { + asyncLazy.AssertInvariants_NoLock(); + s_gate.Release(); + } + } - // If the value is already available, return it immediately - if (TryGetValue(out var value)) + private void AssertInvariants_NoLock() { - return value; + // Invariant #1: thou shalt never have an asynchronous computation running without it + // being considered a computation + Contract.ThrowIfTrue(_asynchronousComputationCancellationSource != null && + !_computationActive); + + // Invariant #2: thou shalt never waste memory holding onto empty HashSets + Contract.ThrowIfTrue(_requests != null && + _requests.Count == 0); + + // Invariant #3: thou shalt never have an request if there is not + // something trying to compute it + Contract.ThrowIfTrue(_requests != null && + !_computationActive); + + // Invariant #4: thou shalt never have a cached value and any computation function + Contract.ThrowIfTrue(_cachedResult != null && + (_synchronousComputeFunction != null || _asynchronousComputeFunction != null)); + + // Invariant #5: thou shalt never have a synchronous computation function but not an + // asynchronous one + Contract.ThrowIfTrue(_asynchronousComputeFunction == null && _synchronousComputeFunction != null); } - Request? request = null; - AsynchronousComputationToStart? newAsynchronousComputation = null; + #endregion - using (TakeLock(cancellationToken)) + public override bool TryGetValue([MaybeNullWhen(false)] out T result) { - // If cached, get immediately + // No need to lock here since this is only a fast check to + // see if the result is already computed. if (_cachedResult != null) { - return _cachedResult.Result; - } - - // If there is an existing computation active, we'll just create another request - if (_computationActive) - { - request = CreateNewRequest_NoLock(); + result = _cachedResult.Result; + return true; } - else if (_synchronousComputeFunction == null) - { - // A synchronous request, but we have no synchronous function. Start off the async work - request = CreateNewRequest_NoLock(); - newAsynchronousComputation = RegisterAsynchronousComputation_NoLock(); - } - else - { - // We will do the computation here - _computationActive = true; - } + result = default; + return false; } - // If we simply created a new asynchronous request, so wait for it. Yes, we're blocking the thread - // but we don't want multiple threads attempting to compute the same thing. - if (request != null) + public override T GetValue(CancellationToken cancellationToken) { - request.RegisterForCancellation(OnAsynchronousRequestCancelled, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); - // Since we already registered for cancellation, it's possible that the registration has - // cancelled this new computation if we were the only requester. - if (newAsynchronousComputation != null) + // If the value is already available, return it immediately + if (TryGetValue(out var value)) { - StartAsynchronousComputation(newAsynchronousComputation.Value, requestToCompleteSynchronously: request, callerCancellationToken: cancellationToken); + return value; } - // The reason we have synchronous codepaths in AsyncLazy is to support the synchronous requests for syntax trees - // that we may get from the compiler. Thus, it's entirely possible that this will be requested by the compiler or - // an analyzer on the background thread when another part of the IDE is requesting the same tree asynchronously. - // In that case we block the synchronous request on the asynchronous request, since that's better than alternatives. - return request.Task.WaitAndGetResult_CanCallOnBackground(cancellationToken); - } - else - { - Contract.ThrowIfNull(_synchronousComputeFunction); - - T result; + Request? request = null; + AsynchronousComputationToStart? newAsynchronousComputation = null; - // We are the active computation, so let's go ahead and compute. - try + using (TakeLock(cancellationToken)) { - result = _synchronousComputeFunction(cancellationToken); - } - catch (OperationCanceledException) - { - // This cancelled for some reason. We don't care why, but - // it means anybody else waiting for this result isn't going to get it - // from us. - using (TakeLock(CancellationToken.None)) + // If cached, get immediately + if (_cachedResult != null) { - _computationActive = false; + return _cachedResult.Result; + } - if (_requests != null) - { - // There's a possible improvement here: there might be another synchronous caller who - // also wants the value. We might consider stealing their thread rather than punting - // to the thread pool. - newAsynchronousComputation = RegisterAsynchronousComputation_NoLock(); - } + // If there is an existing computation active, we'll just create another request + if (_computationActive) + { + request = CreateNewRequest_NoLock(); } + else if (_synchronousComputeFunction == null) + { + // A synchronous request, but we have no synchronous function. Start off the async work + request = CreateNewRequest_NoLock(); + newAsynchronousComputation = RegisterAsynchronousComputation_NoLock(); + } + else + { + // We will do the computation here + _computationActive = true; + } + } + + // If we simply created a new asynchronous request, so wait for it. Yes, we're blocking the thread + // but we don't want multiple threads attempting to compute the same thing. + if (request != null) + { + request.RegisterForCancellation(OnAsynchronousRequestCancelled, cancellationToken); + + // Since we already registered for cancellation, it's possible that the registration has + // cancelled this new computation if we were the only requester. if (newAsynchronousComputation != null) { - StartAsynchronousComputation(newAsynchronousComputation.Value, requestToCompleteSynchronously: null, callerCancellationToken: cancellationToken); + StartAsynchronousComputation(newAsynchronousComputation.Value, requestToCompleteSynchronously: request, callerCancellationToken: cancellationToken); } - throw; + // The reason we have synchronous codepaths in AsyncLazy is to support the synchronous requests for syntax trees + // that we may get from the compiler. Thus, it's entirely possible that this will be requested by the compiler or + // an analyzer on the background thread when another part of the IDE is requesting the same tree asynchronously. + // In that case we block the synchronous request on the asynchronous request, since that's better than alternatives. + return request.Task.WaitAndGetResult_CanCallOnBackground(cancellationToken); } - catch (Exception ex) + else { - // We faulted for some unknown reason. We should simply fault everything. - CompleteWithTask(Task.FromException(ex), CancellationToken.None); - throw; - } + Contract.ThrowIfNull(_synchronousComputeFunction); - // We have a value, so complete - CompleteWithTask(Task.FromResult(result), CancellationToken.None); + T result; - // Optimization: if they did cancel and the computation never observed it, let's throw so we don't keep - // processing a value somebody never wanted - cancellationToken.ThrowIfCancellationRequested(); + // We are the active computation, so let's go ahead and compute. + try + { + result = _synchronousComputeFunction(_data, cancellationToken); + } + catch (OperationCanceledException) + { + // This cancelled for some reason. We don't care why, but + // it means anybody else waiting for this result isn't going to get it + // from us. + using (TakeLock(CancellationToken.None)) + { + _computationActive = false; - // Because we called CompleteWithTask with an actual result, _cachedResult must be set. However, it's possible that result is a different result than - // what is in our local variable here; it's possible that an asynchronous computation was running and cancelled, but eventually completed (ignoring cancellation) - // and then set the cached value. Because that could be a different instance than what we have locally, we can't use the local result if the other value - // got cached first. Always returning the cached value ensures we always return a single value from AsyncLazy once we got one. - Contract.ThrowIfNull(_cachedResult, $"We called {nameof(CompleteWithTask)} with a result, there should be a cached result."); - return _cachedResult.Result; - } - } + if (_requests != null) + { + // There's a possible improvement here: there might be another synchronous caller who + // also wants the value. We might consider stealing their thread rather than punting + // to the thread pool. + newAsynchronousComputation = RegisterAsynchronousComputation_NoLock(); + } + } - private Request CreateNewRequest_NoLock() - { - _requests ??= []; + if (newAsynchronousComputation != null) + { + StartAsynchronousComputation(newAsynchronousComputation.Value, requestToCompleteSynchronously: null, callerCancellationToken: cancellationToken); + } - var request = new Request(); - _requests.Add(request); - return request; - } + throw; + } + catch (Exception ex) + { + // We faulted for some unknown reason. We should simply fault everything. + CompleteWithTask(Task.FromException(ex), CancellationToken.None); + throw; + } - public Task GetValueAsync(CancellationToken cancellationToken) - { - // Optimization: if we're already cancelled, do not pass go - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); + // We have a value, so complete + CompleteWithTask(Task.FromResult(result), CancellationToken.None); + + // Optimization: if they did cancel and the computation never observed it, let's throw so we don't keep + // processing a value somebody never wanted + cancellationToken.ThrowIfCancellationRequested(); + + // Because we called CompleteWithTask with an actual result, _cachedResult must be set. However, it's possible that result is a different result than + // what is in our local variable here; it's possible that an asynchronous computation was running and cancelled, but eventually completed (ignoring cancellation) + // and then set the cached value. Because that could be a different instance than what we have locally, we can't use the local result if the other value + // got cached first. Always returning the cached value ensures we always return a single value from AsyncLazy once we got one. + Contract.ThrowIfNull(_cachedResult, $"We called {nameof(CompleteWithTask)} with a result, there should be a cached result."); + return _cachedResult.Result; + } } - // Avoid taking the lock if a cached value is available - var cachedResult = _cachedResult; - if (cachedResult != null) + private Request CreateNewRequest_NoLock() { - return cachedResult; - } + _requests ??= []; - Request request; - AsynchronousComputationToStart? newAsynchronousComputation = null; + var request = new Request(); + _requests.Add(request); + return request; + } - using (TakeLock(cancellationToken)) + public override Task GetValueAsync(CancellationToken cancellationToken) { - // If cached, get immediately - if (_cachedResult != null) + // Optimization: if we're already cancelled, do not pass go + if (cancellationToken.IsCancellationRequested) { - return _cachedResult; + return Task.FromCanceled(cancellationToken); } - request = CreateNewRequest_NoLock(); - - // If we have either synchronous or asynchronous work current in flight, we don't need to do anything. - // Otherwise, we shall start an asynchronous computation for this - if (!_computationActive) + // Avoid taking the lock if a cached value is available + var cachedResult = _cachedResult; + if (cachedResult != null) { - newAsynchronousComputation = RegisterAsynchronousComputation_NoLock(); + return cachedResult; } - } - // We now have the request counted for, register for cancellation. It is critical this is - // done outside the lock, as our registration may immediately fire and we want to avoid the - // reentrancy - request.RegisterForCancellation(OnAsynchronousRequestCancelled, cancellationToken); + Request request; + AsynchronousComputationToStart? newAsynchronousComputation = null; - if (newAsynchronousComputation != null) - { - StartAsynchronousComputation(newAsynchronousComputation.Value, requestToCompleteSynchronously: request, callerCancellationToken: cancellationToken); - } + using (TakeLock(cancellationToken)) + { + // If cached, get immediately + if (_cachedResult != null) + { + return _cachedResult; + } - return request.Task; - } + request = CreateNewRequest_NoLock(); - private AsynchronousComputationToStart RegisterAsynchronousComputation_NoLock() - { - Contract.ThrowIfTrue(_computationActive); - Contract.ThrowIfNull(_asynchronousComputeFunction); + // If we have either synchronous or asynchronous work current in flight, we don't need to do anything. + // Otherwise, we shall start an asynchronous computation for this + if (!_computationActive) + { + newAsynchronousComputation = RegisterAsynchronousComputation_NoLock(); + } + } - _asynchronousComputationCancellationSource = new CancellationTokenSource(); - _computationActive = true; + // We now have the request counted for, register for cancellation. It is critical this is + // done outside the lock, as our registration may immediately fire and we want to avoid the + // reentrancy + request.RegisterForCancellation(OnAsynchronousRequestCancelled, cancellationToken); - return new AsynchronousComputationToStart(_asynchronousComputeFunction, _asynchronousComputationCancellationSource); - } + if (newAsynchronousComputation != null) + { + StartAsynchronousComputation(newAsynchronousComputation.Value, requestToCompleteSynchronously: request, callerCancellationToken: cancellationToken); + } - private readonly struct AsynchronousComputationToStart(Func> asynchronousComputeFunction, CancellationTokenSource cancellationTokenSource) - { - public readonly Func> AsynchronousComputeFunction = asynchronousComputeFunction; - public readonly CancellationTokenSource CancellationTokenSource = cancellationTokenSource; - } + return request.Task; + } - private void StartAsynchronousComputation(AsynchronousComputationToStart computationToStart, Request? requestToCompleteSynchronously, CancellationToken callerCancellationToken) - { - var cancellationToken = computationToStart.CancellationTokenSource.Token; - - // DO NOT ACCESS ANY FIELDS OR STATE BEYOND THIS POINT. Since this function - // runs unsynchronized, it's possible that during this function this request - // might be cancelled, and then a whole additional request might start and - // complete inline, and cache the result. By grabbing state before we check - // the cancellation token, we can be assured that we are only operating on - // a state that was complete. - try + private AsynchronousComputationToStart RegisterAsynchronousComputation_NoLock() { - cancellationToken.ThrowIfCancellationRequested(); + Contract.ThrowIfTrue(_computationActive); + Contract.ThrowIfNull(_asynchronousComputeFunction); - var task = computationToStart.AsynchronousComputeFunction(cancellationToken); + _asynchronousComputationCancellationSource = new CancellationTokenSource(); + _computationActive = true; - // As an optimization, if the task is already completed, mark the - // request as being completed as well. - // - // Note: we want to do this before we do the .ContinueWith below. That way, - // when the async call to CompleteWithTask runs, it sees that we've already - // completed and can bail immediately. - if (requestToCompleteSynchronously != null && task.IsCompleted) - { - using (TakeLock(CancellationToken.None)) - { - task = GetCachedValueAndCacheThisValueIfNoneCached_NoLock(task); - } - - requestToCompleteSynchronously.CompleteFromTask(task); - } - - // We avoid creating a full closure just to pass the token along - // Also, use TaskContinuationOptions.ExecuteSynchronously so that we inline - // the continuation if asynchronousComputeFunction completes synchronously - task.ContinueWith( - (t, s) => CompleteWithTask(t, ((CancellationTokenSource)s!).Token), - computationToStart.CancellationTokenSource, - cancellationToken, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); + return new AsynchronousComputationToStart(_asynchronousComputeFunction, _asynchronousComputationCancellationSource); } - catch (OperationCanceledException e) when (e.CancellationToken == cancellationToken) + + private readonly struct AsynchronousComputationToStart(Func> asynchronousComputeFunction, CancellationTokenSource cancellationTokenSource) { - // The underlying computation cancelled with the correct token, but we must ourselves ensure that the caller - // on our stack gets an OperationCanceledException thrown with the right token - callerCancellationToken.ThrowIfCancellationRequested(); - - // We can only be here if the computation was cancelled, which means all requests for the value - // must have been cancelled. Therefore, the ThrowIfCancellationRequested above must have thrown - // because that token from the requester was cancelled. - throw ExceptionUtilities.Unreachable(); + public readonly Func> AsynchronousComputeFunction = asynchronousComputeFunction; + public readonly CancellationTokenSource CancellationTokenSource = cancellationTokenSource; } - catch (Exception e) when (FatalError.ReportAndPropagate(e)) + + private void StartAsynchronousComputation( + AsynchronousComputationToStart computationToStart, + Request? requestToCompleteSynchronously, + CancellationToken callerCancellationToken) { - throw ExceptionUtilities.Unreachable(); - } - } + var cancellationToken = computationToStart.CancellationTokenSource.Token; + + // DO NOT ACCESS ANY FIELDS OR STATE BEYOND THIS POINT. Since this function + // runs unsynchronized, it's possible that during this function this request + // might be cancelled, and then a whole additional request might start and + // complete inline, and cache the result. By grabbing state before we check + // the cancellation token, we can be assured that we are only operating on + // a state that was complete. + try + { + cancellationToken.ThrowIfCancellationRequested(); - private void CompleteWithTask(Task task, CancellationToken cancellationToken) - { - IEnumerable requestsToComplete; + var task = computationToStart.AsynchronousComputeFunction(_data, cancellationToken); - using (TakeLock(cancellationToken)) - { - // If the underlying computation was cancelled, then all state was already updated in OnAsynchronousRequestCancelled - // and there is no new work to do here. We *must* use the local one since this completion may be running far after - // the background computation was cancelled and a new one might have already been enqueued. We must do this - // check here under the lock to ensure proper synchronization with OnAsynchronousRequestCancelled. - cancellationToken.ThrowIfCancellationRequested(); + // As an optimization, if the task is already completed, mark the + // request as being completed as well. + // + // Note: we want to do this before we do the .ContinueWith below. That way, + // when the async call to CompleteWithTask runs, it sees that we've already + // completed and can bail immediately. + if (requestToCompleteSynchronously != null && task.IsCompleted) + { + using (TakeLock(CancellationToken.None)) + { + task = GetCachedValueAndCacheThisValueIfNoneCached_NoLock(task); + } - // The computation is complete, so get all requests to complete and null out the list. We'll create another one - // later if it's needed - requestsToComplete = _requests ?? (IEnumerable)[]; - _requests = null; + requestToCompleteSynchronously.CompleteFromTask(task); + } - // The computations are done - _asynchronousComputationCancellationSource = null; - _computationActive = false; - task = GetCachedValueAndCacheThisValueIfNoneCached_NoLock(task); + // We avoid creating a full closure just to pass the token along + // Also, use TaskContinuationOptions.ExecuteSynchronously so that we inline + // the continuation if asynchronousComputeFunction completes synchronously + task.ContinueWith( + (t, s) => CompleteWithTask(t, ((CancellationTokenSource)s!).Token), + computationToStart.CancellationTokenSource, + cancellationToken, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + catch (OperationCanceledException e) when (e.CancellationToken == cancellationToken) + { + // The underlying computation cancelled with the correct token, but we must ourselves ensure that the caller + // on our stack gets an OperationCanceledException thrown with the right token + callerCancellationToken.ThrowIfCancellationRequested(); + + // We can only be here if the computation was cancelled, which means all requests for the value + // must have been cancelled. Therefore, the ThrowIfCancellationRequested above must have thrown + // because that token from the requester was cancelled. + throw ExceptionUtilities.Unreachable(); + } + catch (Exception e) when (FatalError.ReportAndPropagate(e)) + { + throw ExceptionUtilities.Unreachable(); + } } - // Complete the requests outside the lock. It's not necessary to do this (none of this is touching any shared state) - // but there's no reason to hold the lock so we could reduce any theoretical lock contention. - foreach (var requestToComplete in requestsToComplete) + private void CompleteWithTask(Task task, CancellationToken cancellationToken) { - requestToComplete.CompleteFromTask(task); - } - } + IEnumerable requestsToComplete; - [SuppressMessage("Style", "VSTHRD200:Use \"Async\" suffix for async methods", Justification = "This is a Task wrapper, not an asynchronous method.")] - private Task GetCachedValueAndCacheThisValueIfNoneCached_NoLock(Task task) - { - if (_cachedResult != null) - return _cachedResult; + using (TakeLock(cancellationToken)) + { + // If the underlying computation was cancelled, then all state was already updated in OnAsynchronousRequestCancelled + // and there is no new work to do here. We *must* use the local one since this completion may be running far after + // the background computation was cancelled and a new one might have already been enqueued. We must do this + // check here under the lock to ensure proper synchronization with OnAsynchronousRequestCancelled. + cancellationToken.ThrowIfCancellationRequested(); + + // The computation is complete, so get all requests to complete and null out the list. We'll create another one + // later if it's needed + requestsToComplete = _requests ?? (IEnumerable)[]; + _requests = null; + + // The computations are done + _asynchronousComputationCancellationSource = null; + _computationActive = false; + task = GetCachedValueAndCacheThisValueIfNoneCached_NoLock(task); + } + + // Complete the requests outside the lock. It's not necessary to do this (none of this is touching any shared state) + // but there's no reason to hold the lock so we could reduce any theoretical lock contention. + foreach (var requestToComplete in requestsToComplete) + { + requestToComplete.CompleteFromTask(task); + } + } - if (task.Status == TaskStatus.RanToCompletion) + [SuppressMessage("Style", "VSTHRD200:Use \"Async\" suffix for async methods", Justification = "This is a Task wrapper, not an asynchronous method.")] + private Task GetCachedValueAndCacheThisValueIfNoneCached_NoLock(Task task) { - // Hold onto the completed task. We can get rid of the computation functions for good - _cachedResult = task; + if (_cachedResult != null) + return _cachedResult; - _asynchronousComputeFunction = null; - _synchronousComputeFunction = null; - } + if (task.Status == TaskStatus.RanToCompletion) + { + // Hold onto the completed task. We can get rid of the computation functions for good + _cachedResult = task; - return task; - } + _asynchronousComputeFunction = null; + _synchronousComputeFunction = null; + _data = default!; + } - private void OnAsynchronousRequestCancelled(object? state) - { - var request = (Request)state!; - CancellationTokenSource? cancellationTokenSource = null; + return task; + } - using (TakeLock(CancellationToken.None)) + private void OnAsynchronousRequestCancelled(object? state) { - // Now try to remove it. It's possible that requests may already be null. You could - // imagine that cancellation was requested, but before we could acquire the lock - // here the computation completed and the entire CompleteWithTask synchronized - // block ran. In that case, the requests collection may already be null, or it - // (even scarier!) may have been replaced with another collection because another - // computation has started. - if (_requests != null) + var request = (Request)state!; + CancellationTokenSource? cancellationTokenSource = null; + + using (TakeLock(CancellationToken.None)) { - if (_requests.Remove(request)) + // Now try to remove it. It's possible that requests may already be null. You could + // imagine that cancellation was requested, but before we could acquire the lock + // here the computation completed and the entire CompleteWithTask synchronized + // block ran. In that case, the requests collection may already be null, or it + // (even scarier!) may have been replaced with another collection because another + // computation has started. + if (_requests != null) { - if (_requests.Count == 0) + if (_requests.Remove(request)) { - _requests = null; - - if (_asynchronousComputationCancellationSource != null) + if (_requests.Count == 0) { - cancellationTokenSource = _asynchronousComputationCancellationSource; - _asynchronousComputationCancellationSource = null; - _computationActive = false; + _requests = null; + + if (_asynchronousComputationCancellationSource != null) + { + cancellationTokenSource = _asynchronousComputationCancellationSource; + _asynchronousComputationCancellationSource = null; + _computationActive = false; + } } } } } - } - - request.Cancel(); - cancellationTokenSource?.Cancel(); - } - /// - /// This inherits from to avoid allocating two objects when we can just use one. - /// The public surface area of should probably be avoided in favor of the public - /// methods on this class for correct behavior. - /// - private sealed class Request : TaskCompletionSource - { - /// - /// The associated with this request. This field will be initialized before - /// any cancellation is observed from the token. - /// - private CancellationToken _cancellationToken; - private CancellationTokenRegistration _cancellationTokenRegistration; - - // We want to always run continuations asynchronously. Running them synchronously could result in deadlocks: - // if we're looping through a bunch of Requests and completing them one by one, and the continuation for the - // first Request was then blocking waiting for a later Request, we would hang. It also could cause performance - // issues. If the first request then consumes a lot of CPU time, we're not letting other Requests complete that - // could use another CPU core at the same time. - public Request() : base(TaskCreationOptions.RunContinuationsAsynchronously) - { - } - - public void RegisterForCancellation(Action callback, CancellationToken cancellationToken) - { - _cancellationToken = cancellationToken; - _cancellationTokenRegistration = cancellationToken.Register(callback, this); + request.Cancel(); + cancellationTokenSource?.Cancel(); } - public void CompleteFromTask(Task task) + /// + /// This inherits from to avoid allocating two objects when we can just use one. + /// The public surface area of should probably be avoided in favor of the public + /// methods on this class for correct behavior. + /// + private sealed class Request : TaskCompletionSource { - // As an optimization, we'll cancel the request even we did get a value for it. - // That way things abort sooner. - if (task.IsCanceled || _cancellationToken.IsCancellationRequested) + /// + /// The associated with this request. This field will be initialized before + /// any cancellation is observed from the token. + /// + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationTokenRegistration; + + // We want to always run continuations asynchronously. Running them synchronously could result in deadlocks: + // if we're looping through a bunch of Requests and completing them one by one, and the continuation for the + // first Request was then blocking waiting for a later Request, we would hang. It also could cause performance + // issues. If the first request then consumes a lot of CPU time, we're not letting other Requests complete that + // could use another CPU core at the same time. + public Request() : base(TaskCreationOptions.RunContinuationsAsynchronously) { - Cancel(); } - else if (task.IsFaulted) + + public void RegisterForCancellation(Action callback, CancellationToken cancellationToken) { - // TrySetException wraps its argument in an AggregateException, so we pass the inner exceptions from - // the antecedent to avoid wrapping in two layers of AggregateException. - RoslynDebug.AssertNotNull(task.Exception); - if (task.Exception.InnerExceptions.Count > 0) - this.TrySetException(task.Exception.InnerExceptions); - else - this.TrySetException(task.Exception); + _cancellationToken = cancellationToken; + _cancellationTokenRegistration = cancellationToken.Register(callback, this); } - else + + public void CompleteFromTask(Task task) { - this.TrySetResult(task.Result); + // As an optimization, we'll cancel the request even we did get a value for it. + // That way things abort sooner. + if (task.IsCanceled || _cancellationToken.IsCancellationRequested) + { + Cancel(); + } + else if (task.IsFaulted) + { + // TrySetException wraps its argument in an AggregateException, so we pass the inner exceptions from + // the antecedent to avoid wrapping in two layers of AggregateException. + RoslynDebug.AssertNotNull(task.Exception); + if (task.Exception.InnerExceptions.Count > 0) + this.TrySetException(task.Exception.InnerExceptions); + else + this.TrySetException(task.Exception); + } + else + { + this.TrySetResult(task.Result); + } + + _cancellationTokenRegistration.Dispose(); } - _cancellationTokenRegistration.Dispose(); + public void Cancel() + => this.TrySetCanceled(_cancellationToken); } - - public void Cancel() - => this.TrySetCanceled(_cancellationToken); } }