Repository navigation
Thread starvation on high load with System.Text.Json #40072
Description
Description
After changing the serializer from Newtonsoft to System.Text.Json in a streaming application, where a high number of objects are serialized concurrently, we observe a thread starvation issue.
After a few hours under heavy load the number of native threads in the process increase slowly until ~500, which results in severe throughput degradation of the application. As a consequence we had to switch back to Newtonsoft Json.
A memory dump revealed that most of the threads wait on a critical section, see attached data.
Configuration
- Net Core 3.1, x64
- Windows Server 2016
- Azure D4 VM (8 Cores, 32 GB RAM)
Data
Callstack:
ntdll!NtWaitForAlertByThreadId+14
ntdll!RtlpWaitOnAddressWithTimeout+43
ntdll!RtlpWaitOnAddress+b2
ntdll!RtlpWaitOnCriticalSection+db
ntdll!RtlpEnterCriticalSectionContended+cc
ntdll!RtlEnterCriticalSection+40
coreclr!CEEInfo::reportInliningDecision+51a [f:\workspace_work\1\s\src\vm\jitinterface.cpp @ 8292 + 87] f:\workspace_work\1\s\src\vm\jitinterface.cpp @ 8292 + 87
clrjit!InlineResult::Report+de [f:\workspace_work\1\s\src\jit\inline.cpp @ 749] f:\workspace_work\1\s\src\jit\inline.cpp @ 749
clrjit!Compiler::fgInline+3b0 [f:\workspace_work\1\s\src\jit\flowgraph.cpp @ 22013 + a] f:\workspace_work\1\s\src\jit\flowgraph.cpp @ 22013 + a
clrjit!Compiler::fgMorph+131 [f:\workspace_work\1\s\src\jit\morph.cpp @ 17025] f:\workspace_work\1\s\src\jit\morph.cpp @ 17025
clrjit!Compiler::compCompile+4f2 [f:\workspace_work\1\s\src\jit\compiler.cpp @ 4483] f:\workspace_work\1\s\src\jit\compiler.cpp @ 4483
clrjit!Compiler::compCompileHelper+296 [f:\workspace_work\1\s\src\jit\compiler.cpp @ 6035] f:\workspace_work\1\s\src\jit\compiler.cpp @ 6035
clrjit!Compiler::compCompile+21d [f:\workspace_work\1\s\src\jit\compiler.cpp @ 5367 + 20] f:\workspace_work\1\s\src\jit\compiler.cpp @ 5367 + 20
clrjit!jitNativeCode+273 [f:\workspace_work\1\s\src\jit\compiler.cpp @ 6663 + 53] f:\workspace_work\1\s\src\jit\compiler.cpp @ 6663 + 53
clrjit!CILJit::compileMethod+92 [f:\workspace_work\1\s\src\jit\ee_il_dll.cpp @ 319] f:\workspace_work\1\s\src\jit\ee_il_dll.cpp @ 319
coreclr!invokeCompileMethod+dd [f:\workspace_work\1\s\src\vm\jitinterface.cpp @ 12496 + 6e] f:\workspace_work\1\s\src\vm\jitinterface.cpp @ 12496 + 6e
coreclr!CallCompileMethodWithSEHWrapper+50 [f:\workspace_work\1\s\src\vm\jitinterface.cpp @ 12550 + 25] f:\workspace_work\1\s\src\vm\jitinterface.cpp @ 12550 + 25
coreclr!UnsafeJitFunction+96b [f:\workspace_work\1\s\src\vm\jitinterface.cpp @ 13039 + 6a] f:\workspace_work\1\s\src\vm\jitinterface.cpp @ 13039 + 6a
coreclr!MethodDesc::JitCompileCodeLocked+2f9 [f:\workspace_work\1\s\src\vm\prestub.cpp @ 965 + 5c] f:\workspace_work\1\s\src\vm\prestub.cpp @ 965 + 5c
coreclr!MethodDesc::JitCompileCodeLockedEventWrapper+32d [f:\workspace_work\1\s\src\vm\prestub.cpp @ 827 + 18] f:\workspace_work\1\s\src\vm\prestub.cpp @ 827 + 18
coreclr!MethodDesc::JitCompileCode+15f [f:\workspace_work\1\s\src\vm\prestub.cpp @ 773 + f] f:\workspace_work\1\s\src\vm\prestub.cpp @ 773 + f
coreclr!MethodDesc::PrepareILBasedCode+185 [f:\workspace_work\1\s\src\vm\prestub.cpp @ 432 + b] f:\workspace_work\1\s\src\vm\prestub.cpp @ 432 + b
coreclr!MethodDesc::PrepareInitialCode+40 [f:\workspace_work\1\s\src\vm\prestub.cpp @ 321] f:\workspace_work\1\s\src\vm\prestub.cpp @ 321
coreclr!MethodDesc::DoPrestub+44b [f:\workspace_work\1\s\src\vm\prestub.cpp @ 2025 + 8] f:\workspace_work\1\s\src\vm\prestub.cpp @ 2025 + 8
coreclr!PreStubWorker+24c [f:\workspace_work\1\s\src\vm\prestub.cpp @ 1781 + d] f:\workspace_work\1\s\src\vm\prestub.cpp @ 1781 + d
coreclr!ThePreStub+55 [F:\workspace_work\1\s\src\vm\amd64\ThePreStubAMD64.asm @ 22] F:\workspace_work\1\s\src\vm\amd64\ThePreStubAMD64.asm @ 22
System.Text.Json.JsonPropertyInfoCommon4[[System.__Canon, System.Private.CoreLib],[System.__Canon, System.Private.CoreLib],[System.__Canon, System.Private.CoreLib],[System.__Canon, System.Private.CoreLib]].GetValueAsObject(System.Object)+23 0x000001a7bb9197a8
0x000001a7bb9197a8 0x000001a77e92afc8
0x0000006e0a6bd070 0x000001a7820ab460
System.Text.Json.JsonSerializer.HandleEnumerable(System.Text.Json.JsonClassInfo, System.Text.Json.JsonSerializerOptions, System.Text.Json.Utf8JsonWriter, System.Text.Json.WriteStack ByRef)+33
0x0000006e0a6bd030 0x000001a77fa9e6f8
System.Text.Json.JsonSerializer.Write(System.Text.Json.Utf8JsonWriter, Int32, Int32, System.Text.Json.JsonSerializerOptions, System.Text.Json.WriteStack ByRef)+33c
System.Text.Json.JsonSerializer.WriteCore(System.Text.Json.Utf8JsonWriter, System.Object, System.Type, System.Text.Json.JsonSerializerOptions)+b9
System.Text.Json.JsonSerializer.WriteCore(System.Text.Json.PooledByteBufferWriter, System.Object, System.Type, System.Text.Json.JsonSerializerOptions)+86
System.Text.Json.JsonSerializer.WriteCoreBytes(System.Object, System.Type, System.Text.Json.JsonSerializerOptions)+7a
Analysis
Serialization code in application looks like this:
var options = new JsonSerializerOptions
{
Converters = { new CustomJsonStringEnumConverter() },
IgnoreNullValues = true
};
return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes<Message>(toSerialize, options);
The code for CustomJsonStringEnumConverter can be found here.

