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

Optimizations for test case serialization. #103

Closed
wants to merge 1 commit into from

Conversation

codito
Copy link
Contributor

@codito codito commented Feb 20, 2017

Introduced two flags to control SourceInformation and Serialization of xunit test case
as a TestProperty in VSTestCase object. Serialization is enabled for Discovery always and
enabled for Run All or Run Specific test operation only if the test run is triggered from
an IDE.

Moved Discovery (in case of Run All flow) to use the same XunitFrontController as execution
of an assembly. This removed the need for Serialize of XunitTestCase in Discovery[execution]
followed by a Deserialization in actual Execution. A side effect of this is: if multiple assemblies are specified, for each assembly {discovery, execution} be sequential instead of {discovery of all} followed by {execution of all}. This behavior is same as xunit.console runner.

Since the ITestCase objects are MarshalByRef, optimized the code in VsExecutionSink to lookup
a VSTestCase based on ITestCase.UniqueId instead of entire ITestCase objects since each access to ITestCase has the cost of cross-appdomain proxy call.

Introduced two flags to control SourceInformation and Serialization of xunit test case
as a TestProperty in VSTestCase object. Serialization is enabled for Discovery always and
enabled for Run All or Run Specific test operation only if the test run is triggered from
an IDE.

Moved Discovery (in case of Run All flow) to use the same XunitFrontController as execution
of an assembly. This removed the need for Serialize of XunitTestCase in Discovery[execution]
followed by a Deserialization in actual Execution.

Since the ITestCase objects are MarshalByRef, optimized the code in VsExecutionSink to lookup
a VSTestCase based on ITestCase.UniqueId instead of entire ITestCase objects.
@dnfclas
Copy link

dnfclas commented Feb 20, 2017

Hi @codito, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!

The agreement was validated by .NET Foundation and real humans are currently evaluating your PR.

TTYL, DNFBOT;

@codito
Copy link
Contributor Author

codito commented Feb 20, 2017

Tests in xunit.integration run successfully with this change in VS 2015 Update 3. Manually created a port of these tests in VS 2017 and they work as expected.

@bradwilson
Copy link
Member

bradwilson commented Feb 22, 2017

This is a substantial re-write. Can you please provide an overview of what you've done and why?

Also, can you please stop arbitrarily reformatting the file, and following our existing coding conventions? Thanks.

var fqTestMethodName = $"{xunitTestCase.TestMethod.TestClass.Class.Name}.{xunitTestCase.TestMethod.Method.Name}";
var result = new TestCase(fqTestMethodName, uri, source) { DisplayName = Escape(xunitTestCase.DisplayName) };
result.SetPropertyValue(VsTestRunner.SerializedTestCaseProperty, serializedTestCase);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Issue: XunitTestCase property is required only for scenarios where VSTest can send a TestCase object later (e.g. Discovery in IDE, followed by Run Selected tests). A command line test run is a fire-and-forget operation, test property need not be serialized in this case.

Solution: add a RequireXunitTestProperty flag to control this.


result = testCases.Where(tc => tc.Key.UniqueID == testCase.UniqueID).Select(kvp => kvp.Value).FirstOrDefault();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Issue: the test cases here are Proxy objects, comparison operations are costly since they require cross appdomain calls. This function gets called quite frequently (every testcase start, end).

Solution: ITestCase.UniqueID identifies a test case, we built a map with <UniqueID, VSTestCase>. All the lookup operations are O(1) and do not require cross appdomain calls.

/// Provides contextual information on a test run/discovery based on runsettings
/// or the invocation (execution, discovery).
/// </summary>
public struct TestPlatformContext
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In #100, support for checking context was added. In this PR, we are adding another context on when to serialize ITestCase. This object holds all such requirements from vstest for decisions during discovery and execution.

@@ -85,9 +85,22 @@ void ITestDiscoverer.DiscoverTests(IEnumerable<string> sources, IDiscoveryContex
RunSettingsHelper.ReadRunSettings(discoveryContext?.RunSettings?.SettingsXml);
#endif

var testPlatformContext = new TestPlatformContext
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In all 3 operations from VSTest: Discover, Run All, Run Specific tests, this context is defined. The optimizations are as follows:

Discovery Run All Run Specific
RequiresSourceInformation IDE - Yes, CLI - No IDE - Yes, CLI - No IDE - Yes, CLI - No
RequireXunitTestProperty IDE - Yes, CLI - Yes* IDE - Yes, CLI - No IDE - Yes, CLI - No

CLI can be optimized since it is fire and forget. Except for Discovery* since Run Specific Test in CLI also calls Discovery first and then followed by an execution.

@@ -122,7 +143,17 @@ void ITestExecutor.RunTests(IEnumerable<string> sources, IRunContext runContext,
.Where(file => !platformAssemblies.Contains(Path.GetFileName(file))));
}

