Skip to content

Commit

Permalink
Optimize progress reporting by reusing StringBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma committed Apr 26, 2023
1 parent 1ce1ae4 commit ba181eb
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 179 deletions.
127 changes: 11 additions & 116 deletions src/NUnitFramework/framework/Interfaces/TNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
Expand All @@ -23,11 +22,6 @@ namespace NUnit.Framework.Interfaces
[DebuggerDisplay("{OuterXml}")]
public sealed class TNode
{
private static readonly XmlWriterSettings _xmlWriterSettings = new()
{
ConformanceLevel = ConformanceLevel.Fragment
};

internal List<TNode>? _childNodes;
internal Dictionary<string, string>? _attributes;

Expand Down Expand Up @@ -58,7 +52,7 @@ public TNode(string name)
public TNode(string name, string? value, bool valueIsCDATA)
: this(name)
{
Value = EscapeInvalidXmlCharacters(value);
Value = XmlExtensions.EscapeInvalidXmlCharacters(value);
ValueIsCDATA = valueIsCDATA;
}

Expand Down Expand Up @@ -96,15 +90,21 @@ public TNode(string name, string? value, bool valueIsCDATA)
/// </summary>
public TNode? FirstChild => _childNodes?.Count == 0 ? null : ChildNodes[0];

[ThreadStatic]
private static StringBuilder? _outerXmlStringBuilder;

/// <summary>
/// Gets the XML representation of this node.
/// </summary>
public string OuterXml
{
get
{
using var stringWriter = new StringWriter();
using (var xmlWriter = XmlWriter.Create(stringWriter, _xmlWriterSettings))
var stringBuilder = _outerXmlStringBuilder ??= new StringBuilder();
stringBuilder.Clear();

using var stringWriter = new StringWriter(stringBuilder);
using (var xmlWriter = XmlWriter.Create(stringWriter, XmlExtensions.FragmentWriterSettings))
{
WriteTo(xmlWriter);
}
Expand Down Expand Up @@ -267,7 +267,7 @@ public void InsertChildNode(int index, TNode node)
public void AddAttribute(string name, string value)
{
_attributes ??= new Dictionary<string, string>();
_attributes.Add(name, EscapeInvalidXmlCharacters(value));
_attributes.Add(name, XmlExtensions.EscapeInvalidXmlCharacters(value));
}

/// <summary>
Expand Down Expand Up @@ -312,7 +312,7 @@ public void WriteTo(XmlWriter writer)

if (Value != null)
if (ValueIsCDATA)
WriteCDataTo(writer);
writer.WriteCDataSafe(Value);
else
writer.WriteString(Value);

Expand Down Expand Up @@ -385,111 +385,6 @@ private static List<TNode> ApplySelection(List<TNode> nodeList, string xpath)
: resultNodes;
}

[return: NotNullIfNotNull("str")]
private static string? EscapeInvalidXmlCharacters(string? str)
{
if (str == null) return null;

// quick check when we expect valid input
foreach (var c in str)
{
if (c < 0x20 || c > 0x7F)
{
return EscapeInvalidXmlCharactersUnlikely(str);
}
}

return str;
}

private static string EscapeInvalidXmlCharactersUnlikely(string str)
{
StringBuilder? builder = null;
for (int i = 0; i < str.Length; i++)
{
char c = str[i];
if(c > 0x20 && c < 0x7F)
{
// ASCII characters - break quickly for these
builder?.Append(c);
}
// From the XML specification: https://www.w3.org/TR/xml/#charsets
// Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
// Any Unicode character, excluding the surrogate blocks, FFFE, and FFFF.
else if (!(0x0 <= c && c <= 0x8) &&
c != 0xB &&
c != 0xC &&
!(0xE <= c && c <= 0x1F) &&
!(0x7F <= c && c <= 0x84) &&
!(0x86 <= c && c <= 0x9F) &&
!(0xD800 <= c && c <= 0xDFFF) &&
c != 0xFFFE &&
c != 0xFFFF)
{
builder?.Append(c);
}
// Also check if the char is actually a high/low surrogate pair of two characters.
// If it is, then it is a valid XML character (from above based on the surrogate blocks).
else if (char.IsHighSurrogate(c) &&
i + 1 != str.Length &&
char.IsLowSurrogate(str[i + 1]))
{
if (builder != null)
{
builder.Append(c);
builder.Append(str[i + 1]);
}
i++;
}
else
{
// We keep the builder null so that we don't allocate a string
// when doing this conversion until we encounter a unicode character.
// Then, we allocate the rest of the string and escape the invalid
// character.
if (builder == null)
{
builder = new StringBuilder();
for (int index = 0; index < i; index++)
builder.Append(str[index]);
}
builder.Append(CharToUnicodeSequence(c));
}
}

if (builder != null)
return builder.ToString();
else
return str;
}

private static string CharToUnicodeSequence(char symbol)
{
return $"\\u{(int)symbol:x4}";
}

private void WriteCDataTo(XmlWriter writer)
{
int start = 0;
string text = Value ?? throw new InvalidOperationException();

while (true)
{
int illegal = text.IndexOf("]]>", start, StringComparison.Ordinal);
if (illegal < 0)
break;
writer.WriteCData(text.Substring(start, illegal - start + 2));
start = illegal + 2;
if (start >= text.Length)
return;
}

if (start > 0)
writer.WriteCData(text.Substring(start));
else
writer.WriteCData(text);
}

#endregion

#region Nested NodeFilter class
Expand Down
39 changes: 22 additions & 17 deletions src/NUnitFramework/framework/Interfaces/TestMessage.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt

using System;
using System.Diagnostics;
using System.IO;
using System.Xml;

namespace NUnit.Framework.Interfaces
{
Expand All @@ -18,17 +19,10 @@ public sealed class TestMessage
/// <param name="destination">Destination of the message</param>
/// <param name="text">Text to be sent</param>
/// <param name="testId">ID of the test that produced the message</param>
public TestMessage(string destination, string text, string testId)
public TestMessage(string destination, string text, string? testId)
{
if (destination == null)
{
throw new ArgumentNullException(nameof(destination));
}

if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
Guard.ArgumentNotNull(destination, nameof(destination));
Guard.ArgumentNotNull(text, nameof(text));

Destination = destination;
Message = text;
Expand Down Expand Up @@ -56,22 +50,33 @@ public override string ToString()
/// <summary>
/// The ID of the test that sent the message
/// </summary>
public string TestId { get; }
public string? TestId { get; }

/// <summary>
/// Returns the XML representation of the <see cref="TestMessage"/> object.
/// </summary>
public string ToXml()
{
TNode tnode = new TNode("test-message", Message, true);
using var stringWriter = new StringWriter();
using (var writer = XmlWriter.Create(stringWriter, XmlExtensions.FragmentWriterSettings))
{
ToXml(writer);
}
return stringWriter.ToString();
}

if (Destination != null)
tnode.AddAttribute("destination", Destination);
internal void ToXml(XmlWriter writer)
{
writer.WriteStartElement("test-message");

writer.WriteAttributeString("destination", Destination);

if (TestId != null)
tnode.AddAttribute("testid", TestId);
writer.WriteAttributeString("testid", TestId);

writer.WriteCDataSafe(Message);

return tnode.OuterXml;
writer.WriteEndElement();
}
}
}
32 changes: 25 additions & 7 deletions src/NUnitFramework/framework/Interfaces/TestOutput.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt

using System.IO;
using System.Xml;

namespace NUnit.Framework.Interfaces
{
/// <summary>
/// The TestOutput class holds a unit of output from
/// a test to a specific output stream
/// </summary>
public class TestOutput
{
public class TestOutput
{
/// <summary>
/// Construct with text, output destination type and
/// the name of the test that produced the output.
Expand All @@ -18,6 +21,9 @@ public class TestOutput
/// <param name="testName">FullName of test that produced the output</param>
public TestOutput(string text, string stream, string? testId, string? testName)
{
Guard.ArgumentNotNull(text, nameof(text));
Guard.ArgumentNotNull(stream, nameof(stream));

Text = text;
Stream = stream;
TestId = testId;
Expand Down Expand Up @@ -58,16 +64,28 @@ public override string ToString()
/// </summary>
public string ToXml()
{
TNode tnode = new TNode("test-output", Text, true);
using var stringWriter = new StringWriter();
using (var writer = XmlWriter.Create(stringWriter, XmlExtensions.FragmentWriterSettings))
{
ToXml(writer);
}
return stringWriter.ToString();
}

internal void ToXml(XmlWriter writer)
{
writer.WriteStartElement("test-output");
writer.WriteAttributeString("stream", Stream);

tnode.AddAttribute("stream", Stream);
if (TestId != null)
tnode.AddAttribute("testid", TestId);
writer.WriteAttributeString("testid", TestId);

if (TestName != null)
tnode.AddAttribute("testname", TestName);
writer.WriteAttributeStringSafe("testname", TestName);

writer.WriteCDataSafe(Text);

return tnode.OuterXml;
writer.WriteEndElement();
}
}
}
Loading

0 comments on commit ba181eb

Please sign in to comment.