From 8ee61d633405b8ce45000f598838c1794e034ce6 Mon Sep 17 00:00:00 2001 From: CyrusNajmabadi Date: Wed, 1 Jun 2016 16:51:42 -0700 Subject: [PATCH] Provide a synchronous path for outlining so it won't block hte UI thread waiting for a threadpool thread. --- .../Outlining/AbstractOutliningService.cs | 32 ++++- .../Outlining/IOutliningService.cs | 5 + .../Outlining/OutliningTaggerProvider.cs | 126 +++++++++++++----- .../Outlining/RegionCollector.cs | 13 +- ...ousTaggerProvider.TagSource_ProduceTags.cs | 24 +++- .../AbstractAsynchronousTaggerProvider.cs | 35 ++++- .../Portable/Workspace/Solution/Document.cs | 68 ++++++++-- .../Workspace/Solution/DocumentState.cs | 9 ++ .../Workspace/Solution/SyntacticDocument.cs | 13 +- 9 files changed, 253 insertions(+), 72 deletions(-) diff --git a/src/EditorFeatures/Core/Implementation/Outlining/AbstractOutliningService.cs b/src/EditorFeatures/Core/Implementation/Outlining/AbstractOutliningService.cs index fec05eb4a2b..360e63b1d41 100644 --- a/src/EditorFeatures/Core/Implementation/Outlining/AbstractOutliningService.cs +++ b/src/EditorFeatures/Core/Implementation/Outlining/AbstractOutliningService.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.Outlining { - internal abstract class AbstractOutliningService : IOutliningService + internal abstract class AbstractOutliningService : ISynchronousOutliningService { private readonly ImmutableDictionary> _nodeOutlinerMap; private readonly ImmutableDictionary> _triviaOutlinerMap; @@ -23,16 +23,40 @@ internal abstract class AbstractOutliningService : IOutliningService _triviaOutlinerMap = defaultTriviaOutlinerMap; } - public async Task> GetOutliningSpansAsync(Document document, CancellationToken cancellationToken) + /// + /// Keep in sync with + /// + public IList GetOutliningSpans( + Document document, CancellationToken cancellationToken) { try { - var syntaxDocument = await SyntacticDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var syntaxRoot = document.GetSyntaxRootSynchronously(cancellationToken); // change this to shared pool once RI var regions = new List(); - RegionCollector.CollectOutliningSpans(syntaxDocument, _nodeOutlinerMap, _triviaOutlinerMap, regions, cancellationToken); + RegionCollector.CollectOutliningSpans(document, syntaxRoot, _nodeOutlinerMap, _triviaOutlinerMap, regions, cancellationToken); + return regions; + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + /// + /// Keep in sync with + /// + public async Task> GetOutliningSpansAsync( + Document document, CancellationToken cancellationToken) + { + try + { + var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + // change this to shared pool once RI + var regions = new List(); + RegionCollector.CollectOutliningSpans(document, syntaxRoot, _nodeOutlinerMap, _triviaOutlinerMap, regions, cancellationToken); return regions; } catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) diff --git a/src/EditorFeatures/Core/Implementation/Outlining/IOutliningService.cs b/src/EditorFeatures/Core/Implementation/Outlining/IOutliningService.cs index a461c9bd80a..67d31a1a1e9 100644 --- a/src/EditorFeatures/Core/Implementation/Outlining/IOutliningService.cs +++ b/src/EditorFeatures/Core/Implementation/Outlining/IOutliningService.cs @@ -11,4 +11,9 @@ internal interface IOutliningService : ILanguageService { Task> GetOutliningSpansAsync(Document document, CancellationToken cancellationToken); } + + internal interface ISynchronousOutliningService : IOutliningService + { + IList GetOutliningSpans(Document document, CancellationToken cancellationToken); + } } diff --git a/src/EditorFeatures/Core/Implementation/Outlining/OutliningTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Outlining/OutliningTaggerProvider.cs index 198dc2759a4..bb4fe38ff70 100644 --- a/src/EditorFeatures/Core/Implementation/Outlining/OutliningTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Outlining/OutliningTaggerProvider.cs @@ -92,49 +92,51 @@ protected override ITaggerEventSource CreateEventSource(ITextView textViewOpt, I TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer, TaggerDelay.OnIdle)); } - protected override async Task ProduceTagsAsync(TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition) + /// + /// Keep this in sync with + /// + protected override async Task ProduceTagsAsync( + TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition) { try { - var cancellationToken = context.CancellationToken; - using (Logger.LogBlock(FunctionId.Tagger_Outlining_TagProducer_ProduceTags, cancellationToken)) + var outliningService = TryGetOutliningService(context, documentSnapshotSpan); + if (outliningService != null) + { + var regions = await outliningService.GetOutliningSpansAsync( + documentSnapshotSpan.Document, context.CancellationToken).ConfigureAwait(false); + ProcessOutliningSpans(context, documentSnapshotSpan.SnapshotSpan, outliningService, regions); + } + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + /// + /// Keep this in sync with + /// + protected override void ProduceTagsSynchronously( + TaggerContext context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition) + { + try + { + var outliningService = TryGetOutliningService(context, documentSnapshotSpan); + if (outliningService != null) { var document = documentSnapshotSpan.Document; - var snapshotSpan = documentSnapshotSpan.SnapshotSpan; - var snapshot = snapshotSpan.Snapshot; + var cancellationToken = context.CancellationToken; - if (document != null) - { - var outliningService = document.Project.LanguageServices.GetService(); - if (outliningService != null) - { - var regions = await outliningService.GetOutliningSpansAsync(document, cancellationToken).ConfigureAwait(false); - if (regions != null) - { - regions = GetMultiLineRegions(outliningService, regions, snapshotSpan.Snapshot); - - // Create the outlining tags. - var tagSpans = - from region in regions - let spanToCollapse = new SnapshotSpan(snapshot, region.TextSpan.ToSpan()) - let hintSpan = new SnapshotSpan(snapshot, region.HintSpan.ToSpan()) - let tag = new Tag(snapshot.TextBuffer, - region.BannerText, - hintSpan, - region.AutoCollapse, - region.IsDefaultCollapsed, - _textEditorFactoryService, - _projectionBufferFactoryService, - _editorOptionsFactoryService) - select new TagSpan(spanToCollapse, tag); - - foreach (var tagSpan in tagSpans) - { - context.AddTag(tagSpan); - } - } - } - } + // Try to call through the synchronous service if possible. Otherwise, fallback + // and make a blocking call against the async service. + var synchronousOutliningService = outliningService as ISynchronousOutliningService; + + var regions = synchronousOutliningService != null + ? synchronousOutliningService.GetOutliningSpans(document, cancellationToken) + : outliningService.GetOutliningSpansAsync(document, cancellationToken).WaitAndGetResult(cancellationToken); + + ProcessOutliningSpans(context, documentSnapshotSpan.SnapshotSpan, outliningService, regions); } } catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) @@ -143,6 +145,56 @@ protected override async Task ProduceTagsAsync(TaggerContext context, + DocumentSnapshotSpan documentSnapshotSpan) + { + var cancellationToken = context.CancellationToken; + using (Logger.LogBlock(FunctionId.Tagger_Outlining_TagProducer_ProduceTags, cancellationToken)) + { + var document = documentSnapshotSpan.Document; + var snapshotSpan = documentSnapshotSpan.SnapshotSpan; + var snapshot = snapshotSpan.Snapshot; + + if (document != null) + { + return document.Project.LanguageServices.GetService(); + } + } + + return null; + } + + private void ProcessOutliningSpans( + TaggerContext context, SnapshotSpan snapshotSpan, IOutliningService outliningService, IList regions) + { + if (regions != null) + { + var snapshot = snapshotSpan.Snapshot; + regions = GetMultiLineRegions(outliningService, regions, snapshot); + + // Create the outlining tags. + var tagSpans = + from region in regions + let spanToCollapse = new SnapshotSpan(snapshot, region.TextSpan.ToSpan()) + let hintSpan = new SnapshotSpan(snapshot, region.HintSpan.ToSpan()) + let tag = new Tag(snapshot.TextBuffer, + region.BannerText, + hintSpan, + region.AutoCollapse, + region.IsDefaultCollapsed, + _textEditorFactoryService, + _projectionBufferFactoryService, + _editorOptionsFactoryService) + select new TagSpan(spanToCollapse, tag); + + foreach (var tagSpan in tagSpans) + { + context.AddTag(tagSpan); + } + } + } + private static bool s_exceptionReported = false; private IList GetMultiLineRegions(IOutliningService service, IList regions, ITextSnapshot snapshot) diff --git a/src/EditorFeatures/Core/Implementation/Outlining/RegionCollector.cs b/src/EditorFeatures/Core/Implementation/Outlining/RegionCollector.cs index a699d5d3443..806eb605c62 100644 --- a/src/EditorFeatures/Core/Implementation/Outlining/RegionCollector.cs +++ b/src/EditorFeatures/Core/Implementation/Outlining/RegionCollector.cs @@ -9,14 +9,14 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.Outlining { internal class RegionCollector { - private readonly SyntacticDocument _document; + private readonly Document _document; private readonly ImmutableDictionary> _nodeOutlinerMap; private readonly ImmutableDictionary> _triviaOutlinerMap; private readonly List _regions; private readonly CancellationToken _cancellationToken; private RegionCollector( - SyntacticDocument document, + Document document, ImmutableDictionary> nodeOutlinerMap, ImmutableDictionary> triviaOutlinerMap, List spans, @@ -30,14 +30,15 @@ internal class RegionCollector } public static void CollectOutliningSpans( - SyntacticDocument document, + Document document, + SyntaxNode syntaxRoot, ImmutableDictionary> nodeOutlinerMap, ImmutableDictionary> triviaOutlinerMap, List spans, CancellationToken cancellationToken) { var collector = new RegionCollector(document, nodeOutlinerMap, triviaOutlinerMap, spans, cancellationToken); - collector.Collect(document.Root); + collector.Collect(syntaxRoot); } private void Collect(SyntaxNode root) @@ -66,7 +67,7 @@ private void GetOutliningSpans(SyntaxNode node) { _cancellationToken.ThrowIfCancellationRequested(); - outliner.CollectOutliningSpans(_document.Document, node, _regions, _cancellationToken); + outliner.CollectOutliningSpans(_document, node, _regions, _cancellationToken); } } } @@ -90,7 +91,7 @@ private void GetOutliningSpans(SyntaxTriviaList triviaList) { _cancellationToken.ThrowIfCancellationRequested(); - outliner.CollectOutliningSpans(_document.Document, trivia, _regions, _cancellationToken); + outliner.CollectOutliningSpans(_document, trivia, _regions, _cancellationToken); } } } diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 4c20f72cd5d..3e9ff504c7b 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -556,13 +556,18 @@ private IEnumerable> GetNonIntersectingTagSpans(IEnumerable context) + private bool ShouldSkipTagProduction() { var options = _dataSource.Options ?? SpecializedCollections.EmptyEnumerable>(); var perLanguageOptions = _dataSource.PerLanguageOptions ?? SpecializedCollections.EmptyEnumerable>(); - if (options.Any(option => !_subjectBuffer.GetOption(option)) || - perLanguageOptions.Any(option => !_subjectBuffer.GetOption(option))) + return options.Any(option => !_subjectBuffer.GetOption(option)) || + perLanguageOptions.Any(option => !_subjectBuffer.GetOption(option)); + } + + private Task ProduceTagsAsync(TaggerContext context) + { + if (ShouldSkipTagProduction()) { // If the feature is disabled, then just produce no tags. return SpecializedTasks.EmptyTask; @@ -571,6 +576,16 @@ private Task ProduceTagsAsync(TaggerContext context) return _dataSource.ProduceTagsAsync(context); } + private void ProduceTagsSynchronously(TaggerContext context) + { + if (ShouldSkipTagProduction()) + { + return; + } + + _dataSource.ProduceTagsSynchronously(context); + } + private void ProcessContext( List spansToTag, ImmutableDictionary> oldTagTrees, @@ -724,7 +739,8 @@ public TagSpanIntervalTree GetAccurateTagIntervalTreeForBuffer(ITextBuffer var context = new TaggerContext( this.State, spansToTag, caretPoint, this.AccumulatedTextChanges, oldTagTrees, cancellationToken); - ProduceTagsAsync(context).Wait(cancellationToken); + + ProduceTagsSynchronously(context); ProcessContext(spansToTag, oldTagTrees, context); } diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs index 4012e02bde9..02d502b4346 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs @@ -202,13 +202,31 @@ internal Task ProduceTagsAsync_ForTestingPurposesOnly(TaggerContext contex /// /// Produce tags for the given context. + /// Keep in sync with /// protected virtual async Task ProduceTagsAsync(TaggerContext context) { foreach (var spanToTag in context.SpansToTag) { context.CancellationToken.ThrowIfCancellationRequested(); - await ProduceTagsAsync(context, spanToTag, GetCaretPosition(context.CaretPosition, spanToTag.SnapshotSpan)).ConfigureAwait(false); + await ProduceTagsAsync( + context, spanToTag, + GetCaretPosition(context.CaretPosition, spanToTag.SnapshotSpan)).ConfigureAwait(false); + } + } + + /// + /// Produce tags for the given context. + /// Keep in sync with + /// + protected void ProduceTagsSynchronously(TaggerContext context) + { + foreach (var spanToTag in context.SpansToTag) + { + context.CancellationToken.ThrowIfCancellationRequested(); + ProduceTagsSynchronously( + context, spanToTag, + GetCaretPosition(context.CaretPosition, spanToTag.SnapshotSpan)); } } @@ -223,6 +241,21 @@ protected virtual Task ProduceTagsAsync(TaggerContext context, DocumentSna return SpecializedTasks.EmptyTask; } + protected virtual void ProduceTagsSynchronously(TaggerContext context, DocumentSnapshotSpan spanToTag, int? caretPosition) + { + // By default we implement the sync version of this by blocking on the async version. + // + // The benefit of this is that all taggers can implicitly be used as IAccurateTaggers + // without any code changes. + // + // However, the drawback is that it means the UI thread might be blocked waiting for + // tasks to be scheduled and run on the threadpool. + // + // Taggers that need to be called accurately should override this method to produce + // results quickly if possible. + ProduceTagsAsync(context, spanToTag, caretPosition).Wait(context.CancellationToken); + } + private struct DiffResult { public NormalizedSnapshotSpanCollection Added { get; } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs index b8f99d4ddd1..147d2354c88 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs @@ -154,18 +154,8 @@ public bool SupportsSemanticModel } } - /// - /// Gets the for this document asynchronously. - /// - public Task GetSyntaxTreeAsync(CancellationToken cancellationToken = default(CancellationToken)) + private Task GetExistingSyntaxTreeTask() { - // If the language doesn't support getting syntax trees for a document, then bail out - // immediately. - if (!this.SupportsSyntaxTree) - { - return SpecializedTasks.Default(); - } - if (_syntaxTreeResultTask != null) { return _syntaxTreeResultTask; @@ -195,11 +185,51 @@ public Task GetSyntaxTreeAsync(CancellationToken cancellationToken = return _syntaxTreeResultTask; } + return null; + } + + /// + /// Gets the for this document asynchronously. + /// + public Task GetSyntaxTreeAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + // If the language doesn't support getting syntax trees for a document, then bail out + // immediately. + if (!this.SupportsSyntaxTree) + { + return SpecializedTasks.Default(); + } + + var syntaxTreeTask = GetExistingSyntaxTreeTask(); + if (syntaxTreeTask != null) + { + return syntaxTreeTask; + } + // we can't cache this result, since internally it uses AsyncLazy which // care about cancellation token return _state.GetSyntaxTreeAsync(cancellationToken); } + private SyntaxTree GetSyntaxTree(CancellationToken cancellationToken) + { + if (!this.SupportsSyntaxTree) + { + return null; + } + + // if we already have a stask for getting this syntax tree, and the task + // has completed, then we can just return that value. + var syntaxTreeTask = GetExistingSyntaxTreeTask(); + if (syntaxTreeTask?.Status == TaskStatus.RanToCompletion) + { + return syntaxTreeTask.Result; + } + + // Otherwise defer to our state to get this value. + return _state.GetSyntaxTree(cancellationToken); + } + /// /// Gets the root node of the current syntax tree if the syntax tree has already been parsed and the tree is still cached. /// In almost all cases, you should call to fetch the root node, which will parse @@ -226,6 +256,22 @@ public async Task GetSyntaxRootAsync(CancellationToken cancellationT return await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); } + /// + /// Only for features that absolutely must run synchronously (probably because they're + /// on the UI thread). Right now, the only feature this is for is Outlining as VS will + /// block on that feature from the UI thread when a document is opened. + /// + internal SyntaxNode GetSyntaxRootSynchronously(CancellationToken cancellationToken) + { + if (!this.SupportsSyntaxTree) + { + return null; + } + + var tree = this.GetSyntaxTree(cancellationToken); + return tree.GetRoot(cancellationToken); + } + /// /// Gets the current semantic model for this document if the model is already computed and still cached. /// In almost all cases, you should call , which will compute the semantic model diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs index 4f884ab1551..de9d04badc5 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs @@ -559,6 +559,15 @@ public async Task GetSyntaxTreeAsync(CancellationToken cancellationT return treeAndVersion.Tree; } + internal SyntaxTree GetSyntaxTree(CancellationToken cancellationToken) + { + var treeAndVersion = _treeSource.GetValue(cancellationToken); + + // make sure there is an association between this tree and this doc id before handing it out + BindSyntaxTreeToId(treeAndVersion.Tree, this.Id); + return treeAndVersion.Tree; + } + public bool TryGetTopLevelChangeTextVersion(out VersionStamp version) { TreeAndVersion treeAndVersion; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SyntacticDocument.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SyntacticDocument.cs index 409ac179b03..218182bff43 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SyntacticDocument.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SyntacticDocument.cs @@ -1,11 +1,8 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis { @@ -24,16 +21,14 @@ protected SyntacticDocument(Document document, SourceText text, SyntaxTree tree, this.Root = root; } - public Project Project - { - get { return this.Document.Project; } - } + public Project Project => this.Document.Project; - public static async Task CreateAsync(Document document, CancellationToken cancellationToken) + public static async Task CreateAsync( + Document document, CancellationToken cancellationToken) { var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); return new SyntacticDocument(document, text, root.SyntaxTree, root); } } -} +} \ No newline at end of file -- GitLab