RunTests(runContext, frameworkHandle, logger, () => GetTests(sources, logger, runContext, RunSettingsHelper.DesignMode));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Issue: in case of Run All, a Discovery was first triggered in separate appdomain, tests were serialized, Execution is triggered in another appdomain.

Solution: in case of Run All, delay the actual discovery. First create an appdomain (XunitFrontController), then do a discovery within that, serialization is not required (if RequireXunitTestProperty is false), do the execution within same appdomain.

Since it is the same assembly, discovery and execution may happen in same appdomain (similar to functionality of xunit.console).

{
if (cancelled)
if (!DiscoverTestsInSource(framework, logger, testPlatformContext, visitorFactory, visitComplete, assemblyFileName, shadowCopy, configuration))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

DiscoverTestsInSource just extracts the common parts in Discovery and Discovery in case of Run All.

}
}
catch (Exception e)
if (!IsXunitTestAssembly(assemblyFileName))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no new code in this method, just refactoring to allow reuse.

var configuration = LoadConfiguration(assemblyFileName);
var fileName = Path.GetFileNameWithoutExtension(assemblyFileName);
var shadowCopy = configuration.ShadowCopyOrDefault;

try
var diagnosticSink = new DiagnosticMessageSink(logger, fileName, configuration.DiagnosticMessagesOrDefault);
using (var framework = new XunitFrontController(AppDomainDefaultBehavior,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This XunitFrontController is only created for Discovery. We provide an existing controller to DiscoverTestsInSource in case of Discovery during Run All.

(source, discoverer, discoveryOptions) => new VsExecutionDiscoverySink(() => cancelled),
(source, discoverer, discoveryOptions, visitor) =>
{
if (discoveryOptions.GetDiagnosticMessagesOrDefault())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This logic and the filtering logic is moved to RunTestsInAssembly to allow reuse of same XunitFrontController for discovery and execution.

.ToDictionary(tc => tc.xunit, tc => tc.vs);
var testCasesMap = new Dictionary<string, TestCase>();
var testCases = new List<ITestCase>();
if (runInfo.TestCases == null || !runInfo.TestCases.Any())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This indicates discovery has been delayed for a Run All operation; we already have the controller here. We do the discovery and filtering.

{
var xunitTestCases = runInfo.TestCases.Select(tc => new { vs = tc, xunit = Deserialize(logger, controller, tc) })
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Issue: in case of Run All; there was an extra cost of Serialize after discovery and Deserialize before execution.

Solution: remove deserialize and make serialize conditional based on RequireXunitTestProperty.

{
// PERF: the filteredTestCase.TestCase object is proxy to ITestCase in the discovery appdomain.
// We're using the UniqueId only (cached in this appdomain).
testCasesMap.Add(filteredTestCase.UniqueId, filteredTestCase.VSTestCase);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We pre-create a map of UniqueID, VSTestCase to allow faster lookups in VSExecutionSink.

// PERF: note that TestCase is a proxy to actual object in the discovery appdomain. UniqueId is used multiple times,
// hence cached to reduce cross-appdomain access cost.
TestCase = testCase;
UniqueId = testCase.UniqueID;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We use UniqueID several times later, create a copy once since it is a cross domain access.

@codito
Copy link
Contributor Author

codito commented Feb 22, 2017

This is a substantial re-write. Can you please provide an overview of what you've done and why?

This PR does three things:

  • Optimize Serialize/Deserialize of XunitTestCase as a Test Property in VSTestCase object
  • Optimize cases where cross appdomain calls were costly
  • Move Discovery and Execution to use the same XunitFrontController in case of Run All scenario

Added more details of issues and proposed solutions along side source changes as comments in this PR.

Also, can you please stop arbitrarily reformatting the file, and following our existing coding conventions? Thanks.

Sorry, this was not intentional. I will fix it in the next commit to this PR (along with any review feedback).

@codito
Copy link
Contributor Author

codito commented Feb 27, 2017

@bradwilson @onovotny please let me know if there's any more feedback on these changes.

@bradwilson
Copy link
Member

I cleaned up the conflicts and merged the result. Thanks!

@bradwilson bradwilson closed this Apr 6, 2017
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.

3 participants