-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed cases where 1:1 mapping between Roslyn and editor text buffer/s… #24849
Changes from all commits
ec2547b
a595eaa
97f9a1c
8700a69
2fa75b1
518f45d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,6 @@ | |
using System.Runtime.CompilerServices; | ||
using System.Text; | ||
using System.Threading; | ||
using Microsoft.CodeAnalysis.ErrorReporting; | ||
using Microsoft.CodeAnalysis.Internal.Log; | ||
using Microsoft.CodeAnalysis.Text.Shared.Extensions; | ||
using Microsoft.VisualStudio.Text; | ||
|
@@ -23,17 +22,6 @@ public static partial class Extensions | |
/// </summary> | ||
private class SnapshotSourceText : SourceText | ||
{ | ||
/// <summary> | ||
/// Use a separate class for closed files to simplify memory leak investigations | ||
/// </summary> | ||
internal sealed class ClosedSnapshotSourceText : SnapshotSourceText | ||
{ | ||
public ClosedSnapshotSourceText(ITextImage textImage, Encoding encodingOpt) | ||
: base(textImage, encodingOpt, containerOpt: null) | ||
{ | ||
} | ||
} | ||
|
||
private static readonly Func<int, int, string> s_textLog = (v1, v2) => string.Format("FullRange : from {0} to {1}", v1, v2); | ||
|
||
/// <summary> | ||
|
@@ -43,16 +31,19 @@ public ClosedSnapshotSourceText(ITextImage textImage, Encoding encodingOpt) | |
|
||
private readonly Encoding _encodingOpt; | ||
private readonly TextBufferContainer _containerOpt; | ||
private readonly int _reiteratedVersion; | ||
|
||
private SnapshotSourceText(ITextSnapshot editorSnapshot, Encoding encodingOpt) | ||
private SnapshotSourceText(ITextSnapshot editorSnapshot) : | ||
this(editorSnapshot, TextBufferContainer.From(editorSnapshot.TextBuffer)) | ||
{ | ||
} | ||
|
||
private SnapshotSourceText(ITextSnapshot editorSnapshot, TextBufferContainer container) | ||
{ | ||
Contract.ThrowIfNull(editorSnapshot); | ||
|
||
this.TextImage = TextBufferMapper.RecordTextSnapshotAndGetImage(editorSnapshot); | ||
_containerOpt = TextBufferContainer.From(editorSnapshot.TextBuffer); | ||
_reiteratedVersion = editorSnapshot.Version.ReiteratedVersionNumber; | ||
_encodingOpt = encodingOpt; | ||
this.TextImage = RecordReverseMapAndGetImage(editorSnapshot); | ||
_encodingOpt = editorSnapshot.TextBuffer.GetEncodingOrUTF8(); | ||
_containerOpt = container; | ||
} | ||
|
||
public SnapshotSourceText(ITextImage textImage, Encoding encodingOpt, TextBufferContainer containerOpt) | ||
|
@@ -68,7 +59,12 @@ public SnapshotSourceText(ITextImage textImage, Encoding encodingOpt, TextBuffer | |
/// A weak map of all Editor ITextSnapshots and their associated SourceText | ||
/// </summary> | ||
private static readonly ConditionalWeakTable<ITextSnapshot, SnapshotSourceText> s_textSnapshotMap = new ConditionalWeakTable<ITextSnapshot, SnapshotSourceText>(); | ||
private static readonly ConditionalWeakTable<ITextSnapshot, SnapshotSourceText>.CreateValueCallback s_createTextCallback = CreateText; | ||
|
||
/// <summary> | ||
/// Reverse map of roslyn text to editor snapshot. unlike forward map, this doesn't strongly hold onto editor snapshot so that | ||
/// we don't leak editor snapshot which should go away once editor is closed. roslyn source's lifetime is not usually tied to view. | ||
/// </summary> | ||
private static readonly ConditionalWeakTable<ITextImage, WeakReference<ITextSnapshot>> s_textImageToEditorSnapshotMap = new ConditionalWeakTable<ITextImage, WeakReference<ITextSnapshot>>(); | ||
|
||
public static SourceText From(ITextSnapshot editorSnapshot) | ||
{ | ||
|
@@ -77,44 +73,21 @@ public static SourceText From(ITextSnapshot editorSnapshot) | |
throw new ArgumentNullException(nameof(editorSnapshot)); | ||
} | ||
|
||
return s_textSnapshotMap.GetValue(editorSnapshot, s_createTextCallback); | ||
return s_textSnapshotMap.GetValue(editorSnapshot, s => new SnapshotSourceText(s)); | ||
} | ||
|
||
// Use this as a secondary cache to catch ITextSnapshots that have the same ReiteratedVersionNumber as a previously created SnapshotSourceText | ||
private static readonly ConditionalWeakTable<ITextBuffer, StrongBox<SnapshotSourceText>> s_textBufferLatestSnapshotMap = new ConditionalWeakTable<ITextBuffer, StrongBox<SnapshotSourceText>>(); | ||
|
||
private static SnapshotSourceText CreateText(ITextSnapshot editorSnapshot) | ||
/// <summary> | ||
/// This only exist to break circular dependency on creating buffer. nobody except extension itself should use it | ||
/// </summary> | ||
internal static SourceText From(ITextSnapshot editorSnapshot, TextBufferContainer container) | ||
{ | ||
var strongBox = s_textBufferLatestSnapshotMap.GetOrCreateValue(editorSnapshot.TextBuffer); | ||
var text = strongBox.Value; | ||
if (text != null && text._reiteratedVersion == editorSnapshot.Version.ReiteratedVersionNumber) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is actual bug where 1:1 mapping got broken. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @heejaechang For clarity, what was the problem? If we reiterated a version, we'd use the same SourceText. That meant that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "the older snapshot"
|
||
if (editorSnapshot == null) | ||
{ | ||
if (text.Length == editorSnapshot.Length) | ||
{ | ||
return text; | ||
} | ||
else | ||
{ | ||
// In editors with non-compliant Undo/Redo implementations, you can end up | ||
// with two Versions with the same ReiteratedVersionNumber but with very | ||
// different text. We've never provably seen this problem occur in Visual | ||
// Studio, but we have seen crashes that look like they could have been | ||
// caused by incorrect results being returned from this cache. | ||
try | ||
{ | ||
throw new InvalidOperationException( | ||
$"The matching cached SnapshotSourceText with <Reiterated Version, Length> = <{text._reiteratedVersion}, {text.Length}> " + | ||
$"does not match the given editorSnapshot with <{editorSnapshot.Version.ReiteratedVersionNumber}, {editorSnapshot.Length}>"); | ||
} | ||
catch (Exception e) when (FatalError.ReportWithoutCrash(e)) | ||
{ | ||
} | ||
} | ||
throw new ArgumentNullException(nameof(editorSnapshot)); | ||
} | ||
|
||
text = new SnapshotSourceText(editorSnapshot, editorSnapshot.TextBuffer.GetEncodingOrUTF8()); | ||
strongBox.Value = text; | ||
return text; | ||
Contract.ThrowIfFalse(editorSnapshot.TextBuffer == container.GetTextBuffer()); | ||
return s_textSnapshotMap.GetValue(editorSnapshot, s => new SnapshotSourceText(s, container)); | ||
} | ||
|
||
public override Encoding Encoding | ||
|
@@ -123,7 +96,7 @@ public override Encoding Encoding | |
} | ||
|
||
public ITextSnapshot TryFindEditorSnapshot() | ||
=> TextBufferMapper.TryFindEditorSnapshot(this.TextImage); | ||
=> TryFindEditorSnapshot(this.TextImage); | ||
|
||
protected static ITextBufferCloneService TextBufferFactory | ||
{ | ||
|
@@ -262,6 +235,52 @@ public override SourceText WithChanges(IEnumerable<TextChange> changes) | |
currentSnapshot: ((ITextSnapshot2)buffer.CurrentSnapshot).TextImage); | ||
} | ||
|
||
private static ITextImage RecordReverseMapAndGetImage(ITextSnapshot editorSnapshot) | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these are also just moved from deleted file. |
||
Contract.ThrowIfNull(editorSnapshot); | ||
|
||
var textImage = ((ITextSnapshot2)editorSnapshot).TextImage; | ||
Contract.ThrowIfNull(textImage); | ||
|
||
// If we're already in the map, there's nothing to update. Do a quick check | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there edits that don't change the ITextImage, such as content type changes? Because it seems we'll skip this and once again be holding onto a weak reference of an old version. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dont believe there is such case. if there is new Snapshot there will be new Image There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for confirming. |
||
// to avoid two allocations per call to RecordTextSnapshotAndGetImage. | ||
if (!s_textImageToEditorSnapshotMap.TryGetValue(textImage, out var weakReference)) | ||
{ | ||
// put reverse entry that won't hold onto anything | ||
weakReference = s_textImageToEditorSnapshotMap.GetValue( | ||
textImage, _ => new WeakReference<ITextSnapshot>(editorSnapshot)); | ||
} | ||
|
||
#if DEBUG | ||
// forward and reversed map is 1:1 map. snapshot can't be different | ||
var snapshot = weakReference.GetTarget(); | ||
Contract.ThrowIfFalse(snapshot == editorSnapshot); | ||
#endif | ||
return textImage; | ||
} | ||
|
||
private static ITextSnapshot TryFindEditorSnapshot(ITextImage textImage) | ||
{ | ||
if (!s_textImageToEditorSnapshotMap.TryGetValue(textImage, out var weakReference) || | ||
!weakReference.TryGetTarget(out var editorSnapshot)) | ||
{ | ||
return null; | ||
} | ||
|
||
return editorSnapshot; | ||
} | ||
|
||
/// <summary> | ||
/// Use a separate class for closed files to simplify memory leak investigations | ||
/// </summary> | ||
internal sealed class ClosedSnapshotSourceText : SnapshotSourceText | ||
{ | ||
public ClosedSnapshotSourceText(ITextImage textImage, Encoding encodingOpt) | ||
: base(textImage, encodingOpt, containerOpt: null) | ||
{ | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Perf: Optimize calls to GetChangeRanges after WithChanges by using editor snapshots | ||
/// </summary> | ||
|
@@ -362,8 +381,8 @@ private IReadOnlyList<TextChangeRange> GetChangeRanges(ITextImage oldImage, int | |
|
||
private static bool AreSameReiteratedVersion(ITextImage oldImage, ITextImage newImage) | ||
{ | ||
var oldSnapshot = TextBufferMapper.TryFindEditorSnapshot(oldImage); | ||
var newSnapshot = TextBufferMapper.TryFindEditorSnapshot(newImage); | ||
var oldSnapshot = TryFindEditorSnapshot(oldImage); | ||
var newSnapshot = TryFindEditorSnapshot(newImage); | ||
|
||
return oldSnapshot != null && newSnapshot != null && oldSnapshot.Version.ReiteratedVersionNumber == newSnapshot.Version.ReiteratedVersionNumber; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,13 +23,12 @@ internal class TextBufferContainer : SourceTextContainer | |
private event EventHandler<TextChangeEventArgs> EtextChanged; | ||
private SourceText _currentText; | ||
|
||
private TextBufferContainer(ITextBuffer editorBuffer, Encoding encoding) | ||
private TextBufferContainer(ITextBuffer editorBuffer) | ||
{ | ||
Contract.ThrowIfNull(editorBuffer); | ||
Contract.ThrowIfNull(encoding); | ||
|
||
_weakEditorBuffer = new WeakReference<ITextBuffer>(editorBuffer); | ||
_currentText = new SnapshotSourceText(TextBufferMapper.RecordTextSnapshotAndGetImage(editorBuffer.CurrentSnapshot), encoding, this); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this another 1:1 mapping bug. |
||
_currentText = SnapshotSourceText.From(editorBuffer.CurrentSnapshot, this); | ||
} | ||
|
||
/// <summary> | ||
|
@@ -50,7 +49,7 @@ public static TextBufferContainer From(ITextBuffer buffer) | |
|
||
private static TextBufferContainer CreateContainer(ITextBuffer editorBuffer) | ||
{ | ||
return new TextBufferContainer(editorBuffer, editorBuffer.GetEncodingOrUTF8()); | ||
return new TextBufferContainer(editorBuffer); | ||
} | ||
|
||
public ITextBuffer TryFindEditorTextBuffer() | ||
|
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this just moved down