Skip to content
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

Keep track of the associated text snapshot when diagnotsics are created. #24721

Merged
merged 3 commits into from
Feb 14, 2018

Conversation

CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Feb 9, 2018

This allows us to accurately map the span of a diagnostic returned by the diagnostic service forward to whatever snapshot we currently have.

Customer scenario

User codes C#/VB/Typscript/F# and squiggles shows up in wrong places and take sometime before it gets corrected.

Bugs this fixes

Fixes #24714

Workarounds, if any

N/A

Risk

I don't see any major risk due to this change.

Performance impact

we use a map to associates buffer to right events, but it is small map and entries go away once either errors are fixed or files are closed. so no big issue here.

Is this a regression from a previous update?

Yes. this is due to this change (#23448) which fixed one of biggest memory issue we had.

Root cause analysis

when one of diagnostics got updated, our existing tagger updated all diagnostics currently marked on open files. and that is an issue since some of diagnostics are not synced to latest source yet. only one of them has. so except that one, all other diagnostics can point to wrong span which cause the issue above.
the fix make sure when we use diagnostics from old source, we map that to current text correctly.

How was the bug found?

Dogfooding

@CyrusNajmabadi
Copy link
Member Author

tagging @heejaechang

}

var document = e.Solution.GetDocument(e.DocumentId);
if (document != null && document.IsOpen() && document.TryGetText(out var sourceText))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasonmalinowski TryGetText should pretty much always work for Documents that are open right?

I realize there is a slight race here in that IsOpen might return true, but then the file might lose, and TryGetText could fail. but that's ok in terms of how this is coded up.

What i want to make sure is that in the case where a Document really is open, we should always feel confident hat TryGetText succeeds. Is that something one can depend on?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I'd delete those Try* methods before depending on them. 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: i could also just hold onto the Document. The theory there being that since this is only for open Documents it's not expensive to hold onto. While this could root some solutions, it would only be for a short while as the diagnostic subsystem would then recompute the diagnostics for those open documents quickly, replacing any exiting diagnostics. however, it seems riskier.

@heejaechang what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyrusNajmabadi Any reason not to just call GetText()? If your theory that TryGetText works is correct, it's instant. If not, well, you will still work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can also just do a synchronous call to GetTextAsync().WaitAndGetResult(). But i feel very skeezy doing that. i believe TryGetText should be safe, and i believe that's what rename uses and the same assumption it makes, right @jasonmalinowski ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess given right now there's a PR (#24693) out fixing that function from complexity like this, I'm not sure how much I'd trust the invariants of the system.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have GetTextSynchronously() anyways? We have it for fetching trees, which implies we should be able to fetch text synchronously!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SourceText for open document is referenced strongly in solution itself. so TryGetText should always return BUT!! Document.IsOpen() is mutable state not tied to Solution IsOpen is called off. so in other words, you could ask Solution that is created before Document is opened with Workspace which currently think Document is opened :)

so, TryGetText can return null... BUT! then text is most likely just came from closed file. so your fallback logic most likely give right text... :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved to GetTextSynchronously.

/// we're tagging.
/// </summary>
private static readonly ConditionalWeakTable<object, ITextSnapshot> _diagnosticIdToTextSnapshot =
new ConditionalWeakTable<object, ITextSnapshot>();
Copy link
Member Author

@CyrusNajmabadi CyrusNajmabadi Feb 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@heejaechang I think the lifetimes here are exactly what we want. Namely, as long as the diagnostics subsystem is holding onto these IDs, then we'll hold onto the snapshot here. And the diagnostic subsystem holds onto this ID until it is replaced/removed with the most up to date version of the diagnostics for that analyzer/document-id pair, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically, as long as we could observe the diagnostics associated with an id, then we'll have the snapshot for it. And once we can't observe them anymore (because they were replaced/removed), and thus GetDiagnosticsUpdatedEventArgs won't return them, then this will release the snapshot as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep.

@heejaechang
Copy link
Contributor

integration tests failed with same timeout not sure whether it is related to this PR or not though.

return;
}

