11using System ;
22using System . Collections . Generic ;
3+ using System . Runtime . CompilerServices ;
34using System . Text ;
45using RESPite ;
56
@@ -10,9 +11,14 @@ namespace StackExchange.Redis
1011 /// </summary>
1112 public sealed class CommandMap
1213 {
13- private readonly AsciiHash [ ] map ;
14+ private readonly CommandBytes [ ] map ;
15+ private readonly byte [ ] bytes ;
1416
15- internal CommandMap ( AsciiHash [ ] map ) => this . map = map ;
17+ private CommandMap ( CommandBytes [ ] map , byte [ ] bytes )
18+ {
19+ this . map = map ;
20+ this . bytes = bytes ;
21+ }
1622
1723 /// <summary>
1824 /// The default commands specified by redis.
@@ -184,22 +190,31 @@ internal void AppendDeltas(StringBuilder sb)
184190 var knownCmd = all [ i ] ;
185191 if ( knownCmd is RedisCommand . UNKNOWN ) continue ;
186192 var keyString = knownCmd . ToString ( ) ;
187- var keyBytes = new AsciiHash ( keyString ) ;
188193 var value = map [ i ] ;
189- if ( ! keyBytes . Equals ( value ) )
194+ var valueBytes = value . GetCommandBytes ( bytes ) ;
195+ if ( ! AsciiHash . EqualsCI ( keyString , valueBytes ) )
190196 {
191197 if ( sb . Length != 0 ) sb . Append ( ',' ) ;
192- sb . Append ( '$' ) . Append ( keyString ) . Append ( '=' ) . Append ( value ) ;
198+ sb . Append ( '$' ) . Append ( keyString ) . Append ( '=' ) ;
199+ if ( ! valueBytes . IsEmpty )
200+ {
201+ sb . Append ( Encoding . ASCII . GetString ( valueBytes ) ) ;
202+ }
193203 }
194204 }
195205 }
196206
197207 internal void AssertAvailable ( RedisCommand command )
198208 {
199- if ( map [ ( int ) command ] . IsEmpty ) throw ExceptionFactory . CommandDisabled ( command ) ;
209+ if ( map [ ( int ) command ] . IsEmpty ) ThrowCommandDisabled ( command ) ;
210+
211+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
212+ static void ThrowCommandDisabled ( RedisCommand command ) => throw ExceptionFactory . CommandDisabled ( command ) ;
200213 }
201214
202- internal AsciiHash GetBytes ( RedisCommand command ) => map [ ( int ) command ] ;
215+ internal ReadOnlySpan < byte > GetCommandBytes ( RedisCommand command ) => map [ ( int ) command ] . GetCommandBytes ( bytes ) ;
216+
217+ internal ReadOnlySpan < byte > GetResp ( RedisCommand command ) => map [ ( int ) command ] . GetResp ( bytes ) ;
203218
204219 internal bool IsAvailable ( RedisCommand command ) => ! map [ ( int ) command ] . IsEmpty ;
205220
@@ -211,27 +226,107 @@ private static CommandMap CreateImpl(Dictionary<string, string?>? caseInsensitiv
211226 {
212227 var commands = AllCommands ;
213228
214- // todo: optimize and support ad-hoc overrides/disables, and shared buffer rather than multiple arrays
215- var map = new AsciiHash [ commands . Length ] ;
229+ int totalLength = 0 ;
216230 for ( int i = 0 ; i < commands . Length ; i ++ )
217231 {
218- int idx = ( int ) commands [ i ] ;
219- string ? name = commands [ i ] . ToString ( ) , value = name ;
232+ var value = GetCommandValue ( commands [ i ] , caseInsensitiveOverrides , exclusions ) ;
233+ if ( string . IsNullOrEmpty ( value ) ) continue ;
220234
221- if ( commands [ i ] is RedisCommand . UNKNOWN || exclusions ? . Contains ( commands [ i ] ) == true )
222- {
223- map [ idx ] = default ;
224- }
225- else
226- {
227- if ( caseInsensitiveOverrides != null && caseInsensitiveOverrides . TryGetValue ( name , out string ? tmp ) )
228- {
229- value = tmp ? . ToUpperInvariant ( ) ;
230- }
231- map [ idx ] = new AsciiHash ( value ) ;
232- }
235+ totalLength += GetBulkStringLength ( Encoding . ASCII . GetByteCount ( value ) ) ;
236+ }
237+
238+ // Store all mapped command names as RESP bulk-string fragments in one buffer - everything is then
239+ // ready to throw directly into the stream.
240+ var map = new CommandBytes [ commands . Length ] ;
241+
242+ // Currently (8.8-ish) this is approx 3k; that's very reasonable to avoid a ton of CPU cycles in
243+ // the most common write path.
244+ var bytes = totalLength == 0 ? Array . Empty < byte > ( ) : new byte [ totalLength ] ;
245+ int offset = 0 ;
246+ for ( int i = 0 ; i < commands . Length ; i ++ )
247+ {
248+ var command = commands [ i ] ;
249+ var value = GetCommandValue ( command , caseInsensitiveOverrides , exclusions ) ;
250+ if ( string . IsNullOrEmpty ( value ) ) continue ;
251+
252+ int payloadLength = Encoding . ASCII . GetByteCount ( value ) ;
253+ int respLength = GetBulkStringLength ( payloadLength ) ;
254+ map [ ( int ) command ] = new CommandBytes ( offset , respLength , GetPayloadOffset ( payloadLength ) ) ;
255+
256+ var span = bytes . AsSpan ( offset , respLength ) ;
257+ span [ 0 ] = ( byte ) '$' ;
258+ int payloadOffset = MessageWriter . WriteRaw ( span , payloadLength , offset : 1 ) ;
259+ var payload = span . Slice ( payloadOffset , payloadLength ) ;
260+ int written = Encoding . ASCII . GetBytes ( value . AsSpan ( ) , payload ) ;
261+ if ( written != payloadLength ) ThrowAsciiEncodeLengthCheckFailure ( ) ;
262+ AsciiHash . ToUpper ( payload ) ;
263+ MessageWriter . WriteCrlf ( span , payloadOffset + payloadLength ) ;
264+ offset += respLength ;
265+ }
266+ return new CommandMap ( map , bytes ) ;
267+
268+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
269+ static void ThrowAsciiEncodeLengthCheckFailure ( ) => throw new InvalidOperationException ( "ASCII encode length check failure" ) ;
270+ }
271+
272+ private static string ? GetCommandValue (
273+ RedisCommand command ,
274+ Dictionary < string , string ? > ? caseInsensitiveOverrides ,
275+ HashSet < RedisCommand > ? exclusions )
276+ {
277+ if ( command is RedisCommand . UNKNOWN || exclusions ? . Contains ( command ) == true ) return null ;
278+
279+ var name = command . ToString ( ) ;
280+ if ( caseInsensitiveOverrides != null && caseInsensitiveOverrides . TryGetValue ( name , out string ? value ) )
281+ {
282+ return value ;
283+ }
284+
285+ return name ;
286+ }
287+
288+ // ${N}\r\n{RAW}\r\n
289+ private static int GetBulkStringLength ( int payloadLength ) => 5 + GetDigitCount ( payloadLength ) + payloadLength ;
290+
291+ // ${N}\r\n
292+ private static byte GetPayloadOffset ( int payloadLength ) => checked ( ( byte ) ( 3 + GetDigitCount ( payloadLength ) ) ) ;
293+
294+ private static int GetDigitCount ( int value )
295+ {
296+ if ( value < 10 ) return 1 ;
297+ if ( value < 100 ) return 2 ;
298+ int digits = 1 ;
299+ while ( ( value /= 10 ) != 0 )
300+ {
301+ digits ++ ;
302+ }
303+ return digits ;
304+ }
305+
306+ private readonly struct CommandBytes ( int offset , int length , byte payloadOffset )
307+ {
308+ // Tracks position inside a shared buffer; given
309+ // $3\r\nFOO\r\n$3\r\nBAR\r\n we have the positions (for BAR):
310+ // ^ a ^ b ^c
311+ // We know that the trailer is always exactly 2 bytes, so we don't need to store the
312+ // length of the command itself - we can infer from the other values.
313+ // offset is a, payloadOffset is a-to-b, length is a-to-c
314+ private readonly uint offset = checked ( ( uint ) offset ) ;
315+ private readonly ushort length = checked ( ( ushort ) length ) ;
316+
317+ public bool IsEmpty => length == 0 ;
318+
319+ // this will be fine even for a default instance
320+ public ReadOnlySpan < byte > GetResp ( byte [ ] bytes ) => new ( bytes , ( int ) offset , length ) ;
321+
322+ public ReadOnlySpan < byte > GetCommandBytes ( byte [ ] bytes )
323+ {
324+ if ( IsEmpty ) return default ;
325+ return new ReadOnlySpan < byte > (
326+ bytes ,
327+ checked ( ( int ) offset ) + payloadOffset ,
328+ length - payloadOffset - 2 ) ;
233329 }
234- return new CommandMap ( map ) ;
235330 }
236331 }
237332}
0 commit comments