Skip to content

first stage of async support - async#1

Open
alexsku wants to merge 12 commits intomasterfrom
async-support
Open

first stage of async support - async#1
alexsku wants to merge 12 commits intomasterfrom
async-support

Conversation

@alexsku
Copy link
Owner

@alexsku alexsku commented Dec 19, 2016

multithreaded support for timings

multithreaded support for timings
-->
<system.web>
<compilation debug="true" targetFramework="4.5">
<compilation debug="true" targetFramework="4.6.1">
Copy link
Owner Author

Choose a reason for hiding this comment

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

i needed this to have AsyncLocal

}

[Test]
public async void Current_WhenAsyncMethodReturns_IsCarried(
Copy link
Owner Author

@alexsku alexsku Dec 19, 2016

Choose a reason for hiding this comment

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

typo - configureAwait

Copy link

Choose a reason for hiding this comment

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

you can use [Theory] instead of [Test] and [Values]

{
// This assumes that trivial items do not have any non-trivial children
timing.Children.RemoveAll(child => child.IsTrivial);
timing.AccessChildren()?.RemoveAll(child => child.IsTrivial);
Copy link
Owner Author

Choose a reason for hiding this comment

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

there are a couple of places where they modify the children timing directly.

context.Items[WcfCacheKey] = profiler;
}

public override Timing GetHead()
Copy link
Owner Author

Choose a reason for hiding this comment

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

this is more to support the new methods, the implementation is not very advanced here since i don't really care about wcf

/// <param name="action">The <see cref="T:System.Action`1"/> delegate to perform on each element of the
/// <see cref="T:System.Collections.Generic.List`1"/>.</param><exception cref="T:System.ArgumentNullException">
/// <paramref name="action"/> is null.</exception><exception cref="T:System.InvalidOperationException">An element in the collection has been modified. CautionThis exception is thrown starting with the .NET Framework 4.5. </exception>
public static void ForEach<T>(this IList<T> list, Action<T> action)
Copy link
Owner Author

Choose a reason for hiding this comment

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

here i could lock on LockObj as well i guess

/// <param name="list">the list to perform the operations for</param>
/// <param name="action">The <see cref="T:System.Action`1"/> delegate to perform on each element of the
/// <see cref="T:System.Collections.Generic.List`1"/>.</param><exception cref="T:System.ArgumentNullException">
/// <paramref name="action"/> is null.</exception><exception cref="T:System.InvalidOperationException">An element in the collection has been modified. CautionThis exception is thrown starting with the .NET Framework 4.5. </exception>
Copy link
Owner Author

Choose a reason for hiding this comment

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

i'm not very happy that this method is public, i would rather prefer if it was internal. and an alternative to this method is to update the couple of places which try to modify the list so they don't need ForEach and RemoveAll at all

Copy link

Choose a reason for hiding this comment

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

are these only here because you changed an List to an IList?
If so, then I just think we replace them with traditional for loops

/// </summary>
[DataMember(Order = 5)]
public List<Timing> Children { get; set; }
public IList<Timing> Children
Copy link
Owner Author

Choose a reason for hiding this comment

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

Children now returns a copy of children, for 99% of the cases i think this is the best option

/// <summary>
/// Gets or sets All sub-steps that occur within this Timing step. Add new children through <see cref="AddChild"/>
/// </summary>
public IList<Timing> AccessChildren()
Copy link
Owner Author

Choose a reason for hiding this comment

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

this method is for the remaining 1% of the cases where the caller wants to modify Timings list. I'm thinking to replace it with explicit methods RemoveTimings or something like this.

Copy link

Choose a reason for hiding this comment

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

one option is to replace Children with an IImmutableList<T>.
This method only appears to be used to remove all. Perhaps we can just rename this method RemoveChildren()?

Copy link
Owner Author

Choose a reason for hiding this comment

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

I will need to add dependency on collections in order to start using iimmutablelist. I guess using that interface would make sense to emphasize that the list is not modifiable, not modifying those lists is rather pointless since i return copies.

@alexsku
Copy link
Owner Author

alexsku commented Dec 19, 2016

Ideally i think miniprofiler should be split into 2 classes: one class will be responsible for capturing timings. It would provide very limited ways for manipulating captured timings. Then, there would be a way to "complete" the capturing which would return a miniprofiler snapshot that can be modified easily but there would be no multithreaded access to it

}

