Repository: Azure/azure-functions-host
Pull request #11076: Reduce allocations in FeatureFlags.IsEnabled
Author: login: kshyju
Created: 2025-05-22T22:50:47Z | Merged: 2025-05-29T18:16:45Z

Fixes #10883

Optimizes memory allocations in `FeatureFlags.IsEnabled` by adopting the zero-allocation `ContainsToken` extension method for efficient token search in delimited strings.

This method gets called many times during specialization.

![Allocation Snapshot](https://github.com/user-attachments/assets/d09ffeb0-347b-4d7e-bfa0-1de8cebc5136)

I tested with a non-empty value for `AzureWebJobsFeatureFlags`.

```json
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
```

### Allocation Comparison

#### Before

| Name                  | Total Allocations | Self Allocations | Total Size (Bytes) | Self Size (Bytes) |
|-----------------------|-------------------|------------------|--------------------|-------------------|
| `FeatureFlags.IsEnabled` | 645               | 0                | 27,090             | 0                 |

#### After

| Name                  | Total Allocations | Self Allocations | Total Size (Bytes) | Self Size (Bytes) |
|-----------------------|-------------------|------------------|--------------------|-------------------|
| `FeatureFlags.IsEnabled` | 215               | 0                | 13,330             | 0                 |



#### Delta
- `FeatureFlags.IsEnabled(string, IEnvironment)` – Total allocations dropped by **430 (66.67%)**
- `FeatureFlags.IsEnabled(string, IEnvironment)` – Total allocated size dropped by **13,760 bytes (50.80%)**


---

### Benchmark.NET Micro Benchmarks

Also ran micro benchmark tests to compare the performance of the new implementation with the old one.
source code used: https://github.com/kshyju/Benchmarks/blob/9fe638a43458591991317a72af4b7b7cd30ed6a9/src/Benchmarks.ConsoleApp/StringSplitBenchmarks.cs#L6

#### Windows  
[Benchmark Report](https://github.com/kshyju/Benchmarks/actions/runs/15189525120/job/42718386961#step:7:1395)

| Method                                 | Mean      | Allocated |
|----------------------------------------|-----------|-----------|
| `ContainsUsingStringSplit_Short_Present`   | 41.45 ns | 64 B      |
| `ContainsToken_Short_Present`             | 11.52 ns | -         |
| `ContainsUsingStringSplit_Short_Absent`    | 44.09 ns | 64 B      |
| `ContainsToken_Short_Absent`              | 10.59 ns | -         |
| `ContainsUsingStringSplit_Medium_Present`  | 121.10 ns | 272 B     |
| `ContainsToken_Medium_Present`            | 31.06 ns | -         |
| `ContainsUsingStringSplit_Medium_Absent`   | 118.37 ns | 280 B     |
| `ContainsToken_Medium_Absent`             | 32.21 ns | -         |
| `ContainsUsingStringSplit_Empty`          | 0.33 ns  | -         |
| `ContainsToken_Empty`                     | 1.59 ns  | -         |

#### Linux (Ubuntu)  
[Benchmark Report](https://github.com/kshyju/Benchmarks/actions/runs/15189525120/job/42718386079#step:7:1384)

| Method                                 | Mean      | Allocated |
|----------------------------------------|-----------|-----------|
| `ContainsUsingStringSplit_Short_Present`   | 39.69 ns | 64 B      |
| `ContainsToken_Short_Present`             | 10.58 ns | -         |
| `ContainsUsingStringSplit_Short_Absent`    | 42.72 ns | 64 B      |
| `ContainsToken_Short_Absent`              | 10.63 ns | -         |
| `ContainsUsingStringSplit_Medium_Present`  | 135.73 ns | 272 B     |
| `ContainsToken_Medium_Present`            | 31.16 ns | -         |
| `ContainsUsingStringSplit_Medium_Absent`   | 138.68 ns | 280 B     |
| `ContainsToken_Medium_Absent`             | 32.59 ns | -         |
| `ContainsUsingStringSplit_Empty`          | 0.35 ns  | -         |
| `ContainsToken_Empty`                     | 1.26 ns  | -         |



### Pull request checklist

**IMPORTANT**: Currently, changes must be backported to the `in-proc` branch to be included in Core Tools and non-Flex deployments.

* [x] Backporting to the `in-proc` branch is not required
    * Otherwise: Link to backporting PR
* [ ] My changes **do not** require documentation changes
    * [x] Otherwise: Documentation issue linked to PR
* [ ] My changes **should not** be added to the release notes for the next release
    * [x] Otherwise: I've added my notes to `release_notes.md`
* [x] My changes **do not** need to be backported to a previous version
    * [ ] Otherwise: Backport tracked by issue/PR #issue_or_pr
* [x] My changes **do not** require diagnostic events changes
    * Otherwise: I have added/updated all related diagnostic events and their documentation (Documentation issue linked to PR)
* [ ] I have added all required tests (Unit tests, E2E tests)

<!-- Optional: delete if not applicable  -->
### Additional information

Additional PR information


Linked issue #10883: Optimize FeatureFlags.IsEnabled
The `FeatureFlags.IsEnabled` method currently uses `string.Split`, which results in a string array allocation. Instead, we can optimize this by using `ReadOnlySpan<char>` to check if the requested feature name is present, avoiding the extra allocation.

https://github.com/Azure/azure-functions-host/blob/5a873034be67cb2300f008bb39ef75fbc9e8b63e/src/WebJobs.Script/Config/FeatureFlags.cs#L22-L23

Diff:
diff --git a/release_notes.md b/release_notes.md
index 10c2fa9125..e963dcf95d 100644
--- a/release_notes.md
+++ b/release_notes.md
@@ -12,3 +12,4 @@
 - Memory allocation optimizations in `DependencyHelper.GetExtensionRequirements` (#11022)
 - Fix Instance Manager for CV1 Migration (#11072)
 - Update Java Worker Version to [2.19.1](https://github.com/Azure/azure-functions-java-worker/releases/tag/2.19.1)
+- Memory allocation optimizations in `FeatureFlags.IsEnabled` by adopting zero-allocation `ContainsToken` for efficient delimited token search (#11075)
diff --git a/src/WebJobs.Script/Config/FeatureFlags.cs b/src/WebJobs.Script/Config/FeatureFlags.cs
index a643aa33a5..da8df39eb4 100644
--- a/src/WebJobs.Script/Config/FeatureFlags.cs
+++ b/src/WebJobs.Script/Config/FeatureFlags.cs
@@ -2,7 +2,7 @@
 // Licensed under the MIT License. See License.txt in the project root for license information.
 
 using System;
-using System.Linq;
+using Microsoft.Azure.WebJobs.Script.Extensions;
 
 namespace Microsoft.Azure.WebJobs.Script.Config
 {
@@ -16,14 +16,14 @@ public static class FeatureFlags
 
         public static bool IsEnabled(string name, IEnvironment environment)
         {
-            string featureFlags = environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags);
-            if (!string.IsNullOrEmpty(featureFlags))
+            var featureFlags = environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags);
+
+            if (string.IsNullOrEmpty(featureFlags))
             {
-                string[] flags = featureFlags.Split(',');
-                return flags.Contains(name, StringComparer.OrdinalIgnoreCase);
+                return false;
             }
 
-            return false;
+            return featureFlags.ContainsToken(name, separator: ',', StringComparison.OrdinalIgnoreCase);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/WebJobs.Script/Extensions/TokenExtensions.cs b/src/WebJobs.Script/Extensions/TokenExtensions.cs
new file mode 100644
index 0000000000..31a05cd92e
--- /dev/null
+++ b/src/WebJobs.Script/Extensions/TokenExtensions.cs
@@ -0,0 +1,95 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+
+namespace Microsoft.Azure.WebJobs.Script.Extensions
+{
+    public static class TokenExtensions
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="source">The string containing one or more tokens separated by a delimiter (e.g., "FeatureA,FeatureB").</param>
+        /// <param name="token">The token to search for. Must not contain the separator character. A match is determined using the specified comparison type.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Example ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>
+        /// <c>true</c> if the token is found as an exact match in the delimited string; otherwise, <c>false</c>.
+        /// </returns>
+        /// <exception cref="ArgumentException">
+        /// Thrown if <paramref name="token"/> contains the separator character.
+        /// </exception>
+        /// <remarks>
+        /// If <paramref name="source"/> is empty or <paramref name="token"/> is empty, the method returns <c>false</c>.
+        /// </remarks>
+        public static bool ContainsToken(this string source, string token, char separator, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+        {
+            if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(token))
+            {
+                return false;
+            }
+
+            return source.AsSpan().ContainsToken(token.AsSpan(), separator, comparisonType);
+        }
+
+        /// <summary>
+        /// Determines whether a delimited <see cref="ReadOnlySpan{Char}"/> contains a specific token,
+        /// using the specified separator and string comparison. This method is a high-performance,
+        /// zero-allocation alternative that avoids splitting or heap allocations.
+        /// </summary>
+        /// <param name="source">The span containing one or more tokens separated by a delimiter (e.g., "FeatureA,FeatureB").</param>
+        /// <param name="token">The token to search for. Must not contain the separator character. A match is determined using the specified comparison type.</param>
+        /// <param name="separator">The character used to separate tokens in the span. Example ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>
+        /// <c>true</c> if the token is found as an exact match in the delimited span; otherwise, <c>false</c>.
+        /// </returns>
+        /// <exception cref="ArgumentException">
+        /// Thrown if <paramref name="token"/> contains the separator character.
+        /// </exception>
+        /// <remarks>
+        /// If <paramref name="source"/> is empty or <paramref name="token"/> is empty, the method returns <c>false</c>.
+        /// </remarks>
+        public static bool ContainsToken(this ReadOnlySpan<char> source, ReadOnlySpan<char> token, char separator, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+        {
+            if (token.IsEmpty)
+            {
+                return false;
+            }
+
+            if (token.Contains(separator))
+            {
+                throw new ArgumentException($"The search token must not contain the separator character '{separator}'.", nameof(token));
+            }
+
+            var remaining = source;
+
+            while (!remaining.IsEmpty)
+            {
+                var separatorIndex = remaining.IndexOf(separator);
+                ReadOnlySpan<char> currentToken;
+
+                if (separatorIndex >= 0)
+                {
+                    currentToken = remaining.Slice(0, separatorIndex);
+                    remaining = remaining.Slice(separatorIndex + 1);
+                }
+                else
+                {
+                    currentToken = remaining;
+                    remaining = default;
+                }
+
+                if (currentToken.Equals(token, comparisonType))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+    }
+}
diff --git a/test/WebJobs.Script.Tests/Utils/TokenExtensionTests.cs b/test/WebJobs.Script.Tests/Utils/TokenExtensionTests.cs
new file mode 100644
index 0000000000..7ba1856ed9
--- /dev/null
+++ b/test/WebJobs.Script.Tests/Utils/TokenExtensionTests.cs
@@ -0,0 +1,82 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Azure.WebJobs.Script.Extensions;
+using Xunit;
+
+namespace Microsoft.Azure.WebJobs.Script.Tests.Utils
+{
+    public sealed class TokenExtensionTests
+    {
+        [Theory]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureB", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureD", ',', false)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "featureb", ',', true)]
+        [InlineData("FeatureA|FeatureB|FeatureC", "FeatureC", '|', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureA", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureC", ',', true)]
+        [InlineData(null, "FeatureA", ',', false)]
+        [InlineData("", "FeatureA", ',', false)]
+        [InlineData("", "", ',', false)]
+        [InlineData("FeatureA,,FeatureB", "FeatureA", ',', true)]
+        [InlineData("FeatureA,,FeatureB", "FeatureB", ',', true)]
+        [InlineData(",FeatureA", "FeatureA", ',', true)]
+        [InlineData("FeatureA,", "FeatureA", ',', true)]
+        [InlineData("FeatureA,FeatureB,", "FeatureB", ',', true)]
+
+        public void ContainsToken_WorksAsExpected(string delimited, string token, char separator, bool expected)
+        {
+            var result = delimited.ContainsToken(token, separator);
+
+            Assert.Equal(expected, result);
+        }
+
+        [Theory]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureB", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureD", ',', false)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "featureb", ',', true)]
+        [InlineData("FeatureA|FeatureB|FeatureC", "FeatureC", '|', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureA", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureC", ',', true)]
+        [InlineData("FeatureA,,FeatureB", "FeatureA", ',', true)]
+        [InlineData("FeatureA,,FeatureB", "FeatureB", ',', true)]
+        [InlineData(",FeatureA", "FeatureA", ',', true)]
+        [InlineData("FeatureA,", "FeatureA", ',', true)]
+        [InlineData("FeatureA,FeatureB,", "FeatureB", ',', true)]
+        public void ContainsToken_SpanOverload_WorksAsExpected(string delimited, string token, char separator, bool expected)
+        {
+            var result = delimited.AsSpan().ContainsToken(token.AsSpan(), separator);
+
+            Assert.Equal(expected, result);
+        }
+
+        [Fact]
+        public void ContainsToken_ThrowsArgumentException_WhenTokenContainsSeparator_StringOverload()
+        {
+            var ex = Assert.Throws<ArgumentException>(() =>
+            {
+                string source = "FeatureA,FeatureB";
+                string invalidToken = "FeatureA,FeatureB";
+                char separator = ',';
+
+                source.ContainsToken(invalidToken, separator);
+            });
+
+            Assert.Contains("must not contain the separator", ex.Message);
+        }
+
+        [Fact]
+        public void ContainsToken_ThrowsArgumentException_WhenTokenContainsSeparator_SpanOverload()
+        {
+            var ex = Assert.Throws<ArgumentException>(() =>
+            {
+                ReadOnlySpan<char> source = "FeatureA|FeatureB".AsSpan();
+                ReadOnlySpan<char> invalidToken = "FeatureA|FeatureB".AsSpan();
+                source.ContainsToken(invalidToken, '|');
+            });
+
+            Assert.Contains("must not contain the separator", ex.Message);
+        }
+    }
+}


Reviews:
user: login: RohitRanjanMS
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-23T02:31:25Z
user: login: kshyju
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T04:05:51Z
user: login: kshyju
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T04:23:36Z
user: login: RohitRanjanMS
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T04:37:13Z
user: login: RohitRanjanMS
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T04:50:10Z
user: login: safihamid
body: :shipit:
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T16:04:17Z
user: login: kshyju
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T17:01:18Z
user: login: RohitRanjanMS
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T17:05:53Z
user: login: jviau
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T22:37:58Z
user: login: kshyju
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T02:22:53Z
user: login: kshyju
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T02:23:33Z
user: login: kshyju
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T02:24:35Z
user: login: jviau
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T16:12:01Z
user: login: kshyju
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T17:03:27Z
user: login: jviau
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T17:49:47Z

Review comments:
diff_hunk: @@ -0,0 +1,28 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using Microsoft.Azure.WebJobs.Script.Utils;
+using Xunit;
+
+namespace Microsoft.Azure.WebJobs.Script.Tests.Utils
+{
+    public sealed class StringUtilsTests
+    {
+        [Theory]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureB", ',', true)]
path: test/WebJobs.Script.Tests/Utils/StringUtilsTests.cs
user: login: RohitRanjanMS
body: Should we account for whitespace and empty tokens? 

"FeatureA, FeatureB,FeatureC"
created_at: 2025-05-23T02:27:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,54 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Azure.WebJobs.Script.Utils
+{
+    internal static class StringUtils
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="delimitedString">The string containing delimited tokens to search(Ex: "FeatureA,FeatureB").</param>
+        /// <param name="searchToken">The token to search for within the delimited string.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>True if the token is found; otherwise, false.</returns>
+        internal static bool ContainsToken(string delimitedString, string searchToken, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+        {
+            if (string.IsNullOrEmpty(delimitedString) || string.IsNullOrEmpty(searchToken))
+            {
+                return false;
+            }
+
+            var remaining = delimitedString.AsSpan();
+            var searchSpan = searchToken.AsSpan();
+
+            while (!remaining.IsEmpty)
+            {
+                var separatorIndex = remaining.IndexOf(separator);
+                ReadOnlySpan<char> currentToken;
+
+                if (separatorIndex >= 0)
+                {
+                    currentToken = remaining.Slice(0, separatorIndex);
path: src/WebJobs.Script/Utils/StringUtils.cs
user: login: RohitRanjanMS
body: Comparing currentToken and searchToken here should save some nanoseconds. You can skip a slice if currentToken matches the target.

created_at: 2025-05-23T02:30:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,28 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using Microsoft.Azure.WebJobs.Script.Utils;
+using Xunit;
+
+namespace Microsoft.Azure.WebJobs.Script.Tests.Utils
+{
+    public sealed class StringUtilsTests
+    {
+        [Theory]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureB", ',', true)]
path: test/WebJobs.Script.Tests/Utils/StringUtilsTests.cs
user: login: kshyju
body: I intentionally left that out to maintain parity with the previous version, which also didn’t handle trimming. In my benchmark repository, I’ve included a unit test that compares the outputs of both implementations to ensure the functionality remains consistent.

https://github.com/kshyju/Benchmarks/blob/9fe638a43458591991317a72af4b7b7cd30ed6a9/tests/Benchmarks.Tests/Tests.cs#L17-L26
created_at: 2025-05-23T04:05:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,54 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Azure.WebJobs.Script.Utils
+{
+    internal static class StringUtils
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="delimitedString">The string containing delimited tokens to search(Ex: "FeatureA,FeatureB").</param>
+        /// <param name="searchToken">The token to search for within the delimited string.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>True if the token is found; otherwise, false.</returns>
+        internal static bool ContainsToken(string delimitedString, string searchToken, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+        {
+            if (string.IsNullOrEmpty(delimitedString) || string.IsNullOrEmpty(searchToken))
+            {
+                return false;
+            }
+
+            var remaining = delimitedString.AsSpan();
+            var searchSpan = searchToken.AsSpan();
+
+            while (!remaining.IsEmpty)
+            {
+                var separatorIndex = remaining.IndexOf(separator);
+                ReadOnlySpan<char> currentToken;
+
+                if (separatorIndex >= 0)
+                {
+                    currentToken = remaining.Slice(0, separatorIndex);
path: src/WebJobs.Script/Utils/StringUtils.cs
user: login: kshyju
body: Okay, I tried [that approach](https://github.com/kshyju/Benchmarks/blob/471002fec0a589bd1a1b81ff395e22c034968353/src/Benchmarks.ConsoleApp/StringUtils.cs#L66-L69) and ran benchmarks to compare those 2.

##### Linux 
https://github.com/kshyju/Benchmarks/actions/runs/15201876152/job/42757382146#step:7:1378

| Method                                   | Mean      |
|-----------------------------------------|-----------|
| ContainsToken_Short_Present              | 11.548 ns |
| ContainsToken_CheckInline_Short_Present  | 10.645 ns |
| ContainsToken_Short_Absent               | 10.644 ns |
| ContainsToken_CheckInline_Short_Absent   | 10.330 ns |
| ContainsToken_Medium_Present             | 31.076 ns |
| ContainsToken_CheckInline_Medium_Present | 31.335 ns |
| ContainsToken_Medium_Absent              | 32.303 ns |
| ContainsToken_CheckInline_Medium_Absent  | 32.730 ns |
| ContainsToken_Empty                      |  1.332 ns |
| ContainsToken_CheckInline_Empty          |  1.338 ns |


##### Windows
https://github.com/kshyju/Benchmarks/actions/runs/15201876152/job/42757382306#step:7:1387

| Method                                   | Mean      |
|-----------------------------------------|-----------|
| ContainsToken_Short_Present              | 11.196 ns |
| ContainsToken_CheckInline_Short_Present  | 10.909 ns |
| ContainsToken_Short_Absent               | 10.588 ns |
| ContainsToken_CheckInline_Short_Absent   | 10.291 ns |
| ContainsToken_Medium_Present             | 30.436 ns |
| ContainsToken_CheckInline_Medium_Present | 36.290 ns |
| ContainsToken_Medium_Absent              | 31.417 ns |
| ContainsToken_CheckInline_Medium_Absent  | 30.823 ns |
| ContainsToken_Empty                      |  1.586 ns |
| ContainsToken_CheckInline_Empty          |  1.593 ns |

Comparing them:

| Platform | Input   | Match   | Variable (ns) | Inline (ns) | Δ (ns) | Winner  |
|----------|---------|---------|---------------|-------------|--------|---------|
| Linux    | Short   | Present | 11.55         | 10.65       | 0.90   | Inline  |
| Linux    | Short   | Absent  | 10.64         | 10.33       | 0.31   | Inline  |
| Linux    | Medium  | Present | 31.08         | 31.34       | -0.26  | Variable |
| Linux    | Medium  | Absent  | 32.30         | 32.73       | -0.43  | Variable |
| Linux    | Empty   | —       | 1.33          | 1.34        | -0.01  | Tie     |
| Windows  | Short   | Present | 11.20         | 10.91       | 0.29   | Inline  |
| Windows  | Short   | Absent  | 10.59         | 10.29       | 0.30   | Inline  |
| Windows  | Medium  | Present | 30.44         | 36.29       | -5.85  | Variable |
| Windows  | Medium  | Absent  | 31.42         | 30.82       | 0.60   | Inline  |
| Windows  | Empty   | —       | 1.59          | 1.59        | 0.00   | Tie     |

TLDR:
- The inline version is slightly faster for short inputs, but the difference is typically less than 1 nanosecond.  
- For medium and empty inputs, performance is nearly identical, with the variable version sometimes ahead.

I personally prefer the variable version (currently in the PR) since I find it more readable with 2 sections. The top section snippet handles token extraction, and the bottom handles the comparison. Do you find the inline version more readable? I am open to switching to that if more people find that readable.


created_at: 2025-05-23T04:23:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,54 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Azure.WebJobs.Script.Utils
+{
+    internal static class StringUtils
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="delimitedString">The string containing delimited tokens to search(Ex: "FeatureA,FeatureB").</param>
+        /// <param name="searchToken">The token to search for within the delimited string.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>True if the token is found; otherwise, false.</returns>
+        internal static bool ContainsToken(string delimitedString, string searchToken, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+        {
+            if (string.IsNullOrEmpty(delimitedString) || string.IsNullOrEmpty(searchToken))
+            {
+                return false;
+            }
+
+            var remaining = delimitedString.AsSpan();
+            var searchSpan = searchToken.AsSpan();
+
+            while (!remaining.IsEmpty)
+            {
+                var separatorIndex = remaining.IndexOf(separator);
+                ReadOnlySpan<char> currentToken;
+
+                if (separatorIndex >= 0)
+                {
+                    currentToken = remaining.Slice(0, separatorIndex);
path: src/WebJobs.Script/Utils/StringUtils.cs
user: login: RohitRanjanMS
body: Interesting, I didn't expect inline version to take longer compared to the variable version especially when there's a match (we will save one slice). I find the variable version more readable as you would need another level of nesting in the inline version. I am fine if you want to keep your existing version.
created_at: 2025-05-23T04:37:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,54 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Azure.WebJobs.Script.Utils
+{
+    internal static class StringUtils
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="delimitedString">The string containing delimited tokens to search(Ex: "FeatureA,FeatureB").</param>
+        /// <param name="searchToken">The token to search for within the delimited string.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>True if the token is found; otherwise, false.</returns>
+        internal static bool ContainsToken(string delimitedString, string searchToken, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+        {
+            if (string.IsNullOrEmpty(delimitedString) || string.IsNullOrEmpty(searchToken))
+            {
+                return false;
+            }
+
+            var remaining = delimitedString.AsSpan();
+            var searchSpan = searchToken.AsSpan();
+
+            while (!remaining.IsEmpty)
+            {
+                var separatorIndex = remaining.IndexOf(separator);
+                ReadOnlySpan<char> currentToken;
+
+                if (separatorIndex >= 0)
+                {
+                    currentToken = remaining.Slice(0, separatorIndex);
path: src/WebJobs.Script/Utils/StringUtils.cs
user: login: RohitRanjanMS
body: I ran your test locally, inline is promising in all the scenarios.

<img width="698" alt="image" src="https://github.com/user-attachments/assets/090e27fb-2a49-4125-b812-57c88d87cf3a" />

created_at: 2025-05-23T04:50:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,54 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Azure.WebJobs.Script.Utils
+{
+    internal static class StringUtils
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="delimitedString">The string containing delimited tokens to search(Ex: "FeatureA,FeatureB").</param>
+        /// <param name="searchToken">The token to search for within the delimited string.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>True if the token is found; otherwise, false.</returns>
+        internal static bool ContainsToken(string delimitedString, string searchToken, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+        {
+            if (string.IsNullOrEmpty(delimitedString) || string.IsNullOrEmpty(searchToken))
+            {
+                return false;
+            }
+
+            var remaining = delimitedString.AsSpan();
+            var searchSpan = searchToken.AsSpan();
+
+            while (!remaining.IsEmpty)
+            {
+                var separatorIndex = remaining.IndexOf(separator);
+                ReadOnlySpan<char> currentToken;
+
+                if (separatorIndex >= 0)
+                {
+                    currentToken = remaining.Slice(0, separatorIndex);
path: src/WebJobs.Script/Utils/StringUtils.cs
user: login: kshyju
body: Yea, I expect results from local dev workstation to have higher variance/noise (std dev) than the one on servers. From all these 3 results, looks like the Linux server has the least variance. I still vote for readability over 5 ns.

created_at: 2025-05-23T17:01:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,28 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using Microsoft.Azure.WebJobs.Script.Utils;
+using Xunit;
+
+namespace Microsoft.Azure.WebJobs.Script.Tests.Utils
+{
+    public sealed class StringUtilsTests
+    {
+        [Theory]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureB", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureD", ',', false)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "featureb", ',', true)]
+        [InlineData("FeatureA|FeatureB|FeatureC", "FeatureC", '|', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureA", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureC", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureA,FeatureB", ',', false)]
+        [InlineData(null, "FeatureA", ',', false)]
+        [InlineData("", "FeatureA", ',', false)]
+        public void ContainsToken_LargeInputScenarios(string delimited, string token, char separator, bool expected)
path: test/WebJobs.Script.Tests/Utils/StringUtilsTests.cs
user: login: jviau
body: Some edge cases to also test:

- multiple separators: `FeatureA,,FeatureB`
- leading separator: `,FeatureA`
- trailing separator: `FeatureA,`
- only separator: `,`
created_at: 2025-05-27T22:33:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Azure.WebJobs.Script.Utils
+{
+    internal static class StringUtils
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="delimitedString">The string containing delimited tokens to search(Ex: "FeatureA,FeatureB").</param>
+        /// <param name="searchToken">The token to search for within the delimited string.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>True if the token is found; otherwise, false.</returns>
+        internal static bool ContainsToken(string delimitedString, string searchToken, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
path: src/WebJobs.Script/Utils/StringUtils.cs
user: login: jviau
body: Is it worth taking in `ReadOnlySpan<char>` for the arguments?

```suggestion
        internal static bool ContainsToken(ReadOnlySpan<char> source, ReadOnlySpan<char> token, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
```

Also changed some param names for conciseness.

Maybe even make this an extension method for `string`?
created_at: 2025-05-27T22:34:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,28 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using Microsoft.Azure.WebJobs.Script.Utils;
+using Xunit;
+
+namespace Microsoft.Azure.WebJobs.Script.Tests.Utils
+{
+    public sealed class StringUtilsTests
+    {
+        [Theory]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureB", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureD", ',', false)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "featureb", ',', true)]
+        [InlineData("FeatureA|FeatureB|FeatureC", "FeatureC", '|', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureA", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureC", ',', true)]
+        [InlineData("FeatureA,FeatureB,FeatureC", "FeatureA,FeatureB", ',', false)]
+        [InlineData(null, "FeatureA", ',', false)]
+        [InlineData("", "FeatureA", ',', false)]
+        public void ContainsToken_LargeInputScenarios(string delimited, string token, char separator, bool expected)
path: test/WebJobs.Script.Tests/Utils/StringUtilsTests.cs
user: login: kshyju
body: Improved test coverage.
created_at: 2025-05-28T02:22:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,54 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Azure.WebJobs.Script.Utils
+{
+    internal static class StringUtils
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="delimitedString">The string containing delimited tokens to search(Ex: "FeatureA,FeatureB").</param>
+        /// <param name="searchToken">The token to search for within the delimited string.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>True if the token is found; otherwise, false.</returns>
+        internal static bool ContainsToken(string delimitedString, string searchToken, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
path: src/WebJobs.Script/Utils/StringUtils.cs
user: login: kshyju
body: Changed param names to concise ones.
created_at: 2025-05-28T02:23:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,54 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Azure.WebJobs.Script.Utils
+{
+    internal static class StringUtils
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="delimitedString">The string containing delimited tokens to search(Ex: "FeatureA,FeatureB").</param>
+        /// <param name="searchToken">The token to search for within the delimited string.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>True if the token is found; otherwise, false.</returns>
+        internal static bool ContainsToken(string delimitedString, string searchToken, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
path: src/WebJobs.Script/Utils/StringUtils.cs
user: login: kshyju
body: Switched to extension method. Made another overload which takes ReadOnlySpan<char> as well.
created_at: 2025-05-28T02:24:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,89 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+
+namespace Microsoft.Azure.WebJobs.Script.Extensions
+{
+    public static class TokenExtensions
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="source">The string containing one or more tokens separated by a delimiter (e.g., "FeatureA,FeatureB").</param>
+        /// <param name="token">The token to search for. Must not contain the separator character. A match is determined using the specified comparison type.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>
+        /// <c>true</c> if the token is found as an exact match in the delimited string; otherwise, <c>false</c>.
+        /// </returns>
+        /// <exception cref="ArgumentException">
+        /// Thrown if <paramref name="token"/> contains the separator character.
+        /// </exception>
+        public static bool ContainsToken(this string source, string token, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
path: src/WebJobs.Script/Extensions/TokenExtensions.cs
user: login: jviau
body: Final nit: consider removing the default param on `separator`.

My reasoning is the method name "ContainsToken" (which I like, no need to change it), doesn't define what a "token" is exactly. By having the caller always supply `separator`, this forces them to acknowledge a separator is in play here. And hopefully they understand a _token_ are the parts between the separators.
created_at: 2025-05-28T16:11:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,89 @@
+﻿// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+
+namespace Microsoft.Azure.WebJobs.Script.Extensions
+{
+    public static class TokenExtensions
+    {
+        /// <summary>
+        /// Determines whether a delimited string contains a specific token, using the specified separator and string comparison.
+        /// This method is a zero-allocation, faster alternative to splitting the string and using Contains, as it avoids unnecessary allocations.
+        /// </summary>
+        /// <param name="source">The string containing one or more tokens separated by a delimiter (e.g., "FeatureA,FeatureB").</param>
+        /// <param name="token">The token to search for. Must not contain the separator character. A match is determined using the specified comparison type.</param>
+        /// <param name="separator">The character used to separate tokens in the string. Defaults to ','.</param>
+        /// <param name="comparisonType">The string comparison type to use. Defaults to OrdinalIgnoreCase.</param>
+        /// <returns>
+        /// <c>true</c> if the token is found as an exact match in the delimited string; otherwise, <c>false</c>.
+        /// </returns>
+        /// <exception cref="ArgumentException">
+        /// Thrown if <paramref name="token"/> contains the separator character.
+        /// </exception>
+        public static bool ContainsToken(this string source, string token, char separator = ',', StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
path: src/WebJobs.Script/Extensions/TokenExtensions.cs
user: login: kshyju
body: I agree with your reasoning. 
Pushed an update to remove default value for `separator` parameter.
created_at: 2025-05-28T17:03:27Z
author_association: MEMBER

PR comments:
user: login: jviau
created_at: 2025-05-27T16:52:58Z
body: .NET8 has a type called `SearchValues<T>` which I think can be used for highly optimized string searching. Maybe this can help here? Not entirely sure how to use it or if it is applicable, but worth a look.
author_association: CONTRIBUTOR
user: login: kshyju
created_at: 2025-05-27T18:43:38Z
body: > .NET8 has a type called `SearchValues<T>` which I think can be used for highly optimized string searching. Maybe this can help here? Not entirely sure how to use it or if it is applicable, but worth a look.

I think `SearchValues<T>` is useful when we have to search using multiple delimiters. It still causes some allocation with the use of ` SearchValues.Create`. In the context of this method, it will not offer anything better since we care about single char delimiter. I ran some benchmarks to compare both approaches, and the results confirm that using SearchValues<T> here does not do better than the char approach we have in the `ContainsToken` method.

1. [Linux](https://github.com/kshyju/Benchmarks/actions/runs/15283035314/job/42986465395#step:7:1384)
2. [Windows](https://github.com/kshyju/Benchmarks/actions/runs/15283035314/job/42986465379#step:7:1386)


author_association: MEMBER