// 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); } /// /// Fired on the main thread. /// public event EventHandler 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 changes) { return TreesAreDifferent(leftTree, rightTree, changes, CancellationToken.None); } internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable 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 _changes = new List(); public MainThreadState(string fileName) { _fileName = fileName; SetThreadId(Thread.CurrentThread.ManagedThreadId); } public event EventHandler 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(); 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 _previouslyDiscarded = new List(); 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 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 changes, CancellationToken cancelToken) { Changes = changes; CancelToken = cancelToken; } public CancellationToken CancelToken { get; private set; } public IList Changes { get; private set; } } } }