aspnetcore/src/Microsoft.AspNet.Razor/Editor/BackgroundParser.cs

493 lines
20 KiB
C#

// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Text;
using Microsoft.AspNet.Razor.Utils;
namespace Microsoft.AspNet.Razor.Editor
{
internal class BackgroundParser : IDisposable
{
private MainThreadState _main;
private BackgroundThread _bg;
public BackgroundParser(RazorEngineHost host, string fileName)
{
_main = new MainThreadState(fileName);
_bg = new BackgroundThread(_main, host, fileName);
_main.ResultsReady += (sender, args) => OnResultsReady(args);
}
/// <summary>
/// Fired on the main thread.
/// </summary>
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
public bool IsIdle
{
get { return _main.IsIdle; }
}
public void Start()
{
_bg.Start();
}
public void Cancel()
{
_main.Cancel();
}
public void QueueChange(TextChange change)
{
_main.QueueChange(change);
}
[SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_main", Justification = "MainThreadState is disposed when the background thread shuts down")]
public void Dispose()
{
_main.Cancel();
}
public IDisposable SynchronizeMainThreadState()
{
return _main.Lock();
}
protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args)
{
var handler = ResultsReady;
if (handler != null)
{
handler(this, args);
}
}
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<TextChange> changes)
{
return TreesAreDifferent(leftTree, rightTree, changes, CancellationToken.None);
}
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<TextChange> changes, CancellationToken cancelToken)
{
// Apply all the pending changes to the original tree
// PERF: If this becomes a bottleneck, we can probably do it the other way around,
// i.e. visit the tree and find applicable changes for each node.
foreach (TextChange change in changes)
{
cancelToken.ThrowIfCancellationRequested();
Span changeOwner = leftTree.LocateOwner(change);
// Apply the change to the tree
if (changeOwner == null)
{
return true;
}
EditResult result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
changeOwner.ReplaceWith(result.EditedSpan);
}
// Now compare the trees
bool treesDifferent = !leftTree.EquivalentTo(rightTree);
return treesDifferent;
}
private abstract class ThreadStateBase
{
#if DEBUG
private int _id = -1;
#endif
protected ThreadStateBase()
{
}
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")]
[Conditional("DEBUG")]
protected void SetThreadId(int id)
{
#if DEBUG
_id = id;
#endif
}
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")]
[Conditional("DEBUG")]
protected void EnsureOnThread()
{
#if DEBUG
Debug.Assert(_id != -1, "SetThreadId was never called!");
Debug.Assert(Thread.CurrentThread.ManagedThreadId == _id, "Called from an unexpected thread!");
#endif
}
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")]
[Conditional("DEBUG")]
protected void EnsureNotOnThread()
{
#if DEBUG
Debug.Assert(_id != -1, "SetThreadId was never called!");
Debug.Assert(Thread.CurrentThread.ManagedThreadId != _id, "Called from an unexpected thread!");
#endif
}
}
private class MainThreadState : ThreadStateBase, IDisposable
{
private CancellationTokenSource _cancelSource = new CancellationTokenSource();
private ManualResetEventSlim _hasParcel = new ManualResetEventSlim(false);
private CancellationTokenSource _currentParcelCancelSource;
[SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Field is used in debug code and may be used later")]
private string _fileName;
private object _stateLock = new object();
private IList<TextChange> _changes = new List<TextChange>();
public MainThreadState(string fileName)
{
_fileName = fileName;
SetThreadId(Thread.CurrentThread.ManagedThreadId);
}
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
public CancellationToken CancelToken
{
get { return _cancelSource.Token; }
}
public bool IsIdle
{
get
{
lock (_stateLock)
{
return _currentParcelCancelSource == null;
}
}
}
public void Cancel()
{
EnsureOnThread();
_cancelSource.Cancel();
}
public IDisposable Lock()
{
Monitor.Enter(_stateLock);
return new DisposableAction(() => Monitor.Exit(_stateLock));
}
public void QueueChange(TextChange change)
{
RazorEditorTrace.TraceLine(RazorResources.FormatTrace_QueuingParse(Path.GetFileName(_fileName), change));
EnsureOnThread();
lock (_stateLock)
{
// CurrentParcel token source is not null ==> There's a parse underway
if (_currentParcelCancelSource != null)
{
_currentParcelCancelSource.Cancel();
}
_changes.Add(change);
_hasParcel.Set();
}
}
public WorkParcel GetParcel()
{
EnsureNotOnThread(); // Only the background thread can get a parcel
_hasParcel.Wait(_cancelSource.Token);
_hasParcel.Reset();
lock (_stateLock)
{
// Create a cancellation source for this parcel
_currentParcelCancelSource = new CancellationTokenSource();
var changes = _changes;
_changes = new List<TextChange>();
return new WorkParcel(changes, _currentParcelCancelSource.Token);
}
}
public void ReturnParcel(DocumentParseCompleteEventArgs args)
{
lock (_stateLock)
{
// Clear the current parcel cancellation source
if (_currentParcelCancelSource != null)
{
_currentParcelCancelSource.Dispose();
_currentParcelCancelSource = null;
}
// If there are things waiting to be parsed, just don't fire the event because we're already out of date
if (_changes.Any())
{
return;
}
}
var handler = ResultsReady;
if (handler != null)
{
handler(this, args);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (_currentParcelCancelSource != null)
{
_currentParcelCancelSource.Dispose();
_currentParcelCancelSource = null;
}
_cancelSource.Dispose();
_hasParcel.Dispose();
}
}
}
private class BackgroundThread : ThreadStateBase
{
private MainThreadState _main;
private Thread _backgroundThread;
private CancellationToken _shutdownToken;
private RazorEngineHost _host;
private string _fileName;
private Block _currentParseTree;
private IList<TextChange> _previouslyDiscarded = new List<TextChange>();
public BackgroundThread(MainThreadState main, RazorEngineHost host, string fileName)
{
// Run on MAIN thread!
_main = main;
_backgroundThread = new Thread(WorkerLoop);
_shutdownToken = _main.CancelToken;
_host = host;
_fileName = fileName;
SetThreadId(_backgroundThread.ManagedThreadId);
}
// **** ANY THREAD ****
public void Start()
{
_backgroundThread.Start();
}
// **** BACKGROUND THREAD ****
private void WorkerLoop()
{
long? elapsedMs = null;
string fileNameOnly = Path.GetFileName(_fileName);
#if EDITOR_TRACING
Stopwatch sw = new Stopwatch();
#endif
try
{
RazorEditorTrace.TraceLine(RazorResources.FormatTrace_BackgroundThreadStart(fileNameOnly));
EnsureOnThread();
#if K10
var spinWait = new SpinWait();
#endif
while (!_shutdownToken.IsCancellationRequested)
{
// Grab the parcel of work to do
WorkParcel parcel = _main.GetParcel();
if (parcel.Changes.Any())
{
RazorEditorTrace.TraceLine(RazorResources.FormatTrace_ChangesArrived(fileNameOnly, parcel.Changes.Count));
try
{
DocumentParseCompleteEventArgs args = null;
using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
{
if (!linkedCancel.IsCancellationRequested)
{
// Collect ALL changes
#if EDITOR_TRACING
if (_previouslyDiscarded != null && _previouslyDiscarded.Any())
{
RazorEditorTrace.TraceLine(RazorResources.Trace_CollectedDiscardedChanges, fileNameOnly, _previouslyDiscarded.Count);
}
#endif
List<TextChange> allChanges;
if (_previouslyDiscarded != null)
{
allChanges = Enumerable.Concat(_previouslyDiscarded, parcel.Changes).ToList();
}
else
{
allChanges = parcel.Changes.ToList();
}
TextChange finalChange = allChanges.Last();
#if EDITOR_TRACING
sw.Start();
#endif
GeneratorResults results = ParseChange(finalChange.NewBuffer, linkedCancel.Token);
#if EDITOR_TRACING
sw.Stop();
elapsedMs = sw.ElapsedMilliseconds;
sw.Reset();
#endif
RazorEditorTrace.TraceLine(
RazorResources.FormatTrace_ParseComplete(
fileNameOnly,
elapsedMs.HasValue ? elapsedMs.Value.ToString(CultureInfo.InvariantCulture) : "?"));
if (results != null && !linkedCancel.IsCancellationRequested)
{
// Clear discarded changes list
_previouslyDiscarded = null;
// Take the current tree and check for differences
#if EDITOR_TRACING
sw.Start();
#endif
bool treeStructureChanged = _currentParseTree == null || TreesAreDifferent(_currentParseTree, results.Document, allChanges, parcel.CancelToken);
#if EDITOR_TRACING
sw.Stop();
elapsedMs = sw.ElapsedMilliseconds;
sw.Reset();
#endif
_currentParseTree = results.Document;
RazorEditorTrace.TraceLine(RazorResources.FormatTrace_TreesCompared(
fileNameOnly,
elapsedMs.HasValue ? elapsedMs.Value.ToString(CultureInfo.InvariantCulture) : "?",
treeStructureChanged));
// Build Arguments
args = new DocumentParseCompleteEventArgs()
{
GeneratorResults = results,
SourceChange = finalChange,
TreeStructureChanged = treeStructureChanged
};
}
else
{
// Parse completed but we were cancelled in the mean time. Add these to the discarded changes set
RazorEditorTrace.TraceLine(RazorResources.FormatTrace_ChangesDiscarded(fileNameOnly, allChanges.Count));
_previouslyDiscarded = allChanges;
}
#if CHECK_TREE
if (args != null)
{
// Rewind the buffer and sanity check the line mappings
finalChange.NewBuffer.Position = 0;
int lineCount = finalChange.NewBuffer.ReadToEnd().Split(new string[] { Environment.NewLine, "\r", "\n" }, StringSplitOptions.None).Count();
Debug.Assert(
!args.GeneratorResults.DesignTimeLineMappings.Any(pair => pair.Value.StartLine > lineCount),
"Found a design-time line mapping referring to a line outside the source file!");
Debug.Assert(
!args.GeneratorResults.Document.Flatten().Any(span => span.Start.LineIndex > lineCount),
"Found a span with a line number outside the source file");
Debug.Assert(
!args.GeneratorResults.Document.Flatten().Any(span => span.Start.AbsoluteIndex > parcel.NewBuffer.Length),
"Found a span with an absolute offset outside the source file");
}
#endif
}
}
if (args != null)
{
_main.ReturnParcel(args);
}
}
catch (OperationCanceledException)
{
}
}
else
{
RazorEditorTrace.TraceLine(RazorResources.FormatTrace_NoChangesArrived(fileNameOnly));
#if NET45
// No Yield in CoreCLR
Thread.Yield();
#else
// This does the equivalent of thread.yield under the covers.
spinWait.SpinOnce();
#endif
}
}
}
catch (OperationCanceledException)
{
// Do nothing. Just shut down.
}
finally
{
RazorEditorTrace.TraceLine(RazorResources.FormatTrace_BackgroundThreadShutdown(fileNameOnly));
// Clean up main thread resources
_main.Dispose();
}
}
private GeneratorResults ParseChange(ITextBuffer buffer, CancellationToken token)
{
EnsureOnThread();
// Create a template engine
RazorTemplateEngine engine = new RazorTemplateEngine(_host);
// Seek the buffer to the beginning
buffer.Position = 0;
try
{
return engine.GenerateCode(
input: buffer,
className: null,
rootNamespace: null,
sourceFileName: _fileName,
cancelToken: token);
}
catch (OperationCanceledException)
{
return null;
}
}
}
private class WorkParcel
{
public WorkParcel(IList<TextChange> changes, CancellationToken cancelToken)
{
Changes = changes;
CancelToken = cancelToken;
}
public CancellationToken CancelToken { get; private set; }
public IList<TextChange> Changes { get; private set; }
}
}
}