1
ļ»æ// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2
// Licensed under the Microsoft Public License (MS-PL)
5
using System.Collections.Generic;
6
using System.Diagnostics;
7
using System.Diagnostics.CodeAnalysis;
11
using System.Threading;
12
using System.Threading.Tasks;
13
using System.Web.Razor.Parser.SyntaxTree;
14
using System.Web.Razor.Resources;
15
using System.Web.Razor.Text;
16
using System.Web.Razor.Utils;
17
using System.Web.Razor;
18
using System.Web.Razor.Editor;
20
namespace RazorEditorParserFixed
22
internal class BackgroundParser : IDisposable
24
private MainThreadState _main;
25
private BackgroundThread _bg;
27
public BackgroundParser(RazorEngineHost host, string fileName)
29
_main = new MainThreadState(fileName);
30
_bg = new BackgroundThread(_main, host, fileName);
32
_main.ResultsReady += (sender, args) => OnResultsReady(args);
36
/// Fired on the main thread.
38
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
42
get { return _main.IsIdle; }
55
public void QueueChange(TextChange change)
57
_main.QueueChange(change);
60
[SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_main", Justification = "MainThreadState is disposed when the background thread shuts down")]
66
public IDisposable SynchronizeMainThreadState()
71
protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args)
73
var handler = ResultsReady;
80
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<TextChange> changes)
82
return TreesAreDifferent(leftTree, rightTree, changes, CancellationToken.None);
85
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<TextChange> changes, CancellationToken cancelToken)
87
// Apply all the pending changes to the original tree
88
// PERF: If this becomes a bottleneck, we can probably do it the other way around,
89
// i.e. visit the tree and find applicable changes for each node.
90
foreach (TextChange change in changes)
92
cancelToken.ThrowIfCancellationRequested();
93
Span changeOwner = leftTree.LocateOwner(change);
95
// Apply the change to the tree
96
if (changeOwner == null)
100
EditResult result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
101
changeOwner.ReplaceWith(result.EditedSpan);
104
// Now compare the trees
105
bool treesDifferent = !leftTree.EquivalentTo(rightTree);
106
return treesDifferent;
109
private abstract class ThreadStateBase
111
#if RAZOREDITOR_DEBUG
112
private int _id = -1;
114
protected ThreadStateBase()
118
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")]
119
[Conditional("RAZOREDITOR_DEBUG")]
120
protected void SetThreadId(int id)
122
#if RAZOREDITOR_DEBUG
127
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")]
128
[Conditional("RAZOREDITOR_DEBUG")]
129
protected void EnsureOnThread()
131
#if RAZOREDITOR_DEBUG
132
Debug.Assert(_id != -1, "SetThreadId was never called!");
133
Debug.Assert(Thread.CurrentThread.ManagedThreadId == _id, "Called from an unexpected thread!");
137
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")]
138
[Conditional("RAZOREDITOR_DEBUG")]
139
protected void EnsureNotOnThread()
141
#if RAZOREDITOR_DEBUG
142
Debug.Assert(_id != -1, "SetThreadId was never called!");
143
Debug.Assert(Thread.CurrentThread.ManagedThreadId != _id, "Called from an unexpected thread!");
148
private class MainThreadState : ThreadStateBase, IDisposable
150
private CancellationTokenSource _cancelSource = new CancellationTokenSource();
151
private ManualResetEventSlim _hasParcel = new ManualResetEventSlim(false);
152
private CancellationTokenSource _currentParcelCancelSource;
155
private string _fileName;
157
private object _stateLock = new object();
158
private IList<TextChange> _changes = new List<TextChange>();
160
public MainThreadState(string fileName)
163
_fileName = fileName;
166
SetThreadId(Thread.CurrentThread.ManagedThreadId);
169
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
171
public CancellationToken CancelToken
173
get { return _cancelSource.Token; }
182
return _currentParcelCancelSource == null;
190
_cancelSource.Cancel();
193
public IDisposable Lock()
195
Monitor.Enter(_stateLock);
196
return new DisposableAction(() => Monitor.Exit(_stateLock));
199
public void QueueChange(TextChange change)
202
RazorEditorTrace.TraceLine(RazorResources.Trace_QueuingParse, Path.GetFileName(_fileName), change);
207
// CurrentParcel token source is not null ==> There's a parse underway
208
if (_currentParcelCancelSource != null)
210
_currentParcelCancelSource.Cancel();
213
_changes.Add(change);
218
public WorkParcel GetParcel()
220
EnsureNotOnThread(); // Only the background thread can get a parcel
221
_hasParcel.Wait(_cancelSource.Token);
225
// Create a cancellation source for this parcel
226
_currentParcelCancelSource = new CancellationTokenSource();
228
var changes = _changes;
229
_changes = new List<TextChange>();
230
return new WorkParcel(changes, _currentParcelCancelSource.Token);
234
public void ReturnParcel(DocumentParseCompleteEventArgs args)
238
// Clear the current parcel cancellation source
239
if (_currentParcelCancelSource != null)
241
_currentParcelCancelSource.Dispose();
242
_currentParcelCancelSource = null;
245
// If there are things waiting to be parsed, just don't fire the event because we're already out of date
251
var handler = ResultsReady;
258
public void Dispose()
261
GC.SuppressFinalize(this);
264
protected virtual void Dispose(bool disposing)
268
if (_currentParcelCancelSource != null)
270
_currentParcelCancelSource.Dispose();
271
_currentParcelCancelSource = null;
273
_cancelSource.Dispose();
274
_hasParcel.Dispose();
279
private class BackgroundThread : ThreadStateBase
281
private MainThreadState _main;
282
private Thread _backgroundThread;
283
private CancellationToken _shutdownToken;
284
private RazorEngineHost _host;
285
private string _fileName;
286
private Block _currentParseTree;
287
private IList<TextChange> _previouslyDiscarded = new List<TextChange>();
289
public BackgroundThread(MainThreadState main, RazorEngineHost host, string fileName)
291
// Run on MAIN thread!
293
_backgroundThread = new Thread(WorkerLoop) {
296
_shutdownToken = _main.CancelToken;
298
_fileName = fileName;
300
SetThreadId(_backgroundThread.ManagedThreadId);
303
// **** ANY THREAD ****
306
_backgroundThread.Start();
309
// **** BACKGROUND THREAD ****
310
private void WorkerLoop()
312
long? elapsedMs = null;
313
string fileNameOnly = Path.GetFileName(_fileName);
315
Stopwatch sw = new Stopwatch();
320
RazorEditorTrace.TraceLine(RazorResources.Trace_BackgroundThreadStart, fileNameOnly);
322
while (!_shutdownToken.IsCancellationRequested)
324
// Grab the parcel of work to do
325
WorkParcel parcel = _main.GetParcel();
326
if (parcel.Changes.Any())
328
RazorEditorTrace.TraceLine(RazorResources.Trace_ChangesArrived, fileNameOnly, parcel.Changes.Count);
331
DocumentParseCompleteEventArgs args = null;
332
using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
334
if (parcel != null && !linkedCancel.IsCancellationRequested)
336
// Collect ALL changes
338
if (_previouslyDiscarded != null && _previouslyDiscarded.Any())
340
RazorEditorTrace.TraceLine(RazorResources.Trace_CollectedDiscardedChanges, fileNameOnly, _previouslyDiscarded.Count);
343
var allChanges = Enumerable.Concat(
344
_previouslyDiscarded ?? Enumerable.Empty<TextChange>(), parcel.Changes).ToList();
345
var finalChange = allChanges.LastOrDefault();
346
if (finalChange != default (TextChange))
351
GeneratorResults results = ParseChange(finalChange.NewBuffer, linkedCancel.Token);
354
elapsedMs = sw.ElapsedMilliseconds;
357
RazorEditorTrace.TraceLine(
358
RazorResources.Trace_ParseComplete,
360
elapsedMs.HasValue ? elapsedMs.Value.ToString() : "?");
362
if (results != null && !linkedCancel.IsCancellationRequested)
364
// Clear discarded changes list
365
_previouslyDiscarded = null;
367
// Take the current tree and check for differences
371
bool treeStructureChanged = _currentParseTree == null || TreesAreDifferent(_currentParseTree, results.Document, allChanges, parcel.CancelToken);
374
elapsedMs = sw.ElapsedMilliseconds;
377
_currentParseTree = results.Document;
378
RazorEditorTrace.TraceLine(RazorResources.Trace_TreesCompared,
380
elapsedMs.HasValue ? elapsedMs.Value.ToString() : "?",
381
treeStructureChanged);
384
args = new DocumentParseCompleteEventArgs()
386
GeneratorResults = results,
387
SourceChange = finalChange,
388
TreeStructureChanged = treeStructureChanged
393
// Parse completed but we were cancelled in the mean time. Add these to the discarded changes set
394
RazorEditorTrace.TraceLine(RazorResources.Trace_ChangesDiscarded, fileNameOnly, allChanges.Count);
395
_previouslyDiscarded = allChanges;
401
// Rewind the buffer and sanity check the line mappings
402
finalChange.NewBuffer.Position = 0;
403
int lineCount = finalChange.NewBuffer.ReadToEnd().Split(new string[] { Environment.NewLine, "\r", "\n" }, StringSplitOptions.None).Count();
405
!args.GeneratorResults.DesignTimeLineMappings.Any(pair => pair.Value.StartLine > lineCount),
406
"Found a design-time line mapping referring to a line outside the source file!");
408
!args.GeneratorResults.Document.Flatten().Any(span => span.Start.LineIndex > lineCount),
409
"Found a span with a line number outside the source file");
411
!args.GeneratorResults.Document.Flatten().Any(span => span.Start.AbsoluteIndex > parcel.NewBuffer.Length),
412
"Found a span with an absolute offset outside the source file");
420
_main.ReturnParcel(args);
423
catch (OperationCanceledException)
429
RazorEditorTrace.TraceLine(RazorResources.Trace_NoChangesArrived, fileNameOnly, parcel.Changes.Count);
434
catch (OperationCanceledException)
436
// Do nothing. Just shut down.
440
MonoDevelop.Core.LoggingService.LogError ("Internal error in Razor parser", ex);
441
MonoDevelop.Core.LogReporting.LogReportingService.ReportUnhandledException (ex, false);
445
RazorEditorTrace.TraceLine(RazorResources.Trace_BackgroundThreadShutdown, fileNameOnly);
447
// Clean up main thread resources
452
private GeneratorResults ParseChange(ITextBuffer buffer, CancellationToken token)
456
// Create a template engine
457
RazorTemplateEngine engine = new RazorTemplateEngine(_host);
459
// Seek the buffer to the beginning
464
return engine.GenerateCode(
468
sourceFileName: _fileName,
471
catch (OperationCanceledException)
478
private class WorkParcel
480
public WorkParcel(IList<TextChange> changes, CancellationToken cancelToken)
483
CancelToken = cancelToken;
486
public CancellationToken CancelToken { get; private set; }
487
public IList<TextChange> Changes { get; private set; }