Skip to content

Commit 5536593

Browse files
Timeout after lack of activity - related #598
1 parent 7185410 commit 5536593

13 files changed

+191
-47
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
44

55
## [Unreleased]
66

7+
* Timeout cosmetic operations (formatting/comments) after 15 minutes of inactivity [#598](https://github.com/icsharpcode/CodeConverter/issues/598)
78

89
### Vsix
9-
10+
* Options page to adjust timeout
1011

1112
### VB -> C#
1213
* Convert parameterized properties with optional parameters [#597](https://github.com/icsharpcode/CodeConverter/issues/597)
@@ -15,7 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1516
* Don't generate unnecessary properties for WithEvents fields [#572](https://github.com/icsharpcode/CodeConverter/issues/572)
1617

1718
### C# -> VB
18-
19+
* Performance increase for large files/projects
1920

2021
## [8.1.6] - 2020-07-12
2122

CodeConverter/CSharp/VBToCSConversion.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public class VBToCSConversion : ILanguageConversion
2929

3030
public ConversionOptions ConversionOptions { get; set; }
3131

32-
3332
public async Task<IProjectContentsConverter> CreateProjectContentsConverterAsync(Project project, IProgress<ConversionProgress> progress, CancellationToken cancellationToken)
3433
{
3534
_progress = progress;

CodeConverter/CSharp/VBToCSProjectContentsConverter.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ public VBToCSProjectContentsConverter(ConversionOptions conversionOptions, bool
3636
this._useProjectLevelWinformsAdjustments = useProjectLevelWinformsAdjustments;
3737
_progress = progress;
3838
_cancellationToken = cancellationToken;
39+
OptionalOperations = new OptionalOperations(conversionOptions.AbandonOptionalTasksAfter, progress, cancellationToken);
3940
}
4041

42+
public OptionalOperations OptionalOperations { get; }
43+
4144
public string RootNamespace => _conversionOptions.RootNamespaceOverride ??
4245
((VisualBasicCompilationOptions)SourceProject.CompilationOptions).RootNamespace;
4346

@@ -65,7 +68,7 @@ private async Task<Project> WithProjectLevelWinformsAdjustmentsAsync(Project pro
6568

6669
public async Task<SyntaxNode> SingleFirstPassAsync(Document document)
6770
{
68-
return await VisualBasicConverter.ConvertCompilationTreeAsync(document, _csharpViewOfVbSymbols, _csharpReferenceProject, _cancellationToken);
71+
return await VisualBasicConverter.ConvertCompilationTreeAsync(document, _csharpViewOfVbSymbols, _csharpReferenceProject, OptionalOperations, _cancellationToken);
6972
}
7073

7174
public async Task<(Project project, List<WipFileConversion<DocumentId>> firstPassDocIds)> GetConvertedProjectAsync(WipFileConversion<SyntaxNode>[] firstPassResults)

CodeConverter/CSharp/VisualBasicConverter.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax;
99
using CSS = Microsoft.CodeAnalysis.CSharp.Syntax;
1010
using System.Threading;
11+
using ICSharpCode.CodeConverter.VB;
1112

1213
namespace ICSharpCode.CodeConverter.CSharp
1314
{
1415
internal static class VisualBasicConverter
1516
{
1617
public static async Task<SyntaxNode> ConvertCompilationTreeAsync(Document document,
17-
CSharpCompilation csharpViewOfVbSymbols, Project csharpReferenceProject, CancellationToken cancellationToken)
18+
CSharpCompilation csharpViewOfVbSymbols, Project csharpReferenceProject,
19+
OptionalOperations optionalOperations, CancellationToken cancellationToken)
1820
{
1921
document = await document.WithExpandedRootAsync(cancellationToken);
2022
var root = await document.GetSyntaxRootAsync(cancellationToken) as VBSyntax.CompilationUnitSyntax ??
@@ -30,13 +32,7 @@ public static async Task<SyntaxNode> ConvertCompilationTreeAsync(Document docume
3032
DeclarationNodeVisitor(document, compilation, semanticModel, csharpViewOfVbSymbols, csSyntaxGenerator);
3133
var converted = (CSS.CompilationUnitSyntax)await root.AcceptAsync(visualBasicSyntaxVisitor.TriviaConvertingDeclarationVisitor);
3234

33-
try {
34-
// This call is very expensive for large documents. Should look for a more performant version, e.g. Is NormalizeWhitespace good enough?
35-
converted = (CSS.CompilationUnitSyntax)Formatter.Format(converted, document.Project.Solution.Workspace, cancellationToken: cancellationToken);
36-
return LineTriviaMapper.MapSourceTriviaToTarget(root, converted);
37-
} catch (Exception) { //TODO log
38-
return converted;
39-
}
35+
return optionalOperations.MapSourceTriviaToTargetHandled(root, converted, document);
4036
}
4137

4238
private static string NullRootError(Document document)
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
using Microsoft.CodeAnalysis;
1+
using System;
2+
using Microsoft.CodeAnalysis;
23

34
namespace ICSharpCode.CodeConverter.Shared
45
{
56
public class ConversionOptions
67
{
78
public string RootNamespaceOverride { get; set; }
89
public CompilationOptions TargetCompilationOptionsOverride { get; set; }
10+
public TimeSpan AbandonOptionalTasksAfter { get; set; } = TimeSpan.FromMinutes(30);
911
}
1012
}

CodeConverter/Shared/IProjectContentsConverter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public interface IProjectContentsConverter
1212
string LanguageVersion { get; }
1313
string RootNamespace { get; }
1414
Project SourceProject { get; }
15+
OptionalOperations OptionalOperations { get; }
1516
Task<SyntaxNode> SingleFirstPassAsync(Document document);
1617
Task<(Project project, List<WipFileConversion<DocumentId>> firstPassDocIds)> GetConvertedProjectAsync(WipFileConversion<SyntaxNode>[] firstPassResults);
1718
public IAsyncEnumerable<ConversionResult> GetAdditionalConversionResults(IReadOnlyCollection<TextDocument> additionalDocumentsToConvert, CancellationToken cancellationToken);
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System;
2+
using System.Threading;
3+
using System.Timers;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.Formatting;
6+
7+
namespace ICSharpCode.CodeConverter.Shared
8+
{
9+
/// <summary>
10+
/// https://github.com/icsharpcode/CodeConverter/issues/598#issuecomment-663773878
11+
/// </summary>
12+
public class OptionalOperations
13+
{
14+
private readonly IProgress<ConversionProgress> _progress;
15+
private readonly CancellationToken _wholeTaskCancellationToken;
16+
private readonly ActivityMonitor _activityMonitor;
17+
private readonly CancellationTokenSource _optionalTaskCts;
18+
19+
public OptionalOperations(TimeSpan abandonTasksIfNoActivityFor, IProgress<ConversionProgress> progress,
20+
CancellationToken wholeTaskCancellationToken)
21+
{
22+
_progress = progress;
23+
_wholeTaskCancellationToken = wholeTaskCancellationToken;
24+
_optionalTaskCts = CancellationTokenSource.CreateLinkedTokenSource(wholeTaskCancellationToken);
25+
_activityMonitor = new ActivityMonitor(abandonTasksIfNoActivityFor, _optionalTaskCts);
26+
}
27+
28+
public SyntaxNode MapSourceTriviaToTargetHandled<TSource, TTarget>(TSource root,
29+
TTarget converted, Document document)
30+
where TSource : SyntaxNode, ICompilationUnitSyntax where TTarget : SyntaxNode, ICompilationUnitSyntax
31+
{
32+
try
33+
{
34+
converted = (TTarget) Format(converted, document);
35+
return LineTriviaMapper.MapSourceTriviaToTarget(root, converted);
36+
}
37+
catch (Exception e)
38+
{
39+
_progress.Report(new ConversionProgress($"Error while formatting and converting comments: {e}"));
40+
return converted;
41+
}
42+
}
43+
44+
public SyntaxNode Format(SyntaxNode node, Document document)
45+
{
46+
if (!_optionalTaskCts.IsCancellationRequested) {
47+
try {
48+
_optionalTaskCts.Token.ThrowIfCancellationRequested();
49+
_activityMonitor.ActivityStarted();
50+
// This call is very expensive for large documents. Should look for a more performant version, e.g. Is NormalizeWhitespace good enough?
51+
return Formatter.Format(node, document.Project.Solution.Workspace,
52+
cancellationToken: _optionalTaskCts.Token);
53+
54+
} catch (OperationCanceledException) {
55+
if (!_wholeTaskCancellationToken.IsCancellationRequested) {
56+
_progress.Report(new ConversionProgress(
57+
"Aborting all further formatting and comment mapping, you can increase the timeout for this in Tools -> Options -> Code Converter."));
58+
}
59+
} finally {
60+
_activityMonitor.ActivityFinished();
61+
}
62+
}
63+
return node.NormalizeWhitespace();
64+
}
65+
66+
/// <summary>
67+
/// Reasonably lightweight check that there's been some activity within the given timeout.
68+
/// </summary>
69+
private class ActivityMonitor
70+
{
71+
private readonly TimeSpan _timeout;
72+
private readonly CancellationTokenSource _cts;
73+
private volatile int _activeOperations;
74+
/// <summary>
75+
/// Must check <see cref="_activeOperations"/> within the lock before changed timer.Enabled
76+
/// This avoids race conditions between the last task of a set finishing and the first of a new set starting
77+
/// </summary>
78+
private readonly object _timerEnabledWriteLock = new object();
79+
private static System.Timers.Timer _timer;
80+
81+
82+
private void OnTimedEvent(object source, ElapsedEventArgs e)
83+
{
84+
if (!_cts.IsCancellationRequested && _timer.Enabled) {
85+
_cts.Cancel();
86+
}
87+
}
88+
89+
public ActivityMonitor(TimeSpan timeout, CancellationTokenSource cts)
90+
{
91+
_timeout = timeout;
92+
_cts = cts;
93+
_timer = new System.Timers.Timer(timeout.TotalMilliseconds) {AutoReset = true};
94+
_timer.Elapsed += OnTimedEvent;
95+
}
96+
97+
public void ActivityStarted()
98+
{
99+
if (Interlocked.Increment(ref _activeOperations) == 1) {
100+
lock (_timerEnabledWriteLock) {
101+
if (_activeOperations > 0) {
102+
_timer.Enabled = true;
103+
}
104+
}
105+
}
106+
ActivityObserved();
107+
}
108+
109+
public void ActivityFinished()
110+
{
111+
ActivityObserved();
112+
if (Interlocked.Decrement(ref _activeOperations) == 0) {
113+
lock (_timerEnabledWriteLock) {
114+
if (_activeOperations == 0) {
115+
_timer.Enabled = false;
116+
}
117+
}
118+
}
119+
}
120+
121+
private void ActivityObserved()
122+
{
123+
try {
124+
_timer.Interval = _timeout.TotalMilliseconds;
125+
} catch (ObjectDisposedException e) {
126+
// Race condition if we try to set the interval after disabling the timer
127+
} catch (NullReferenceException e) {
128+
// Race condition if we try to set the interval after disabling the timer
129+
}
130+
}
131+
}
132+
}
133+
}

CodeConverter/Shared/ProjectConversion.cs

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
using ICSharpCode.CodeConverter.CSharp;
1111
using ICSharpCode.CodeConverter.Util;
1212
using Microsoft.CodeAnalysis;
13-
using Microsoft.CodeAnalysis.Formatting;
1413
using Microsoft.CodeAnalysis.Text;
15-
using Microsoft.VisualStudio.Threading;
1614

1715
namespace ICSharpCode.CodeConverter.Shared
1816
{
@@ -69,7 +67,7 @@ private ProjectConversion(IProjectContentsConverter projectContentsConverter, IE
6967
document = projectContentsConverter.SourceProject.GetDocument(document.Id);
7068

7169
var conversion = new ProjectConversion(projectContentsConverter, new[] { document }, Enumerable.Empty<TextDocument>(), languageConversion, cancellationToken, conversionOptions.ShowCompilationErrors, returnSelectedNode);
72-
var conversionResults = await conversion.Convert(progress).ToArrayAsync();
70+
var conversionResults = await conversion.Convert(progress).ToArrayAsync(cancellationToken);
7371
return GetSingleResultForDocument(conversionResults, document);
7472
}
7573

@@ -93,7 +91,7 @@ public static async IAsyncEnumerable<ConversionResult> ConvertProject(Project pr
9391
var convertProjectContents = ConvertProjectContents(projectContentsConverter, languageConversion, progress, cancellationToken);
9492

9593
var results = WithProjectFile(projectContentsConverter, languageConversion, sourceFilePaths, convertProjectContents, replacements);
96-
await foreach (var result in results) yield return result;
94+
await foreach (var result in results.WithCancellation(cancellationToken)) yield return result;
9795
}
9896

9997
/// <remarks>Perf: Keep lazy so that we don't keep an extra copy of all files in memory at once</remarks>
@@ -164,23 +162,23 @@ private static async IAsyncEnumerable<ConversionResult> ConvertProjectContents(
164162
{
165163
var documentsWithLengths = await projectContentsConverter.SourceProject.Documents
166164
.Where(d => !BannedPaths.Any(d.FilePath.Contains))
167-
.SelectAsync(async d => (Doc: d, Length: (await d.GetTextAsync()).Length));
165+
.SelectAsync(async d => (Doc: d, Length: (await d.GetTextAsync(cancellationToken)).Length));
168166

169167
//Perf heuristic: Decrease memory pressure on the simplification phase by converting large files first https://github.com/icsharpcode/CodeConverter/issues/524#issuecomment-590301594
170168
var documentsToConvert = documentsWithLengths.OrderByDescending(d => d.Length).Select(d => d.Doc);
171169

172170
var projectConversion = new ProjectConversion(projectContentsConverter, documentsToConvert, projectContentsConverter.SourceProject.AdditionalDocuments, languageConversion, cancellationToken, false);
173171

174172
var results = projectConversion.Convert(progress);
175-
await foreach (var result in results) yield return result;
173+
await foreach (var result in results.WithCancellation(cancellationToken)) yield return result;
176174
}
177175

178176

179177
private async IAsyncEnumerable<ConversionResult> Convert(IProgress<ConversionProgress> progress)
180178
{
181179
var phaseProgress = StartPhase(progress, "Phase 1 of 2:");
182180
var firstPassResults = _documentsToConvert.ParallelSelectAwait(d => FirstPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken);
183-
var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArrayAsync());
181+
var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArrayAsync(_cancellationToken));
184182

185183
var warnings = await GetProjectWarningsAsync(_projectContentsConverter.SourceProject, proj1);
186184
if (!string.IsNullOrWhiteSpace(warnings)) {
@@ -190,7 +188,7 @@ private async IAsyncEnumerable<ConversionResult> Convert(IProgress<ConversionPro
190188

191189
phaseProgress = StartPhase(progress, "Phase 2 of 2:");
192190
var secondPassResults = proj1.GetDocuments(docs1).ParallelSelectAwait(d => SecondPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken);
193-
await foreach (var result in secondPassResults.Select(CreateConversionResult)) {
191+
await foreach (var result in secondPassResults.Select(CreateConversionResult).WithCancellation(_cancellationToken)) {
194192
yield return result;
195193
}
196194
await foreach (var result in _projectContentsConverter.GetAdditionalConversionResults(_additionalDocumentsToConvert, _cancellationToken)) {
@@ -233,29 +231,29 @@ private async Task<WipFileConversion<SyntaxNode>> SecondPassLoggedAsync(WipFileC
233231
selectedNode = await GetSelectedNodeAsync(document);
234232
var extraLeadingTrivia = selectedNode.GetFirstToken().GetPreviousToken().TrailingTrivia;
235233
var extraTrailingTrivia = selectedNode.GetLastToken().GetNextToken().LeadingTrivia;
236-
selectedNode = Formatter.Format(selectedNode, document.Project.Solution.Workspace);
234+
selectedNode = _projectContentsConverter.OptionalOperations.Format(selectedNode, document);
237235
if (extraLeadingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine())) selectedNode = selectedNode.WithPrependedLeadingTrivia(extraLeadingTrivia);
238236
if (extraTrailingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine())) selectedNode = selectedNode.WithAppendedTrailingTrivia(extraTrailingTrivia);
239237
} else {
240-
selectedNode = await document.GetSyntaxRootAsync();
241-
selectedNode = Formatter.Format(selectedNode, document.Project.Solution.Workspace);
238+
selectedNode = await document.GetSyntaxRootAsync(_cancellationToken);
239+
selectedNode = _projectContentsConverter.OptionalOperations.Format(selectedNode, document);
242240
var convertedDoc = document.WithSyntaxRoot(selectedNode);
243-
selectedNode = await convertedDoc.GetSyntaxRootAsync();
241+
selectedNode = await convertedDoc.GetSyntaxRootAsync(_cancellationToken);
244242
}
245243
} catch (Exception e) {
246244
errors = new[] { e.ToString() };
247245
}
248246

249-
var convertedNode = selectedNode ?? await convertedDocument.GetSyntaxRootAsync();
247+
var convertedNode = selectedNode ?? await convertedDocument.GetSyntaxRootAsync(_cancellationToken);
250248
return (convertedNode, errors);
251249
}
252250

253251
private async Task<string> GetProjectWarningsAsync(Project source, Project converted)
254252
{
255253
if (!_showCompilationErrors) return null;
256254

257-
var sourceCompilation = await source.GetCompilationAsync();
258-
var convertedCompilation = await converted.GetCompilationAsync();
255+
var sourceCompilation = await source.GetCompilationAsync(_cancellationToken);
256+
var convertedCompilation = await converted.GetCompilationAsync(_cancellationToken);
259257
return CompilationWarnings.WarningsForCompilation(sourceCompilation, "source") + CompilationWarnings.WarningsForCompilation(convertedCompilation, "target");
260258
}
261259

@@ -297,7 +295,7 @@ private static async Task<Document> WithAnnotatedSelectionAsync(Document documen
297295

298296
private async Task<SyntaxNode> GetSelectedNodeAsync(Document document)
299297
{
300-
var resultNode = await document.GetSyntaxRootAsync();
298+
var resultNode = await document.GetSyntaxRootAsync(_cancellationToken);
301299
var selectedNode = resultNode.GetAnnotatedNodes(AnnotationConstants.SelectedNodeAnnotationKind)
302300
.FirstOrDefault();
303301
if (selectedNode != null) {

0 commit comments

Comments
 (0)