if (_diagnosticIdToTextSnapshot.TryGetValue(e.Id, out var snapshot))
Copy link
Contributor

@heejaechang heejaechang Feb 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there could be issue where build only error staying long time (until next build) and we holding snapshot for it quite long. but at the same time, this will make those build error to show up in correct place even after open/close/modification of a file. so trade off I guess.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth clearing the entry on final tagger disposal or something? At that point that buffer ain't useful anymore.

@jinujoseph
Copy link
Contributor

@heejaechang , should we consider to bring this for 15.6 ?

@CyrusNajmabadi
Copy link
Member Author

@heejaechang , should we consider to bring this for 15.6 ?

IMO, this needs to go in asap. This is a pretty severe regression. That's ok for a preview, but unacceptable for the real release.

@CyrusNajmabadi
Copy link
Member Author

@jasonmalinowski @dotnet/roslyn-ide can we get another pair of eyes here. This is a pretty severe regression that needs fixing asap. Thanks!

@CyrusNajmabadi
Copy link
Member Author

Note: i've validated this fix. But it would be good to get secondary smoke testing on it as well.

@jasonmalinowski
Copy link
Member

@CyrusNajmabadi Can you retarget this to the dev15.6.x branch? Not sure what bar checks this would need, but it'll be good to get that out of the way.

@heejaechang
Copy link
Contributor

@jinujoseph can we ask vendor for verification?

@jinujoseph
Copy link
Contributor

Sure , we can get it validated ... @heejaechang can you get a signed build for this.

Add comment.

Add comment.

Switch to getting the text synchronously.

Add comment.
@CyrusNajmabadi CyrusNajmabadi changed the base branch from master to dev15.6.x February 9, 2018 20:08
@CyrusNajmabadi
Copy link
Member Author

Base has been changed.

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's one possible reliability problem I've called out in individual comments we should get answered.

Otherwise, mostly suggestions. Switching holding onto ITextVersion instead of ITextBuffer might be a nice improvement (might add a line or two of code at most) and at least demonstrates how to be good editor citizens.