[Test]
public async void Current_WhenAsyncMethodReturns_IsCarried(
Copy link

Choose a reason for hiding this comment

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

you can use [Theory] instead of [Test] and [Values]

}

private AsyncLocal<MiniProfiler> _currentProfiler = new AsyncLocal<MiniProfiler>();
private AsyncLocal<Timing> _currentTiming = new AsyncLocal<Timing>();
Copy link

Choose a reason for hiding this comment

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

readonly

}
}

protected override Timing CurrentHead {
Copy link

Choose a reason for hiding this comment

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

formatting

/// </summary>
public abstract MiniProfiler GetCurrentProfiler();

public abstract Timing GetHead();
Copy link

Choose a reason for hiding this comment

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

documentation

Copy link

Choose a reason for hiding this comment

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

I'm not sure I understand why these methods are necessary.

Since the MiniProfiler.Current is stored in the exact same manner as Head. Seems like you could continue to use MiniProfiler.Current.Head?

Copy link
Owner Author

Choose a reason for hiding this comment

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

the documentation is provided in the interface

/// <param name="list">the list to perform the operations for</param>
/// <param name="action">The <see cref="T:System.Action`1"/> delegate to perform on each element of the
/// <see cref="T:System.Collections.Generic.List`1"/>.</param><exception cref="T:System.ArgumentNullException">
/// <paramref name="action"/> is null.</exception><exception cref="T:System.InvalidOperationException">An element in the collection has been modified. CautionThis exception is thrown starting with the .NET Framework 4.5. </exception>
Copy link

Choose a reason for hiding this comment

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

are these only here because you changed an List to an IList?
If so, then I just think we replace them with traditional for loops

/// <summary>
/// Gets or sets All sub-steps that occur within this Timing step. Add new children through <see cref="AddChild"/>
/// </summary>
public IList<Timing> AccessChildren()
Copy link

Choose a reason for hiding this comment

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

one option is to replace Children with an IImmutableList<T>.
This method only appears to be used to remove all. Perhaps we can just rename this method RemoveChildren()?

@@ -299,6 +325,7 @@ internal void RemoveCustomTiming(string category, CustomTiming customTiming)
}

private readonly object _lockObject = new object();
Copy link

Choose a reason for hiding this comment

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

Not your code, but I don't understand the locking around CustomTimingList.

The existing GetCustomTimingList acquires the same lock twice, and protects access to the dictionary. But the underlying values in the dictionary (lists of custom timings) are not protected by the lock. I don't see why they would need to protect the dictionary but the lists.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I think the existing method acquires _lockObject first and then the specific list if it is not null, i dont think this makes sense, since there are many cases where racing might occur.

}
}

private readonly AsyncLocal<MiniProfiler> _currentProfiler = new AsyncLocal<MiniProfiler>();
Copy link
Owner Author

Choose a reason for hiding this comment

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

btw, if we wanted to stay compatible with .net 4 we could use our RevAsyncLocal here instead. functionally it is pretty much the same and available as long as CallContext.SetLogicalData is available.

If we are to suggest a pr for miniprofiler v4 we definitely will need to think about a substitution for AsyncLocal since they try to be backward compatible there

/// </summary>
[DataMember(Order = 2)]
public List<ClientTiming> Timings { get; set; }
public List<ClientTiming> Timings
Copy link
Owner Author

Choose a reason for hiding this comment

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

i'm not sure mt access is possible for custom timings, but i guess it makes sense to do just in case

Copy link

Choose a reason for hiding this comment

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

seems like we should the same here as what we do for Timings.Children - regarding IReadOnlyCollection or ImmutableArray

/// </summary>
[DataMember(Order = 5)]
public List<Timing> Children { get; set; }
public List<Timing> Children
Copy link
Owner Author

@alexsku alexsku Dec 20, 2016

Choose a reason for hiding this comment

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

i think using IImmutableList for _children is an interesting idea. That would have indicated to the caller that the collection is immutable and make accessing _children naturally thread safe.

I think adding children might be a bit more expensive since the tree is going to be built underneath but perhaps the downside is not that high, do you think we should make the change? I guess we (nor miniprofiler v4) are not going to support .net below 4.5 (where Immutable would not be available)

It looks like in a single threaded scenario immutablelist is significantly underperforming comparing to list: https://ayende.com/blog/164739/immutable-collections-performance

Copy link

Choose a reason for hiding this comment

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

hmm yeah, the ImmutableList perf isn't great because inside it's actually a tree.
But they have an ImmutableArray which is faster to read, but more expensive to write http://stackoverflow.com/a/28621409/830724

Anyway, I think perhaps the best thing to do is to have this return an IReadOnlyList or IReadOnlyCollection that way it is obvious that the caller shouldn't mutate it. If we really cared about performance we could return an ImmutableArray or IReadOnlyList here and cache the result, so we don't have to create the copy on every access.

get
{
lock(_lockObject)
return new List<ClientTiming>(_timings);
Copy link
Owner Author

Choose a reason for hiding this comment

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

a case when _timings is null

Copy link

Choose a reason for hiding this comment

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

if we're going to go w/ ImmutableList for the Miniprofiler.Children, seems like we should do that here too.

set
{
lock(_lockObject)
_timings = new List<ClientTiming>(value);
Copy link
Owner Author

Choose a reason for hiding this comment

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

a case when value is null

Copy link

Choose a reason for hiding this comment

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

what does this comment mean? you need to handle null?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes

{
lock(_lockObject)
{
return _customTimings.ToDictionary(p => p.Key, p => new List<CustomTiming>(p.Value));
Copy link
Owner Author

Choose a reason for hiding this comment

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

a case when _customTimings is null

{
lock(_lockObject)
{
_customTimings = value.ToDictionary(p => p.Key, p => new List<CustomTiming>(p.Value));
Copy link
Owner Author

Choose a reason for hiding this comment

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

a case when value is null

get
{
lock(_lockObject)
return new List<ClientTiming>(_timings);
Copy link

Choose a reason for hiding this comment

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

if we're going to go w/ ImmutableList for the Miniprofiler.Children, seems like we should do that here too.

set
{
lock(_lockObject)
_timings = new List<ClientTiming>(value);
Copy link

Choose a reason for hiding this comment

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

what does this comment mean? you need to handle null?

/// </summary>
[DataMember(Order = 5)]
public List<Timing> Children { get; set; }
public List<Timing> Children
Copy link

Choose a reason for hiding this comment

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

hmm yeah, the ImmutableList perf isn't great because inside it's actually a tree.
But they have an ImmutableArray which is faster to read, but more expensive to write http://stackoverflow.com/a/28621409/830724

Anyway, I think perhaps the best thing to do is to have this return an IReadOnlyList or IReadOnlyCollection that way it is obvious that the caller shouldn't mutate it. If we really cared about performance we could return an ImmutableArray or IReadOnlyList here and cache the result, so we don't have to create the copy on every access.

{
lock (_lockObject)
{

Copy link

Choose a reason for hiding this comment

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

empty line

/// </summary>
[DataMember(Order = 2)]
public List<ClientTiming> Timings { get; set; }
public List<ClientTiming> Timings
Copy link

Choose a reason for hiding this comment

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

seems like we should the same here as what we do for Timings.Children - regarding IReadOnlyCollection or ImmutableArray

/// </summary>
[DataMember(Order = 5)]
public List<Timing> Children { get; set; }
public IReadOnlyList<Timing> Children
Copy link
Owner Author

Choose a reason for hiding this comment

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

so v4 breaks if I use IReadOnlyList there. It breaks because protobuf that is used there doesn't support read only collections. There is an option to use IImmutableList instead (which is supported by protobuf) but then there would be a problem with Dictionary, there is no lightweight implementation of IImmutableDictionary available.

I think the best way is to revert to using List and Dictionary, what do you think?

<id>MiniProfilerAsync.EF6</id>
<version>3.0.12</version>
<authors>rev.com</authors>
<owners>rev.com</owners>
Copy link
Owner Author

Choose a reason for hiding this comment

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

not sure what else should go here

<authors>Jarrod Dixon, Yaakov Ellis</authors>
<owners>Jarrod Dixon, Yaakov Ellis</owners>
<id>MiniProfilerAsync.EF6</id>
<version>3.0.12</version>
Copy link
Owner Author

Choose a reason for hiding this comment

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

the version needs to be bumped up i believe

<version>3.0.11</version>
<authors>Jarrod Dixon, Yaakov Ellis</authors>
<owners>Jarrod Dixon, Yaakov Ellis</owners>
<id>MiniProfilerAsync.EF6</id>
Copy link
Owner Author

Choose a reason for hiding this comment

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

i think it's better if id is different

[assembly: AssemblyVersion("3.0.11")]
[assembly: AssemblyInformationalVersion("3.0.11")] No newline at end of file
[assembly: AssemblyVersion("3.0.12")]
[assembly: AssemblyInformationalVersion("3.0.12")] No newline at end of file
Copy link
Owner Author

Choose a reason for hiding this comment

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

just in case i bumped up versions of dlls

return new JavaScriptSerializer { MaxJsonLength = Settings.MaxJsonResponseSize };
var res = new JavaScriptSerializer { MaxJsonLength = Settings.MaxJsonResponseSize };

res.RegisterConverters(new[] { new ReadOnlyDictionaryConverter<IReadOnlyList<CustomTiming>>() });
Copy link
Owner Author

Choose a reason for hiding this comment

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

So this is a total disaster, it looks like JavaScriptSerializer doesn't support natively IReadOnlyDictionary, so i had to implement and use a converter for it, luckily i don't think we much depend on JavaScriptSerializer except for unit tests. @eugenep-rev, am I correct that we don't rely on JavaScriptSerializer?

lock(_lockObject)
{
return _customTimings?.ToDictionary(p => p.Key, p => new List<CustomTiming>(p.Value));
return _customTimings?.ToDictionary(p => p.Key, p => (IReadOnlyList<CustomTiming>)new List<CustomTiming>(p.Value));
Copy link

Choose a reason for hiding this comment

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

I think it would be better to get rid of all these copies on the get (here and Children, and ClientTimings)
I'd rather have explicity Set() methods and have the properties with no setters.

Copy link

@pjhuck pjhuck left a comment

Choose a reason for hiding this comment

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

only questions are about assembly and nuget versions.

<owners>Jarrod Dixon, Yaakov Ellis</owners>
<id>RevMiniProfiler.EF6</id>
<version>3.1.0.2</version>
<authors>rev.com</authors>
Copy link

Choose a reason for hiding this comment

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

i would keep the original authors and just append rev

<authors>Jarrod Dixon, Yaakov Ellis</authors>
<owners>Jarrod Dixon, Yaakov Ellis</owners>
<id>RevMiniProfiler.EF6</id>
<version>3.1.0.2</version>
Copy link

Choose a reason for hiding this comment

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

how did you come up with this version number? why not 3.1.0?

Copy link
Owner Author

Choose a reason for hiding this comment

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

it was 3.1.0 but then i bumped it up while making changes.

[assembly: AssemblyVersion("3.0.11")]
[assembly: AssemblyInformationalVersion("3.0.11")] No newline at end of file
[assembly: AssemblyVersion("3.1.0.0")]
[assembly: AssemblyInformationalVersion("3.1.0.0")] No newline at end of file
Copy link

Choose a reason for hiding this comment

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

seems like this should match the version in the nuspec

Copy link
Owner Author

Choose a reason for hiding this comment

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

I don't think this is necessary nor it is common practice as it seems (e.g. MiniProfiler's nuspec doesn't match with the assembly version). In general i found bumping up assembly version too cumbersome since i needed to go and update the versions in the config files

[assembly: Guid("44f8ac97-8e2c-4dba-adae-d971fb086a19")]
[assembly: AssemblyVersion("3.0.11")]
[assembly: AssemblyInformationalVersion("3.0.11")]
[assembly: AssemblyVersion("3.1.0.0")]
Copy link

Choose a reason for hiding this comment

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

same comment re version matching

[assembly: AssemblyVersion("3.1")]
[assembly: AssemblyInformationalVersion("3.1")]
[assembly: AssemblyVersion("3.3.0.2")]
[assembly: AssemblyInformationalVersion("3.3.0.2")]
Copy link

Choose a reason for hiding this comment

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

version should match nuspec

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants