diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5121b833..4c79e8c1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,5 +1,8 @@ name: Docs +env: + DOCFX_VERSION: 2.80.1 + on: push: branches: @@ -23,7 +26,7 @@ jobs: 10.0.x - name: Setup DocFX - run: dotnet tool install -g docfx + run: dotnet tool install -g docfx --version ${{ env.DOCFX_VERSION }} - name: DocFX Build working-directory: docs diff --git a/README.md b/README.md index 1936f0b1..af37b758 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,16 @@ A fast and low allocation StringBuilder for .NET. +The package exposes two builders: + +- `ValueStringBuilder`: the general-purpose choice for almost every user of this library +- `FixedSizeValueStringBuilder`: a specialized builder for hard capacity limits where the buffer must never grow + ## Getting Started Install the package: > PM> Install-Package LinkDotNet.StringBuilder -Afterward, use the package as follow: +Afterward, use the package as follows: ```csharp using LinkDotNet.StringBuilder; // Namespace of the package @@ -33,6 +38,19 @@ using ValueStringBuilder stringBuilder = new(stackalloc char[128]); ``` Note that this will prevent you from returning `stringBuilder` or assigning it to an `out` parameter. +## Which builder should I use? + +Start here: + +| Situation | Recommended type | +|---|---| +| Unsure, or the output length can vary | `new ValueStringBuilder()` | +| The output is usually small and bounded, but growing is acceptable | `new ValueStringBuilder(stackalloc char[N])` | +| The output must never grow past a caller-owned buffer | `new FixedSizeValueStringBuilder(stackalloc char[N])` | +| The code is async, long-lived, or needs to escape the current stack frame | `System.Text.StringBuilder` | + +If you are new to the library, start with `ValueStringBuilder`. `FixedSizeValueStringBuilder` is intentionally more specialized and should be chosen only when "never grow" is part of the requirement. + ### A buffer that is never replaced: `FixedSizeValueStringBuilder` If the content *outgrows* that stack buffer, `ValueStringBuilder` quietly rents a larger one from `ArrayPool.Shared`. @@ -50,6 +68,30 @@ half. The first drop latches `Overflowed`, and further appends are ignored until deliberately or `Clear()` to start over. There is no `Dispose` - nothing is ever rented. See the [documentation](https://linkdotnet.github.io/StringBuilder/articles/fixed_size.html) for details. +If you want to start with a fixed buffer and only rarely fall back to a growing builder, you can move the content into a `ValueStringBuilder`: + +```csharp +const int userId = 42; +const string userName = "Ada"; +const string suffix = " name="; + +var builder = new FixedSizeValueStringBuilder(stackalloc char[12]); +builder.Append("id="); +builder.Append(userId); + +if (builder.Remaining < suffix.Length + userName.Length) +{ + using var grown = builder.MoveToValueStringBuilder(); + grown.Append(suffix); + grown.Append(userName); + return grown.ToString(); +} + +builder.Append(suffix); +builder.Append(userName); +return builder.ToString(); +``` + ## What does it solve? The dotnet version of the `StringBuilder` is an all-purpose version that normally fits a wide variety of needs. But sometimes, low allocation is key. Therefore I created the `ValueStringBuilder`. It is not a class but a `ref struct` that tries to allocate as little as possible. @@ -64,7 +106,15 @@ The library works best for a small to medium length strings (not hundreds of tho The normal use case is to concatenate strings in a hot path where the goal is to put as minimal pressure on the GC as possible. ## Documentation -More detailed documentation can be found [here](https://linkdotnet.github.io/StringBuilder). It is really important to understand how the `ValueStringBuilder` works so that you did not run into weird situations where performance/allocations can even rise. +More detailed documentation can be found [here](https://linkdotnet.github.io/StringBuilder). Good starting points are: + +- [Getting started](https://linkdotnet.github.io/StringBuilder/articles/getting_started.html) +- [Choosing between builders](https://linkdotnet.github.io/StringBuilder/articles/choosing_builder.html) +- [Fixed-size string building](https://linkdotnet.github.io/StringBuilder/articles/fixed_size.html) +- [Best practices and pitfalls](https://linkdotnet.github.io/StringBuilder/articles/best_practices.html) +- [Known limitations](https://linkdotnet.github.io/StringBuilder/articles/known_limitations.html) + +For agents and other tooling that prefer source markdown over rendered HTML, the published docs also expose an [`llms.txt`](https://linkdotnet.github.io/StringBuilder/llms.txt) index with direct links to the markdown sources. ## Benchmark diff --git a/docs/serve_docs.cmd b/docs/serve_docs.cmd index 2c6f398c..8f617fbf 100644 --- a/docs/serve_docs.cmd +++ b/docs/serve_docs.cmd @@ -1,4 +1,5 @@ @echo off -echo "This script uses docfx - Please make sure it is installed on your machine" +set DOCFX_VERSION=2.80.1 +dotnet tool update -g docfx --version %DOCFX_VERSION% || dotnet tool install -g docfx --version %DOCFX_VERSION% docfx site/docfx.json docfx serve site/_site \ No newline at end of file diff --git a/docs/site/articles/choosing_builder.md b/docs/site/articles/choosing_builder.md new file mode 100644 index 00000000..17f3ff31 --- /dev/null +++ b/docs/site/articles/choosing_builder.md @@ -0,0 +1,121 @@ +--- +uid: choosing_builder +--- + +# Choosing between builders + +This library has two main string-building types, but they are not peers: + +- [`ValueStringBuilder`](xref:LinkDotNet.StringBuilder.ValueStringBuilder) is the default, general-purpose type +- [`FixedSizeValueStringBuilder`](xref:LinkDotNet.StringBuilder.FixedSizeValueStringBuilder) is the specialized type for hard no-growth requirements + +If you are not sure which one to use, start with `ValueStringBuilder`. + +## Quick decision table + +| Situation | Recommended type | Why | +|---|---|---| +| General application code | `new ValueStringBuilder()` | Simple usage, low allocations, can grow when needed | +| Small, bounded output where avoiding the first pool rent matters | `new ValueStringBuilder(stackalloc char[N])` | Fast path while the content fits, but still able to grow | +| Predictable medium-sized output | `new ValueStringBuilder(capacity)` | Avoids repeated growth without stack-only restrictions | +| Hard limit, no replacement buffer allowed | `new FixedSizeValueStringBuilder(stackalloc char[N])` | Never rents and never grows | +| The value must survive async/iterator/lambda boundaries | `System.Text.StringBuilder` | `ref struct` rules make both builders unsuitable | + +## The recommended default + +Use `ValueStringBuilder` unless you have a specific reason not to: + +```csharp +using var builder = new ValueStringBuilder(); +builder.Append("Hello "); +builder.Append("World"); +return builder.ToString(); +``` + +This gives you the easiest lifecycle: the builder can grow if needed, and `Dispose()` returns any rented buffer to `ArrayPool.Shared`. + +## `ValueStringBuilder` with a stack buffer + +If the output is usually short and bounded, you can provide the initial buffer yourself: + +```csharp +Span buffer = stackalloc char[64]; +using var builder = new ValueStringBuilder(buffer); +``` + +This is still a `ValueStringBuilder`, not the fixed-size type. If the content does not fit, it transparently grows by renting from the array pool. That makes it a good optimization when you want the fast path, but do not want the failure mode of a hard limit. + +## When `FixedSizeValueStringBuilder` is the right tool + +Use `FixedSizeValueStringBuilder` when all of the following are true: + +- the buffer size is known and caller-controlled +- growing would be incorrect, not just slower +- you want overflow to be observable via `Overflowed` + +```csharp +const string userName = "Ada"; + +var builder = new FixedSizeValueStringBuilder(stackalloc char[16]); +builder.Append("user="); +builder.Append(userName); + +if (builder.Overflowed) +{ + return ""; +} + +return builder.ToString(); +``` + +This builder never grows, never rents, and has no `Dispose()`. It is ideal for fixed-width formatting, bounded protocol fields, and other places where "best effort" growth would hide a bug or violate a contract. + +## Why the fixed-size builder is more specialized + +`FixedSizeValueStringBuilder` deliberately asks more of the caller: + +- appends are atomic, so a value either fits entirely or is dropped +- `Overflowed` latches after the first failed append +- further appends are ignored until you call `ClearOverflow()` or `Clear()` +- you must check `Overflowed` before trusting the output + +That behavior is powerful when you need it, but it is heavier than the normal `ValueStringBuilder` workflow. That is why the fixed-size builder should be the exception, not the starting point. + +## Start fixed, then grow only on the rare fallback + +Sometimes you want a hard limit for the common path but a slower escape hatch for rare oversized values. In that case, start with `FixedSizeValueStringBuilder` and move to `ValueStringBuilder` only when needed: + +```csharp +const int userId = 42; +const string userName = "Ada"; +const string suffix = " name="; + +var builder = new FixedSizeValueStringBuilder(stackalloc char[12]); +builder.Append("id="); +builder.Append(userId); + +if (builder.Remaining < suffix.Length + userName.Length) +{ + using var grown = builder.MoveToValueStringBuilder(); + grown.Append(suffix); + grown.Append(userName); + return grown.ToString(); +} + +builder.Append(suffix); +builder.Append(userName); +return builder.ToString(); +``` + +This keeps the common path allocation-free while still giving you a safe way to continue building when the fixed buffer is no longer enough. + +## What to tell humans and agents + +When documenting or generating code for this library, the safest default guidance is: + +1. use `ValueStringBuilder` first +2. use `stackalloc` only for measured, bounded hot paths +3. use `FixedSizeValueStringBuilder` only when "must never grow" is an explicit requirement +4. always mention `Overflowed` when showing `FixedSizeValueStringBuilder` + +For the surrounding usage rules, continue with [Getting started](xref:getting_started), [Best practices and pitfalls](xref:best_practices), and [Fixed-size string building](xref:fixed_size). diff --git a/docs/site/articles/getting_started.md b/docs/site/articles/getting_started.md index db5d91a8..de247bba 100644 --- a/docs/site/articles/getting_started.md +++ b/docs/site/articles/getting_started.md @@ -42,7 +42,16 @@ If you are new to the library, these defaults are usually the right choice: - call `ToString()` only when you actually need a `string` - pass the builder by `ref` to helper methods - reach for `stackalloc` only after you know the output is small and bounded -- use [`FixedSizeValueStringBuilder`](xref:fixed_size) when growing the buffer must be impossible +- use [`FixedSizeValueStringBuilder`](xref:fixed_size) only when growing the buffer must be impossible + +The package does expose two builder types, but only one of them is the normal entry point: + +| Type | When to pick it | +|---|---| +| [`ValueStringBuilder`](xref:LinkDotNet.StringBuilder.ValueStringBuilder) | Almost always - this is the default choice | +| [`FixedSizeValueStringBuilder`](xref:LinkDotNet.StringBuilder.FixedSizeValueStringBuilder) | Only when "never grow" is a functional requirement | + +If you need a fuller decision guide, see [Choosing between builders](xref:choosing_builder). For the common pitfalls and the more advanced performance-oriented guidance, see [Best practices and pitfalls](xref:best_practices). diff --git a/docs/site/articles/toc.yml b/docs/site/articles/toc.yml index 289e7686..b12da223 100644 --- a/docs/site/articles/toc.yml +++ b/docs/site/articles/toc.yml @@ -1,6 +1,8 @@ - name: Getting started href: getting_started.md items: + - name: Choosing between builders + href: choosing_builder.md - name: Best practices and pitfalls href: best_practices.md - name: How does it work? diff --git a/docs/site/docfx.json b/docs/site/docfx.json index 9d4fcc18..48328f8d 100644 --- a/docs/site/docfx.json +++ b/docs/site/docfx.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", "metadata": [ { "src": [ @@ -34,7 +35,8 @@ "resource": [ { "files": [ - "images/**" + "images/**", + "*.txt" ] } ], @@ -66,7 +68,9 @@ "_disableBreadcrumb": true, "_disableFooter": true }, - "postProcessors": [], + "postProcessors": [ + "ExtractSearchIndex" + ], "markdownEngineName": "markdig", "noLangKeyword": false, "keepFileLink": false, diff --git a/docs/site/index.md b/docs/site/index.md index 8617ba10..4492dd29 100644 --- a/docs/site/index.md +++ b/docs/site/index.md @@ -4,10 +4,29 @@ # ValueStringBuilder: A fast and low allocation StringBuilder for .NET -**ValueStringBuilder** aims to be as fast as possible with a minimal amount of allocation memory. This documentation will showcase to you how to use the `ValueStringBuilder` as well as what are some limitations coming with it. If you have questions or feature requests just head over to the [GitHub](https://github.com/linkdotnet/StringBuilder) repository and file an issue. +**ValueStringBuilder** aims to be as fast as possible with a minimal amount of allocation memory. This documentation explains when to use it, when to reach for the more specialized `FixedSizeValueStringBuilder`, and what trade-offs come with both. If you have questions or feature requests just head over to the [GitHub](https://github.com/linkdotnet/StringBuilder) repository and file an issue. The library makes heavy use of `Span`, `stackalloc` and `ArrayPool`s to achieve low allocations and fast performance. It also avoids boxing common value types passed to `AppendJoin`, `Concat`, `AppendFormat`, and interpolated strings, and vectorizes `Trim`/`TrimStart`/`TrimEnd` via `SearchValues`. See the [Comparison](xref:comparison) article for benchmarks. +## Start here + +Most users should start with [`ValueStringBuilder`](xref:LinkDotNet.StringBuilder.ValueStringBuilder). The library also includes [`FixedSizeValueStringBuilder`](xref:LinkDotNet.StringBuilder.FixedSizeValueStringBuilder), but that type is specialized for hard no-growth limits and should only be used when that constraint is part of the requirement. + +| Situation | Recommended type | +|---|---| +| General use | `ValueStringBuilder` | +| Small bounded hot path, but growing is still acceptable | `ValueStringBuilder(stackalloc char[N])` | +| Hard limit, caller-owned buffer must never be replaced | `FixedSizeValueStringBuilder` | +| Async or long-lived text building | `System.Text.StringBuilder` | + +Recommended reading order: + +1. [Getting started](xref:getting_started) +2. [Choosing between builders](xref:choosing_builder) +3. [Best practices and pitfalls](xref:best_practices) +4. [Fixed-size string building](xref:fixed_size) +5. [Known limitations](xref:known_limitations) + ## Download The package is hosted on [nuget.org](https://www.nuget.org/packages/LinkDotNet.StringBuilder/), so easily add the package reference: > PM> Install-Package LinkDotNet.StringBuilder @@ -46,4 +65,8 @@ There are also convenient helper methods like this: ```csharp _ = ValueStringBuilder.Concat("Hello", " ", "World"); // "Hello World" _ = ValueStringBuilder.Concat("Hello", 1, 2, 3, "!"); // "Hello123!" -``` \ No newline at end of file +``` + +## Agent and markdown-friendly access + +The documentation is authored in markdown in the repository and published as HTML through DocFX. For agents and other tooling that want a compact entry point, the site also exposes an `llms.txt` file with direct links to the canonical markdown sources and the most relevant guidance pages. \ No newline at end of file diff --git a/docs/site/llms.txt b/docs/site/llms.txt new file mode 100644 index 00000000..ea4e30f5 --- /dev/null +++ b/docs/site/llms.txt @@ -0,0 +1,27 @@ +# LinkDotNet.StringBuilder / ValueStringBuilder + +Concise guidance for agents and other tooling: + +- Default to `ValueStringBuilder` for examples, recommendations, and generated code. +- Treat `FixedSizeValueStringBuilder` as a specialized builder for hard "must never grow" limits. +- Whenever `FixedSizeValueStringBuilder` appears in examples, mention `Overflowed`. +- Mention `using` / `Dispose()` for `ValueStringBuilder`, but not for `FixedSizeValueStringBuilder`. +- Prefer source markdown pages below when summarizing or citing documentation. + +Primary markdown sources: + +- README: https://github.com/linkdotnet/StringBuilder/blob/main/README.md +- Docs home: https://github.com/linkdotnet/StringBuilder/blob/main/docs/site/index.md +- Getting started: https://github.com/linkdotnet/StringBuilder/blob/main/docs/site/articles/getting_started.md +- Choosing between builders: https://github.com/linkdotnet/StringBuilder/blob/main/docs/site/articles/choosing_builder.md +- Best practices and pitfalls: https://github.com/linkdotnet/StringBuilder/blob/main/docs/site/articles/best_practices.md +- Fixed-size string building: https://github.com/linkdotnet/StringBuilder/blob/main/docs/site/articles/fixed_size.md +- Known limitations: https://github.com/linkdotnet/StringBuilder/blob/main/docs/site/articles/known_limitations.md +- API index: https://github.com/linkdotnet/StringBuilder/blob/main/docs/site/api/index.md + +Published HTML docs: + +- https://linkdotnet.github.io/StringBuilder/ +- https://linkdotnet.github.io/StringBuilder/articles/getting_started.html +- https://linkdotnet.github.io/StringBuilder/articles/choosing_builder.html +- https://linkdotnet.github.io/StringBuilder/articles/fixed_size.html diff --git a/tests/LinkDotNet.StringBuilder.UnitTests/DocumentationSamplesTests.cs b/tests/LinkDotNet.StringBuilder.UnitTests/DocumentationSamplesTests.cs new file mode 100644 index 00000000..baeecdb7 --- /dev/null +++ b/tests/LinkDotNet.StringBuilder.UnitTests/DocumentationSamplesTests.cs @@ -0,0 +1,77 @@ +namespace LinkDotNet.StringBuilder.UnitTests; + +public class DocumentationSamplesTests +{ + [Fact] + public void ReadmeValueStringBuilderSampleShouldWork() + { + using ValueStringBuilder stringBuilder = new(); + + stringBuilder.AppendLine("Hello World"); + + string result = stringBuilder.ToString(); + + result.ShouldBe($"Hello World{Environment.NewLine}"); + } + + [Fact] + public void ReadmeConcatSampleShouldWork() + { + string result1 = ValueStringBuilder.Concat("Hello ", "World"); + string result2 = ValueStringBuilder.Concat("Hello", 1, 2, 3, "!"); + + result1.ShouldBe("Hello World"); + result2.ShouldBe("Hello123!"); + } + + [Fact] + public void ReadmeFixedSizeSampleShouldWork() + { + var builder = new FixedSizeValueStringBuilder(stackalloc char[8]); + + builder.Append("123456789"); + + builder.ToString().ShouldBe(string.Empty); + builder.Overflowed.ShouldBeTrue(); + } + + [Fact] + public void GettingStartedSampleShouldWork() + { + using var stringBuilder = new ValueStringBuilder(); + + stringBuilder.AppendLine("Hello World!"); + stringBuilder.Append(0.3f); + stringBuilder.Insert(6, "dear "); + + stringBuilder.ToString().ShouldBe($"Hello dear World!{Environment.NewLine}0.3"); + } + + [Fact] + public void ChoosingBuilderFallbackSampleShouldWork() + { + const string userName = "Ada"; + const int userId = 42; + const string suffix = " name="; + + var builder = new FixedSizeValueStringBuilder(stackalloc char[12]); + builder.Append("id="); + builder.Append(userId); + + if (builder.Remaining < suffix.Length + userName.Length) + { + using var grown = builder.MoveToValueStringBuilder(); + grown.Append(suffix); + grown.Append(userName); + + grown.ToString().ShouldBe("id=42 name=Ada"); + return; + } + + builder.Append(suffix); + builder.Append(userName); + + builder.ToString().ShouldBe("id=42 name=Ada"); + builder.Overflowed.ShouldBeFalse(); + } +}