-
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
Conversation
@jinujoseph @dotnet/roslyn-ide can you take a look? |
…napshot are broken. First case was where the code reused same Roslyn text snapshot to 2 different editor snapshots. that can cause getting editor snapshot from roslyn snapshot to fail. Second case was where same editor snapshot can give 2 different roslyn text snapshot breaking invariant. didn't do much cleanup except fixing those above two.
bb657fa
to
8700a69
Compare
ping? |
retest windows_coreclr_test_prtest please |
if this solves the issue where FindCorrespondingEditorTextSnapshot returns null even if the snapshot is currently strongly held in another place, we should remove all the workaround we have put so far. |
can I get a code review? |
ping? |
@jinujoseph @jasonmalinowski can I get some code review? @mavasani @sharwell |
/// 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_roslynToEditorSnapshotMap = new ConditionalWeakTable<ITextImage, WeakReference<ITextSnapshot>>(); |
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 from deleted file
{ | ||
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 comment
The 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 comment
The 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 s_roslynToEditorSnapshotMap
was pointing to the older snapshot rather than the newer one, which could have gotten collected?
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.
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.
"the older snapshot"
- it could be older or newer, which ever called AsSourceText first.
cc @chborl |
@@ -262,6 +239,53 @@ 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 comment
The reason will be displayed to describe this comment to others. Learn more.
these are also just moved from deleted file.
@@ -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 |
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
|
||
_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 comment
The reason will be displayed to describe this comment to others. Learn more.
this another 1:1 mapping bug.
thank you! @jinujoseph @Pilchie can I get approval for 15.7? |
we need one more review @jasonmalinowski can you take a look |
How do you propose we tackle this? Can we identify a set of non-fatal Watsons or some other telemetry event to watch? |
I was thinking doing this (#24875) which will let me to audit all TryFindCorrespondingSnapshot/Buffer usages. while doing it I can remove workaround if one is added there. |
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.
Many questions, but critically I believe there's an assert with an incorrect condition that would have caught this bug long ago.
I also admit, I do wish our SourceTexts for open files just held onto ITextSnapshots. I understand the concern about "if we leak a snapshot with open files, we're holding open ITextBuffers" but I reject that concern. "If we leak something bad, we might leak something bad" isn't something I think warrants additional complexity here.
|
||
private SnapshotSourceText(ITextSnapshot editorSnapshot, Encoding encodingOpt) | ||
private SnapshotSourceText(ITextSnapshot editorSnapshot) |
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.
Can this just do : this(editorSnapshot, TextBufferContainer.From(editorSnapshot.TextBuffer)
to avoid the duplicate constructor logic?
(or for that matter, why do we have both?...)
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.
yep. looks like we can merge these two
{ | ||
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 comment
The 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 s_roslynToEditorSnapshotMap
was pointing to the older snapshot rather than the newer one, which could have gotten collected?
|
||
// If we're already in the map, there's nothing to update. Do a quick check | ||
// to avoid two allocations per call to RecordTextSnapshotAndGetImage. | ||
if (!s_roslynToEditorSnapshotMap.TryGetValue(textImage, out var weakReference)) |
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.
I recognize this got moved but it's still confusing: should this be called s_textImageToEditorSnapshotMap or something more precise?
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.
sure.
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I dont believe there is such case.
http://index/?leftProject=Microsoft.VisualStudio.Platform.VSEditor&leftSymbol=tkm9ha9jkucm&file=BaseSnapshot.cs&line=32
if there is new Snapshot there will be new Image
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.
Thanks for confirming.
// forward and reversed map is 1:1 map. as long as snapshot is not null, it can't be | ||
// different | ||
var snapshot = weakReference.GetTarget(); | ||
Contract.ThrowIfFalse(snapshot == null || snapshot == editorSnapshot); |
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.
The snapshot == null
part seems bad here, as I suspect had this been enabled we would have caught this bug long ago: if that is ever true, it means s_roslynToEditorSnapshotMap should have been updated and we've retriggered this bug. Should we remove this condition?
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.
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.
Yes, but this function isn't called there. :-) The weak reference can obviously be lost (otherwise what's the point), but it should never disappear in this function.
Imagine we took the case where the if branch was taken, and we did create a weak reference for the text image that was passed. In debug, we have GC roots for editorSnapshot (because of this line) and textImage (because it's going to be returned), therefore, the weak reference can't possibly have been collected yet. Thus, the latter assert will be true, and snapshot can never be null.
Now consider the case where the branch wasn't taken; that means that the textImage -> textSnapshot weak entry has already been created. By your statement to my earlier question, there is a 1:1 mapping here, so if the textImage is alive, the it must be the same snapshot as editorSnapshot, which is also alive (because it was handed to this function). Therefore, it can't be null. If it was null, that means there are two different snapshots with the same TextImage, which invalidates your assertion when you linked to BaseSnapshot.cs earlier, and if so, this entire approach is broken.
Follow me?
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.
ah. yes. it can't be null :)
"I also admit, I do wish our SourceTexts for open files just held onto ITextSnapshots. I understand the concern about "if we leak a snapshot with open files, we're holding open ITextBuffers" but I reject that concern. "If we leak something bad, we might leak something bad" isn't something I think warrants additional complexity here." well, editor and roslyn holding each other strongly will certainly make things more convenient. but once we allowed anyone to be able to access VSWorkspace.CurrentSolution, I am not sure whether that is okay. I added this once I got bunch of issues from customer dump showing us holding to a lot of things that are inside of ITextBuffer property bags. snapshot itself or buffer itself wasn't the biggest issue. issue was property bags. and people put all kinds of things there including refernece back to WpfViews. anyway, without the bug, only thing roslyn side make sure is that holding onto ITextSnapshot somewhere while working on Roslyn snapshot created off that ITextSnapshot. it doesn't need to pass along. it could be just a local variable. |
I guess I would have pushed those bugs all upstream. If a SourceText is causing a leak of an ITextBuffer, that means somebody is leaking the SourceText, probably because they're holding onto a Solution. I would have just pushed those bugs to whoever leaked that since leaking Solutions is still expensive. I don't see the reason to be resilient to misuse bugs when the misuse is still plenty bad, and it means we create really complicated lifetime bugs like this. 😄 The cure seems worse than the disease. |
well. once we let public to hold onto Solution, I think it is up to them to decide when is appropriate for them to release solution. and I think that is different than what they expect from holding onto Solution (roslyn objects). In reality, they are not only holding onto roslyn objects, but unbound number of objects (since it has property bag, it is not just editor objects either. it is truely everything there.) |
retest windows_debug_vs-integration_prtest please |
@jasonmalinowski ping. addressed all your feedback. |
} | ||
|
||
#if DEBUG | ||
// forward and reversed map is 1:1 map. as long as snapshot is not null, it can't be |
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.
"as long as snapshot is not null" comment is now stale.
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.
will update
@jinujoseph @Pilchie can I get approval for 15.7? |
Approved - thanks. |
…t change. this is needed to bring workspace up to date with latest text snapshot after we don't merge 2 text snapshot to 1 source text if content is same. see this for more detail - dotnet#24849
* made sure we raise workspace changed events even when there is no text change. this is needed to bring workspace up to date with latest text snapshot after we don't merge 2 text snapshot to 1 source text if content is same. see this for more detail - #24849 * added comments on why we need to process all text change events
* made sure we raise workspace changed events even when there is no text change. this is needed to bring workspace up to date with latest text snapshot after we don't merge 2 text snapshot to 1 source text if content is same. see this for more detail - dotnet#24849 * added comments on why we need to process all text change events
…apshot without changes (#25576) * Fixed DDRIT failure (#25551) * made sure we raise workspace changed events even when there is no text change. this is needed to bring workspace up to date with latest text snapshot after we don't merge 2 text snapshot to 1 source text if content is same. see this for more detail - #24849 * added comments on why we need to process all text change events * removed changes empty check. now it is possible to have empty changes. * remove non-empty from comment since set now can be empty.
…napshot are broken.
First case was where the code reused same Roslyn text snapshot to 2 different editor snapshots.
that can cause getting editor snapshot from roslyn snapshot to fail.
Second case was where same editor snapshot can give 2 different roslyn text snapshot breaking
invariant.
didn't do much cleanup except fixing those above two.
Customer scenario
Customer doing rename and in rare case, some identifier doesn't get renamed correctly.
Bugs this fixes
#7364
Workarounds, if any
No
Risk
I don't see any risk
Performance impact
I believe this code that broke 1:1 mapping was added long time ago hoping improve typing perf number by reusing same buffer when possible. but we are seeing issue due to it. and not sure whether this optimization still matter now since the optimization was to reduce allocations and current allocation behavior is completely different than when this optimization was added. also we now reuse ITextImage rather than cloning. can't say for sure until we see RPS result, but I bet is we won't see much difference since the optimization was targeting very specific case and we noticed that case only because the perf test we used (not RPS test) at the moment happen to have a case where this gets hit.
Is this a regression from a previous update?
No
Root cause analysis
internally we have a bi-directional map between roslyn text buffer and editor text buffer. this map must be 1:1 map each other. otherwise, we can't reliably move between roslyn's world and editor's world. we use this map to move from editor's snapshot to roslyn's snapshot, and move back from roslyn to editor.
if this 1:1 map got broken, we might move to wrong snapshot from one to the other or failed to move at all which cause issue like (#7364)
How was the bug found?
Watson, feedback