Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions src/DocumentFormat.OpenXml.Framework/Framework/StringBuilderPool.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Provides a shared, reusable pool of <see cref="StringBuilder"/> instances to reduce allocations.
/// </summary>
/// <remarks>
/// Design mirrors DefaultObjectPool from Microsoft.Extensions.ObjectPool:
/// a single lock-free fast-path slot plus a bounded overflow queue capped at
/// <c>Environment.ProcessorCount * 2</c>.
/// </remarks>
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<StringBuilder> _items = new ConcurrentQueue<StringBuilder>();
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

/// <summary>
/// Acquires a <see cref="StringBuilder"/> from the pool, or creates a new one if the pool is empty.
/// </summary>
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();
}

/// <summary>
/// Returns the string value of the <see cref="StringBuilder"/> and releases it back to the pool.
/// </summary>
public static string GetValueAndRelease(StringBuilder sb)
{
var result = sb.ToString();
Release(sb);
return result;
}

/// <summary>
/// Releases a <see cref="StringBuilder"/> back to the pool after clearing it.
/// </summary>
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;

namespace DocumentFormat.OpenXml
Expand Down Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -73,6 +70,8 @@ static char ToCharUpper(int value)
/// </summary>
/// <param name="bytes">A byte array to use to create a new hex string.</param>
/// <returns>A hex string that corresponds to the value parameter.</returns>
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<byte>(bytes));
}
}
6 changes: 3 additions & 3 deletions src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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)
Expand All @@ -207,7 +207,7 @@ public override string? InnerText
}
}

TextValue = textString.ToString();
TextValue = StringBuilderPool.GetValueAndRelease(textString);
}

return TextValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -94,6 +93,8 @@ private AttributeMutualExclusive(OpenXmlQualifiedName[] attributes)

if (existAttributeSb.Length == 0)
{
StringBuilderPool.Release(attributesSb);
StringBuilderPool.Release(existAttributeSb);
return null;
}

Expand All @@ -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)),
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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()
{
Expand Down
5 changes: 2 additions & 3 deletions src/DocumentFormat.OpenXml.Framework/XmlPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Xml;

namespace DocumentFormat.OpenXml
Expand Down Expand Up @@ -113,7 +112,7 @@ private static string TryBuildXPath(Stack<OpenXmlElement> elements, out XmlNames
return string.Empty;
}

var xpath = new StringBuilder();
var xpath = StringBuilderPool.Acquire();
namespaces = new XmlNamespaceManager(new NameTable());

foreach (var element in elements)
Expand Down Expand Up @@ -152,7 +151,7 @@ private static string TryBuildXPath(Stack<OpenXmlElement> elements, out XmlNames
}
}

return xpath.ToString();
return StringBuilderPool.GetValueAndRelease(xpath);
}

private static Stack<OpenXmlElement> GetElements(OpenXmlElement? element)
Expand Down