1 #region Copyright notice and license
2 
3 // Copyright 2015 gRPC authors.
4 //
5 // Licensed under the Apache License, Version 2.0 (the "License");
6 // you may not use this file except in compliance with the License.
7 // You may obtain a copy of the License at
8 //
9 //     http://www.apache.org/licenses/LICENSE-2.0
10 //
11 // Unless required by applicable law or agreed to in writing, software
12 // distributed under the License is distributed on an "AS IS" BASIS,
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 // See the License for the specific language governing permissions and
15 // limitations under the License.
16 
17 #endregion
18 
19 using System;
20 using System.Collections.Concurrent;
21 using System.Collections.Generic;
22 using System.Diagnostics;
23 using System.IO;
24 using System.Linq;
25 using System.Text.RegularExpressions;
26 using System.Threading;
27 using System.Threading.Tasks;
28 using Google.Protobuf;
29 using Grpc.Core;
30 using Grpc.Core.Internal;
31 using Grpc.Core.Logging;
32 using Grpc.Core.Profiling;
33 using Grpc.Core.Utils;
34 using NUnit.Framework;
35 using Grpc.Testing;
36 
37 namespace Grpc.IntegrationTesting
38 {
39     /// <summary>
40     /// Helper methods to start client runners for performance testing.
41     /// </summary>
42     public class ClientRunners
43     {
44         static readonly ILogger Logger = GrpcEnvironment.Logger.ForType<ClientRunners>();
45 
46         // Profilers to use for clients.
47         static readonly BlockingCollection<BasicProfiler> profilers = new BlockingCollection<BasicProfiler>();
48 
AddProfiler(BasicProfiler profiler)49         internal static void AddProfiler(BasicProfiler profiler)
50         {
51             GrpcPreconditions.CheckNotNull(profiler);
52             profilers.Add(profiler);
53         }
54 
55         /// <summary>
56         /// Creates a started client runner.
57         /// </summary>
CreateStarted(ClientConfig config)58         public static IClientRunner CreateStarted(ClientConfig config)
59         {
60             Logger.Debug("ClientConfig: {0}", config);
61 
62             if (config.AsyncClientThreads != 0)
63             {
64                 Logger.Warning("ClientConfig.AsyncClientThreads is not supported for C#. Ignoring the value");
65             }
66             if (config.CoreLimit != 0)
67             {
68                 Logger.Warning("ClientConfig.CoreLimit is not supported for C#. Ignoring the value");
69             }
70             if (config.CoreList.Count > 0)
71             {
72                 Logger.Warning("ClientConfig.CoreList is not supported for C#. Ignoring the value");
73             }
74 
75             var channels = CreateChannels(config.ClientChannels, config.ServerTargets, config.SecurityParams, config.ChannelArgs);
76 
77             return new ClientRunnerImpl(channels,
78                 config.ClientType,
79                 config.RpcType,
80                 config.OutstandingRpcsPerChannel,
81                 config.LoadParams,
82                 config.PayloadConfig,
83                 config.HistogramParams,
84                 () => GetNextProfiler());
85         }
86 
CreateChannels(int clientChannels, IEnumerable<string> serverTargets, SecurityParams securityParams, IEnumerable<ChannelArg> channelArguments)87         private static List<Channel> CreateChannels(int clientChannels, IEnumerable<string> serverTargets, SecurityParams securityParams, IEnumerable<ChannelArg> channelArguments)
88         {
89             GrpcPreconditions.CheckArgument(clientChannels > 0, "clientChannels needs to be at least 1.");
90             GrpcPreconditions.CheckArgument(serverTargets.Count() > 0, "at least one serverTarget needs to be specified.");
91 
92             var credentials = securityParams != null ? TestCredentials.CreateSslCredentials() : ChannelCredentials.Insecure;
93             var channelOptions = new List<ChannelOption>();
94             if (securityParams != null && securityParams.ServerHostOverride != "")
95             {
96                 channelOptions.Add(new ChannelOption(ChannelOptions.SslTargetNameOverride, securityParams.ServerHostOverride));
97             }
98             foreach (var channelArgument in channelArguments)
99             {
100                 channelOptions.Add(channelArgument.ToChannelOption());
101             }
102 
103             var result = new List<Channel>();
104             for (int i = 0; i < clientChannels; i++)
105             {
106                 var target = serverTargets.ElementAt(i % serverTargets.Count());
107                 var channel = new Channel(target, credentials, channelOptions);
108                 result.Add(channel);
109             }
110             return result;
111         }
112 
GetNextProfiler()113         private static BasicProfiler GetNextProfiler()
114         {
115             BasicProfiler result = null;
116             profilers.TryTake(out result);
117             return result;
118         }
119     }
120 
121     internal class ClientRunnerImpl : IClientRunner
122     {
123         const double SecondsToNanos = 1e9;
124 
125         readonly List<Channel> channels;
126         readonly ClientType clientType;
127         readonly RpcType rpcType;
128         readonly PayloadConfig payloadConfig;
129         readonly Lazy<byte[]> cachedByteBufferRequest;
130         readonly ThreadLocal<Histogram> threadLocalHistogram;
131 
132         readonly List<Task> runnerTasks;
133         readonly CancellationTokenSource stoppedCts = new CancellationTokenSource();
134         readonly TimeStats timeStats = new TimeStats();
135         readonly AtomicCounter statsResetCount = new AtomicCounter();
136 
ClientRunnerImpl(List<Channel> channels, ClientType clientType, RpcType rpcType, int outstandingRpcsPerChannel, LoadParams loadParams, PayloadConfig payloadConfig, HistogramParams histogramParams, Func<BasicProfiler> profilerFactory)137         public ClientRunnerImpl(List<Channel> channels, ClientType clientType, RpcType rpcType, int outstandingRpcsPerChannel, LoadParams loadParams, PayloadConfig payloadConfig, HistogramParams histogramParams, Func<BasicProfiler> profilerFactory)
138         {
139             GrpcPreconditions.CheckArgument(outstandingRpcsPerChannel > 0, "outstandingRpcsPerChannel");
140             GrpcPreconditions.CheckNotNull(histogramParams, "histogramParams");
141             this.channels = new List<Channel>(channels);
142             this.clientType = clientType;
143             this.rpcType = rpcType;
144             this.payloadConfig = payloadConfig;
145             this.cachedByteBufferRequest = new Lazy<byte[]>(() => new byte[payloadConfig.BytebufParams.ReqSize]);
146             this.threadLocalHistogram = new ThreadLocal<Histogram>(() => new Histogram(histogramParams.Resolution, histogramParams.MaxPossible), true);
147 
148             this.runnerTasks = new List<Task>();
149             foreach (var channel in this.channels)
150             {
151                 for (int i = 0; i < outstandingRpcsPerChannel; i++)
152                 {
153                     var timer = CreateTimer(loadParams, 1.0 / this.channels.Count / outstandingRpcsPerChannel);
154                     var optionalProfiler = profilerFactory();
155                     this.runnerTasks.Add(RunClientAsync(channel, timer, optionalProfiler));
156                 }
157             }
158         }
159 
GetStats(bool reset)160         public ClientStats GetStats(bool reset)
161         {
162             var histogramData = new HistogramData();
163             foreach (var hist in threadLocalHistogram.Values)
164             {
165                 hist.GetSnapshot(histogramData, reset);
166             }
167 
168             var timeSnapshot = timeStats.GetSnapshot(reset);
169 
170             if (reset)
171             {
172                 statsResetCount.Increment();
173             }
174 
175             GrpcEnvironment.Logger.Info("[ClientRunnerImpl.GetStats] GC collection counts: gen0 {0}, gen1 {1}, gen2 {2}, (histogram reset count:{3}, seconds since reset: {4})",
176                 GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2), statsResetCount.Count, timeSnapshot.WallClockTime.TotalSeconds);
177 
178             return new ClientStats
179             {
180                 Latencies = histogramData,
181                 TimeElapsed = timeSnapshot.WallClockTime.TotalSeconds,
182                 TimeUser = timeSnapshot.UserProcessorTime.TotalSeconds,
183                 TimeSystem = timeSnapshot.PrivilegedProcessorTime.TotalSeconds
184             };
185         }
186 
StopAsync()187         public async Task StopAsync()
188         {
189             stoppedCts.Cancel();
190             foreach (var runnerTask in runnerTasks)
191             {
192                 await runnerTask;
193             }
194             foreach (var channel in channels)
195             {
196                 await channel.ShutdownAsync();
197             }
198         }
199 
RunUnary(Channel channel, IInterarrivalTimer timer, BasicProfiler optionalProfiler)200         private void RunUnary(Channel channel, IInterarrivalTimer timer, BasicProfiler optionalProfiler)
201         {
202             if (optionalProfiler != null)
203             {
204                 Profilers.SetForCurrentThread(optionalProfiler);
205             }
206 
207             bool profilerReset = false;
208 
209             var client = new BenchmarkService.BenchmarkServiceClient(channel);
210             var request = CreateSimpleRequest();
211             var stopwatch = new Stopwatch();
212 
213             while (!stoppedCts.Token.IsCancellationRequested)
214             {
215                 // after the first stats reset, also reset the profiler.
216                 if (optionalProfiler != null && !profilerReset && statsResetCount.Count > 0)
217                 {
218                     optionalProfiler.Reset();
219                     profilerReset = true;
220                 }
221 
222                 stopwatch.Restart();
223                 client.UnaryCall(request);
224                 stopwatch.Stop();
225 
226                 // spec requires data point in nanoseconds.
227                 threadLocalHistogram.Value.AddObservation(stopwatch.Elapsed.TotalSeconds * SecondsToNanos);
228 
229                 timer.WaitForNext();
230             }
231         }
232 
RunUnaryAsync(Channel channel, IInterarrivalTimer timer)233         private async Task RunUnaryAsync(Channel channel, IInterarrivalTimer timer)
234         {
235             var client = new BenchmarkService.BenchmarkServiceClient(channel);
236             var request = CreateSimpleRequest();
237             var stopwatch = new Stopwatch();
238 
239             while (!stoppedCts.Token.IsCancellationRequested)
240             {
241                 stopwatch.Restart();
242                 await client.UnaryCallAsync(request);
243                 stopwatch.Stop();
244 
245                 // spec requires data point in nanoseconds.
246                 threadLocalHistogram.Value.AddObservation(stopwatch.Elapsed.TotalSeconds * SecondsToNanos);
247 
248                 await timer.WaitForNextAsync();
249             }
250         }
251 
RunStreamingPingPongAsync(Channel channel, IInterarrivalTimer timer)252         private async Task RunStreamingPingPongAsync(Channel channel, IInterarrivalTimer timer)
253         {
254             var client = new BenchmarkService.BenchmarkServiceClient(channel);
255             var request = CreateSimpleRequest();
256             var stopwatch = new Stopwatch();
257 
258             using (var call = client.StreamingCall())
259             {
260                 while (!stoppedCts.Token.IsCancellationRequested)
261                 {
262                     stopwatch.Restart();
263                     await call.RequestStream.WriteAsync(request);
264                     await call.ResponseStream.MoveNext();
265                     stopwatch.Stop();
266 
267                     // spec requires data point in nanoseconds.
268                     threadLocalHistogram.Value.AddObservation(stopwatch.Elapsed.TotalSeconds * SecondsToNanos);
269 
270                     await timer.WaitForNextAsync();
271                 }
272 
273                 // finish the streaming call
274                 await call.RequestStream.CompleteAsync();
275                 Assert.IsFalse(await call.ResponseStream.MoveNext());
276             }
277         }
278 
RunGenericStreamingAsync(Channel channel, IInterarrivalTimer timer)279         private async Task RunGenericStreamingAsync(Channel channel, IInterarrivalTimer timer)
280         {
281             var request = cachedByteBufferRequest.Value;
282             var stopwatch = new Stopwatch();
283 
284             var callDetails = new CallInvocationDetails<byte[], byte[]>(channel, GenericService.StreamingCallMethod, new CallOptions());
285 
286             using (var call = Calls.AsyncDuplexStreamingCall(callDetails))
287             {
288                 while (!stoppedCts.Token.IsCancellationRequested)
289                 {
290                     stopwatch.Restart();
291                     await call.RequestStream.WriteAsync(request);
292                     await call.ResponseStream.MoveNext();
293                     stopwatch.Stop();
294 
295                     // spec requires data point in nanoseconds.
296                     threadLocalHistogram.Value.AddObservation(stopwatch.Elapsed.TotalSeconds * SecondsToNanos);
297 
298                     await timer.WaitForNextAsync();
299                 }
300 
301                 // finish the streaming call
302                 await call.RequestStream.CompleteAsync();
303                 Assert.IsFalse(await call.ResponseStream.MoveNext());
304             }
305         }
306 
RunClientAsync(Channel channel, IInterarrivalTimer timer, BasicProfiler optionalProfiler)307         private Task RunClientAsync(Channel channel, IInterarrivalTimer timer, BasicProfiler optionalProfiler)
308         {
309             if (payloadConfig.PayloadCase == PayloadConfig.PayloadOneofCase.BytebufParams)
310             {
311                 GrpcPreconditions.CheckArgument(clientType == ClientType.AsyncClient, "Generic client only supports async API");
312                 GrpcPreconditions.CheckArgument(rpcType == RpcType.Streaming, "Generic client only supports streaming calls");
313                 return RunGenericStreamingAsync(channel, timer);
314             }
315 
316             GrpcPreconditions.CheckNotNull(payloadConfig.SimpleParams);
317             if (clientType == ClientType.SyncClient)
318             {
319                 GrpcPreconditions.CheckArgument(rpcType == RpcType.Unary, "Sync client can only be used for Unary calls in C#");
320                 // create a dedicated thread for the synchronous client
321                 return Task.Factory.StartNew(() => RunUnary(channel, timer, optionalProfiler), TaskCreationOptions.LongRunning);
322             }
323             else if (clientType == ClientType.AsyncClient)
324             {
325                 switch (rpcType)
326                 {
327                     case RpcType.Unary:
328                         return RunUnaryAsync(channel, timer);
329                     case RpcType.Streaming:
330                         return RunStreamingPingPongAsync(channel, timer);
331                 }
332             }
333             throw new ArgumentException("Unsupported configuration.");
334         }
335 
CreateSimpleRequest()336         private SimpleRequest CreateSimpleRequest()
337         {
338             GrpcPreconditions.CheckNotNull(payloadConfig.SimpleParams);
339             return new SimpleRequest
340             {
341                 Payload = CreateZerosPayload(payloadConfig.SimpleParams.ReqSize),
342                 ResponseSize = payloadConfig.SimpleParams.RespSize
343             };
344         }
345 
CreateZerosPayload(int size)346         private static Payload CreateZerosPayload(int size)
347         {
348             return new Payload { Body = ByteString.CopyFrom(new byte[size]) };
349         }
350 
CreateTimer(LoadParams loadParams, double loadMultiplier)351         private static IInterarrivalTimer CreateTimer(LoadParams loadParams, double loadMultiplier)
352         {
353             switch (loadParams.LoadCase)
354             {
355                 case LoadParams.LoadOneofCase.ClosedLoop:
356                     return new ClosedLoopInterarrivalTimer();
357                 case LoadParams.LoadOneofCase.Poisson:
358                     return new PoissonInterarrivalTimer(loadParams.Poisson.OfferedLoad * loadMultiplier);
359                 default:
360                     throw new ArgumentException("Unknown load type");
361             }
362         }
363     }
364 }
365