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

Lower reflection overhead #2839

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft

Lower reflection overhead #2839

wants to merge 9 commits into from

Conversation

nohwnd
Copy link
Member

@nohwnd nohwnd commented May 10, 2024

This PR simplifies the way we access reflection in discovery and run. By improving the attribute cache and using it whenever possible.

It also simplifies ReflectHelper code, by removing dependencies between methods, and giving them more descriptive names. To make it much easier to reason about the current and desired behavior.

I am keeping the existing non-consistent behavior, to avoid breaking changes.

  • IsAttributeDefined => IsNonDerivedAttributeDefined

GetCustomAttribute => GetFirstDerivedAttributeOrDefault + inherit: true - when used with attributes that allow only one instance

GetCustomAttributes -> GetDerivedAttributes + inherit: true - for attributes that allow multiple.

Before

Initial measurements, on SDK 3.3.1.

Discovery of 10k empty tests:
discovered: 10000 tests in 1992,1448 ms, sent them in 289,5684 ms, total: 2281,7132
discovered: 10000 tests in 899,1829 ms, sent them in 212,4562 ms, total: 1111,6391
discovered: 10000 tests in 552,0988 ms, sent them in 321,4334 ms, total: 873,5322

Run 10k empty tests:

image

After

Discover 10 empty tests:
discovered: 10000 tests in 1219,4222 ms, sent them in 227,5277 ms, total: 1446,9499 <<<
2088
discovered: 10000 tests in 324,3993 ms, sent them in 193,5436 ms, total: 517,9429 <<<
1024
discovered: 10000 tests in 259,1427 ms, sent them in 291,6613 ms, total: 550,804 <<<
discovered: discovery alloc: 50 MB send alloc: 72 MB
971

Run 10k empty tests:

image

@@ -219,7 +219,7 @@ internal virtual TypeEnumerator GetTypeEnumerator(Type type, string assemblyFile
typeFullName = type.FullName;
TypeEnumerator testTypeEnumerator = GetTypeEnumerator(type, assemblyFileName, discoverInternals, testIdGenerationStrategy);
ICollection<UnitTestElement>? unitTestCases = testTypeEnumerator.Enumerate(out ICollection<string>? warningsFromTypeEnumerator);
bool typeIgnored = ReflectHelper.IsAttributeDefined<IgnoreAttribute>(type, false);
Copy link
Member Author

Choose a reason for hiding this comment

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

IsAttributeDefined checks attribute entry by fully qualified assembly name. So it is a non-derived attribute (it needs to be exactly the same type).

@@ -174,29 +190,29 @@ internal UnitTestElement GetTestFromMethod(MethodInfo method, bool isDeclaredInT

testElement.Traits = traits.ToArray();

if (_reflectHelper.GetCustomAttribute<CssIterationAttribute>(method) is CssIterationAttribute cssIteration)
Copy link
Member Author

Choose a reason for hiding this comment

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

GetCustomAttribute checks for a derived attribute (e.g. MyTestClass when we look for TestClass), and inherits (inspects parent types).

All our attributes allow inheritance, even though we don't always check for it.

Almost none of our attributes allow multiple, just testCategory, testProperty, and workItem.

So getting first without checking that there is only Single is okay, because there never won't be more, unless you are using the attributes above.

@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
Copy link
Member Author

Choose a reason for hiding this comment

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

This helps me measure, I will revert, or we can log it?


// Update class initialize/cleanup method
UpdateInfoIfClassInitializeOrCleanupMethod(classInfo, methodInfo, false, ref initAndCleanupMethods);
if (methodInfo.IsPublic && !methodInfo.IsStatic)
Copy link
Member Author

Choose a reason for hiding this comment

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

This is done in the other place as well, but seems inconsistent with the rest of the code, where we inspect even shapes of methods that are obviously incorrect, and throw when they have incorrect shape.

So remove in both places?

@@ -426,16 +441,15 @@ private TestAssemblyInfo GetAssemblyInfo(Type type)
{
assemblyInfo.AssemblyInitializeMethod = methodInfo;

if (_reflectionHelper.IsAttributeDefined<TimeoutAttribute>(methodInfo, false))
Copy link
Member Author

@nohwnd nohwnd May 10, 2024

Choose a reason for hiding this comment

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

Yum yum, this code checks for the presence of timeout 3 times per check.

//if (!methodInfo.IsStatic)
//{
// return false;
//}
Copy link
Member Author

Choose a reason for hiding this comment

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

This is another similar inconcistency.

@@ -300,7 +300,7 @@ private class ClassCleanupManager
g => new HashSet<string>(g.Select(t => t.TestMethod.UniqueName))));
_lifecycleFromMsTest = lifecycleFromMsTest;
_lifecycleFromAssembly = lifecycleFromAssembly;
_reflectHelper = reflectHelper ?? new ReflectHelper();
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 code above already does not accept null, and we don't want to duplicate our cache.

return false;
}
// TODO: redesign this, probably change this to GetTimeout? so we don't have to do this weird dance?
timeoutAttribute ??= ReflectHelper.Instance.GetSingleNonDerivedAttributeOrDefault<TimeoutAttribute>(method, inherit: false, nullOnMultiple: true);
Copy link
Member Author

Choose a reason for hiding this comment

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

Or better ideas to redesign? Before the change it was grabbing the attributes 3 times to check:

  • if the attribute is defined
  • then again in validation method
  • then again to grab the timeout

@@ -125,7 +122,7 @@ internal static bool IsValidReturnType(this MethodInfo method)
/// <returns>Compiler generated type name for given async test method..</returns>
internal static string? GetAsyncTypeName(this MethodInfo method)
{
AsyncStateMachineAttribute? asyncStateMachineAttribute = ReflectHelper.GetCustomAttributes<AsyncStateMachineAttribute>(method, false).FirstOrDefault();
Copy link
Member Author

Choose a reason for hiding this comment

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

This method does not allow multiple or inherit. So we replace with getting first, non derived, without inheritance.

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

1 participant