From 9d5a379e109e2e1ef0b875a6d7e6aa0d868834b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:16:22 +0000 Subject: [PATCH 1/7] Initial plan From e9617793d16169cc1621692a30ce3b387229ab9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:27:24 +0000 Subject: [PATCH 2/7] Use a pooled StringBuilder for all instances Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/64d34d63-d66c-4446-b6e3-3697d0681811 Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../OpenXmlCompositeElement.cs | 5 +- .../SimpleTypes/HexStringFactory.cs | 8 +-- .../SimpleTypes/ListValue.cs | 5 +- .../System/StringBuilderPool.cs | 71 +++++++++++++++++++ .../AttributeAbsentConditionToNonValue.cs | 5 +- .../AttributeAbsentConditionToValue.cs | 5 +- .../Semantic/AttributeMutualExclusive.cs | 11 +-- .../AttributeValueConditionToAnother.cs | 9 ++- .../XmlPath.cs | 5 +- 9 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs diff --git a/src/DocumentFormat.OpenXml.Framework/OpenXmlCompositeElement.cs b/src/DocumentFormat.OpenXml.Framework/OpenXmlCompositeElement.cs index 9a7312433..8b76fe681 100644 --- a/src/DocumentFormat.OpenXml.Framework/OpenXmlCompositeElement.cs +++ b/src/DocumentFormat.OpenXml.Framework/OpenXmlCompositeElement.cs @@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Text; using System.Xml; namespace DocumentFormat.OpenXml @@ -122,14 +121,14 @@ public override string InnerText { get { - var innerText = new StringBuilder(); + var innerText = StringBuilderPool.Acquire(); foreach (var child in ChildElements) { innerText.Append(child.InnerText); } - return innerText.ToString(); + return StringBuilderPool.GetValueAndRelease(innerText); } } diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs index 196a432ee..796eaef83 100644 --- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs +++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs @@ -3,10 +3,6 @@ using System; -#if !NET6_0_OR_GREATER -using System.Text; -#endif - namespace DocumentFormat.OpenXml { /// @@ -43,7 +39,7 @@ static string Create(ReadOnlySpan bytes) return new string(chars); #else - var sb = new StringBuilder(bytes.Length * 2); + var sb = StringBuilderPool.Acquire(); foreach (var b in bytes) { @@ -51,7 +47,7 @@ static string Create(ReadOnlySpan bytes) sb.Append(ToCharUpper(b)); } - return sb.ToString(); + return StringBuilderPool.GetValueAndRelease(sb); #endif static char ToCharUpper(int value) diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs index c4a1b2cad..782014aa4 100644 --- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs +++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs @@ -7,7 +7,6 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; -using System.Text; namespace DocumentFormat.OpenXml { @@ -194,7 +193,7 @@ public override string? InnerText { if (TextValue is null && _list is not null) { - var textString = new StringBuilder(); + var textString = StringBuilderPool.Acquire(); string separator = string.Empty; foreach (var value in _list) @@ -207,7 +206,7 @@ public override string? InnerText } } - TextValue = textString.ToString(); + TextValue = StringBuilderPool.GetValueAndRelease(textString); } return TextValue; diff --git a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs new file mode 100644 index 000000000..4b11e241c --- /dev/null +++ b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text; + +namespace DocumentFormat.OpenXml; + +/// +/// Provides a reusable pool of instances to reduce allocations. +/// +internal static class StringBuilderPool +{ + // Two slots per thread to handle cases where two StringBuilders are active simultaneously. + [System.ThreadStatic] + private static StringBuilder? _primary; + + [System.ThreadStatic] + private static StringBuilder? _secondary; + + /// + /// Acquires a from the pool, or creates a new one if the pool is empty. + /// + public static StringBuilder Acquire() + { + var sb = _primary; + if (sb is not null) + { + _primary = null; + return sb; + } + + sb = _secondary; + if (sb is not null) + { + _secondary = null; + return sb; + } + + return new StringBuilder(); + } + + /// + /// Returns the string value of the and releases it back to the pool. + /// + public static string GetValueAndRelease(StringBuilder sb) + { + var result = sb.ToString(); + Release(sb); + return result; + } + + /// + /// Releases a back to the pool after clearing it. + /// + public static void Release(StringBuilder sb) + { + // Use Length = 0 instead of Clear() for .NET 3.5 compatibility + sb.Length = 0; + + if (_primary is null) + { + _primary = sb; + } + else if (_secondary is null) + { + _secondary = sb; + } + + // If both slots are full, the instance is abandoned and will be garbage collected. + } +} diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs index 74bf90d85..4a6a5b036 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs +++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Framework; -using System.Text; namespace DocumentFormat.OpenXml.Validation.Semantic { @@ -60,7 +59,7 @@ public AttributeAbsentConditionToNonValue(OpenXmlQualifiedName absentAttribute, } } - var sb = new StringBuilder(); + var sb = StringBuilderPool.Acquire(); sb.Append('\'').Append(_values[0]).Append('\''); if (_values.Length > 1) { @@ -72,7 +71,7 @@ public AttributeAbsentConditionToNonValue(OpenXmlQualifiedName absentAttribute, sb.Append(" and '").Append(_values[_values.Length - 1]).Append('\''); } - string valueString = sb.ToString(); + string valueString = StringBuilderPool.GetValueAndRelease(sb); return new ValidationErrorInfo() { diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs index 0bd5f3c0d..6de68d1a7 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs +++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Framework; -using System.Text; namespace DocumentFormat.OpenXml.Validation.Semantic { @@ -56,7 +55,7 @@ public AttributeAbsentConditionToValue(OpenXmlQualifiedName absentAttribute, Ope { if (AttributeValueEquals(conditionAttribute.Value, value, false)) { - var sb = new StringBuilder(); + var sb = StringBuilderPool.Acquire(); sb.Append('\'').Append(_values[0]).Append('\''); if (_values.Length > 1) { @@ -68,7 +67,7 @@ public AttributeAbsentConditionToValue(OpenXmlQualifiedName absentAttribute, Ope sb.Append(" or '").Append(_values[_values.Length - 1]).Append('\''); } - string valueString = sb.ToString(); + string valueString = StringBuilderPool.GetValueAndRelease(sb); return new ValidationErrorInfo() { diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeMutualExclusive.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeMutualExclusive.cs index 8ad401a7c..d64d8e9fd 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeMutualExclusive.cs +++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeMutualExclusive.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Framework; -using System.Text; namespace DocumentFormat.OpenXml.Validation.Semantic { @@ -58,8 +57,8 @@ private AttributeMutualExclusive(OpenXmlQualifiedName[] attributes) return null; } - var attributesSb = new StringBuilder(); - var existAttributeSb = new StringBuilder(); + var attributesSb = StringBuilderPool.Acquire(); + var existAttributeSb = StringBuilderPool.Acquire(); string? existAttribute2 = null; foreach (var attribute in _attributes) @@ -94,6 +93,8 @@ private AttributeMutualExclusive(OpenXmlQualifiedName[] attributes) if (existAttributeSb.Length == 0) { + StringBuilderPool.Release(attributesSb); + StringBuilderPool.Release(existAttributeSb); return null; } @@ -104,9 +105,9 @@ private AttributeMutualExclusive(OpenXmlQualifiedName[] attributes) Node = element, Description = SR.Format( ValidationResources.Sem_AttributeMutualExclusive, - existAttributeSb.ToString(), + StringBuilderPool.GetValueAndRelease(existAttributeSb), existAttribute2, - attributesSb.ToString()), + StringBuilderPool.GetValueAndRelease(attributesSb)), }; } } diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs index 37744f78f..a699dcde9 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs +++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Framework; -using System.Text; namespace DocumentFormat.OpenXml.Validation.Semantic { @@ -66,7 +65,7 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ { if (AttributeValueEquals(conditionAttribute.Value, value, false)) { - var sb = new StringBuilder(); + var sb = StringBuilderPool.Acquire(); sb.Append('\'').Append(_values[0]).Append('\''); if (_values.Length > 1) { @@ -78,9 +77,9 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ sb.Append(" or '").Append(_values[_values.Length - 1]).Append('\''); } - string attributeValueString = sb.ToString(); + string attributeValueString = StringBuilderPool.GetValueAndRelease(sb); - var otherSb = new StringBuilder(); + var otherSb = StringBuilderPool.Acquire(); otherSb.Append('\'').Append(_otherValues[0]).Append('\''); if (_otherValues.Length > 1) { @@ -92,7 +91,7 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ otherSb.Append(" or '").Append(_otherValues[_otherValues.Length - 1]).Append('\''); } - string otherAttributeValueString = otherSb.ToString(); + string otherAttributeValueString = StringBuilderPool.GetValueAndRelease(otherSb); return new ValidationErrorInfo() { diff --git a/src/DocumentFormat.OpenXml.Framework/XmlPath.cs b/src/DocumentFormat.OpenXml.Framework/XmlPath.cs index 510c9dfa7..8478b9423 100644 --- a/src/DocumentFormat.OpenXml.Framework/XmlPath.cs +++ b/src/DocumentFormat.OpenXml.Framework/XmlPath.cs @@ -9,7 +9,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; using System.Xml; namespace DocumentFormat.OpenXml @@ -113,7 +112,7 @@ private static string TryBuildXPath(Stack elements, out XmlNames return string.Empty; } - var xpath = new StringBuilder(); + var xpath = StringBuilderPool.Acquire(); namespaces = new XmlNamespaceManager(new NameTable()); foreach (var element in elements) @@ -152,7 +151,7 @@ private static string TryBuildXPath(Stack elements, out XmlNames } } - return xpath.ToString(); + return StringBuilderPool.GetValueAndRelease(xpath); } private static Stack GetElements(OpenXmlElement? element) From 31bceaa11bc294e7c07f7ec5384902eba926da22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:32:24 +0000 Subject: [PATCH 3/7] Address code review: add capacity cap and EnsureCapacity hint Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/64d34d63-d66c-4446-b6e3-3697d0681811 Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../SimpleTypes/HexStringFactory.cs | 1 + .../System/StringBuilderPool.cs | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs index 796eaef83..9b67da17f 100644 --- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs +++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs @@ -40,6 +40,7 @@ static string Create(ReadOnlySpan bytes) return new string(chars); #else var sb = StringBuilderPool.Acquire(); + sb.EnsureCapacity(bytes.Length * 2); foreach (var b in bytes) { diff --git a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs index 4b11e241c..7f62dfe2e 100644 --- a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs +++ b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs @@ -10,6 +10,9 @@ namespace DocumentFormat.OpenXml; /// internal static class StringBuilderPool { + // Maximum capacity (in characters) a StringBuilder may have before it is not returned to the pool. + private const int MaxBuilderCapacity = 360; + // Two slots per thread to handle cases where two StringBuilders are active simultaneously. [System.ThreadStatic] private static StringBuilder? _primary; @@ -54,6 +57,12 @@ public static string GetValueAndRelease(StringBuilder sb) /// public static void Release(StringBuilder sb) { + // Don't pool excessively large instances to avoid holding onto large memory allocations. + if (sb.Capacity > MaxBuilderCapacity) + { + return; + } + // Use Length = 0 instead of Clear() for .NET 3.5 compatibility sb.Length = 0; From 725c004f7e53803e20943d058ea1877719e45660 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:14:13 +0000 Subject: [PATCH 4/7] Replace ThreadStatic pool with shared DefaultObjectPool-style pool; move to Framework/; use InternalsVisibleTo for Features Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/b9e3379d-a520-4210-aa32-03e0ec470e6e Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../Framework/StringBuilderPool.cs | 103 ++++++++++++++++++ .../Properties/Properties.cs | 1 + .../SimpleTypes/HexStringFactory.cs | 3 +- .../SimpleTypes/ListValue.cs | 1 + .../System/StringBuilderPool.cs | 80 -------------- 5 files changed, 107 insertions(+), 81 deletions(-) create mode 100644 src/DocumentFormat.OpenXml.Framework/Framework/StringBuilderPool.cs delete mode 100644 src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs diff --git a/src/DocumentFormat.OpenXml.Framework/Framework/StringBuilderPool.cs b/src/DocumentFormat.OpenXml.Framework/Framework/StringBuilderPool.cs new file mode 100644 index 000000000..deac3307d --- /dev/null +++ b/src/DocumentFormat.OpenXml.Framework/Framework/StringBuilderPool.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text; +using System.Threading; + +#if !NET35 +using System; +using System.Collections.Concurrent; +#endif + +namespace DocumentFormat.OpenXml.Framework +{ + /// + /// Provides a shared, reusable pool of instances to reduce allocations. + /// + /// + /// Design mirrors DefaultObjectPool from Microsoft.Extensions.ObjectPool: + /// a single lock-free fast-path slot plus a bounded overflow queue capped at + /// Environment.ProcessorCount * 2. + /// + internal static class StringBuilderPool + { + // Maximum capacity (in characters) a StringBuilder may have before it is not returned to the pool. + private const int MaxBuilderCapacity = 360; + +#if !NET35 + // Secondary queue for concurrent overflow; capacity is ProcessorCount * 2 minus 1 for _fastItem. + private static readonly ConcurrentQueue _items = new ConcurrentQueue(); + private static readonly int _maxCapacity = (Environment.ProcessorCount * 2) - 1; +#endif + + // Single fast-path slot; accessed via Interlocked.CompareExchange for lock-free hand-off. + private static StringBuilder? _fastItem; + +#if !NET35 + private static int _numItems; +#endif + + /// + /// Acquires a from the pool, or creates a new one if the pool is empty. + /// + public static StringBuilder Acquire() + { + var item = _fastItem; + if (item != null && Interlocked.CompareExchange(ref _fastItem, null, item) == item) + { + return item; + } + +#if !NET35 + if (_items.TryDequeue(out item)) + { + Interlocked.Decrement(ref _numItems); + return item; + } +#endif + + return new StringBuilder(); + } + + /// + /// Returns the string value of the and releases it back to the pool. + /// + public static string GetValueAndRelease(StringBuilder sb) + { + var result = sb.ToString(); + Release(sb); + return result; + } + + /// + /// Releases a back to the pool after clearing it. + /// + public static void Release(StringBuilder sb) + { + // Don't pool excessively large instances to avoid retaining large memory allocations. + if (sb.Capacity > MaxBuilderCapacity) + { + return; + } + + // Use Length = 0 instead of Clear() for .NET 3.5 compatibility. + sb.Length = 0; + + if (Interlocked.CompareExchange(ref _fastItem, sb, null) == null) + { + return; + } + +#if !NET35 + if (Interlocked.Increment(ref _numItems) <= _maxCapacity) + { + _items.Enqueue(sb); + return; + } + + // Pool is full; drop the instance so it can be GC'd. + Interlocked.Decrement(ref _numItems); +#endif + } + } +} diff --git a/src/DocumentFormat.OpenXml.Framework/Properties/Properties.cs b/src/DocumentFormat.OpenXml.Framework/Properties/Properties.cs index 3fd12daf7..3571df3a7 100644 --- a/src/DocumentFormat.OpenXml.Framework/Properties/Properties.cs +++ b/src/DocumentFormat.OpenXml.Framework/Properties/Properties.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("DocumentFormat.OpenXml, Publickey=002400000480000094000000060200000024000052534131000400000100010061d8931836c82bf25ca6b773dfd6e7b3ab4e43fba60cf4a86347170373415a165ccc40da3da4a52163822db9fa91f15828236d32d6a9fe754859f10d1f8262646c1f3fb6b4348123f14d733db0ff11c3198b7cf56caaebbf14563990446a6c32aff36d5a7097194294c127fe3cdf9f2609daae5f4daf26f8b6227f203d2a8bbf")] +[assembly: InternalsVisibleTo("DocumentFormat.OpenXml.Features, Publickey=002400000480000094000000060200000024000052534131000400000100010061d8931836c82bf25ca6b773dfd6e7b3ab4e43fba60cf4a86347170373415a165ccc40da3da4a52163822db9fa91f15828236d32d6a9fe754859f10d1f8262646c1f3fb6b4348123f14d733db0ff11c3198b7cf56caaebbf14563990446a6c32aff36d5a7097194294c127fe3cdf9f2609daae5f4daf26f8b6227f203d2a8bbf")] [assembly: InternalsVisibleTo("DocumentFormat.OpenXml.Linq, Publickey=002400000480000094000000060200000024000052534131000400000100010061d8931836c82bf25ca6b773dfd6e7b3ab4e43fba60cf4a86347170373415a165ccc40da3da4a52163822db9fa91f15828236d32d6a9fe754859f10d1f8262646c1f3fb6b4348123f14d733db0ff11c3198b7cf56caaebbf14563990446a6c32aff36d5a7097194294c127fe3cdf9f2609daae5f4daf26f8b6227f203d2a8bbf")] // For testing diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs index 9b67da17f..4bb35a7a1 100644 --- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs +++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using DocumentFormat.OpenXml.Framework; using System; namespace DocumentFormat.OpenXml @@ -70,6 +71,6 @@ static char ToCharUpper(int value) /// /// A byte array to use to create a new hex string. /// A hex string that corresponds to the value parameter. - public static string Create(params byte[] bytes) => Create(bytes.AsSpan()); + public static string Create(params byte[] bytes) => Create(new ReadOnlySpan(bytes)); } } diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs index 782014aa4..78bcb1784 100644 --- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs +++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using DocumentFormat.OpenXml.Framework; using System; using System.Collections; using System.Collections.Generic; diff --git a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs deleted file mode 100644 index 7f62dfe2e..000000000 --- a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Text; - -namespace DocumentFormat.OpenXml; - -/// -/// Provides a reusable pool of instances to reduce allocations. -/// -internal static class StringBuilderPool -{ - // Maximum capacity (in characters) a StringBuilder may have before it is not returned to the pool. - private const int MaxBuilderCapacity = 360; - - // Two slots per thread to handle cases where two StringBuilders are active simultaneously. - [System.ThreadStatic] - private static StringBuilder? _primary; - - [System.ThreadStatic] - private static StringBuilder? _secondary; - - /// - /// Acquires a from the pool, or creates a new one if the pool is empty. - /// - public static StringBuilder Acquire() - { - var sb = _primary; - if (sb is not null) - { - _primary = null; - return sb; - } - - sb = _secondary; - if (sb is not null) - { - _secondary = null; - return sb; - } - - return new StringBuilder(); - } - - /// - /// Returns the string value of the and releases it back to the pool. - /// - public static string GetValueAndRelease(StringBuilder sb) - { - var result = sb.ToString(); - Release(sb); - return result; - } - - /// - /// Releases a back to the pool after clearing it. - /// - public static void Release(StringBuilder sb) - { - // Don't pool excessively large instances to avoid holding onto large memory allocations. - if (sb.Capacity > MaxBuilderCapacity) - { - return; - } - - // Use Length = 0 instead of Clear() for .NET 3.5 compatibility - sb.Length = 0; - - if (_primary is null) - { - _primary = sb; - } - else if (_secondary is null) - { - _secondary = sb; - } - - // If both slots are full, the instance is abandoned and will be garbage collected. - } -} From 490955e04a852d755c2fff07e7d6504de53bbef3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:14:56 +0000 Subject: [PATCH 5/7] Add comment explaining ReadOnlySpan constructor use in HexStringFactory Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/b9e3379d-a520-4210-aa32-03e0ec470e6e Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../SimpleTypes/HexStringFactory.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs index 4bb35a7a1..75a458006 100644 --- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs +++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs @@ -71,6 +71,8 @@ static char ToCharUpper(int value) /// /// A byte array to use to create a new hex string. /// A hex string that corresponds to the value parameter. + // Use constructor instead of bytes.AsSpan() to avoid SpanExtensions ambiguity + // when this file is linked into DocumentFormat.OpenXml.Features. public static string Create(params byte[] bytes) => Create(new ReadOnlySpan(bytes)); } } From c6244f4c323a6efe29ebef4432d6b53d00c163ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:25:58 +0000 Subject: [PATCH 6/7] Un-chain .Append calls to one per line for readability Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/07f56062-8462-44e4-9b69-bba899c6df89 Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../AttributeAbsentConditionToNonValue.cs | 12 +++++++--- .../AttributeAbsentConditionToValue.cs | 12 +++++++--- .../AttributeValueConditionToAnother.cs | 24 ++++++++++++++----- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs index 4a6a5b036..0ae39800f 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs +++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs @@ -60,15 +60,21 @@ public AttributeAbsentConditionToNonValue(OpenXmlQualifiedName absentAttribute, } var sb = StringBuilderPool.Acquire(); - sb.Append('\'').Append(_values[0]).Append('\''); + sb.Append('\''); + sb.Append(_values[0]); + sb.Append('\''); if (_values.Length > 1) { for (int i = 1; i < _values.Length - 1; i++) { - sb.Append(", '").Append(_values[i]).Append('\''); + sb.Append(", '"); + sb.Append(_values[i]); + sb.Append('\''); } - sb.Append(" and '").Append(_values[_values.Length - 1]).Append('\''); + sb.Append(" and '"); + sb.Append(_values[_values.Length - 1]); + sb.Append('\''); } string valueString = StringBuilderPool.GetValueAndRelease(sb); diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs index 6de68d1a7..e7074b6eb 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs +++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs @@ -56,15 +56,21 @@ public AttributeAbsentConditionToValue(OpenXmlQualifiedName absentAttribute, Ope if (AttributeValueEquals(conditionAttribute.Value, value, false)) { var sb = StringBuilderPool.Acquire(); - sb.Append('\'').Append(_values[0]).Append('\''); + sb.Append('\''); + sb.Append(_values[0]); + sb.Append('\''); if (_values.Length > 1) { for (int i = 1; i < _values.Length - 1; i++) { - sb.Append(", '").Append(_values[i]).Append('\''); + sb.Append(", '"); + sb.Append(_values[i]); + sb.Append('\''); } - sb.Append(" or '").Append(_values[_values.Length - 1]).Append('\''); + sb.Append(" or '"); + sb.Append(_values[_values.Length - 1]); + sb.Append('\''); } string valueString = StringBuilderPool.GetValueAndRelease(sb); diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs index a699dcde9..7c2ff8071 100644 --- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs +++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs @@ -66,29 +66,41 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ if (AttributeValueEquals(conditionAttribute.Value, value, false)) { var sb = StringBuilderPool.Acquire(); - sb.Append('\'').Append(_values[0]).Append('\''); + sb.Append('\''); + sb.Append(_values[0]); + sb.Append('\''); if (_values.Length > 1) { for (int i = 1; i < _values.Length - 1; i++) { - sb.Append(", '").Append(_values[i]).Append('\''); + sb.Append(", '"); + sb.Append(_values[i]); + sb.Append('\''); } - sb.Append(" or '").Append(_values[_values.Length - 1]).Append('\''); + sb.Append(" or '"); + sb.Append(_values[_values.Length - 1]); + sb.Append('\''); } string attributeValueString = StringBuilderPool.GetValueAndRelease(sb); var otherSb = StringBuilderPool.Acquire(); - otherSb.Append('\'').Append(_otherValues[0]).Append('\''); + otherSb.Append('\''); + otherSb.Append(_otherValues[0]); + otherSb.Append('\''); if (_otherValues.Length > 1) { for (int i = 1; i < _otherValues.Length - 1; i++) { - otherSb.Append(", '").Append(_otherValues[i]).Append('\''); + otherSb.Append(", '"); + otherSb.Append(_otherValues[i]); + otherSb.Append('\''); } - otherSb.Append(" or '").Append(_otherValues[_otherValues.Length - 1]).Append('\''); + otherSb.Append(" or '"); + otherSb.Append(_otherValues[_otherValues.Length - 1]); + otherSb.Append('\''); } string otherAttributeValueString = StringBuilderPool.GetValueAndRelease(otherSb); From 84dad9cef821092bd942fb0de75f6ca171011268 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:41:25 +0000 Subject: [PATCH 7/7] Fix CS0122: use new StringBuilder() in HexStringFactory instead of internal StringBuilderPool Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/dba14ab9-b782-474a-8a60-32881273ee7a Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com> --- .../SimpleTypes/HexStringFactory.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs index 75a458006..d321ee52d 100644 --- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs +++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using DocumentFormat.OpenXml.Framework; using System; +using System.Text; namespace DocumentFormat.OpenXml { @@ -40,8 +40,7 @@ static string Create(ReadOnlySpan bytes) return new string(chars); #else - var sb = StringBuilderPool.Acquire(); - sb.EnsureCapacity(bytes.Length * 2); + var sb = new StringBuilder(bytes.Length * 2); foreach (var b in bytes) { @@ -49,7 +48,7 @@ static string Create(ReadOnlySpan bytes) sb.Append(ToCharUpper(b)); } - return StringBuilderPool.GetValueAndRelease(sb); + return sb.ToString(); #endif static char ToCharUpper(int value)