Activity
stephentoub commentedon Jul 29, 2020
Your repro is creating a new options instance each time. Does the problem go away if you instead cache and reuse a single instance of it?
berndku commentedon Jul 30, 2020
Thanks, after changing the options to a single instance the issue could not be observed any longer during a 2 hour load test.
stephentoub commentedon Jul 30, 2020
@steveharter, can we handle this better? While we encourage folks to cache, not doing so leads to really bad perf with all the code generation and jit'ing.
jkotas commentedon Jul 30, 2020
Add analyzer for this?
scalablecory commentedon Aug 4, 2020
This is
HttpClientall over again, I would find a better solution than an analyzer.steveharter commentedon Aug 4, 2020
That would be good.
We should doc this better. Changing to doc issue.
If just the default options are used (no explicit options instance passed in) then the default options instance is automatically cached. It is also cached under ASP.NET. Also we plan on making it easier in 6.0 to specify the default options as well, so one can use the serialization APIs that don't take the options instance but still specify a custom options instance and its location.
Also, the stack appears to enter the critical section on calling the "getter" delegate. This is likely trying to JIT the IL that the serializer emitted to call the getter. With the code-gen effort that will release for 6.0, there will be no IL generated for the getter so the code for the POCOs are generated ahead-of-time, that should avoid the critical section plus the startup time will significantly improve. However, even with that, there is metadata that needs to be built up and cached, so caching of the options is still recommended even with code-gen.
steveharter commentedon Aug 4, 2020
Also dup of #38982
10 remaining items
eiriktsarpalis commentedon Jan 27, 2022
Wouldn't this require defining a notion of structural equality for
JsonSerializerOptionsinstances? I'm not sure there's a bullet-proof way to do this, consider the following common anti-pattern:How can we determine if two lists of custom converters are equivalent? A conservative approach might be to check for reference equality but in this case it would clearly result in false negatives. We certainly want to avoid false positives as we'd be exposing ourselves to concurrency bugs impacting serialization settings.
I'm also not a huge fan of authoring mitigating APIs a la
HttpClientFactory, since it doesn't address what I think is the core problem: making developers aware of the flaw. Once the developer becomes aware of the issue withJsonSerializerOptions, applying their own mitigation becomes a trivial code change: store as a static, use DI, lift theoptionsinstantiation outside of a hot loop.stephentoub commentedon Jan 27, 2022
Why? We generate IL for some Type. A cache could be at the type level, keyed off Type identity plus any other values we use as part of the IL generation. It also needn't be perfect, just good enough to mitigate the pit of failure it currently is.
Solution needn't be this if there's something better. But a solution is needed.
eiriktsarpalis commentedon Jan 27, 2022
We could definitely try caching the generated IL globally, but I'm assuming that substantial part of the cost stems from all the reflection code run at converter construction time which we can't avoid unless we can determine that the converter caches scoped to a particular options instance are functionally identical.
stephentoub commentedon Jan 27, 2022
That may contribute as well, but all the stacks from all the repros I've seen are due to JIT'ing IL over and over and over.
Like I said, it doesn't need to be perfect. I bet you'd get 90% of the way there if you just handled default cases where there's nothing custom in the mix, e.g. only using the default set of converters we supply.
eiriktsarpalis commentedon Jan 27, 2022
My only concern is that converters are one of the most common customizations applied to serialization. For the X% of users that modify anything but converters, we could be setting them up for major performance regressions just because they happen to apply a custom converter in later iterations of their app. I'd err towards predictability here.
stephentoub commentedon Jan 27, 2022
Not to be snarky, but the current situation is "predictably" bad. The cliff devs fall of off just by doing
new JsonSerializerOptions()is abysmal, and there's nothing to suggest that's not a totally fine thing to do. A quick grep.app shows tons of places where new instances are created and converters aren't customized. There are 68 pages of search results for justnew JsonSerializerOptions(): here are some problematic uses I see just from the first couple pages, and skipping over tests and the like, that don't involve custom converters:https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/RestHelper.cs#L21
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/RestHelper.cs#L85
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/GraphHelper.cs#L33
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/GraphHelper.cs#L128
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/GraphHelper.cs#L179
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/GraphHelper.cs#L201
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/GraphHelper.cs#L297
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/GraphHelper.cs#L316
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/GraphHelper.cs#L328
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/GraphHelper.cs#L334
https://github.com/pnp/powershell/blob/ea27996fad86082cfb0fe4a38a91c8b95166d2d6/src/Commands/Utilities/REST/GraphHelper.cs#L366
https://github.com/Pixeval/Pixeval/blob/3c13928070242a71ee266ecfe3c2da881ab5d78b/src/Pixeval.Utilities/Objects.cs#L124
https://github.com/Pixeval/Pixeval/blob/3c13928070242a71ee266ecfe3c2da881ab5d78b/src/Pixeval.Utilities/Objects.cs#L132
https://github.com/Pixeval/Pixeval/blob/3c13928070242a71ee266ecfe3c2da881ab5d78b/src/Pixeval.Utilities/Objects.cs#L140
https://github.com/Pixeval/Pixeval/blob/3c13928070242a71ee266ecfe3c2da881ab5d78b/src/Pixeval.Utilities/Objects.cs#L147
Heck, even we do it wrong in our sample code:
https://github.com/dotnet/tye/blob/cc61cc866bb97e8c70d1810be8c98f6d2b30b528/samples/dapr/pub-sub/store/Startup.cs#L65-L72
Predictability is important. But the current cliff is so large that we need to do something.
eiriktsarpalis commentedon Jan 27, 2022
Agreed. If it does turn out that caching dynamic methods eliminates 80% of the performance issues with 20% of the effort we should just do that. But there are diminishing returns in reusing equivalent
JsonSerializerOptionsinstances.stephentoub commentedon Jan 27, 2022
@eiriktsarpalis and I chatted offline. We agreed at a minimum we need an analyzer, e.g. that warns about any JsonSerializerOptions construction that flows only to a call to a JsonSerializer method argument. Beyond that, he's going to profile to get a strong sense for where the actual costs are, and based on that we can decide how much further to go. If the primary costs are in JIT'ing, for example, we could look to employ some kind of tiering strategy, where we start with a purely reflection-based approach and only upgrade to reflection emit after N uses. We could look at having a copy-on-write solution for the existing caching, so that at least
new JsonSerializerOptions()would all share the same existing cache. We could look at having a set of caches based on the most common properties set, though we'd also need to figure out if/what eviction policy we might need (we probably already need to think about that with the built-in cached instances, in support of things like collectable assemblies). Etc.eiriktsarpalis commentedon Jan 27, 2022
FWIW we recently added a
JsonSerializerOptions.Defaultproperty to partially mitigate this very problem (the default singleton not being accessible to users until recently).eiriktsarpalis commentedon Jan 27, 2022
I knocked together a prototype implementing memoization for dynamic methods and compared it with
mainusing a couple of new benchmarks. The performance improvements seem impressive:eiriktsarpalis commentedon Feb 15, 2022
The performance issues have been partially mitigated by #64646. Closing in favor of #65396 which tracks pending analyzer work.