Skip to content

[repo-assist] perf: XmlDoc.escapeXml fast-path + UniqueNameGenerator ToLowerInvariant dedup#429

Merged
sergey-tihon merged 3 commits into
masterfrom
repo-assist/perf-escapexml-uniquename-20260510-d1b63da0cc0be898
May 10, 2026
Merged

[repo-assist] perf: XmlDoc.escapeXml fast-path + UniqueNameGenerator ToLowerInvariant dedup#429
sergey-tihon merged 3 commits into
masterfrom
repo-assist/perf-escapexml-uniquename-20260510-d1b63da0cc0be898

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

🤖 This PR was created by Repo Assist, an automated AI assistant.

Summary

Two low-risk design-time performance improvements in Utils.fs:

1. XmlDoc.escapeXmlIndexOfAny fast-path

Before: every call to escapeXml called .Replace() three times, allocating two intermediate strings even when the input contained no XML special characters (&, <, >).

After: a single IndexOfAny(['&'; '<'; '>']) check gates all three Replace calls. When the string is plain text (the common case for OpenAPI summaries and descriptions), zero intermediate strings are allocated and the function returns the original string reference unchanged.

// Before — always allocates 2 intermediate strings
let private escapeXml(s: string) =
    s.Replace("&", "&").Replace("<", "<").Replace(">", ">")

// After — fast path when no special chars present
let private xmlSpecialChars = [| '&'; '<'; '>' |]

let private escapeXml(s: string) =
    if s.IndexOfAny(xmlSpecialChars) < 0 then
        s
    else
        s.Replace("&", "&").Replace("<", "<").Replace(">", ">")

This function is called once per summary, once per description, once per parameter, and once per return doc during schema compilation. For a schema with 50 operations × 5 parameters on average, that's ~350 calls — previously always allocating, now virtually free for plain-text content.

2. UniqueNameGenerator.findUniq — precompute lowercase key

Before: newName.ToLowerInvariant() was called on every recursive iteration of findUniq, even though only the numeric suffix changes between iterations.

After: prefix.ToLowerInvariant() is computed once in MakeUnique and passed as prefixLower to the recursive function. On collision, only the (small) integer suffix is appended to the already-lowercased prefix.

// Before — ToLowerInvariant on every iteration
let rec findUniq prefix i =
    let newName = sprintf "%s%s" prefix (if i = 0 then "" else i.ToString())
    let key = newName.ToLowerInvariant()
    ...
    | true -> findUniq prefix (i + 1)

// After — one ToLowerInvariant per MakeUnique call
let rec findUniq prefix prefixLower i =
    let newName = if i = 0 then prefix else $"{prefix}{i}"
    let key = if i = 0 then prefixLower else $"{prefixLower}{i}"
    ...
    | true -> findUniq prefix prefixLower (i + 1)

member _.MakeUnique methodName =
    findUniq methodName (methodName.ToLowerInvariant()) 0

Test Status

✅ All 389 unit tests pass after the change.

SwaggerProvider.Tests  Total: 389, Errors: 0, Failed: 0, Skipped: 1, Time: 1.017s

Generated by 🌈 Repo Assist, see workflow run. Learn more.

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md

Generated by 🌈 Repo Assist, see workflow run. Learn more.

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@c7d030cd6d4607b90d9ac3ffc8b24aff4f251632

… in UniqueNameGenerator

- XmlDoc.escapeXml: skip all 3 Replace allocations when the input string
  contains no XML special characters ('&', '<', '>'). The vast majority of
  OpenAPI operation summaries and descriptions are plain English text, so
  the IndexOfAny check amortises to near-zero and avoids 2 intermediate
  string allocations per call on the hot design-time compilation path.

- UniqueNameGenerator.findUniq: precompute prefix.ToLowerInvariant() once
  per MakeUnique call (in MakeUnique itself) rather than re-computing it on
  every recursive iteration. This eliminates repeated allocations when two or
  more schema names collide and the generator appends a numeric suffix.

All 389 unit tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sergey-tihon sergey-tihon marked this pull request as ready for review May 10, 2026 20:50
Copilot AI review requested due to automatic review settings May 10, 2026 20:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces two design-time performance optimizations in Utils.fs aimed at reducing allocations and repeated string operations during schema/type compilation.

Changes:

  • Add an IndexOfAny fast-path to XmlDoc.escapeXml to avoid unnecessary string allocations when no XML special characters are present.
  • Optimize UniqueNameGenerator.findUniq by precomputing the lowercased prefix once per MakeUnique call to avoid repeated ToLowerInvariant() work during collision resolution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/SwaggerProvider.DesignTime/Utils.fs
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@sergey-tihon sergey-tihon merged commit b99dfff into master May 10, 2026
2 checks passed
@sergey-tihon sergey-tihon deleted the repo-assist/perf-escapexml-uniquename-20260510-d1b63da0cc0be898 branch May 10, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants