Skip to content

Commit 3c37047

Browse files
authored
Merge pull request #1338 from lahma/optimize-WorkItemTracker
Optimize WorkItemTracker
2 parents 8f472b0 + e065acf commit 3c37047

File tree

1 file changed

+113
-50
lines changed

1 file changed

+113
-50
lines changed
Lines changed: 113 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
22

3+
using System;
34
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Text;
47
using System.Threading;
58
using System.Xml;
6-
using NUnit.Engine.Internal;
79

810
namespace NUnit.Engine.Runners
911
{
@@ -25,18 +27,60 @@ namespace NUnit.Engine.Runners
2527
/// Once the test has been cancelled, it provide notifications to the runner
2628
/// so the information may be displayed.
2729
/// </summary>
28-
internal class WorkItemTracker : ITestEventListener
30+
internal sealed class WorkItemTracker : ITestEventListener
2931
{
30-
private List<XmlNode> _itemsInProcess = new List<XmlNode>();
31-
private ManualResetEvent _allItemsComplete = new ManualResetEvent(false);
32-
private object _trackerLock = new object();
33-
32+
/// <summary>
33+
/// Holds data about recorded test that started.
34+
/// </summary>
35+
private sealed class InProgressItem : IComparable<InProgressItem>
36+
{
37+
private readonly int _order;
38+
39+
public InProgressItem(int order, string name, XmlReader reader)
40+
{
41+
_order = order;
42+
Name = name;
43+
44+
var attributeCount = reader.AttributeCount;
45+
Properties = new Dictionary<string, string>(attributeCount);
46+
for (var i = 0; i < attributeCount; i++)
47+
{
48+
reader.MoveToNextAttribute();
49+
Properties.Add(reader.Name, reader.Value);
50+
}
51+
}
52+
53+
public string Name { get; }
54+
public Dictionary<string, string> Properties { get; }
55+
56+
public int CompareTo(InProgressItem other)
57+
{
58+
// for signaling purposes, return in reverse order
59+
return _order.CompareTo(other._order) * -1;
60+
}
61+
}
62+
63+
// items are keyed by id
64+
private readonly Dictionary<string, InProgressItem> _itemsInProcess = new Dictionary<string, InProgressItem>();
65+
private readonly ManualResetEvent _allItemsComplete = new ManualResetEvent(false);
66+
private readonly object _trackerLock = new object();
67+
68+
// incrementing ordering id for work items so we can traverse in correct order
69+
private int _itemOrderNumberCounter = 1;
70+
71+
// when sending thousands of cancelled notifications, it makes sense to reuse string builder, used inside a lock
72+
private readonly StringBuilder _notificationBuilder = new StringBuilder();
73+
74+
// we want to write just the main element without XML declarations
75+
private static readonly XmlWriterSettings XmlWriterSettings = new XmlWriterSettings { ConformanceLevel = ConformanceLevel.Fragment };
76+
3477
public void Clear()
3578
{
3679
lock (_trackerLock)
3780
{
3881
_itemsInProcess.Clear();
3982
_allItemsComplete.Reset();
83+
_itemOrderNumberCounter = 1;
4084
}
4185
}
4286

@@ -49,66 +93,85 @@ public void SendPendingTestCompletionEvents(ITestEventListener listener)
4993
{
5094
lock (_trackerLock)
5195
{
52-
int count = _itemsInProcess.Count;
53-
5496
// Signal completion of all pending suites, in reverse order
55-
while (count > 0)
56-
listener.OnTestEvent(CreateNotification(_itemsInProcess[--count]));
57-
}
58-
}
97+
var toNotify = new List<InProgressItem>(_itemsInProcess.Values);
98+
toNotify.Sort();
5999

60-
private static string CreateNotification(XmlNode startElement)
61-
{
62-
bool isSuite = startElement.Name == "start-suite";
63-
64-
XmlNode notification = XmlHelper.CreateTopLevelElement(isSuite ? "test-suite" : "test-case");
65-
if (isSuite)
66-
notification.AddAttribute("type", startElement.GetAttribute("type"));
67-
notification.AddAttribute("id", startElement.GetAttribute("id"));
68-
notification.AddAttribute("name", startElement.GetAttribute("name"));
69-
notification.AddAttribute("fullname", startElement.GetAttribute("fullname"));
70-
notification.AddAttribute("result", "Failed");
71-
notification.AddAttribute("label", "Cancelled");
72-
XmlNode failure = notification.AddElement("failure");
73-
XmlNode message = failure.AddElementWithCDataSection("message", "Test run cancelled by user");
74-
return notification.OuterXml;
100+
foreach (var item in toNotify)
101+
listener.OnTestEvent(CreateNotification(item));
102+
}
75103
}
76104

77-
void ITestEventListener.OnTestEvent(string report)
105+
private string CreateNotification(InProgressItem item)
78106
{
79-
XmlNode xmlNode = XmlHelper.CreateXmlNode(report);
107+
_notificationBuilder.Clear();
80108

81-
lock (_trackerLock)
109+
using (var stringWriter = new StringWriter(_notificationBuilder))
82110
{
83-
switch (xmlNode.Name)
111+
using (var writer = XmlWriter.Create(stringWriter, XmlWriterSettings))
84112
{
85-
case "start-test":
86-
case "start-suite":
87-
_itemsInProcess.Add(xmlNode);
88-
break;
89-
90-
case "test-case":
91-
case "test-suite":
92-
string id = xmlNode.GetAttribute("id");
93-
RemoveItem(id);
94-
95-
if (_itemsInProcess.Count == 0)
96-
_allItemsComplete.Set();
97-
break;
113+
bool isSuite = item.Name == "start-suite";
114+
writer.WriteStartElement(isSuite ? "test-suite" : "test-case");
115+
116+
if (isSuite)
117+
writer.WriteAttributeString("type", item.Properties["type"]);
118+
119+
writer.WriteAttributeString("id", item.Properties["id"]);
120+
writer.WriteAttributeString("name", item.Properties["name"]);
121+
writer.WriteAttributeString("fullname", item.Properties["fullname"]);
122+
writer.WriteAttributeString("result", "Failed");
123+
writer.WriteAttributeString("label", "Cancelled");
124+
125+
writer.WriteStartElement("failure");
126+
writer.WriteStartElement("message");
127+
writer.WriteCData("Test run cancelled by user");
128+
writer.WriteEndElement();
129+
writer.WriteEndElement();
130+
131+
writer.WriteEndElement();
98132
}
133+
134+
return stringWriter.ToString();
99135
}
100136
}
101137

102-
private void RemoveItem(string id)
138+
void ITestEventListener.OnTestEvent(string report)
103139
{
104-
foreach (XmlNode item in _itemsInProcess)
140+
using (var stringReader = new StringReader(report))
141+
using (var reader = XmlReader.Create(stringReader))
105142
{
106-
if (item.GetAttribute("id") == id)
143+
// go to starting point
144+
reader.MoveToContent();
145+
146+
if (reader.NodeType != XmlNodeType.Element)
147+
throw new InvalidOperationException("Expected to find root element");
148+
149+
lock (_trackerLock)
107150
{
108-
_itemsInProcess.Remove(item);
109-
return;
151+
var name = reader.Name;
152+
switch (name)
153+
{
154+
case "start-test":
155+
case "start-suite":
156+
var item = new InProgressItem(_itemOrderNumberCounter++, name, reader);
157+
_itemsInProcess.Add(item.Properties["id"], item);
158+
break;
159+
160+
case "test-case":
161+
case "test-suite":
162+
RemoveItem(reader.GetAttribute("id"));
163+
164+
if (_itemsInProcess.Count == 0)
165+
_allItemsComplete.Set();
166+
break;
167+
}
110168
}
111169
}
112170
}
171+
172+
private void RemoveItem(string id)
173+
{
174+
_itemsInProcess.Remove(id);
175+
}
113176
}
114-
}
177+
}

0 commit comments

Comments
 (0)