if (document != null && document.IsOpen())
{
// This should always be fast since the document is open.
var sourceText = document.State.GetTextSynchronously(cancellationToken: default);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I was missing full context on the previous conversation: should this just be GetTextAsync? In theory still instant, but if we're wrong it's less terrible?

(still OK with this more than TryGetText)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, will fix up.

// a small risk that between calling .IsOpen the file may then close, which then would
// cause TryGetText to fail. However, that's ok. In that case, if we do need to tag this
// document, we'll just use the current editor snapshot. If that's the same, then the tags
// will be hte same. If it is different, we'll eventually hear about the new diagnostics
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the"

// and apply them directly to the snapshot we have. Either no new changes will
// have happened, and these spans will be accurate, or a change will happen
// and we'll hear about and it update the spans shortly to the right position.
_diagnosticIdToTextSnapshot.TryGetValue(id, out var diagnosticSnapshot);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for a file to be closed/reopened so quickly that _diagnosticIdToTextSnapshot will still have a valid ID but a snapshot from the previous buffer? If so, all the mapping stuff below will crash.

(Going to flag this "changes required" to get a confirmation on this. If it's fine then obviously no changes required.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's a possible problem. Question about this. If i switch to ITextImage:

  1. how do i validate that it's for the same 'buffer' as the buffer i currently have?
  2. how do i use that to map spans forward?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, i see you answered that below. Thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So not sure which approach you're doing now. Maybe you have to stay using ITextSnapshot but then you can just compare buffers. Maybe it's best to do that and we can research more efficient ways than me sidetracking you down a crazy long path.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So not sure which approach you're doing now. Maybe you have to stay using ITextSnapshot but then you can just compare buffers. Maybe it's best to do that

Yup. THat's the approach i went with and have included in the PR.

/// diagnostics, we don't know how to map the span of the diagnostic to the current snapshot
/// we're tagging.
/// </summary>
private static readonly ConditionalWeakTable<object, ITextSnapshot> _diagnosticIdToTextSnapshot =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A possible perf optimization here is to hold onto ITextVersion instead. Your .ToSnapshotSpan().TranslateTo() then would become a call to Tracking.TrackSpanForwardInTime.ToSnapshotSpan() to the current version. Should be pretty easy to change.

(Versions are cheaper than snapshots in that they don't hold onto the actual text structure, but just the deltas. A snapshot always exposes it's version from the .Version property.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to the current version. Should be pretty easy to change.

sounds good. However, still not sure how i validate that the snapshots are from teh same buffer. i.e. that the open/close/open scenario doesn't cause problems.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I couldn't get this to work. The issue is that diagnostics store their information as line/cols. Which means i need the actual original snapshot in order to map that information back to a reasonable span in roslyn coordinates. I could attempt to store an ITextImage. However, then i also need a way to map that to a SourceText (since that's the currency that DiagnosticData expects). I could do all the work to provide a SourceText around an ITextImage, but this seems like a lot of extra gunk just to get this work.

Thoughts @jasonmalinowski

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's lame. Why don't we just hold onto it in regular span form to start? Was it just to work around issues here that we're now making better?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question. @heejaechang Do you know why diagnostics are line/column instead of just TextSpan?

note: it may be because of external diagnostics where some tool is reporting the problem... but i don't know for certain.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiagnosticData has line/column data if it came from build. but if it came from live, it will have TextSpan.

the way you do GetExistingOrCalculatedTextSpan should return TextSpan if it is already there or create one if it is not.

I dont calculate text span from build error right away because it requires reading in Text and a lot of time, those errors stay in error list (where it doesn't need span) and never shown to users in editor (which require span)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. But since these errors may have come from build, and not from live, we can't assume there will be TextSpans for them, and we have to provide the SourceText so that the diagnostic data can be mapped to a span. Correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep

return;
}

if (_diagnosticIdToTextSnapshot.TryGetValue(e.Id, out var snapshot))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth clearing the entry on final tagger disposal or something? At that point that buffer ain't useful anymore.

@@ -135,7 +191,8 @@ private void ProduceTags(TaggerContext<TTag> context, DocumentSnapshotSpan spanT
// editorSnapshot.

var diagnosticSpan = diagnosticData.GetExistingOrCalculatedTextSpan(sourceText)
.ToSnapshotSpan(editorSnapshot);
.ToSnapshotSpan(diagnosticSnapshot)
.TranslateTo(editorSnapshot, SpanTrackingMode.EdgeExclusive);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to bump _diagnosticIdToTextSnapshot to hold onto the newer editor snapshot? Consider a case where lots of edits are happening and the diagnostic engine is backed up. Eventually that DiagnosticsUpdated will get triggered and update it no matter what, but is it worth doing it here too?

("no" is a perfectly valid answer, just asking.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth clearing the entry on final tagger disposal or something? At that point that buffer ain't useful anymore.

The provider stays around forever, and is shared across all taggers. We don't currently keep track in any way if there are absolutely no taggers around anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to bump _diagnosticIdToTextSnapshot to hold onto the newer editor snapshot?

Definitely not. Because then the spans the DiagnosticService has for that ID would not be correct wrt to that snapshot.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

@CyrusNajmabadi
Copy link
Member Author

@jasonmalinowski Are you ok with this in its current form?

@CyrusNajmabadi
Copy link
Member Author

Since htis is 15.6 now, can someone write up whatever ask-mode stuff might be necessary? Thanks!

@vsccarl
Copy link

vsccarl commented Feb 13, 2018

I've completed a test pass for this and can sign off.

@CyrusNajmabadi
Copy link
Member Author

@jinujoseph is this ready to go for shiproom approval?

@heejaechang
Copy link
Contributor

@jinujoseph can you take a look? I updated ask mode thing.

@jinujoseph
Copy link
Contributor

yes , waiting for approval from shiproom

@jinujoseph
Copy link
Contributor

approved to merge via Link

@jinujoseph jinujoseph merged commit f28a27a into dotnet:dev15.6.x Feb 14, 2018
@CyrusNajmabadi
Copy link
Member Author

Thanks all!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants