From 9a18cf4b826f82b9647c94decf3797adf42566cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:11:14 +0000 Subject: [PATCH 1/4] perf: avoid FSharpValue.GetUnionFields in toParam via cached tag reader Replace the FSharpValue.GetUnionFields call in toParam (used when unwrapping F# option values for query/form parameters) with a precomputed union-tag reader. GetUnionFields allocates a UnionCaseInfo object and an obj[] on every call. The new approach: - Caches FSharpValue.PreComputeUnionTagReader per option type (O(1) tag check with no allocation after first call) - Caches the Value PropertyInfo per option type (shared with unwrapFSharpOption) - Removes the now-redundant private optionValuePropCache, consolidating both caches in one place This matters for APIs that send many optional query/form parameters; the improvement is visible when profiling clients that call high- frequency endpoints with optional fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 0cfa2ac1..d2e6dede 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -111,6 +111,20 @@ module RuntimeHelpers = let private tryFormatTimeOnly(value: obj) = tryFormatViaMethods timeOnlyTypeName "HH:mm:ss.FFFFFFF" value + // Cache of precomputed union tag readers for F# option types. Avoids the overhead of + // FSharpValue.GetUnionFields (which allocates UnionCaseInfo + obj[]) on each call. + // Stores as (obj -> int) with an explicit wrapper to satisfy nullable annotations. + let private optionTagReaderCache = + Collections.Concurrent.ConcurrentDictionary int>() + + let private makeOptionTagReader(t: Type) : obj -> int = + let reader = Microsoft.FSharp.Reflection.FSharpValue.PreComputeUnionTagReader t + fun (o: obj) -> reader o + + // Cache of the 'Value' PropertyInfo per F# option type, shared with unwrapFSharpOption below. + let private optionValueCache = + Collections.Concurrent.ConcurrentDictionary() + let rec toParam(obj: obj) = match obj with | :? DateTime as dt -> dt.ToString("O") @@ -125,15 +139,20 @@ module RuntimeHelpers = | None -> let ty = obj.GetType() - // Unwrap F# Option: Some(x) -> toParam(x), None -> null + // Unwrap F# Option: Some(x) -> toParam(x), None -> null. + // Uses a precomputed tag reader (cached) to check Some/None without + // allocating a UnionCaseInfo or obj[] on every call. if ty.IsGenericType && ty.GetGenericTypeDefinition() = typedefof> then - let (case, values) = Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(obj, ty) + let tagReader = + optionTagReaderCache.GetOrAdd(ty, System.Func int>(makeOptionTagReader)) + + if tagReader obj = 1 then // 1 = Some + let valueProp = optionValueCache.GetOrAdd(ty, fun t -> t.GetProperty("Value")) - if case.Name = "Some" && values.Length > 0 then - toParam values.[0] + toParam(valueProp.GetValue(obj)) else null else @@ -287,10 +306,7 @@ module RuntimeHelpers = // Unwraps F# option values: returns the inner value for Some, null for None. // This prevents `Some(value)` from being sent as-is in form data. - // The `Value` PropertyInfo is cached per concrete option type to avoid repeated reflection lookups. - let private optionValuePropCache = - Collections.Concurrent.ConcurrentDictionary() - + // Reuses optionValueCache defined alongside toParam above. let private unwrapFSharpOption(value: obj) : obj = if isNull value then null @@ -301,7 +317,7 @@ module RuntimeHelpers = ty.IsGenericType && ty.GetGenericTypeDefinition() = typedefof> then - let prop = optionValuePropCache.GetOrAdd(ty, fun t -> t.GetProperty("Value")) + let prop = optionValueCache.GetOrAdd(ty, fun t -> t.GetProperty("Value")) prop.GetValue(value) else value From 36d7d1e89d4cb0caa592205ec77b3ba7680050e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 24 Apr 2026 22:11:17 +0000 Subject: [PATCH 2/4] ci: trigger checks From 2502fbbf06e00abc1b6c3d263b83762981b7c48e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 06:00:48 +0000 Subject: [PATCH 3/4] perf: hoist GetOrAdd factory delegates into module-level let bindings Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/aa9c5b07-4c10-4389-b812-170f423a4acc Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index d2e6dede..ffc928a2 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -121,10 +121,18 @@ module RuntimeHelpers = let reader = Microsoft.FSharp.Reflection.FSharpValue.PreComputeUnionTagReader t fun (o: obj) -> reader o + // Hoisted factory delegate to avoid allocating a new Func on every GetOrAdd call. + let private optionTagReaderFactory = + System.Func int>(makeOptionTagReader) + // Cache of the 'Value' PropertyInfo per F# option type, shared with unwrapFSharpOption below. let private optionValueCache = Collections.Concurrent.ConcurrentDictionary() + // Hoisted factory delegate to avoid allocating a new lambda on every GetOrAdd call. + let private optionValueFactory = + System.Func(fun t -> t.GetProperty("Value")) + let rec toParam(obj: obj) = match obj with | :? DateTime as dt -> dt.ToString("O") @@ -146,11 +154,10 @@ module RuntimeHelpers = ty.IsGenericType && ty.GetGenericTypeDefinition() = typedefof> then - let tagReader = - optionTagReaderCache.GetOrAdd(ty, System.Func int>(makeOptionTagReader)) + let tagReader = optionTagReaderCache.GetOrAdd(ty, optionTagReaderFactory) if tagReader obj = 1 then // 1 = Some - let valueProp = optionValueCache.GetOrAdd(ty, fun t -> t.GetProperty("Value")) + let valueProp = optionValueCache.GetOrAdd(ty, optionValueFactory) toParam(valueProp.GetValue(obj)) else From ac4b2a82ec6b0e268ba9480cad598dce26a0859a Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Sat, 25 Apr 2026 09:41:07 +0200 Subject: [PATCH 4/4] Update src/SwaggerProvider.Runtime/RuntimeHelpers.fs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index ffc928a2..49beb3f2 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -324,7 +324,7 @@ module RuntimeHelpers = ty.IsGenericType && ty.GetGenericTypeDefinition() = typedefof> then - let prop = optionValueCache.GetOrAdd(ty, fun t -> t.GetProperty("Value")) + let prop = optionValueCache.GetOrAdd(ty, optionValueFactory) prop.GetValue(value) else value