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

Invocations collection improvements #628

Merged
merged 10 commits into from
Jun 10, 2018

Conversation

Code-Grump
Copy link
Contributor

Add tests for Mock.Invocations property.
Improved InvocationCollection to no longer allocate a copy of the collection on enumeration.

@Code-Grump Code-Grump mentioned this pull request Jun 10, 2018
Copy link
Contributor

@stakx stakx left a comment

Choose a reason for hiding this comment

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

Hi there, thanks for the new PR. Just a handful of questions and smallish requests.

mock.Object.CompareTo(0);
mock.Object.CompareTo(1);

var count = mock.Invocations.Count();
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand what you're doing here, but I wonder if this could be implemented more clearly. What if LINQ's .Count() operator has special handling for IReadOnlyList<> (as requested by e.g. https://github.com/dotnet/corefx/issues/24773)? Then this possibly wouldn't enumerate at all! If the test is about enumeration, it would be good to have the enumeration more visible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point. I'll break it out into a foreach to be certain.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually to be 100% certain, I'll call the enumerator manually. I know foreach has some special behaviours for lists.

Assert.Equal(mock.Invocations.Count, count);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you perhaps add an additional test that verifies that mock.ResetCalls() empties mock.Invocations?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds like a good idea to me.

@@ -47,20 +47,20 @@ namespace Moq
{
internal sealed class InvocationCollection : IReadOnlyList<IReadOnlyInvocation>
{
private List<Invocation> invocations;
private Invocation[] invocations;
Copy link
Contributor

Choose a reason for hiding this comment

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

I suppose I might be missing something obvious here, but I'd really like to avoid us reinventing the wheel and programming another version of List<>. Why is this necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we continue to use List<T> you also have to use List<T>.Enumerator which isn't going to behave the way we want in concurrent-execution scenarios. Arrays, being primitives, have a much simpler access model, so we can know and take advantage of what operations are safe to do concurrently and which are not.

Copy link
Contributor

@stakx stakx Jun 10, 2018

Choose a reason for hiding this comment

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

Wouldn't we only end up with List<T>.Enumerator if we used LINQ or a foreach loop? What if we went for a simple for loop that accesses the list by index in the range 0..n where n is the list's Count at the moment when enumeration starts? Count can only increase. If a Clear is performed, that won't have to affect already-started enumerations since they continue seeing an old, detached list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did consider it, but it has the same problem. When you call List<T>.Add(T), it's not concurrency-safe with its indexer. A path exists where you wind up in an unknown state, like if the underlying array has to be resized. We could try to work out how the implementation of List<T> works and try to handle these edge-cases, but I think it's considerably easier to just use an array. The only method with any real complexity is our Add(Invocation) and most of that is delegated to the fantastic Array.Resize method.

Copy link
Contributor

@stakx stakx Jun 10, 2018

Choose a reason for hiding this comment

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

I can't see the problem yet. Due to the locks in InvocationCollection, only one thread can ever access the list at a time. If an Add causes a resize, then that resize happens inside the lock and noone else can read the list. The indexer read access also happens inside a lock, so while that happens, noone can modify the list.

Copy link
Contributor

@stakx stakx Jun 10, 2018

Choose a reason for hiding this comment

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

Sorry if the following might be a little naïve—I'm not sure right now how the C# compiler transforms lock blocks when yield return gets involved, a single lock around the whole method body might actually suffice:

IEnumerator<IReadOnlyInvocation> GetEnumerator()
{
    List<Invocation> invocations;
    int count;
    lock (this.invocationsLock)
    {
        invocations = this.invocations;     // copying the list reference into a local
        count = invocations.Count;          // means that a `Clear` shouldn't affect us
    }

    for (int i = 0; i < count; ++i)         // the list is guaranteed to have at least count items
    {                                       // even if modified between two iterations
        lock (this.invocationsLock)
        {
            yield return invocations[i];    // no modification can happen here, inside a lock 
        }
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A lock around the for loop would actually make it so the enumerator would acquire the collection until all elements had been iterated over. What you have should acquire a lock to the collection every time it yields an item, then release the lock.

I do like how much less code there is here. I also like that my implementation only acquires a lock once, versus once-per-item + 1 times. I could go either way.

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps there is a middle way. I feel it should be possible to let the compiler generate the enumerator class, which would help reduce the amount of additional code. I'll look into this more closely a little later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added an implementation using yield return. It should be functionally identical to the original, except it doesn't support IEnumerator.Reset().

return new Invocation[0];
}

return this.invocations.Take(this.count).Where(predicate).ToArray();
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if you could use Array.Copy or List<T>.CopyTo instead of a LINQ query?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it could be re-written to use those calls, but the predicate makes it a little harder. I'm not sure if there's a benefit to do so.

Copy link
Contributor

Choose a reason for hiding this comment

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

Less unnecessary overhead, basically. This class is pretty low-level, I usually try to avoid LINQ in such places.

Copy link
Contributor

Choose a reason for hiding this comment

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

(Sorry, I was an idiot above. I was looking at the wrong lines of code when I wrote the above, I completely overlooked the predicate.)

{
return this.invocations.ToList().GetEnumerator();
return new Enumerator(this.invocations, this.count);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering, why not the LINQ range-based approach you suggested earlier?

Copy link
Contributor Author

@Code-Grump Code-Grump Jun 10, 2018

Choose a reason for hiding this comment

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

Take creates an enumerator which operates on the underlying collection. If the collection is modified on another thread whilst working with this enumerator, at best you'll get an InvalidOperationException and at worse will end up in an unknown state. Previously, copying was used to prevent modification by another thread. This custom Enumerator instead takes advantage of append-only nature of the collection and allows enumeration over the known state of the collection even when new items are appended to the collection.

Copy link
Contributor

@stakx stakx Jun 10, 2018

Choose a reason for hiding this comment

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

OK, fair enough, the LINQ query wouldn't work. But see my comment above regarding a plain indexing for loop.

Copy link
Contributor

@stakx stakx left a comment

Choose a reason for hiding this comment

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

Thanks for the changes so far, this is basically good to go now. I noticed a few more final details, I'd be happy if you could check them out.

@@ -69,7 +69,7 @@ public int Count
{
get
{
lock (this.invocations)
lock (this.invocationsLock)
{
return this.invocations[index];
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you please add an explicit index-in-range check here / throw IndexOutOfRangeException otherwise?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Won't we already just get one from the array anyway? Did you want to add some specific detail?

Copy link
Contributor

@stakx stakx Jun 10, 2018

Choose a reason for hiding this comment

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

There may be no array; you set the field to null in Clear, so the indexer might leak a NullReferenceException. I believe that an out-of-range index access should reliably throw IndexOutOfRangeException instead of leaking implementation details.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I hadn't thought of this scenario. I wouldn't want a NullReferenceException either.

{
return this.invocations.Count;
return count;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this lock is superfluous... please remove. Reading an int is an atomic operation, there's no danger of struct tearing or anything of the sort. If there is a concurrent mutation happening during a read, it doesn't really matter whether external code sees the original or updated count.

(In fact, we could question the whole point of having a Count property in a concurrent collection, since user code isn't supposed to do non-atomic accesses like if (invocations.Count > 0) firstInvocation = invocations[0];... but having Count can still be useful in single-threaded scenarios.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not there to avoid a torn read, but to protect against reordering optimisations and variable caching. If you want to go down that avenue, it might require marking the backing field as volatile, or otherwise setting up a memory barrier. I don't advise pulling on this thread unless you think it's very important.

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't know reordering happened across different methods. But yeah, this isn't super-important. Let's leave it as is, then.

lock (this.invocationsLock)
{
return this.count > 0;
}
Copy link
Contributor

@stakx stakx Jun 10, 2018

Choose a reason for hiding this comment

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

This lock isn't necessary (for the same reason mentioned at Count), please remove.

(In fact, now that we're implementing IEnumerable, I wonder if this method is still needed at all: We're only using it in a single place—VerifyNoOtherCalls—which in typical user code is perhaps run on the order of once per unit test method, so we could probably afford just letting Moq use LINQ's .Any() operator and going through .GetEnumerator(). What do you think? Feel free to remove just the lock, or the whole method.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think LINQ's Any() might just call Count in this case. It's probably fine to remove the method.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fine with me, let's remove it.

}
}

Array.Resize(ref result, resultSize);
Copy link
Contributor

@stakx stakx Jun 10, 2018

Choose a reason for hiding this comment

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

Haha, I know I previously said that I tend to avoid LINQ queries in low-level places such as this class... but please feel free to just use a List<> with an initial capacity of this.count, then returning .ToArray() in the end. It's a common pattern that would save you some lines of code at only very little additional overhead. (It's your call, though.)

if (this.invocations == null)
{
throw new IndexOutOfRangeException();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately, this still isn't sufficient. The array can have a capacity that exceeds 'count' so you are not going to get an out-of-range exception in some cases. I suggest you just add a straightforward check for 0 <= index && index < this.count instead. ;-)

lock (this.invocationsLock)
{
return this.count > 0;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

That's fine with me, let's remove it.

@stakx
Copy link
Contributor

stakx commented Jun 10, 2018

Looks like you have made all the requested changes, so let's merge this. Thanks for your great work!

@stakx stakx merged commit f44e690 into devlooped:master Jun 10, 2018
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.

None yet

2 participants