Skip to content

Commit 00cc653

Browse files
committed
Reduce string allocations in parsers
Use ordinal comparisons and span-based parsing to avoid temporary string generation across SIP, RTSP, SDP, RTP, and WebRTC paths. Replace allocation-heavy Trim, Substring, Split, regex, and string concatenation patterns where span slicing, destination-span split buffers, or StringBuilder preserve behavior with fewer intermediate strings. Convert staged string.Format and fixed concatenation sites to interpolation, and replace loop-based string concatenation with StringBuilder where strings are built incrementally. Avoid temporary string creation in SIP custom header prefix checks. Change SIPHeader and RTSPHeader ToString implementations to override object.ToString() so interpolating header instances preserves protocol serialization behavior. Replace span-trim length checks with string.IsNullOrWhiteSpace where only null/blank detection is needed. Keep SDP media status parsing close to the original switch structure by using guarded cases with ordinal ignore-case comparisons. Use discard patterns for guarded switch cases where the matched value is not used. Replace the long SIP/SDP test fixture with a raw string literal and normalize it with ReplaceLineEndings(CRLF) so the protocol payload stays stable across LF and CRLF checkouts. Set the unit test project to C# 14 and import Polyfills so the call also compiles for net462. Convert long SIP and SDP test payloads from CRLF-heavy interpolated strings to raw string literals normalized with ReplaceLineEndings. Share the repeated integration INVITE payload through a helper and update test projects to C# 14 so raw strings compile across target frameworks.
1 parent 647fe67 commit 00cc653

73 files changed

Lines changed: 2497 additions & 1540 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/SIPSorcery.VP8/DebugProbe.cs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
//-----------------------------------------------------------------------------
2020

2121
using System.Diagnostics;
22+
using System.Text;
2223

2324
namespace Vpx.Net
2425
{
@@ -30,13 +31,14 @@ public static void DumpMotionVectors(MODE_INFO[] mip, int macroBlockCols, int ma
3031
Debug.WriteLine("Macro Block Modes:");
3132
for (int i = 0; i < macroBlockRows + 1; i++)
3233
{
33-
string rowStr = $"Row {i} | ";
34+
var rowBuilder = new StringBuilder().Append("Row ").Append(i).Append(" | ");
3435
for (int j = 0; j < macroBlockCols + 1; j++)
3536
{
3637
byte yMode = mip[i * (macroBlockRows + 1) + j].mbmi.mode;
3738
byte uvMode = mip[i * (macroBlockRows + 1) + j].mbmi.uv_mode;
38-
rowStr += $"y={yMode}, uvMode={uvMode} | ";
39+
rowBuilder.Append("y=").Append(yMode).Append(", uvMode=").Append(uvMode).Append(" | ");
3940
}
41+
string rowStr = rowBuilder.ToString();
4042
Debug.WriteLine(rowStr);
4143
}
4244

@@ -54,15 +56,20 @@ public static void DumpMotionVectors(MODE_INFO[] mip, int macroBlockCols, int ma
5456
public static string GetBModeInfoMatrix(b_mode_info[] bModes)
5557
{
5658
// The array will always be 16 elements.
57-
string matrixStr = null;
59+
var matrixBuilder = new StringBuilder();
5860

5961
for(int row=0; row<4; row++)
6062
{
61-
matrixStr += $"[{bModes[row * 4].mv.as_int},{bModes[row * 4 + 1].mv.as_int}" +
62-
$",{bModes[row * 4 +2].mv.as_int},{bModes[row * 4 + 3].mv.as_int}]\n";
63+
matrixBuilder
64+
.Append('[')
65+
.Append(bModes[row * 4].mv.as_int).Append(',')
66+
.Append(bModes[row * 4 + 1].mv.as_int).Append(',')
67+
.Append(bModes[row * 4 + 2].mv.as_int).Append(',')
68+
.Append(bModes[row * 4 + 3].mv.as_int)
69+
.Append("]\n");
6370
}
6471

65-
return matrixStr;
72+
return matrixBuilder.ToString();
6673
}
6774

6875
public static unsafe void DumpMacroBlock(MACROBLOCKD macroBlock, int macroBlockIndex)
@@ -89,11 +96,12 @@ public static unsafe void DumpSubBlockCoefficients(MACROBLOCKD macroBlock)
8996
for(int i=0; i< macroBlock.block.Length; i++)
9097
{
9198
var subBlock = macroBlock.block[i];
92-
string qCoeff = null;
99+
var qCoeffBuilder = new StringBuilder();
93100
for(int j=subBlock.qcoeff.Index; j< subBlock.qcoeff.Index+16; j++)
94101
{
95-
qCoeff += subBlock.qcoeff.src()[j].ToString() + ",";
102+
qCoeffBuilder.Append(subBlock.qcoeff.src()[j]).Append(',');
96103
}
104+
string qCoeff = qCoeffBuilder.ToString();
97105
Debug.WriteLine($"block[{i}].qcoeff={qCoeff}");
98106
}
99107

@@ -104,11 +112,12 @@ public static unsafe void DumpSubBlockCoefficients(MACROBLOCKD macroBlock)
104112
for (int i = 0; i < macroBlock.block.Length; i++)
105113
{
106114
var subBlock = macroBlock.block[i];
107-
string dqCoeff = null;
115+
var dqCoeffBuilder = new StringBuilder();
108116
for (int j = subBlock.dqcoeff.Index; j < subBlock.dqcoeff.Index + 16; j++)
109117
{
110-
dqCoeff += subBlock.dqcoeff.src()[j].ToString() + ",";
118+
dqCoeffBuilder.Append(subBlock.dqcoeff.src()[j]).Append(',');
111119
}
120+
string dqCoeff = dqCoeffBuilder.ToString();
112121
Debug.WriteLine($"block[{i}].dqcoeff={dqCoeff}");
113122
}
114123

@@ -160,21 +169,21 @@ public static class DebugProbeHexStr
160169

161170
public unsafe static string ToHexStr(byte* buffer, int length, char? separator = null)
162171
{
163-
string rv = string.Empty;
172+
var builder = new StringBuilder(length * (separator == null ? 2 : 3));
164173

165174
for (int i = 0; i < length; i++)
166175
{
167176
var val = buffer[i];
168-
rv += hexmap[val >> 4];
169-
rv += hexmap[val & 15];
177+
builder.Append(hexmap[val >> 4]);
178+
builder.Append(hexmap[val & 15]);
170179

171180
if (separator != null && i != length - 1)
172181
{
173-
rv += separator;
182+
builder.Append(separator);
174183
}
175184
}
176185

177-
return rv.ToLower();
186+
return builder.ToString().ToLower();
178187
}
179188
}
180189
}

src/SIPSorcery.VP8/VP8Codec.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public byte[] EncodeVideo(int width, int height, byte[] sample, VideoPixelFormat
155155
// The foundation encoder requires multiples of 16 — no
156156
// padding/cropping support yet.
157157
throw new NotSupportedException(
158-
"Width and height must be positive multiples of 16. Got " + width + "x" + height + ".");
158+
$"Width and height must be positive multiples of 16. Got {width}x{height}.");
159159
}
160160

161161
// Convert the input sample into planar I420.
@@ -168,8 +168,7 @@ public byte[] EncodeVideo(int width, int height, byte[] sample, VideoPixelFormat
168168
if (i420.Length != ySize + 2 * cSize)
169169
{
170170
throw new ArgumentException(
171-
"I420 buffer length " + i420.Length + " does not match expected " +
172-
(ySize + 2 * cSize) + " for " + width + "x" + height + ".");
171+
$"I420 buffer length {i420.Length} does not match expected {ySize + 2 * cSize} for {width}x{height}.");
173172
}
174173

175174
if (_srcY == null || _srcY.Length < ySize) { _srcY = new byte[ySize]; }

src/SIPSorcery/SIPSorcery.csproj

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
2323
<PackageReference Include="DnsClient" Version="1.8.0" />
2424
<PackageReference Include="Makaretu.Dns.Multicast" Version="0.27.0" />
25+
<PackageReference Include="Polyfill" Version="10.7.0">
26+
<PrivateAssets>all</PrivateAssets>
27+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
28+
</PackageReference>
2529
<PackageReference Include="SIPSorcery.WebSocketSharp" Version="0.0.1" />
2630
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
2731
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.203" PrivateAssets="All" />
@@ -35,13 +39,17 @@
3539
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
3640
</ItemGroup>
3741

42+
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
43+
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
44+
</ItemGroup>
45+
3846
<ItemGroup>
3947
<ProjectReference Include="..\SIPSorceryMedia.Abstractions\SIPSorceryMedia.Abstractions.csproj" />
4048
</ItemGroup>
4149

4250
<PropertyGroup>
4351
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
44-
<LangVersion>latest</LangVersion>
52+
<LangVersion>14.0</LangVersion>
4553
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
4654
<NoWarn>$(NoWarn);SYSLIB0050</NoWarn>
4755
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
@@ -66,11 +74,12 @@
6674
<RepositoryUrl>https://github.com/sipsorcery-org/sipsorcery</RepositoryUrl>
6775
<RepositoryType>git</RepositoryType>
6876
<RepositoryBranch>master</RepositoryBranch>
77+
<PolyArgumentExceptions>true</PolyArgumentExceptions>
6978
<PackageTags>SIP WebRTC VoIP RTP SDP STUN ICE SIPSorcery</PackageTags>
7079
<PackageReleaseNotes>-v10.0.8: Bug fixes.
7180
-v10.0.7: Network address change fix for Unity.
72-
-v10.0.6: Bug fixes.
73-
-v10.0.5: Stable release. Bug fixes.
81+
-v10.0.6: Bug fixes.
82+
-v10.0.5: Stable release. Bug fixes.
7483
-v10.0.4-pre: New SRTP and DTLS implementation (huge thanks to @jimm98y).
7584
-v10.0.3: Removed null SRTP ciphers.
7685
-v10.0.2: Removed use of master key index for SRTP.
@@ -94,7 +103,7 @@
94103
-v8.0.0: RTP header extension improvements (thanks to @ChristopheI). Major version to 8 to reflect highest .net runtime supported.</PackageReleaseNotes>
95104
<NeutralLanguage>en</NeutralLanguage>
96105
<Version>10.0.8</Version>
97-
<AssemblyVersion>10.0.8</AssemblyVersion>
106+
<AssemblyVersion>10.0.8</AssemblyVersion>
98107
<FileVersion>10.0.8</FileVersion>
99108
</PropertyGroup>
100109

