Skip to content
Merged
2 changes: 1 addition & 1 deletion scripts/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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

/// <summary>
Expand All @@ -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();

Expand All @@ -68,7 +89,33 @@ private JsonDataSerializer()
/// <returns>A <see cref="Message"/> instance.</returns>
public Message DeserializeMessage(string rawMessage)
{
return Deserialize<VersionedMessage>(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<VersionedMessage>(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<MessageHeader>(rawMessage, s_jsonSettings);
version = header.Version;
messageType = header.MessageType;
}

var message = new VersionedMessageWithRawMessage
{
Version = version,
MessageType = messageType,
RawMessage = rawMessage,
};

return message;
}

/// <summary>
Expand All @@ -79,9 +126,145 @@ public Message DeserializeMessage(string rawMessage)
/// <returns>The deserialized payload.</returns>
public T DeserializePayload<T>(Message message)
{
var versionedMessage = message as VersionedMessage;
var payloadSerializer = GetPayloadSerializer(versionedMessage?.Version);
return Deserialize<T>(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<T>(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<T>(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<PayloadedMessage<T>>(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<T>(payloadSerializer, Deserialize<Message>(rawMessage).Payload);
}
}

private bool FastHeaderParse(string rawMessage, out int version, out string messageType)
{
// PERF: This can be also done slightly better using ReadOnlySpan<char> 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;
}
}

/// <summary>
/// Try getting substring until a given delimiter, but don't search more characters than maxSearchLength.
/// </summary>
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;
}

/// <summary>
Expand Down Expand Up @@ -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);
}
}

/// <summary>
Expand Down Expand Up @@ -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."),
};
}

/// <summary>
/// Just the header from versioned messages, to avoid touching the Payload when we deserialize message.
/// </summary>
private class MessageHeader
{
public int Version { get; set; }
public string MessageType { get; set; }
}

/// <summary>
/// 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.
/// </summary>
private class VersionedMessageWithRawMessage : VersionedMessage
{
public string RawMessage { get; set; }
}

/// <summary>
/// This grabs payload from the message, we already know version and message type.
/// </summary>
/// <typeparam name="T"></typeparam>
private class PayloadedMessage<T>
{
public T Payload { get; set; }
}

/// <summary>
/// For serialization directly into string, without first converting to JToken, and then from JToken to string.
/// </summary>
private class VersionedMessageForSerialization
{
/// <summary>
/// Gets or sets the version of the message
/// </summary>
public int Version { get; set; }

/// <summary>
/// Gets or sets the message type.
/// </summary>
public string MessageType { get; set; }

/// <summary>
/// Gets or sets the payload.
/// </summary>
public object Payload { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// <summary>
/// JSON contract resolver for mapping test platform types.
/// </summary>
internal class DefaultTestPlatformContractResolver7 : DefaultContractResolver
{
public DefaultTestPlatformContractResolver7()
{
}
/// <inheritdoc/>
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);

if (typeof(List<KeyValuePair<TestProperty, object>>) == 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;
}
}
Loading