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/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/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 196a432ee..d321ee52d 100644 --- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs +++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs @@ -2,10 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; - -#if !NET6_0_OR_GREATER using System.Text; -#endif namespace DocumentFormat.OpenXml { @@ -73,6 +70,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. - public static string Create(params byte[] bytes) => Create(bytes.AsSpan()); + // 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)); } } diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs index c4a1b2cad..78bcb1784 100644 --- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs +++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs @@ -1,13 +1,13 @@ // 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; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; -using System.Text; namespace DocumentFormat.OpenXml { @@ -194,7 +194,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 +207,7 @@ public override string? InnerText } } - TextValue = textString.ToString(); + TextValue = StringBuilderPool.GetValueAndRelease(textString); } return TextValue; diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs index 74bf90d85..0ae39800f 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,19 +59,25 @@ public AttributeAbsentConditionToNonValue(OpenXmlQualifiedName absentAttribute, } } - var sb = new StringBuilder(); - sb.Append('\'').Append(_values[0]).Append('\''); + var sb = StringBuilderPool.Acquire(); + 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 = 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..e7074b6eb 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,19 +55,25 @@ public AttributeAbsentConditionToValue(OpenXmlQualifiedName absentAttribute, Ope { if (AttributeValueEquals(conditionAttribute.Value, value, false)) { - var sb = new StringBuilder(); - sb.Append('\'').Append(_values[0]).Append('\''); + var sb = StringBuilderPool.Acquire(); + 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 = 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..7c2ff8071 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,33 +65,45 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ { if (AttributeValueEquals(conditionAttribute.Value, value, false)) { - var sb = new StringBuilder(); - sb.Append('\'').Append(_values[0]).Append('\''); + var sb = StringBuilderPool.Acquire(); + 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 = sb.ToString(); + string attributeValueString = StringBuilderPool.GetValueAndRelease(sb); - var otherSb = new StringBuilder(); - otherSb.Append('\'').Append(_otherValues[0]).Append('\''); + var otherSb = StringBuilderPool.Acquire(); + 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 = 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)