diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 99a30fed05..0004f12a21 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -135,7 +135,7 @@ if ((Test-Path $vsixUtilDir) -and -not (Test-Path "$vsixUtilDir\$vsSdkBuildTools # Procdump gets regularly eaten by antivirus or something. Remove the package dir if it gets broken # so nuget restores it correctly. $procdumpDir = "$env:TP_ROOT_DIR\packages\procdump" -if ((Test-Path $procdumpDir) -and 2 -ne @(Get-Item "$procdumpDir\0.0.1\bin").Length) { +if ((Test-Path $procdumpDir) -and (Test-Path "$procdumpDir\0.0.1\bin") -and 2 -ne @(Get-Item "$procdumpDir\0.0.1\bin").Length) { Remove-Item -Recurse -Force $procdumpDir } diff --git a/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeClient.cs b/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeClient.cs index fc251b1c00..17dfdf30d3 100644 --- a/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeClient.cs +++ b/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeClient.cs @@ -347,7 +347,7 @@ public bool AttachDebuggerToProcess(int pid, CancellationToken cancellationToken waitHandle.Set(); }; - _communicationManager.SendMessage(MessageType.EditorAttachDebugger, pid); + _communicationManager.SendMessage(MessageType.EditorAttachDebugger, pid, _protocolConfig.Version); WaitHandle.WaitAny(new WaitHandle[] { waitHandle, cancellationToken.WaitHandle }); diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/JsonDataSerializer.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/JsonDataSerializer.cs index b094495733..3f1ba8c817 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/JsonDataSerializer.cs +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/JsonDataSerializer.cs @@ -6,6 +6,7 @@ using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Serialization; +using Microsoft.VisualStudio.TestPlatform.Utilities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -21,8 +22,12 @@ public class JsonDataSerializer : IDataSerializer { private static JsonDataSerializer s_instance; + private static readonly bool DisableFastJson = FeatureFlag.Instance.IsSet(FeatureFlag.DISABLE_FASTER_JSON_SERIALIZATION); + private static JsonSerializer s_payloadSerializer; // payload serializer for version <= 1 private static JsonSerializer s_payloadSerializer2; // payload serializer for version >= 2 + private static JsonSerializerSettings s_fastJsonSettings; // serializer settings for faster json + private static JsonSerializerSettings s_jsonSettings; // serializer settings for serializer v1, which should use to deserialize message headers private static JsonSerializer s_serializer; // generic serializer /// @@ -36,13 +41,29 @@ private JsonDataSerializer() DateParseHandling = DateParseHandling.DateTimeOffset, DateTimeZoneHandling = DateTimeZoneHandling.Utc, TypeNameHandling = TypeNameHandling.None, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, }; + s_jsonSettings = jsonSettings; + s_serializer = JsonSerializer.Create(); s_payloadSerializer = JsonSerializer.Create(jsonSettings); s_payloadSerializer2 = JsonSerializer.Create(jsonSettings); + s_fastJsonSettings = new JsonSerializerSettings + { + DateFormatHandling = jsonSettings.DateFormatHandling, + DateParseHandling = jsonSettings.DateParseHandling, + DateTimeZoneHandling = jsonSettings.DateTimeZoneHandling, + TypeNameHandling = jsonSettings.TypeNameHandling, + ReferenceLoopHandling = jsonSettings.ReferenceLoopHandling, + // PERF: Null value handling has very small impact on serialization and deserialization. Enabling it does not warrant the risk we run + // of changing how our consumers get their data. + // NullValueHandling = NullValueHandling.Ignore, + + ContractResolver = new DefaultTestPlatformContractResolver(), + }; + s_payloadSerializer.ContractResolver = new TestPlatformContractResolver1(); s_payloadSerializer2.ContractResolver = new DefaultTestPlatformContractResolver(); @@ -68,7 +89,33 @@ private JsonDataSerializer() /// A instance. public Message DeserializeMessage(string rawMessage) { - return Deserialize(s_serializer, rawMessage); + if (DisableFastJson) + { + // PERF: This is slow, we deserialize the message, and the payload into JToken just to get the header. We then + // deserialize the data from the JToken, but that is twice as expensive as deserializing the whole object directly into the final object type. + // We need this for backward compatibility though. + return Deserialize(rawMessage); + } + + // PERF: Try grabbing the version and message type from the string directly, we are pretty certain how the message is serialized + // when the format does not match all we do is that we check if 6th character in the message is 'V' + if (!FastHeaderParse(rawMessage, out int version, out string messageType)) + { + // PERF: If the fast path fails, deserialize into header object that does not have any Payload. When the message type info + // is at the start of the message, this is also pretty fast. Again, this won't touch the payload. + MessageHeader header = JsonConvert.DeserializeObject(rawMessage, s_jsonSettings); + version = header.Version; + messageType = header.MessageType; + } + + var message = new VersionedMessageWithRawMessage + { + Version = version, + MessageType = messageType, + RawMessage = rawMessage, + }; + + return message; } /// @@ -79,9 +126,145 @@ public Message DeserializeMessage(string rawMessage) /// The deserialized payload. public T DeserializePayload(Message message) { - var versionedMessage = message as VersionedMessage; - var payloadSerializer = GetPayloadSerializer(versionedMessage?.Version); - return Deserialize(payloadSerializer, message.Payload); + if (message.GetType() == typeof(Message)) + { + // Message is specifically a Message, and not any of it's child types like VersionedMessage. + // Get the default serializer and deserialize. This would be used for any message from very old test host. + // + // Unit tests also provide a Message in places where using the deserializer would actually + // produce a VersionedMessage or VersionedMessageWithRawMessage. + var serializerV1 = GetPayloadSerializer(null); + return Deserialize(serializerV1, message.Payload); + } + + var versionedMessage = (VersionedMessage)message; + var payloadSerializer = GetPayloadSerializer(versionedMessage.Version); + + if (DisableFastJson) + { + // When fast json is disabled, then the message is a VersionedMessage + // with JToken payload. + return Deserialize(payloadSerializer, message.Payload); + } + + // When fast json is enabled then the message is also a subtype of VersionedMessage, but + // the Payload is not populated, and instead the rawMessage string it passed as is. + var messageWithRawMessage = (VersionedMessageWithRawMessage)message; + var rawMessage = messageWithRawMessage.RawMessage; + + // The deserialized message can still have a version (0 or 1), that should use the old deserializer + if (payloadSerializer == s_payloadSerializer2) + { + // PERF: Fast path is compatibile only with protocol versions that use serializer_2, + // and this is faster than deserializing via deserializer_2. + var messageWithPayload = JsonConvert.DeserializeObject>(rawMessage, s_fastJsonSettings); + return messageWithPayload.Payload; + } + else + { + // PERF: When payloadSerializer1 was resolved we need to deserialize JToken, and then deserialize that. + // This is still better than deserializing the JToken in DeserializeMessage because here we know that the payload + // will actually be used. + return Deserialize(payloadSerializer, Deserialize(rawMessage).Payload); + } + } + + private bool FastHeaderParse(string rawMessage, out int version, out string messageType) + { + // PERF: This can be also done slightly better using ReadOnlySpan but we don't have that available by default in .NET Framework + // and the speed improvement does not warrant additional dependency. This is already taking just few ms for 10k messages. + version = 0; + messageType = null; + + try + { + // The incoming messages look like this, or like this: + // {"Version":6,"MessageType":"TestExecution.GetTestRunnerProcessStartInfoForRunAll","Payload":{ + // {"MessageType":"TestExecution.GetTestRunnerProcessStartInfoForRunAll","Payload":{ + if (rawMessage.Length < 31) + { + // {"MessageType":"T","Payload":1} with length 31 is the smallest valid message we should be able to parse.. + return false; + } + + // If the message is not versioned then the start quote of the message type string is at index 15 {"MessageType":" + int messageTypeStartQuoteIndex = 15; + int versionInt = 0; + if (rawMessage[2] == 'V') + { + // This is a potential versioned message that looks like this: + // {"Version":6,"MessageType":"TestExecution.GetTestRunnerProcessStartInfoForRunAll","Payload":{ + + // Version ':' is on index 10, the number starts at the next index. Find wher the next ',' is and grab that as number. + var versionColonIndex = 10; + if (rawMessage[versionColonIndex] != ':') + { + return false; + } + + var firstVersionNumberIndex = 11; + // The message is versioned, get the version and update the position of first quote that contains message type. + if (!TryGetSubstringUntilDelimiter(rawMessage, firstVersionNumberIndex, ',', maxSearchLength: 4, out string versionString, out int versionCommaIndex)) + { + return false; + } + + // Message type delmiter is at at versionCommaIndex + the length of '"MessageType":"' which is 15 chars + messageTypeStartQuoteIndex = versionCommaIndex + 15; + + if (!int.TryParse(versionString, out versionInt)) + { + return false; + } + } + else if (rawMessage[2] != 'M' || rawMessage[12] != 'e') + { + // Message is not versioned message, and it is also not message that starts with MessageType + return false; + } + + if (rawMessage[messageTypeStartQuoteIndex] != '"') + { + return false; + } + + int messageTypeStartIndex = messageTypeStartQuoteIndex + 1; + // "TestExecution.LaunchAdapterProcessWithDebuggerAttachedCallback" is the longest message type we currently have with 62 chars + if (!TryGetSubstringUntilDelimiter(rawMessage, messageTypeStartIndex, '"', maxSearchLength: 100, out string messageTypeString, out _)) + { + return false; + } + + version = versionInt; + messageType = messageTypeString; + return true; + } + catch + { + return false; + } + } + + /// + /// Try getting substring until a given delimiter, but don't search more characters than maxSearchLength. + /// + private bool TryGetSubstringUntilDelimiter(string rawMessage, int start, char character, int maxSearchLength, out string substring, out int delimiterIndex) + { + var length = rawMessage.Length; + var searchEnd = start + maxSearchLength; + for (int i = start; i < length && i <= searchEnd; i++) + { + if (rawMessage[i] == character) + { + delimiterIndex = i; + substring = rawMessage.Substring(start, i - start); + return true; + } + } + + delimiterIndex = -1; + substring = null; + return false; } /// @@ -128,11 +311,20 @@ public string SerializePayload(string messageType, object payload) public string SerializePayload(string messageType, object payload, int version) { var payloadSerializer = GetPayloadSerializer(version); - var serializedPayload = JToken.FromObject(payload, payloadSerializer); + // Fast json is only equivalent to the serialization that is used for protocol version 2 and upwards (or more precisely for the paths that use s_payloadSerializer2) + // so when we resolved the old serializer we should use non-fast path. + if (DisableFastJson || payloadSerializer == s_payloadSerializer) + { + var serializedPayload = JToken.FromObject(payload, payloadSerializer); - return version > 1 ? - Serialize(s_serializer, new VersionedMessage { MessageType = messageType, Version = version, Payload = serializedPayload }) : - Serialize(s_serializer, new Message { MessageType = messageType, Payload = serializedPayload }); + return version > 1 ? + Serialize(s_serializer, new VersionedMessage { MessageType = messageType, Version = version, Payload = serializedPayload }) : + Serialize(s_serializer, new Message { MessageType = messageType, Payload = serializedPayload }); + } + else + { + return JsonConvert.SerializeObject(new VersionedMessageForSerialization { MessageType = messageType, Version = version, Payload = payload }, s_fastJsonSettings); + } } /// @@ -226,8 +418,60 @@ private JsonSerializer GetPayloadSerializer(int? version) // env variable. 0 or 1 or 3 => s_payloadSerializer, 2 or 4 or 5 or 6 => s_payloadSerializer2, + _ => throw new NotSupportedException($"Protocol version {version} is not supported. " + "Ensure it is compatible with the latest serializer or add a new one."), }; } + + /// + /// Just the header from versioned messages, to avoid touching the Payload when we deserialize message. + /// + private class MessageHeader + { + public int Version { get; set; } + public string MessageType { get; set; } + } + + /// + /// Container for the rawMessage string, to avoid changing how messages are passed. + /// This allows us to pass MessageWithRawMessage the same way that Message is passed for protocol version 1. + /// And VersionedMessage is passed for later protocol versions, but without touching the payload string when we just + /// need to know the header. + /// !! This message does not populate the Payload property even though it is still present because that comes from Message. + /// + private class VersionedMessageWithRawMessage : VersionedMessage + { + public string RawMessage { get; set; } + } + + /// + /// This grabs payload from the message, we already know version and message type. + /// + /// + private class PayloadedMessage + { + public T Payload { get; set; } + } + + /// + /// For serialization directly into string, without first converting to JToken, and then from JToken to string. + /// + private class VersionedMessageForSerialization + { + /// + /// Gets or sets the version of the message + /// + public int Version { get; set; } + + /// + /// Gets or sets the message type. + /// + public string MessageType { get; set; } + + /// + /// Gets or sets the payload. + /// + public object Payload { get; set; } + } } diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/DefaultTestPlatformContractResolver.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/DefaultTestPlatformContractResolver.cs index 623f339d61..325dbcc203 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/DefaultTestPlatformContractResolver.cs +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/DefaultTestPlatformContractResolver.cs @@ -45,3 +45,41 @@ protected override JsonContract CreateContract(Type objectType) return contract; } } + +/// TODO: This is not used now, but I was experimenting with this quite a bit for performance, leaving it here in case I was wrong +/// and the serializer settings actually have signigicant impact on the speed. +/// +/// JSON contract resolver for mapping test platform types. +/// +internal class DefaultTestPlatformContractResolver7 : DefaultContractResolver +{ + public DefaultTestPlatformContractResolver7() + { + } + /// + protected override JsonContract CreateContract(Type objectType) + { + var contract = base.CreateContract(objectType); + + if (typeof(List>) == objectType) + { + // ObjectModel.TestObject provides a custom TestProperty based data store for all + // inherited objects. This converter helps with serialization of TestProperty and values + // over the wire. + // Each object inherited from TestObject handles it's own serialization. Most of them use + // this TestProperty data store for members as well. In such cases, we just ignore those + // properties. E.g. TestCase object's CodeFilePath is ignored for serialization since the + // actual data is already getting serialized by this converter. + // OTOH, TestResult has members that are not based off this store. + contract.Converter = new TestObjectConverter7(); + } + else if (objectType == typeof(ITestRunStatistics)) + { + // This converter is required to hint json.net to use a concrete class for serialization + // of ITestRunStatistics. We can't remove ITestRunStatistics since it is a breaking change. + contract.Converter = new TestRunStatisticsConverter(); + } + + return contract; + } +} diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/TestObjectConverter.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/TestObjectConverter.cs index 2dda8d3acc..93bc433c3c 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/TestObjectConverter.cs +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/TestObjectConverter.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -84,3 +85,107 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s // Create an array of dictionary } } + +/// TODO: This is not used now, but I was experimenting with this quite a bit for performance, leaving it here in case I was wrong +/// and the serializer settings actually have signigicant impact on the speed. +/// +/// JSON converter for the and derived entities. +/// +internal class TestObjectConverter7 : JsonConverter +{ + // Empty is not present everywhere +#pragma warning disable CA1825 // Avoid zero-length array allocations + private static readonly object[] EmptyObjectArray = new object[0]; +#pragma warning restore CA1825 // Avoid zero-length array allocations + + public TestObjectConverter7() + { +#if !NETSTANDARD1_3 + TestPropertyCtor = typeof(TestProperty).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], null); +#endif + } + + /// + public override bool CanRead => true; + + /// + public override bool CanWrite => false; + + public ConstructorInfo TestPropertyCtor { get; } + + /// + public override bool CanConvert(Type objectType) + { + throw new NotImplementedException(); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (objectType != typeof(List>)) + { + // Support only deserialization of KeyValuePair list + throw new ArgumentException("the objectType was not a KeyValuePair list", nameof(objectType)); + } + + if (reader.TokenType == JsonToken.StartArray) + { + var deserializedProperties = serializer.Deserialize>>(reader); + // Initialize the list capacity to be the number of properties we might add. + var propertyList = new List>(deserializedProperties.Count); + + // Every class that inherits from TestObject uses a properties store for + // key value pairs. + foreach (var property in deserializedProperties) + { + var testProperty = (TestProperty)TestPropertyCtor.Invoke(EmptyObjectArray); + testProperty.Id = property.Key.Id; + testProperty.Label = property.Key.Label; + testProperty.Category = property.Key.Category; + testProperty.Description = property.Key.Description; + testProperty.Attributes = (TestPropertyAttributes)property.Key.Attributes; + testProperty.ValueType = property.Key.ValueType; + + + object propertyData = null; + JToken token = property.Value; + if (token.Type != JTokenType.Null) + { + // If the property is already a string. No need to convert again. + if (token.Type == JTokenType.String) + { + propertyData = token.ToObject(typeof(string), serializer); + } + else + { + // On deserialization, the value for each TestProperty is always a string. It is up + // to the consumer to deserialize it further as appropriate. + propertyData = token.ToString(Formatting.None).Trim('"'); + } + } + + propertyList.Add(new KeyValuePair(testProperty, propertyData)); + } + + return propertyList; + } + + return new List>(); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + // Create an array of dictionary + } + + private class TestPropertyTemplate + { + public string Id { get; set; } + public string Label { get; set; } + public string Category { get; set; } + public string Description { get; set; } + public int Attributes { get; set; } + public string ValueType { get; set; } + } +} diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs index e92ad80138..e45a585115 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs @@ -492,6 +492,8 @@ private void OnExecutionMessageReceived(MessageReceivedEventArgs messageReceived // Send raw message first to unblock handlers waiting to send message to IDEs testRunEventsHandler.HandleRawMessage(rawMessage); + // PERF: DeserializeMessage happens in HandleRawMessage above, as well as here. But with fastJson path where we just grab the routing info from + // the raw string, it is not a big issue, it adds a handful of ms at worst. The payload does not get deserialized twice. var message = _dataSerializer.DeserializeMessage(rawMessage); switch (message.MessageType) { diff --git a/src/Microsoft.TestPlatform.CoreUtilities/FeatureFlag/FeatureFlag.cs b/src/Microsoft.TestPlatform.CoreUtilities/FeatureFlag/FeatureFlag.cs index bfe9114704..41ba1e26d8 100644 --- a/src/Microsoft.TestPlatform.CoreUtilities/FeatureFlag/FeatureFlag.cs +++ b/src/Microsoft.TestPlatform.CoreUtilities/FeatureFlag/FeatureFlag.cs @@ -44,6 +44,9 @@ private FeatureFlag() { } // It can be useful if we need to restore old UX in case users are parsing the console output. // Added in 17.2-preview 7.0-preview public const string DISABLE_ARTIFACTS_POSTPROCESSING_NEW_SDK_UX = VSTEST_ + nameof(DISABLE_ARTIFACTS_POSTPROCESSING_NEW_SDK_UX); + + // Faster JSON serialization relies on less internals of NewtonsoftJson, and on some additional caching. + public const string DISABLE_FASTER_JSON_SERIALIZATION = VSTEST_ + nameof(DISABLE_FASTER_JSON_SERIALIZATION); } #endif diff --git a/src/Microsoft.TestPlatform.CoreUtilities/Friends.cs b/src/Microsoft.TestPlatform.CoreUtilities/Friends.cs index 3b639307e1..fd7d102a35 100644 --- a/src/Microsoft.TestPlatform.CoreUtilities/Friends.cs +++ b/src/Microsoft.TestPlatform.CoreUtilities/Friends.cs @@ -5,3 +5,5 @@ [assembly: InternalsVisibleTo("vstest.console, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] [assembly: InternalsVisibleTo("vstest.console.arm64, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.TestPlatform.CommunicationUtilities, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.TestPlatform.ObjectModel, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelDiscoveryEventsHandler.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelDiscoveryEventsHandler.cs index 1dcef2b1ec..fc1a4aaaf1 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelDiscoveryEventsHandler.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelDiscoveryEventsHandler.cs @@ -178,7 +178,7 @@ public void HandleLogMessage(TestMessageLevel level, string message) /// private void ConvertToRawMessageAndSend(string messageType, object payload) { - var rawMessage = _dataSerializer.SerializePayload(messageType, payload); + var rawMessage = _dataSerializer.SerializePayload(messageType, payload, _requestData.ProtocolConfig.Version); _actualDiscoveryEventsHandler.HandleRawMessage(rawMessage); } diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunEventsHandler.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunEventsHandler.cs index 41c71266ed..f9ffec2171 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunEventsHandler.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunEventsHandler.cs @@ -195,7 +195,7 @@ public bool AttachDebuggerToProcess(int pid) private void ConvertToRawMessageAndSend(string messageType, object payload) { - var rawMessage = _dataSerializer.SerializePayload(messageType, payload); + var rawMessage = _dataSerializer.SerializePayload(messageType, payload, _requestData.ProtocolConfig.Version); _actualRunEventsHandler.HandleRawMessage(rawMessage); } } diff --git a/src/Microsoft.TestPlatform.ObjectModel/TestObject.cs b/src/Microsoft.TestPlatform.ObjectModel/TestObject.cs index 4482357034..45ed22d18c 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/TestObject.cs +++ b/src/Microsoft.TestPlatform.ObjectModel/TestObject.cs @@ -214,7 +214,7 @@ public void SetPropertyValue(TestProperty property!!, LazyPropertyValue va /// protected virtual object ProtectedGetPropertyValue(TestProperty property!!, object defaultValue) { - if (!_store.TryGetValue(property, out var value)) + if (!_store.TryGetValue(property, out var value) || value == null) { value = defaultValue; } diff --git a/src/Microsoft.TestPlatform.ObjectModel/TestProperty/CustomKeyValueConverter.cs b/src/Microsoft.TestPlatform.ObjectModel/TestProperty/CustomKeyValueConverter.cs index 6b0567ea77..3098d1ef4c 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/TestProperty/CustomKeyValueConverter.cs +++ b/src/Microsoft.TestPlatform.ObjectModel/TestProperty/CustomKeyValueConverter.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Runtime.Serialization.Json; #nullable disable @@ -18,6 +19,8 @@ namespace Microsoft.VisualStudio.TestPlatform.ObjectModel; /// internal class CustomKeyValueConverter : TypeConverter { + private readonly DataContractJsonSerializer _serializer = new(typeof(TraitObject[])); + /// public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { @@ -38,12 +41,13 @@ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo c // KeyValuePairs are used for traits. if (value is string data) { + // PERF: The values returned here can possibly be cached, but the benefits are very small speed wise, + // and it is unclear how many distinct objects we get, and how much memory this would consume. I was seeing around 100ms improvement on 10k tests. + using var stream = new MemoryStream(Encoding.Unicode.GetBytes(data)); // Converting Json data to array of KeyValuePairs with duplicate keys. - var serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(TraitObject[])); - var listOfTraitObjects = serializer.ReadObject(stream) as TraitObject[]; - - return listOfTraitObjects.Select(i => new KeyValuePair(i.Key, i.Value)).ToArray(); + var listOfTraitObjects = _serializer.ReadObject(stream) as TraitObject[]; + return listOfTraitObjects?.Select(i => new KeyValuePair(i.Key, i.Value)).ToArray() ?? new KeyValuePair[0]; } return null; diff --git a/src/Microsoft.TestPlatform.ObjectModel/TestProperty/CustomStringArrayConverter.cs b/src/Microsoft.TestPlatform.ObjectModel/TestProperty/CustomStringArrayConverter.cs index bd0690db9a..2799f2a982 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/TestProperty/CustomStringArrayConverter.cs +++ b/src/Microsoft.TestPlatform.ObjectModel/TestProperty/CustomStringArrayConverter.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Globalization; using System.IO; +using System.Runtime.Serialization.Json; using System.Text; #nullable disable @@ -13,6 +14,8 @@ namespace Microsoft.VisualStudio.TestPlatform.ObjectModel; internal class CustomStringArrayConverter : TypeConverter { + private readonly DataContractJsonSerializer _serializer = new(typeof(string[])); + /// public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { @@ -28,13 +31,14 @@ public override object ConvertTo(ITypeDescriptorContext context, CultureInfo cul /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { + // PERF: The strings returned here can possibly be cached, but the benefits are not huge speed wise, + // and it is unclear how many distinct strings we get, and how much memory this would consume. I was seeing around 200ms improvement on 10k tests. + // String[] are used by adapters. E.g. TestCategory[] if (value is string data) { using var stream = new MemoryStream(Encoding.Unicode.GetBytes(data)); - var serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(string[])); - var strings = serializer.ReadObject(stream) as string[]; - + var strings = _serializer.ReadObject(stream) as string[]; return strings; } diff --git a/src/Microsoft.TestPlatform.ObjectModel/TestProperty/TestProperty.cs b/src/Microsoft.TestPlatform.ObjectModel/TestProperty/TestProperty.cs index 5d4064a4b6..fae3e581f4 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/TestProperty/TestProperty.cs +++ b/src/Microsoft.TestPlatform.ObjectModel/TestProperty/TestProperty.cs @@ -7,6 +7,10 @@ using System.Reflection; using System.Runtime.Serialization; +#if !NETSTANDARD1_0 +using Microsoft.VisualStudio.TestPlatform.Utilities; +#endif + #nullable disable namespace Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -17,6 +21,15 @@ namespace Microsoft.VisualStudio.TestPlatform.ObjectModel; public class TestProperty : IEquatable { private Type _valueType; + private static readonly Dictionary TypeCache = new(); + +#if NETSTANDARD1_0 + private static bool DisableFastJson { get; set; } = true; +#else + private static bool DisableFastJson { get; set; } = FeatureFlag.Instance.IsSet(FeatureFlag.DISABLE_FASTER_JSON_SERIALIZATION); +#endif + + //public static Stopwatch /// /// Initializes a new instance of the class. @@ -157,6 +170,11 @@ public Type GetValueType() private Type GetType(string typeName!!) { + if (!DisableFastJson && TypeCache.TryGetValue(typeName, out var t)) + { + return t; + } + Type type = null; try @@ -164,6 +182,15 @@ private Type GetType(string typeName!!) // This only works for the type is in the currently executing assembly or in Mscorlib.dll. type = Type.GetType(typeName); + if (!DisableFastJson) + { + if (type != null) + { + TypeCache[typeName] = type; + return type; + } + } + if (type == null) { type = Type.GetType(typeName.Replace("Version=4.0.0.0", "Version=2.0.0.0")); // Try 2.0 version as discovery returns version of 4.0 for all cases @@ -225,6 +252,10 @@ private Type GetType(string typeName!!) } } + if (!DisableFastJson) + { + TypeCache[typeName] = type; + } return type; } diff --git a/src/Microsoft.TestPlatform.PlatformAbstractions/net451/System/ProcessHelper.cs b/src/Microsoft.TestPlatform.PlatformAbstractions/net451/System/ProcessHelper.cs index 6d9dbdc672..1dc87a5e3f 100644 --- a/src/Microsoft.TestPlatform.PlatformAbstractions/net451/System/ProcessHelper.cs +++ b/src/Microsoft.TestPlatform.PlatformAbstractions/net451/System/ProcessHelper.cs @@ -16,6 +16,8 @@ namespace Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; public partial class ProcessHelper : IProcessHelper { + private PlatformArchitecture? _currentProcessArchitecture; + /// public string GetCurrentProcessLocation() => Path.GetDirectoryName(GetCurrentProcessFileName()); @@ -26,7 +28,7 @@ public IntPtr GetProcessHandle(int processId) /// public PlatformArchitecture GetCurrentProcessArchitecture() - => GetProcessArchitecture(Process.GetCurrentProcess().Id); + => _currentProcessArchitecture ??= GetProcessArchitecture(Process.GetCurrentProcess().Id); public PlatformArchitecture GetProcessArchitecture(int processId) diff --git a/test/Microsoft.TestPlatform.CommunicationUtilities.PlatformTests/SocketCommunicationManagerTests.cs b/test/Microsoft.TestPlatform.CommunicationUtilities.PlatformTests/SocketCommunicationManagerTests.cs index 2fc7a16c25..fc376debf3 100644 --- a/test/Microsoft.TestPlatform.CommunicationUtilities.PlatformTests/SocketCommunicationManagerTests.cs +++ b/test/Microsoft.TestPlatform.CommunicationUtilities.PlatformTests/SocketCommunicationManagerTests.cs @@ -221,7 +221,7 @@ public async Task SendMessageWithRawMessageShouldNotSerializeThePayload() #region Message receiver tests [TestMethod] - public async Task ReceiveMessageShouldReceiveDeserializedMessage() + public async Task ReceiveMessageShouldReadMessageTypeButNotDeserializeThePayload() { var client = await StartServerAndWaitForConnection(); WriteToStream(client.GetStream(), TestDiscoveryStartMessageWithDummyPayload); @@ -229,7 +229,10 @@ public async Task ReceiveMessageShouldReceiveDeserializedMessage() var message = _communicationManager.ReceiveMessage(); Assert.AreEqual(MessageType.StartDiscovery, message.MessageType); - Assert.AreEqual(DummyPayload, message.Payload); + // Payload property is present on the Message, but we don't populate it in the newer versions, + // instead we populate internal field with the rawMessage, and wait until Serializer.DeserializePayload(message) + // is called by the message consumer. This avoids deserializing the payload when we just want to route the message. + Assert.IsNull(message.Payload); } [TestMethod] @@ -241,8 +244,11 @@ public async Task ReceiveMessageAsyncShouldReceiveDeserializedMessage() var message = await _communicationManager.ReceiveMessageAsync(CancellationToken.None); var versionedMessage = (VersionedMessage)message; Assert.AreEqual(MessageType.StartDiscovery, versionedMessage.MessageType); - Assert.AreEqual(DummyPayload, versionedMessage.Payload); Assert.AreEqual(2, versionedMessage.Version); + // Payload property is present on the Message, but we don't populate it in the newer versions, + // instead we populate internal field with the rawMessage, and wait until Serializer.DeserializePayload(message) + // is called by the message consumer. This avoids deserializing the payload when we just want to route the message. + Assert.IsNull(versionedMessage.Payload); } [TestMethod] diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelDiscoveryEventsHandlerTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelDiscoveryEventsHandlerTests.cs index d2ad73654d..7baa430ab8 100644 --- a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelDiscoveryEventsHandlerTests.cs +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelDiscoveryEventsHandlerTests.cs @@ -27,6 +27,7 @@ public class ParallelDiscoveryEventsHandlerTests private readonly Mock _mockParallelProxyDiscoveryManager; private readonly Mock _mockDataSerializer; private readonly Mock _mockRequestData; + private readonly int _protocolVersion = 1; public ParallelDiscoveryEventsHandlerTests() { @@ -36,6 +37,7 @@ public ParallelDiscoveryEventsHandlerTests() _mockDataSerializer = new Mock(); _mockRequestData = new Mock(); _mockRequestData.Setup(rd => rd.MetricsCollection).Returns(new NoOpMetricsCollection()); + _mockRequestData.Setup(rd => rd.ProtocolConfig).Returns(new ProtocolConfig { Version = _protocolVersion }); _parallelDiscoveryEventsHandler = new ParallelDiscoveryEventsHandler(_mockRequestData.Object, _mockProxyDiscoveryManager.Object, _mockTestDiscoveryEventsHandler.Object, _mockParallelProxyDiscoveryManager.Object, @@ -71,7 +73,7 @@ public void HandleDiscoveryCompleteShouldCallLastChunkResultsIfPresent() bool aborted = false; var lastChunk = new List(); - _mockDataSerializer.Setup(mds => mds.SerializePayload(MessageType.TestCasesFound, lastChunk)) + _mockDataSerializer.Setup(mds => mds.SerializePayload(MessageType.TestCasesFound, lastChunk, _protocolVersion)) .Returns(payload); _mockParallelProxyDiscoveryManager.Setup(mp => mp.HandlePartialDiscoveryComplete( diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelProxyDiscoveryManagerTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelProxyDiscoveryManagerTests.cs index f72448a993..029c20f860 100644 --- a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelProxyDiscoveryManagerTests.cs +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelProxyDiscoveryManagerTests.cs @@ -52,6 +52,7 @@ public ParallelProxyDiscoveryManagerTests() _discoveryCompleted = new ManualResetEventSlim(false); _mockRequestData = new Mock(); _mockRequestData.Setup(rd => rd.MetricsCollection).Returns(new NoOpMetricsCollection()); + _mockRequestData.Setup(rd => rd.ProtocolConfig).Returns(new ProtocolConfig()); } [TestMethod] diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelProxyExecutionManagerTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelProxyExecutionManagerTests.cs index bb41794e78..345bcf48f9 100644 --- a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelProxyExecutionManagerTests.cs +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelProxyExecutionManagerTests.cs @@ -72,6 +72,7 @@ public ParallelProxyExecutionManagerTests() _testRunCriteriaWithTests = new TestRunCriteria(_testCases, 100); _mockRequestData = new Mock(); _mockRequestData.Setup(rd => rd.MetricsCollection).Returns(new NoOpMetricsCollection()); + _mockRequestData.Setup(rd => rd.ProtocolConfig).Returns(new ProtocolConfig()); } [TestMethod] diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelRunEventsHandlerTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelRunEventsHandlerTests.cs index c7e71623d0..1bdb4387ad 100644 --- a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelRunEventsHandlerTests.cs +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/Parallel/ParallelRunEventsHandlerTests.cs @@ -28,6 +28,7 @@ public class ParallelRunEventsHandlerTests private readonly Mock _mockParallelProxyExecutionManager; private readonly Mock _mockDataSerializer; private readonly Mock _mockRequestData; + private readonly int _protocolVersion; public ParallelRunEventsHandlerTests() { @@ -37,6 +38,8 @@ public ParallelRunEventsHandlerTests() _mockDataSerializer = new Mock(); _mockRequestData = new Mock(); _mockRequestData.Setup(rd => rd.MetricsCollection).Returns(new NoOpMetricsCollection()); + _protocolVersion = 0; + _mockRequestData.Setup(rd => rd.ProtocolConfig).Returns(new ProtocolConfig { Version = _protocolVersion }); _parallelRunEventsHandler = new ParallelRunEventsHandler(_mockRequestData.Object, _mockProxyExecutionManager.Object, _mockTestRunEventsHandler.Object, _mockParallelProxyExecutionManager.Object, @@ -133,7 +136,7 @@ public void HandleRunCompleteShouldCallLastChunkResultsIfPresent() var lastChunk = new TestRunChangedEventArgs(null, null, null); var completeArgs = new TestRunCompleteEventArgs(null, false, false, null, null, null, TimeSpan.Zero); - _mockDataSerializer.Setup(mds => mds.SerializePayload(MessageType.TestRunStatsChange, lastChunk)) + _mockDataSerializer.Setup(mds => mds.SerializePayload(MessageType.TestRunStatsChange, lastChunk, _protocolVersion)) .Returns(payload); _mockParallelProxyExecutionManager.Setup(mp => mp.HandlePartialRunComplete( diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/DataCollection/ParallelDataCollectionEventsHandlerTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/DataCollection/ParallelDataCollectionEventsHandlerTests.cs index 76abe61c29..ee082cf680 100644 --- a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/DataCollection/ParallelDataCollectionEventsHandlerTests.cs +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/DataCollection/ParallelDataCollectionEventsHandlerTests.cs @@ -38,6 +38,7 @@ public class ParallelDataCollectionEventsHandlerTests public ParallelDataCollectionEventsHandlerTests() { _mockRequestData = new Mock(); + _mockRequestData.Setup(r => r.ProtocolConfig).Returns(new ProtocolConfig()); _mockProxyExecutionManager = new Mock(); _mockTestRunEventsHandler = new Mock(); _mockParallelProxyExecutionManager = new Mock(); diff --git a/test/TestAssets/performance/MSTest10kPassing/MSTest10kPassing.csproj b/test/TestAssets/performance/MSTest10kPassing/MSTest10kPassing.csproj index 99073005ba..ee8d912987 100644 --- a/test/TestAssets/performance/MSTest10kPassing/MSTest10kPassing.csproj +++ b/test/TestAssets/performance/MSTest10kPassing/MSTest10kPassing.csproj @@ -1,8 +1,8 @@ - + - net6.0;net48 + net6.0;net48;net472 netcoreapp3.1 false false diff --git a/test/TestAssets/performance/Perfy.TestAdapter/Perfy.cs b/test/TestAssets/performance/Perfy.TestAdapter/Perfy.cs index 39b4efa38a..b9b5954954 100644 --- a/test/TestAssets/performance/Perfy.TestAdapter/Perfy.cs +++ b/test/TestAssets/performance/Perfy.TestAdapter/Perfy.cs @@ -24,7 +24,7 @@ static Perfy() { // No meaning to the number, it is just easy to find when it breaks. Better than returning 1000 or 0 which are both // less suspicious. It could throw, but that is bad for interactive debugging. - Count = int.TryParse(Environment.GetEnvironmentVariable("TEST_COUNT") ?? "356", out var count) ? count : 356; + Count = int.TryParse(Environment.GetEnvironmentVariable("TEST_COUNT") ?? "10000", out var count) ? count : 356; } public const string Id = "executor://perfy.testadapter";