-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Add a general-purpose property bag to Compilation. #5708
Conversation
@@ -33,6 +33,11 @@ namespace Microsoft.CodeAnalysis | |||
public abstract partial class Compilation | |||
{ | |||
/// <summary> | |||
/// For use only in <see cref="Microsoft.CodeAnalysis.Collections.CompilationStuff"/>. | |||
/// </summary> | |||
internal ConcurrentDictionary<object, object> _compilationStuff = new ConcurrentDictionary<object, object>(); |
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 could be lazily initialized with LazyInitializer.EnsureInitialized so you don't pay an allocation penalty unless it's used.
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.
Compilations are not created often. I'd prefer to wait until it is a demonstrated issue.
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.
Actually I think @pharring has a point here. I believe, IDE often mutates compilation by using With... methods, adds syntax trees, changes options, adds references, etc. That is why we are trying to reuse as much computed data as possible when we implement these methods. Allocating a new dictionary per each call like this is probably an overkill.
How about "Annotation" or "CompilationAnnotation"? |
Perhaps |
What's the rationale for putting |
I want Compilation's API to be immutable. These extension methods act as if they are implemented using an external conditional weak table, but the implementation is in fact more efficient. |
Should new compilations derived from an existing compilation by means of With... methods inherit the properties? |
} | ||
} | ||
|
||
public static bool TrySetStuff<T>(this Compilation compilation, Key<T> key, T value) |
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 name of the method implies that one can use it to change the value associated with the key, but this is not what implementation does. Consider renaming to TryAddStuff or adjusting implementation accordingly.
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.
👍 TryAdd....
@AlekseyTs No, these properties should absolutely NOT be inherited. As @gafter said elsewhere, the field on Compilation is implementation detail. The properties should have the same behavior as-if they were stored in an external ConditionalWeakTable. |
@pharring Where the field is stored has nothing to do with my question. It would be valid even if the values were stored in an external ConditionalWeakTable. The question is whether we want to inherit the properties not the dictionary. |
@AlekseyTs No, we do not want to inherit the properties. |
@mattwar What would you think of |
@gafter I don't see the value of pretending that the Compilation is immutable when it's in fact not. |
What isn't immutable about it? |
You put stuff in it? |
Ah. I thought you were saying that the Compilation before this change wasn't immutable. |
@jaredpar I meant with the change. I don't like adding extension methods for types that we control. |
What about defining |
@tmat The intent is that Compilation presents an immutable API, not that its implementation doesn't change any bits. These extension methods act as if they are implemented using an external conditional weak table, so that Compilation can continue to present an immutable API. Would you feel differently about it if it were implemented using a CWT? |
@gafter The purpose of extension methods is to "add" methods to a type that is in a different library. That's not the case here. |
@gafter Yes, it addresses my concern of using extension methods. Still bothers me that we are adding mutable state. Feels like the extra data should go to some analyzer context and other stateful objects that features work with. @pharring it'd be useful to list all the use cases for this API and see in which cases there already is or can be added a storage place that could hold on the bag. |
OK, all, I've revised the API. Please have another look. |
@@ -0,0 +1,66 @@ | |||
using System; |
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.
Copyright?
If we want to keep the accessing methods taking the Now that I've seen it, I'm not opposed to the full-static version. It loses the abstract concept of having a nameable "black box" that you can point to and say "my stuff is stored in and retrieved from there" but it saves allocations and prevents the situation where someone stashes a context somewhere and the compilation gets garbage collected and now we have to produce some kind of error about exceeding lifetimes. |
Also, I'm still not wed to
|
@@ -33,6 +33,11 @@ namespace Microsoft.CodeAnalysis | |||
public abstract partial class Compilation | |||
{ | |||
/// <summary> | |||
/// For use only in <see cref="Microsoft.CodeAnalysis.Collections.CompilationContext"/>. | |||
/// </summary> | |||
internal ConcurrentDictionary<object, object> _compilationContext = new ConcurrentDictionary<object, object>(); |
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.
My original comment is gone now, but I'll re-iterate: Make this a lazily allocated dictionary. An empty ConcurrentDictionary carries quite a bit of overhead.
The canonical pattern is:
private ConcurrentDictionary<object, object> _compilationContext;
internal ConcurrentDictionary<object, object> CompilationContext => LazyInitializer.EnsureInitialized(ref _compilationContext);
Another way to mitigate the allocation hit for an empty ConcurrentDictionary is to specify a smaller concurrency level (the default is 4 times the processor count) and initial capacity (default is 31).
👍 on making it a static class. |
I'm not convinced that all the uses of CWT Paul pointed out are actually justified. Maybe they are, I don't claim to fully understand the code. But I'm wondering... For example, var compilationActionsMap = _compilationScopeMap.GetOrAdd(analyzerAndOptions, new ConditionalWeakTable<Compilation, Task<HostCompilationStartAnalysisScope>>());
return compilationActionsMap.GetValue(analyzerExecutor.Compilation, callback); Couldn't the task be stored in Has 5 CWTs, each adding some data to a compilation. Couldn't we have just one that holds on all state this component needs to add? Also couldn't the SymbolFinder maintain a context with the same lifetime as the compilation or a project? The code seems to get a Compilation from a Project and then look up in a cache implemented as CWT. Perhaps the cache can be stored in ProjectState? |
How internal code keeps associated data with compilation is not interesting. We can cook up anything we want. Exposing it as a public API makes me cringe. Solve this problem in a different way. If analyzer developers need a way to keep state alive, add that to the analyzer context, but don't pollute compilation with this. |
@JohnHamby for the analyzer side of things. An analyzer developer wants to be able to set some state in CompilationStart, modify in a Syntax or Symbol analyzer callbacks, and then report on the state in CompilationEnd. Since analyses can run concurrently over multiple compilations, people have been using the Compilation as the key into their analyzer state dictionary (in a CWT so that they don't keep Compilations alive). The problem with adding to analyzer context is you get one kind of context object for a CompilationStart/End analyzer and another for a Syntax or Symbol analyzer. We could add a property bag field to |
The intended model for managing such state in analyzers is to create the state in a compilation start action and then access it directly in actions registered by the compilation start action. One way to do this is to put the state into a variable local to the compilation start action and have the other actions be lambdas that refer to the local. But we all know that, so presumably there's a deeper issue for analyzer state that I'm not understanding. Sorry to be dense, but could someone explain it further? |
There was an example on the internal discussion alias (rosdisc) recently (9/29/2015 "Scope within an run of analyzers"). In that discussion, the OP had several analyzers all using the same additional file. It was expensive to parse that file, so we were looking for a way to parse just once (per compilation) and share the result among all the analyzers. A couple of solutions were presented, one of which was to use a |
Ah, I see--the issue is sharing state across analyzers. That is explicitly not supported, specifically because it breaks the concurrency model and generally because it breaks the model of being able to think about analyzers as independent entities. The recommended approach to this sort of sharing is to have one analyzer with lots of actions. |
@pharring the context objects are designed to allow us to add properties without breaking user code. |
I sense the prevailing wind is "Won't Fix". Is that right? |
That would be my choice. Which is equivalent to "Solve the problem in a different way." |
Fixes #5542. This is a draft for API feedback. I need help with naming.
@dotnet/roslyn-compiler @pharring @mattwar