Skip to content

Commit bbd254f

Browse files
authored
Reduce allocations in FeatureFlags.IsEnabled (#11076)
* Optimize FeatureFlags.IsEnabled with span based contains check. * PR feedback udpates. - Added a span based overload.s - Made an extension method.s - Test coverage improvements. * Minor improvements to method summary. * Fix linting issue. * Removing separator default value.
1 parent 2ab1077 commit bbd254f

File tree

4 files changed

+185
-7
lines changed

4 files changed

+185
-7
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
- Fix Instance Manager for CV1 Migration (#11072)
1414
- Avoid setting up OTel and AzMon exporter in the placeholder mode. (#11090)
1515
- Update Java Worker Version to [2.19.1](https://github.com/Azure/azure-functions-java-worker/releases/tag/2.19.1)
16+
- Memory allocation optimizations in `FeatureFlags.IsEnabled` by adopting zero-allocation `ContainsToken` for efficient delimited token search (#11075)

src/WebJobs.Script/Config/FeatureFlags.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Linq;
5+
using Microsoft.Azure.WebJobs.Script.Extensions;
66

77
namespace Microsoft.Azure.WebJobs.Script.Config
88
{
@@ -16,14 +16,14 @@ public static class FeatureFlags
1616

1717
public static bool IsEnabled(string name, IEnvironment environment)
1818
{
19-
string featureFlags = environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags);
20-
if (!string.IsNullOrEmpty(featureFlags))
19+
var featureFlags = environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags);
20+
21+
if (string.IsNullOrEmpty(featureFlags))
2122
{
22-
string[] flags = featureFlags.Split(',');
23-
return flags.Contains(name, StringComparer.OrdinalIgnoreCase);
23+
return false;
2424
}
2525

26-
return false;
26+
return featureFlags.ContainsToken(name, separator: ',', StringComparison.OrdinalIgnoreCase);
2727
}
2828
}
29-
}
29+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
7+
namespace Microsoft.Azure.WebJobs.Script.Extensions
8+
{
9+
public static class TokenExtensions
10+
{
11+
/// <summary>
12+
/// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
13+
/// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
14+
/// </summary>
15+
/// <param name="source">The string containing one or more tokens separated by a delimiter (e.g., "FeatureA,FeatureB").</param>
16+
/// <param name="token">The token to search for. Must not contain the separator character. A match is determined using the specified comparison type.</param>
17+
/// <param name="separator">The character used to separate tokens in the string. Example ','.</param>
18+
/// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
19+
/// <returns>
20+
/// <c>true</c> if the token is found as an exact match in the delimited string; otherwise, <c>false</c>.
21+
/// </returns>
22+
/// <exception cref="ArgumentException">
23+
/// Thrown if <paramref name="token"/> contains the separator character.
24+
/// </exception>
25+
/// <remarks>
26+
/// If <paramref name="source"/> is empty or <paramref name="token"/> is empty, the method returns <c>false</c>.
27+
/// </remarks>
28+
public static bool ContainsToken(this string source, string token, char separator, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
29+
{
30+
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(token))
31+
{
32+
return false;
33+
}
34+
35+
return source.AsSpan().ContainsToken(token.AsSpan(), separator, comparisonType);
36+
}
37+
38+
/// <summary>
39+
/// Determines whether a delimited <see cref="ReadOnlySpan{Char}"/> contains a specific token,
40+
/// using the specified separator and string comparison. This method is a high-performance,
41+
/// zero-allocation alternative that avoids splitting or heap allocations.
42+
/// </summary>
43+
/// <param name="source">The span containing one or more tokens separated by a delimiter (e.g., "FeatureA,FeatureB").</param>
44+
/// <param name="token">The token to search for. Must not contain the separator character. A match is determined using the specified comparison type.</param>
45+
/// <param name="separator">The character used to separate tokens in the span. Example ','.</param>
46+
/// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
47+
/// <returns>
48+
/// <c>true</c> if the token is found as an exact match in the delimited span; otherwise, <c>false</c>.
49+
/// </returns>
50+
/// <exception cref="ArgumentException">
51+
/// Thrown if <paramref name="token"/> contains the separator character.
52+
/// </exception>
53+
/// <remarks>
54+
/// If <paramref name="source"/> is empty or <paramref name="token"/> is empty, the method returns <c>false</c>.
55+
/// </remarks>
56+
public static bool ContainsToken(this ReadOnlySpan<char> source, ReadOnlySpan<char> token, char separator, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
57+
{
58+
if (token.IsEmpty)
59+
{
60+
return false;
61+
}
62+
63+
if (token.Contains(separator))
64+
{
65+
throw new ArgumentException($"The search token must not contain the separator character '{separator}'.", nameof(token));
66+
}
67+
68+
var remaining = source;
69+
70+
while (!remaining.IsEmpty)
71+
{
72+
var separatorIndex = remaining.IndexOf(separator);
73+
ReadOnlySpan<char> currentToken;
74+
75+
if (separatorIndex >= 0)
76+
{
77+
currentToken = remaining.Slice(0, separatorIndex);
78+
remaining = remaining.Slice(separatorIndex + 1);
79+
}
80+
else
81+
{
82+
currentToken = remaining;
83+
remaining = default;
84+
}
85+
86+
if (currentToken.Equals(token, comparisonType))
87+
{
88+
return true;
89+
}
90+
}
91+
92+
return false;
93+
}
94+
}
95+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Azure.WebJobs.Script.Extensions;
6+
using Xunit;
7+
8+
namespace Microsoft.Azure.WebJobs.Script.Tests.Utils
9+
{
10+
public sealed class TokenExtensionTests
11+
{
12+
[Theory]
13+
[InlineData("FeatureA,FeatureB,FeatureC", "FeatureB", ',', true)]
14+
[InlineData("FeatureA,FeatureB,FeatureC", "FeatureD", ',', false)]
15+
[InlineData("FeatureA,FeatureB,FeatureC", "featureb", ',', true)]
16+
[InlineData("FeatureA|FeatureB|FeatureC", "FeatureC", '|', true)]
17+
[InlineData("FeatureA,FeatureB,FeatureC", "FeatureA", ',', true)]
18+
[InlineData("FeatureA,FeatureB,FeatureC", "FeatureC", ',', true)]
19+
[InlineData(null, "FeatureA", ',', false)]
20+
[InlineData("", "FeatureA", ',', false)]
21+
[InlineData("", "", ',', false)]
22+
[InlineData("FeatureA,,FeatureB", "FeatureA", ',', true)]
23+
[InlineData("FeatureA,,FeatureB", "FeatureB", ',', true)]
24+
[InlineData(",FeatureA", "FeatureA", ',', true)]
25+
[InlineData("FeatureA,", "FeatureA", ',', true)]
26+
[InlineData("FeatureA,FeatureB,", "FeatureB", ',', true)]
27+
28+
public void ContainsToken_WorksAsExpected(string delimited, string token, char separator, bool expected)
29+
{
30+
var result = delimited.ContainsToken(token, separator);
31+
32+
Assert.Equal(expected, result);
33+
}
34+
35+
[Theory]
36+
[InlineData("FeatureA,FeatureB,FeatureC", "FeatureB", ',', true)]
37+
[InlineData("FeatureA,FeatureB,FeatureC", "FeatureD", ',', false)]
38+
[InlineData("FeatureA,FeatureB,FeatureC", "featureb", ',', true)]
39+
[InlineData("FeatureA|FeatureB|FeatureC", "FeatureC", '|', true)]
40+
[InlineData("FeatureA,FeatureB,FeatureC", "FeatureA", ',', true)]
41+
[InlineData("FeatureA,FeatureB,FeatureC", "FeatureC", ',', true)]
42+
[InlineData("FeatureA,,FeatureB", "FeatureA", ',', true)]
43+
[InlineData("FeatureA,,FeatureB", "FeatureB", ',', true)]
44+
[InlineData(",FeatureA", "FeatureA", ',', true)]
45+
[InlineData("FeatureA,", "FeatureA", ',', true)]
46+
[InlineData("FeatureA,FeatureB,", "FeatureB", ',', true)]
47+
public void ContainsToken_SpanOverload_WorksAsExpected(string delimited, string token, char separator, bool expected)
48+
{
49+
var result = delimited.AsSpan().ContainsToken(token.AsSpan(), separator);
50+
51+
Assert.Equal(expected, result);
52+
}
53+
54+
[Fact]
55+
public void ContainsToken_ThrowsArgumentException_WhenTokenContainsSeparator_StringOverload()
56+
{
57+
var ex = Assert.Throws<ArgumentException>(() =>
58+
{
59+
string source = "FeatureA,FeatureB";
60+
string invalidToken = "FeatureA,FeatureB";
61+
char separator = ',';
62+
63+
source.ContainsToken(invalidToken, separator);
64+
});
65+
66+
Assert.Contains("must not contain the separator", ex.Message);
67+
}
68+
69+
[Fact]
70+
public void ContainsToken_ThrowsArgumentException_WhenTokenContainsSeparator_SpanOverload()
71+
{
72+
var ex = Assert.Throws<ArgumentException>(() =>
73+
{
74+
ReadOnlySpan<char> source = "FeatureA|FeatureB".AsSpan();
75+
ReadOnlySpan<char> invalidToken = "FeatureA|FeatureB".AsSpan();
76+
source.ContainsToken(invalidToken, '|');
77+
});
78+
79+
Assert.Contains("must not contain the separator", ex.Message);
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)