src/SIPSorcery/app/Media/Sources/AudioExtrasSource.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,7 @@ public int AudioSamplePeriodMilliseconds
159159
{
160160
if (value < AUDIO_SAMPLE_PERIOD_MILLISECONDS_MIN || value > AUDIO_SAMPLE_PERIOD_MILLISECONDS_MAX)
161161
{
162-
throw new ApplicationException("Invalid value for the audio sample period. Must be between " +
163-
$"{AUDIO_SAMPLE_PERIOD_MILLISECONDS_MIN} and {AUDIO_SAMPLE_PERIOD_MILLISECONDS_MAX}ms.");
162+
throw new ApplicationException($"Invalid value for the audio sample period. Must be between {AUDIO_SAMPLE_PERIOD_MILLISECONDS_MIN} and {AUDIO_SAMPLE_PERIOD_MILLISECONDS_MAX}ms.");
164163
}
165164
else
166165
{

src/SIPSorcery/app/SIPPacketMangler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static string MangleSDP(string sdpBody, string publicIPAddress, out bool
5555
&& pubaddr.AddressFamily == AddressFamily.InterNetworkV6
5656
&& addr.AddressFamily == AddressFamily.InterNetworkV6)
5757
{
58-
string mangledSDP = Regex.Replace(sdpBody, @"c=IN IP6 (?<ipaddress>([:a-fA-F0-9]+))", "c=IN IP6" + publicIPAddress, RegexOptions.Singleline);
58+
string mangledSDP = Regex.Replace(sdpBody, @"c=IN IP6 (?<ipaddress>([:a-fA-F0-9]+))", $"c=IN IP6{publicIPAddress}", RegexOptions.Singleline);
5959
wasMangled = true;
6060

6161
return mangledSDP;
@@ -65,7 +65,7 @@ public static string MangleSDP(string sdpBody, string publicIPAddress, out bool
6565
&& addr.AddressFamily == AddressFamily.InterNetwork)
6666
{
6767
//logger.LogDebug("MangleSDP replacing private " + sdpAddress + " with " + publicIPAddress + ".");
68-
string mangledSDP = Regex.Replace(sdpBody, @"c=IN IP4 (?<ipaddress>(\d+\.){3}\d+)", "c=IN IP4 " + publicIPAddress, RegexOptions.Singleline);
68+
string mangledSDP = Regex.Replace(sdpBody, @"c=IN IP4 (?<ipaddress>(\d+\.){3}\d+)", $"c=IN IP4 {publicIPAddress}", RegexOptions.Singleline);
6969
wasMangled = true;
7070

7171
return mangledSDP;

src/SIPSorcery/app/SIPUserAgents/SIPCallDescriptor.cs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public class SIPCallDescriptor
160160
public SIPCallDescriptor(ISIPAccount toSIPAccount, string uri, string fromHeader, string contentType, string content)
161161
{
162162
ToSIPAccount = toSIPAccount;
163-
Uri = uri ?? toSIPAccount.SIPUsername + "@" + toSIPAccount.SIPDomain;
163+
Uri = uri ?? $"{toSIPAccount.SIPUsername}@{toSIPAccount.SIPDomain}";
164164
From = fromHeader;
165165
ContentType = contentType;
166166
Content = content;
@@ -291,14 +291,14 @@ public void ParseCallOptions(string options)
291291
options = options.Trim('[', ']');
292292

293293
// Parse delay time option.
294-
Match delayCallMatch = Regex.Match(options, DELAY_CALL_OPTION_KEY + @"=(?<delaytime>\d+)");
294+
Match delayCallMatch = Regex.Match(options, $@"{DELAY_CALL_OPTION_KEY}=(?<delaytime>\d+)");
295295
if (delayCallMatch.Success)
296296
{
297297
int.TryParse(delayCallMatch.Result("${delaytime}"), out DelaySeconds);
298298
}
299299

300300
// Parse redirect mode option.
301-
Match redirectModeMatch = Regex.Match(options, REDIRECT_MODE_OPTION_KEY + @"=(?<redirectmode>\w)");
301+
Match redirectModeMatch = Regex.Match(options, $@"{REDIRECT_MODE_OPTION_KEY}=(?<redirectmode>\w)");
302302
if (redirectModeMatch.Success)
303303
{
304304
string redirectMode = redirectModeMatch.Result("${redirectmode}");
@@ -321,42 +321,42 @@ public void ParseCallOptions(string options)
321321
}
322322

323323
// Parse call duration limit option.
324-
Match callDurationMatch = Regex.Match(options, CALL_DURATION_OPTION_KEY + @"=(?<callduration>\d+)");
324+
Match callDurationMatch = Regex.Match(options, $@"{CALL_DURATION_OPTION_KEY}=(?<callduration>\d+)");
325325
if (callDurationMatch.Success)
326326
{
327327
int.TryParse(callDurationMatch.Result("${callduration}"), out CallDurationLimit);
328328
}
329329

330330
// Parse the mangle option.
331-
Match mangleMatch = Regex.Match(options, MANGLE_MODE_OPTION_KEY + @"=(?<mangle>\w+)");
331+
Match mangleMatch = Regex.Match(options, $@"{MANGLE_MODE_OPTION_KEY}=(?<mangle>\w+)");
332332
if (mangleMatch.Success)
333333
{
334334
bool.TryParse(mangleMatch.Result("${mangle}"), out MangleResponseSDP);
335335
}
336336

337337
// Parse the From header display name option.
338-
Match fromDisplayNameMatch = Regex.Match(options, FROM_DISPLAY_NAME_KEY + @"=(?<displayname>.+?)(,|$)");
338+
Match fromDisplayNameMatch = Regex.Match(options, $@"{FROM_DISPLAY_NAME_KEY}=(?<displayname>.+?)(,|$)");
339339
if (fromDisplayNameMatch.Success)
340340
{
341341
FromDisplayName = fromDisplayNameMatch.Result("${displayname}").Trim();
342342
}
343343

344344
// Parse the From header URI username option.
345-
Match fromUsernameNameMatch = Regex.Match(options, FROM_USERNAME_KEY + @"=(?<username>.+?)(,|$)");
345+
Match fromUsernameNameMatch = Regex.Match(options, $@"{FROM_USERNAME_KEY}=(?<username>.+?)(,|$)");
346346
if (fromUsernameNameMatch.Success)
347347
{
348348
FromURIUsername = fromUsernameNameMatch.Result("${username}").Trim();
349349
}
350350

351351
// Parse the From header URI host option.
352-
Match fromURIHostMatch = Regex.Match(options, FROM_HOST_KEY + @"=(?<host>.+?)(,|$)");
352+
Match fromURIHostMatch = Regex.Match(options, $@"{FROM_HOST_KEY}=(?<host>.+?)(,|$)");
353353
if (fromURIHostMatch.Success)
354354
{
355355
FromURIHost = fromURIHostMatch.Result("${host}").Trim();
356356
}
357357

358358
// Parse the Transfer behaviour option.
359-
Match transferMatch = Regex.Match(options, TRANSFER_MODE_OPTION_KEY + @"=(?<transfermode>.+?)(,|$)");
359+
Match transferMatch = Regex.Match(options, $@"{TRANSFER_MODE_OPTION_KEY}=(?<transfermode>.+?)(,|$)");
360360
if (transferMatch.Success)
361361
{
362362
string transferMode = transferMatch.Result("${transfermode}");
@@ -387,28 +387,28 @@ public void ParseCallOptions(string options)
387387
}
388388

389389
// Parse the request caller details option.
390-
Match callerDetailsMatch = Regex.Match(options, REQUEST_CALLER_DETAILS + @"=(?<callerdetails>\w+)");
390+
Match callerDetailsMatch = Regex.Match(options, $@"{REQUEST_CALLER_DETAILS}=(?<callerdetails>\w+)");
391391
if (callerDetailsMatch.Success)
392392
{
393393
bool.TryParse(callerDetailsMatch.Result("${callerdetails}"), out RequestCallerDetails);
394394
}
395395

396396
// Parse the accountcode.
397-
Match accountCodeMatch = Regex.Match(options, ACCOUNT_CODE_KEY + @"=(?<accountCode>\w+)");
397+
Match accountCodeMatch = Regex.Match(options, $@"{ACCOUNT_CODE_KEY}=(?<accountCode>\w+)");
398398
if (accountCodeMatch.Success)
399399
{
400400
AccountCode = accountCodeMatch.Result("${accountCode}");
401401
}
402402

403403
// Parse the rate code.
404-
Match rateCodeMatch = Regex.Match(options, RATE_CODE_KEY + @"=(?<rateCode>\w+)");
404+
Match rateCodeMatch = Regex.Match(options, $@"{RATE_CODE_KEY}=(?<rateCode>\w+)");
405405
if (rateCodeMatch.Success)
406406
{
407407
RateCode = rateCodeMatch.Result("${rateCode}");
408408
}
409409

410410
// Parse the delayed reinvite option.
411-
Match delayedReinviteMatch = Regex.Match(options, DELAYED_REINVITE_KEY + @"=(?<delayedReinvite>\d+)");
411+
Match delayedReinviteMatch = Regex.Match(options, $@"{DELAYED_REINVITE_KEY}=(?<delayedReinvite>\d+)");
412412
if (delayedReinviteMatch.Success)
413413
{
414414
int.TryParse(delayedReinviteMatch.Result("${delayedReinvite}"), out ReinviteDelay);
@@ -457,7 +457,14 @@ public static List<string> ParseCustomHeaders(string customHeaders)
457457
//string headerName = customHeader.Substring(0, colonIndex).Trim();
458458
//string headerValue = (customHeader.Length > colonIndex) ? customHeader.Substring(colonIndex + 1).Trim() : String.Empty;
459459

460-
if (Regex.Match(customHeader.Trim(), "^(Via|From|Contact|CSeq|Call-ID|Max-Forwards|Content-Length)$", RegexOptions.IgnoreCase).Success)
460+
var trimmedCustomHeader = customHeader.AsSpan().Trim();
461+
if (trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_VIA, StringComparison.OrdinalIgnoreCase) ||
462+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_FROM, StringComparison.OrdinalIgnoreCase) ||
463+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CONTACT, StringComparison.OrdinalIgnoreCase) ||
464+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CSEQ, StringComparison.OrdinalIgnoreCase) ||
465+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CALLID, StringComparison.OrdinalIgnoreCase) ||
466+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_MAXFORWARDS, StringComparison.OrdinalIgnoreCase) ||
467+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CONTENTLENGTH, StringComparison.OrdinalIgnoreCase))
461468
{
462469
logger.LogWarning("ParseCustomHeaders skipping custom header due to an non-permitted string in header name, {CustomHeader}.", customHeader);
463470
continue;

0 commit comments

Comments
 (0)