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

Remove unnecessary remove from ProjectRootElementCache #8576

Merged
merged 4 commits into from
May 4, 2023

Conversation

Forgind
Copy link
Member

@Forgind Forgind commented Mar 17, 2023

It's fairly straightforward to demonstrate that if we reach this block, then it isn't in the _strongCache anyway, so this is entirely unnecessary work.

It's fairly straightforward to demonstrate that if we reach this block, then it isn't in the _strongCache anyway, so this is entirely unnecessary work.
Copy link
Member

@ladipro ladipro left a comment

Choose a reason for hiding this comment

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

Looks great! An idea for further optimization: The iteration pattern foreach (string projectPath in oldWeakCache.Keys) is pretty inefficient.

@@ -460,7 +460,6 @@ internal override void DiscardImplicitReferences()
}
else
{
_strongCache.Remove(rootElement);
RaiseProjectRootElementRemovedFromStrongCache(rootElement);
Copy link
Member

Choose a reason for hiding this comment

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

[nit / pre-existing issue] What guarantees that this PRE was in the old strong cache and is actually being removed? It looks like we also get here if it was a regular weak-only not-explicitly-loaded entry.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. There are multiple ways we could go down this path and erroneously call the remove event. I wanted to make this an unambiguous no-op, so I stopped short of removing the next call, which might be legitimate. If I were to rework it to be more correct, I think this is best:

if (rootElement.IsExplicitlyLoaded)
{
    if (oldStrongCache.Contains(rootElement))
    {
        _strongCache.AddFirst(rootElement);
    }
    else
    {
        // Has already been removed. We can consider adding it to the strong cache here...but it probably isn't worth it
    }
}
else
{
    if (oldStrongCache.Contains(rootElement))
    {
        RaiseProjectRootElementRemovedFromStrongCache(rootElement);
    }
    else
    {
        // Element was not in the strong cache and should not be added to it --> no op
    }
}

or more simply

if (oldStrongCache.Contains(rootElement))
{
    if (rootElement.IsExplicitlyLoaded)
    {
        _strongCache.AddFirst(rootElement);
    }
    else
    {
        RaiseProjectRootElementRemovedFromStrongCache(rootElement);
    }
}

I think either is better from a correctness perspective, but it means we'd always be calling Contains, which is a bit slow.

Copy link
Member

Choose a reason for hiding this comment

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

What if we make it iterate over the strong cache? Should be doable in one pass. And as a bonus, it would preserve the order, which the current implementation does not.

Copy link
Member Author

Choose a reason for hiding this comment

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

The only issue I see with that is that we're also discarding implicit references in the weak cache in this pass, and since there can be ProjectRootElements in the weak cache but not the strong cache, we'd be deleting those from the weak cache whether they're explicitly loaded and not GC'd or not.

Copy link
Member Author

@Forgind Forgind left a comment

Choose a reason for hiding this comment

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

The iteration pattern foreach (string projectPath in oldWeakCache.Keys) is pretty inefficient.

When I was working on the simpler loops PR, this one stuck out to me as one I wanted to optimize but was having trouble finding a way to make it better because I can't iterate over kvps in a weak cache. What do you suggest?

@@ -460,7 +460,6 @@ internal override void DiscardImplicitReferences()
}
else
{
_strongCache.Remove(rootElement);
RaiseProjectRootElementRemovedFromStrongCache(rootElement);
Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. There are multiple ways we could go down this path and erroneously call the remove event. I wanted to make this an unambiguous no-op, so I stopped short of removing the next call, which might be legitimate. If I were to rework it to be more correct, I think this is best:

if (rootElement.IsExplicitlyLoaded)
{
    if (oldStrongCache.Contains(rootElement))
    {
        _strongCache.AddFirst(rootElement);
    }
    else
    {
        // Has already been removed. We can consider adding it to the strong cache here...but it probably isn't worth it
    }
}
else
{
    if (oldStrongCache.Contains(rootElement))
    {
        RaiseProjectRootElementRemovedFromStrongCache(rootElement);
    }
    else
    {
        // Element was not in the strong cache and should not be added to it --> no op
    }
}

or more simply

if (oldStrongCache.Contains(rootElement))
{
    if (rootElement.IsExplicitlyLoaded)
    {
        _strongCache.AddFirst(rootElement);
    }
    else
    {
        RaiseProjectRootElementRemovedFromStrongCache(rootElement);
    }
}

I think either is better from a correctness perspective, but it means we'd always be calling Contains, which is a bit slow.

@ladipro
Copy link
Member

ladipro commented Mar 20, 2023

The iteration pattern foreach (string projectPath in oldWeakCache.Keys) is pretty inefficient.

When I was working on the simpler loops PR, this one stuck out to me as one I wanted to optimize but was having trouble finding a way to make it better because I can't iterate over kvps in a weak cache. What do you suggest?

I was thinking about having WeakValueDictionary<K, V> implement IEnumerable<KeyValuePair<K, V>> like so:

        public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
        {
            foreach (KeyValuePair<K, WeakReference<V>> kvp in _dictionary)
            {
                if (kvp.Value == null)
                {
                    yield return new KeyValuePair<K, V>(kvp.Key, null);
                }
                else if (kvp.Value.TryGetTarget(out V target))
                {
                    yield return new KeyValuePair<K, V>(kvp.Key, target);
                }
            }
        }

@Forgind
Copy link
Member Author

Forgind commented Mar 20, 2023

The iteration pattern foreach (string projectPath in oldWeakCache.Keys) is pretty inefficient.

When I was working on the simpler loops PR, this one stuck out to me as one I wanted to optimize but was having trouble finding a way to make it better because I can't iterate over kvps in a weak cache. What do you suggest?

I was thinking about having WeakValueDictionary<K, V> implement IEnumerable<KeyValuePair<K, V>> like so:

        public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
        {
            foreach (KeyValuePair<K, WeakReference<V>> kvp in _dictionary)
            {
                if (kvp.Value == null)
                {
                    yield return new KeyValuePair<K, V>(kvp.Key, null);
                }
                if (kvp.Value.TryGetTarget(out V target))
                {
                    yield return new KeyValuePair<K, V>(kvp.Key, target);
                }
            }
        }

I totally missed that WeakValueDictionary was something we owned! I'd be happy to implement that as part of this PR or as something separate!

@Forgind
Copy link
Member Author

Forgind commented Mar 21, 2023

I implemented IEnumerable<K, V>. I'm unclear on the merit of returning (key, null) if the V was collected already, so I left that part out, but if there's a good reason for it, I'm happy to add it back. (In the use case in this PR, it just leads to extra checks on the other end.)

@ladipro
Copy link
Member

ladipro commented Mar 22, 2023

I implemented IEnumerable<K, V>. I'm unclear on the merit of returning (key, null) if the V was collected already, so I left that part out, but if there's a good reason for it, I'm happy to add it back. (In the use case in this PR, it just leads to extra checks on the other end.)

(key, null) would be returned if the value stored in the dictionary was actually null (different from a originally non-null but now collected value - that should not be returned). Your implementation works for the use case of enumerating non-null values but it is not consistent with the rest of its API. It should enumerate all keys, for which TryGetValue returns true.

@rainersigwald rainersigwald added this to the VS 17.7 milestone Mar 28, 2023
@Forgind Forgind added the merge-when-branch-open PRs that are approved, except that there is a problem that means we are not merging stuff right now. label Apr 24, 2023
@Forgind Forgind merged commit 57c2833 into dotnet:main May 4, 2023
@Forgind Forgind deleted the delete-extra-remove branch May 4, 2023 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge-when-branch-open PRs that are approved, except that there is a problem that means we are not merging stuff right now.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants