') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Remove XmlConvert.StrEqual and use Span<char>.StartsWith() instead. by TrayanZapryanov · Pull Request #74955 · dotnet/runtime · GitHub
Skip to content

Remove XmlConvert.StrEqual and use Span<char>.StartsWith() instead. - #74955

Merged
eiriktsarpalis merged 12 commits into
dotnet:mainfrom
TrayanZapryanov:improve_xmlconvert_strequal
Oct 7, 2022
Merged

Remove XmlConvert.StrEqual and use Span<char>.StartsWith() instead.#74955
eiriktsarpalis merged 12 commits into
dotnet:mainfrom
TrayanZapryanov:improve_xmlconvert_strequal

Conversation

@TrayanZapryanov

@TrayanZapryanovTrayanZapryanov commented Sep 1, 2022

Copy link
Copy Markdown
Contributor

Replace loop with Span.StartsWith.

On my machine following benchmark:

 private char[] data = " DOCTYPE abc".ToCharArray();
[Benchmark(Baseline = true)]
public bool Before() => StrEqual(data, 1, 7, "DOCTYPE");
[Benchmark]
public bool After() => StrEqual2(data, 1, 7, "DOCTYPE");
internal static bool StrEqual(char[] chars, int strPos1, int strLen1, string str2)
{
if (strLen1 != str2.Length)
{
return false;
}
Debug.Assert(chars != null);
int i = 0;
while (i < strLen1 && chars[strPos1 + i] == str2[i])
{
i++;
}
return i == strLen1;
}
internal static bool StrEqual2(char[] chars, int strPos1, int strLen1, string str2)
{
if (strLen1 != str2.Length)
{
return false;
}
Debug.Assert(chars != null);
return chars.AsSpan(strPos1, strLen1).StartsWith(str2);
}

produces following results:
BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19043.1889/21H1/May2021Update)
11th Gen Intel Core i9-11900K 3.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.2.22153.17
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2

MethodMeanRatioCode SizeAllocatedAlloc Ratio
Before3.599 ns1.0074 B-NA
After2.046 ns0.57391 B-NA

@ghostghost added area-System.Xml community-contribution Indicates that the PR has been added by a community member labels Sep 1, 2022
@ghost

ghost commented Sep 1, 2022

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-xml
See info in area-owners.md if you want to be subscribed.

Issue Details

Replace loop with Span.SequenceEqual.

On my machine following benchmark:

 private char[] data = " DOCTYPE abc".ToCharArray();
[Benchmark(Baseline = true)]
public bool Before() => StrEqual(data, 1, 7, "DOCTYPE");
[Benchmark]
public bool After() => StrEqual2(data, 1, 7, "DOCTYPE");
internal static bool StrEqual(char[] chars, int strPos1, int strLen1, string str2)
{
if (strLen1 != str2.Length)
{
return false;
}
Debug.Assert(chars != null);
int i = 0;
while (i < strLen1 && chars[strPos1 + i] == str2[i])
{
i++;
}
return i == strLen1;
}
internal static bool StrEqual2(char[] chars, int strPos1, int strLen1, string str2)
{
if (strLen1 != str2.Length)
{
return false;
}
Debug.Assert(chars != null);
return chars.AsSpan(strPos1, strLen1).SequenceEqual(str2.AsSpan());
}

produces following results:
BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19043.1889/21H1/May2021Update)
11th Gen Intel Core i9-11900K 3.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.2.22153.17
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2

MethodMeanRatioCode SizeAllocatedAlloc Ratio
Before4.057 ns1.0080 B-NA
After2.385 ns0.59395 B-NA
Author:TrayanZapryanov
Assignees:-
Labels:

area-System.Xml

Milestone:-

Comment threadsrc/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs Outdated
Replace with StartsWith as suggested.
@TrayanZapryanovTrayanZapryanov changed the title Use Span<char>.SequenceEqual instead if manual loop in XmlConvert.StrEqualRemove XmlConvert.StrEqual and use Span<char>.StartsWith() instead.Sep 1, 2022
@TrayanZapryanov

Copy link
Copy Markdown
ContributorAuthor

I think changes are ready for review. Failing tests are not related with changes.

Comment threadsrc/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImpl.cs Outdated
Comment threadsrc/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImpl.cs Outdated

@stephentoubstephentoub left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks good. Do we have tests that appropriately exercise these code paths? e.g. If you do a code coverage run, are all of these hit?

@TrayanZapryanov

Copy link
Copy Markdown
ContributorAuthor

It looks good. Do we have tests that appropriately exercise these code paths? e.g. If you do a code coverage run, are all of these hit?

All that I know is that on first commit several tests failed.
When I try VS "Analyze Code Coverage" menu from tests - cannot see code coverage of System.Xml.dll
image

If somebody tell me how to receive results, I can check.

@TrayanZapryanov

Copy link
Copy Markdown
ContributorAuthor

@stephentoub If possible to ask something not related to this PR?
I saw that there is NameTable class which is behaving as standard HashSet, but can search by string or char[].
My attempt was to replace it with HashSet<Node> and play with Node's equal methods.
It works, but when I benchmark - it says regression with ~ 30%.
The question is : Do you know any other trick that can squeeze few more nanoseconds :)

Here it is how my table looks like :

public class ImprovedNameTable
{
private readonly struct Node : IEquatable<Node>
{
public Node(string data)
{
Data = data;
_dataAsArray = null;
_start = 0;
_len = 0;
}
//Search ctor only
public Node(char[] dataAsArray, int start, int len)
{
Data = null;
_dataAsArray = dataAsArray;
_start = start;
_len = len;
}
public string Data { get; }
private readonly char[] _dataAsArray;
private readonly int _start;
private readonly int _len;
public bool Equals(Node other)
{
return other.Data?.Equals(Data) ?? other._dataAsArray.AsSpan(other._start, other._len).SequenceEqual(Data.AsSpan());
}
public override bool Equals(object obj)
{
return obj is Node n && Equals(n);
}
public override int GetHashCode()
{
return string.GetHashCode(Data != null ? Data.AsSpan() : _dataAsArray.AsSpan(_start, _len));
}
}
private readonly HashSet<Node> _items = new HashSet<Node>();
public string Add(string key)
{
ArgumentNullException.ThrowIfNull(key);
int len = key.Length;
if (len == 0)
{
return string.Empty;
}
var checkItem = new Node(key);
if (_items.TryGetValue(checkItem, out var actualValue))
return actualValue.Data;
_items.Add(checkItem);
return checkItem.Data;
}
public string Add(char[] key, int start, int len)
{
if (len == 0)
{
return string.Empty;
}
// Compatibility check to ensure same exception as previous versions
// independently of any exceptions throw by the hashing function.
// note that NullReferenceException is the first one if key is null.
if (start >= key.Length || start < 0 || (long)start + len > (long)key.Length)
{
throw new IndexOutOfRangeException();
}
// Compatibility check for len < 0, just throw the same exception as new string(key, start, len)
if (len < 0)
{
throw new ArgumentOutOfRangeException(nameof(len));
}
var checkItem = new Node(key, start, len);
if (_items.TryGetValue(checkItem, out var actualValue))
return actualValue.Data;
checkItem = new Node(new string(key, start, len));
_items.Add(checkItem);
return checkItem.Data;
}
}

@stephentoub

Copy link
Copy Markdown
Member

When I try VS "Analyze Code Coverage" menu from tests - cannot see code coverage of System.Xml.dll

dotnet build /t:test /p:Coverage=true

inside of the relevant test directory. Unfortunately the System.Private.Xml tests are a bit of a mess and are spread out over many test projects (I'd really like to see us consolidate them all into a single project, but that's not your problem 😄). @krwq, which is the right test project for this functionality?

The question is : Do you know any other trick that can squeeze few more nanoseconds :)

I'd need to profile it to see where the time is being spent :) But for starters in the case of it needing to add to the table, it's doing two lookups, one in TryGetValue and one in Add; I'm not sure what your test is actually exercising, but if it's the add path, I'd start there and find a way to reduce that to a single lookup rather than two.

@TrayanZapryanov

Copy link
Copy Markdown
ContributorAuthor

which is the right test project for this functionality?

I've check code coverage with System.Private.Xml\tests\XmlSerializer and System.Private.Xml\tests\XmlDocument and was able to find most of code paths covered, but for example changes in XmlTextReaderImpl.IncrementalRead are not executed there.

@stephentoub

Copy link
Copy Markdown
Member

but for example changes in XmlTextReaderImpl.IncrementalRead are not executed there

Thanks for checking. Is it possible to add tests that ensures they are covered?

@TrayanZapryanov

Copy link
Copy Markdown
ContributorAuthor

dotnet build /t:test /p:Coverage=true

I've tried to throw exception in

and then to run all tests in System.Private.Xml, but none of them failed. Looks like this is not covered or maybe some other module is using it. Unfortunately I am not so deep in the code to understand use case of it. If possible somebody to give me a hint of sample xml , and I will create tests for it. I suspect that this is something connected with binary data inside xml, or DataContract serializer is using it.

@TrayanZapryanov

Copy link
Copy Markdown
ContributorAuthor

@stephentoub First of all - thanks for consolidating xml tests.
I've run them and verified that IncrementalRead() is not covered.

privateintIncrementalRead(Arrayarray,intindex,intcount)

I will be glad to add tests, just need help from somebody to give me hint - when it is expected this method to be used.

@eiriktsarpaliseiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes LGTM. Thank you for your contribution! Couple of remarks:

  • Would it be possible to re-run your benchmark, just to validate that perf gains are preserved after review feedback changes?
  • Not very familiar with the codebase to suggest improvements to code coverage, perhaps @krwq can help. But I think we can merge this change without that.

@TrayanZapryanov

Copy link
Copy Markdown
ContributorAuthor

I've tried all benchmarks with Xml in it's name from performance repo and here it is the result:

Statistics

Total: 69
Same: 71.01 %
Slower: 1.45 %
Faster: 18.84 %
Noise: 8.70 %
Unknown: 0.00 %

Statistics per Architecture

ArchitectureSameSlowerFasterNoiseUnknown
X6471.01 %1.45 %18.84 %8.70 %0.00 %

Statistics per Operating System

Operating SystemSameSlowerFasterNoiseUnknown
Windows 1071.01 %1.45 %18.84 %8.70 %0.00 %

Statistics per Namespace

NamespaceSameSlowerFasterNoiseUnknown
MicroBenchmarks.Serializers70.59 %0.00 %29.41 %0.00 %0.00 %
Microsoft.Extensions.Configuration.Xml50.00 %0.00 %50.00 %0.00 %0.00 %
System.Xml.Linq68.42 %5.26 %5.26 %21.05 %0.00 %
System.Xml.Tests100.00 %0.00 %0.00 %0.00 %0.00 %
XmlDocumentTests.XmlDocumentTests100.00 %0.00 %0.00 %0.00 %0.00 %
XmlDocumentTests.XmlNodeListTests50.00 %0.00 %0.00 %50.00 %0.00 %
XmlDocumentTests.XmlNodeTests50.00 %0.00 %0.00 %50.00 %0.00 %

System.Xml.Linq.Perf_XElement.GetValue

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Slower33.4836.970.91+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_FromStream.DataContractSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Same1155.461169.640.99+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_FromStream.XmlSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Same1525.681552.640.98+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_FromStream.DataContractSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Same6778.216722.031.01+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_ToStream.DataContractSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster1113.151079.301.03+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_FromStream.XmlSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster2457.712361.261.04+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_FromStream.XmlSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster1806.911735.981.04+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_ToStream.DataContractSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster37360.9135667.791.05+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

Microsoft.Extensions.Configuration.Xml.XmlConfigurationProviderBenchmarks.Load(FileName: "names.xml")

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster83758.7478715.641.06+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_FromStream.XmlSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster348408.30326648.501.07+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_ToStream.DataContractSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster423996.80397174.841.07+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

System.Xml.Linq.Perf_XElementList.Enumerator

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster142.68133.261.07+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_ToStream.DataContractSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster528.73493.301.07+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHzseveral?

MicroBenchmarks.Serializers.Xml_FromStream.XmlSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster5321.294937.071.08+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_FromStream.XmlSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster50184.2346527.301.08-624Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

Microsoft.Extensions.Configuration.Xml.XmlConfigurationProviderBenchmarks.Load(FileName: "repeated.xml")

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster65864.4160819.091.08+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

MicroBenchmarks.Serializers.Xml_FromStream.XmlSerializer_

ResultBaseDiffRatioAlloc DeltaOperating SystemBitProcessor NameModality
Faster502496.12460189.701.09+0Windows 10X6411th Gen Intel Core i9-11900K 3.50GHz

@eiriktsarpalis

Copy link
Copy Markdown
Member

Thanks, I think this looks good. @stephentoub any objection to merging without added coverage?

Comment threadsrc/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImpl.cs Outdated
@stephentoub

Copy link
Copy Markdown
Member

any objection to merging without added coverage?

How do we know it didn't break something?

@stephentoub

stephentoub commented Sep 21, 2022

Copy link
Copy Markdown
Member

just need help from somebody to give me hint - when it is expected this method to be used

If you start from https://source.dot.net/#q=incrementalread and click on IncrementalRead in the left list, that will bring you to the source for that method. Then click on the method name in the code pane on the right, and the pane on the left will show you all the call sites. You can then click on one of those to navigate the right pane to that usage, and continue that process to look for all the ways to get to this function.

@TrayanZapryanov

Copy link
Copy Markdown
ContributorAuthor

@eiriktsarpalis, @stephentoub
XmlTextReader.IncrementalRead method was used by XmlTextReader.ReadChars.
I've added one test with sample xml found in the solution which contains a lot of different cases and validating output.
Now all changes are covered + we have better coverage.
Before(based on consolidating tests commit) :
+--------------------+--------+--------+--------+
| Module | Line | Branch | Method |
+--------------------+--------+--------+--------+
| System.Private.Xml | 52.68% | 43.27% | 60.92% |
+--------------------+--------+--------+--------+

Now:
+--------------------+--------+--------+--------+
| Module | Line | Branch | Method |
+--------------------+--------+--------+--------+
| System.Private.Xml | 52.84% | 43.44% | 61.01% |
+--------------------+--------+--------+--------+

}

private static string GenerateTestXml(out string expected)
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

consider just using const and and possibly string.Replace for line ending

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I've found this way of generating test xmls in here:

publicstaticvoidCreateGenericTestFile(stringstrFileName)
, and decided that this is the preferred way in this repo.
If you like - I can change it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this code base is old and ugly, let's at least make new code prettier since we don't have time on improving the entire code base

// ParseQName can flush the buffer, so we need to update the startPos, pos and chars after calling it
int endPos = ParseQName(true, 1, out _);
if (XmlConvert.StrEqual(_ps.chars, _ps.charPos + 1, endPos - _ps.charPos - 1, _curNode.localName) &&
if (endPos - _ps.charPos - 1 == _curNode.localName.Length && _ps.chars.AsSpan(_ps.charPos + 1).StartsWith(_curNode.localName) &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why not slice exact length (endPos - _ps.charPos - 1) and check with equals rather than doing check separately?

@krwqkrwq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@eiriktsarpalis
eiriktsarpalis merged commit abffaf8 into dotnet:mainOct 7, 2022
@eiriktsarpalis

Copy link
Copy Markdown
Member

Thanks @TrayanZapryanov!

@ghostghost locked as resolved and limited conversation to collaborators Nov 6, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Xmlcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@TrayanZapryanov@stephentoub@eiriktsarpalis@krwq@Trayan-Zapryanov