Call stack missing frame corresponding to the `.Replace` call
Hello team,
We are working on a project where we are tracing specific performance issues back to the code causing them in the user's code base. We are working on a scenario that behaves in a very different way than what we expected.
For instance, we have the following source code:
public static string StringValidation(string data, char replacementChar, CultureInfo culture)
{
List<string> wordList = DisallowedWords
.Where(word => culture.Equals(CultureInfo.InvariantCulture) || culture.Equals(word.Culture))
.Select(word => word.Text).ToList();
foreach (string word in wordList)
{
data = data.Replace(word, replacementChar.ToString(), ignoreCase: true, culture);
}
return data;
}
.Replace represents a performance issue, and we are trying to find it in the code base given the following stack trace:
System.ReadOnlySpan`1<wchar>,int32*,value class System.Globalization.CompareOptions,bool)
system.private.corelib.il!System.String.ReplaceCore(value class System.ReadOnlySpan`1<wchar>,value class System.ReadOnlySpan`1<wchar>,value class System.ReadOnlySpan`1<wchar>,class System.Globalization.CompareInfo,value class System.Globalization.CompareOptions)
store!Store.Reviews.ReviewValidation.StringValidation(class System.String,wchar,class System.Globalization.CultureInfo)
store!Store.Reviews.BackgroundReviewValidation+<ExecuteAsync>d__0.MoveNext()
However, the stack trace seems to be "skipping" one frame: the actual .Replace call. It goes directly to the internal .NET implementation: String.ReplaceCore. You can see it as well in the following screenshot of the trace in PerfView.
Could you please help us understand what's going on? Is that expected? Shouldn't it also have the .Replace frame?
We have the corresponding .diagsession file, in case you find it useful and want us to share it as well.
CC: @xiaomi7732
Two reasons come to my mind. First is simply the resolution of sampling (I'm assuming you used sampling). You can try cranking up the sampling from default in VS of 1000 samples/s to maximum 8190 samples/s. Other reasons could be because of inlining, a performance optimization JIT could do.
In general profiling/tracing data are not going to give you always exactly the frames/line numbers, because compiler/JIT heavily optimizes the code to make it run as fast as possible, so there's no 1:1 mapping to original code. You can profile with most of these optimizations disabled, but then you're profiling different code compared to what's running "in prod" and getting (often very) different numbers.
BTW I see you're doing some string manipulation, look at SearchValues<T> introduced recently. It might give you the performance boost you're looking for.
Two reasons come to my mind. First is simply the resolution of sampling (I'm assuming you used sampling). You can try cranking up the sampling from default in VS of 1000 samples/s to maximum 8190 samples/s. Other reasons could be because of inlining, a performance optimization JIT could do.
@cincuranet is right here. I suspect this is inlining - what you are seeing is very common. If you want to know for sure, you can enable JIT inlining ETW events (/JitInlining), and make sure you capture your workload from process start to ensure that you capture the method in question being jitted. Then, you can open the JITStats view and look in the list of inlined methods.