diff --git a/src/csharp/Grpc.Core/Profiling/IProfiler.cs b/src/csharp/Grpc.Core/Profiling/IProfiler.cs
index c426c365d2d6a7cc76c216d902f24a24d5264414..e850375004ec42fd3d615f43051e3669e6fdf9bb 100644
--- a/src/csharp/Grpc.Core/Profiling/IProfiler.cs
+++ b/src/csharp/Grpc.Core/Profiling/IProfiler.cs
@@ -41,7 +41,9 @@ namespace Grpc.Core.Profiling
     internal interface IProfiler 
     {
         void Begin(string tag);
+
         void End(string tag);
+
         void Mark(string tag);
     }
 }
diff --git a/src/csharp/Grpc.Core/Profiling/ProfilerEntry.cs b/src/csharp/Grpc.Core/Profiling/ProfilerEntry.cs
index 5cc4c3c05429378b3ecb8bd1ae3d345304bce427..792e3c3cd056dfbd6d8743b160300a2363243d60 100644
--- a/src/csharp/Grpc.Core/Profiling/ProfilerEntry.cs
+++ b/src/csharp/Grpc.Core/Profiling/ProfilerEntry.cs
@@ -40,7 +40,8 @@ namespace Grpc.Core.Profiling
 {
     internal struct ProfilerEntry
     {
-        public enum Type {
+        public enum Type
+        {
             BEGIN,
             END,
             MARK
diff --git a/src/csharp/Grpc.Core/Profiling/Profilers.cs b/src/csharp/Grpc.Core/Profiling/Profilers.cs
index c8123347f2b954295583c46802f713a15722d31c..471ee20875eaecdf52bab596316b5881df6a43c2 100644
--- a/src/csharp/Grpc.Core/Profiling/Profilers.cs
+++ b/src/csharp/Grpc.Core/Profiling/Profilers.cs
@@ -40,12 +40,12 @@ namespace Grpc.Core.Profiling
 {
     internal static class Profilers
     {
-        static readonly NopProfiler defaultProfiler = new NopProfiler();
+        static readonly NopProfiler DefaultProfiler = new NopProfiler();
         static readonly ThreadLocal<IProfiler> profilers = new ThreadLocal<IProfiler>();
 
         public static IProfiler ForCurrentThread()
         {
-            return profilers.Value ?? defaultProfiler;
+            return profilers.Value ?? DefaultProfiler;
         }
 
         public static void SetForCurrentThread(IProfiler profiler)
@@ -89,15 +89,18 @@ namespace Grpc.Core.Profiling
             this.entries = new ProfilerEntry[capacity];
         }
 
-        public void Begin(string tag) {
+        public void Begin(string tag)
+        {
             AddEntry(new ProfilerEntry(Timespec.PreciseNow, ProfilerEntry.Type.BEGIN, tag));
         }
 
-        public void End(string tag) {
+        public void End(string tag)
+        {
             AddEntry(new ProfilerEntry(Timespec.PreciseNow, ProfilerEntry.Type.END, tag));
         }
 
-        public void Mark(string tag) {
+        public void Mark(string tag)
+        {
             AddEntry(new ProfilerEntry(Timespec.PreciseNow, ProfilerEntry.Type.MARK, tag));
         }
 
diff --git a/src/csharp/Grpc.IntegrationTesting.QpsWorker/.gitignore b/src/csharp/Grpc.IntegrationTesting.QpsWorker/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..a382af2294f7a8b9d7789117aeeb80a0a56bc19b
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting.QpsWorker/.gitignore
@@ -0,0 +1,3 @@
+bin
+obj
+
diff --git a/src/csharp/Grpc.IntegrationTesting.QpsWorker/Grpc.IntegrationTesting.QpsWorker.csproj b/src/csharp/Grpc.IntegrationTesting.QpsWorker/Grpc.IntegrationTesting.QpsWorker.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..8b245ade2e2b609f32f0649219ca59fbeab93302
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting.QpsWorker/Grpc.IntegrationTesting.QpsWorker.csproj
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{B82B7DFE-7F7B-40EF-B3D6-064FF2B01294}</ProjectGuid>
+    <OutputType>Exe</OutputType>
+    <RootNamespace>Grpc.IntegrationTesting.QpsWorker</RootNamespace>
+    <AssemblyName>Grpc.IntegrationTesting.QpsWorker</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <PlatformTarget>AnyCPU</PlatformTarget>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <PlatformTarget>AnyCPU</PlatformTarget>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'ReleaseSigned|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\ReleaseSigned</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <SignAssembly>True</SignAssembly>
+    <AssemblyOriginatorKeyFile>C:\keys\Grpc.snk</AssemblyOriginatorKeyFile>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="..\Grpc.Core\Version.cs">
+      <Link>Version.cs</Link>
+    </Compile>
+    <Compile Include="Program.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ItemGroup>
+    <ProjectReference Include="..\Grpc.Core\Grpc.Core.csproj">
+      <Project>{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}</Project>
+      <Name>Grpc.Core</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\Grpc.IntegrationTesting\Grpc.IntegrationTesting.csproj">
+      <Project>{C61154BA-DD4A-4838-8420-0162A28925E0}</Project>
+      <Name>Grpc.IntegrationTesting</Name>
+    </ProjectReference>
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/csharp/Grpc.IntegrationTesting.QpsWorker/Program.cs b/src/csharp/Grpc.IntegrationTesting.QpsWorker/Program.cs
new file mode 100644
index 0000000000000000000000000000000000000000..308463337fc5983b00b2423e74368e8d182712b3
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting.QpsWorker/Program.cs
@@ -0,0 +1,46 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using Grpc.IntegrationTesting;
+
+namespace Grpc.IntegrationTesting
+{
+    class Program
+    {
+        public static void Main(string[] args)
+        {
+            QpsWorker.Run(args);
+        }
+    }
+}
diff --git a/src/csharp/Grpc.IntegrationTesting.QpsWorker/Properties/AssemblyInfo.cs b/src/csharp/Grpc.IntegrationTesting.QpsWorker/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000000000000000000000000000000000..aacfc16ef450a706419ca10970deaa0c84fd684f
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting.QpsWorker/Properties/AssemblyInfo.cs
@@ -0,0 +1,11 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: AssemblyTitle("Grpc.IntegrationTesting.QpsWorker")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("Google Inc.  All rights reserved.")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
diff --git a/src/csharp/Grpc.IntegrationTesting/BenchmarkServiceImpl.cs b/src/csharp/Grpc.IntegrationTesting/BenchmarkServiceImpl.cs
new file mode 100644
index 0000000000000000000000000000000000000000..47a15224f165d8458d4a3589ac46bb27d3ca241d
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/BenchmarkServiceImpl.cs
@@ -0,0 +1,76 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Grpc.Core;
+using Grpc.Core.Utils;
+
+namespace Grpc.Testing
+{
+    /// <summary>
+    /// Implementation of BenchmarkService server
+    /// </summary>
+    public class BenchmarkServiceImpl : BenchmarkService.IBenchmarkService
+    {
+        private readonly int responseSize;
+
+        public BenchmarkServiceImpl(int responseSize)
+        {
+            this.responseSize = responseSize;
+        }
+
+        public Task<SimpleResponse> UnaryCall(SimpleRequest request, ServerCallContext context)
+        {
+            var response = new SimpleResponse { Payload = CreateZerosPayload(responseSize) };
+            return Task.FromResult(response);
+        }
+
+        public async Task StreamingCall(IAsyncStreamReader<SimpleRequest> requestStream, IServerStreamWriter<SimpleResponse> responseStream, ServerCallContext context)
+        {
+            await requestStream.ForEachAsync(async request =>
+            {
+                var response = new SimpleResponse { Payload = CreateZerosPayload(responseSize) };
+                await responseStream.WriteAsync(response);
+            });
+        }
+
+        private static Payload CreateZerosPayload(int size)
+        {
+            return new Payload { Body = ByteString.CopyFrom(new byte[size]) };
+        }
+    }
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/ClientRunners.cs b/src/csharp/Grpc.IntegrationTesting/ClientRunners.cs
new file mode 100644
index 0000000000000000000000000000000000000000..aa0455c089766910e431e375304cedfc018b7c0f
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/ClientRunners.cs
@@ -0,0 +1,149 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+using Grpc.Testing;
+
+namespace Grpc.IntegrationTesting
+{
+    /// <summary>
+    /// Helper methods to start client runners for performance testing.
+    /// </summary>
+    public static class ClientRunners
+    {
+        /// <summary>
+        /// Creates a started client runner.
+        /// </summary>
+        public static IClientRunner CreateStarted(ClientConfig config)
+        {
+            string target = config.ServerTargets.Single();
+            Grpc.Core.Utils.Preconditions.CheckArgument(config.LoadParams.LoadCase == LoadParams.LoadOneofCase.ClosedLoop);
+
+            var credentials = config.SecurityParams != null ? TestCredentials.CreateSslCredentials() : ChannelCredentials.Insecure;
+            var channel = new Channel(target, credentials);
+
+            switch (config.RpcType)
+            {
+                case RpcType.UNARY:
+                    return new SyncUnaryClientRunner(channel, config.PayloadConfig.SimpleParams.ReqSize);
+
+                case RpcType.STREAMING:
+                default:
+                    throw new ArgumentException("Unsupported RpcType.");
+            }
+        }
+    }
+
+    /// <summary>
+    /// Client that starts synchronous unary calls in a closed loop.
+    /// </summary>
+    public class SyncUnaryClientRunner : IClientRunner
+    {
+        readonly Channel channel;
+        readonly int payloadSize;
+        readonly Histogram histogram;
+
+        readonly BenchmarkService.IBenchmarkServiceClient client;
+        readonly Task runnerTask;
+        readonly CancellationTokenSource stoppedCts;
+        readonly WallClockStopwatch wallClockStopwatch = new WallClockStopwatch();
+        
+        public SyncUnaryClientRunner(Channel channel, int payloadSize)
+        {
+            this.channel = Grpc.Core.Utils.Preconditions.CheckNotNull(channel);
+            this.payloadSize = payloadSize;
+            this.histogram = new Histogram(0.01, 60e9);  // TODO: needs to be in sync with test/cpp/qps/histogram.h
+
+            this.stoppedCts = new CancellationTokenSource();
+            this.client = BenchmarkService.NewClient(channel);
+            this.runnerTask = Task.Factory.StartNew(Run, TaskCreationOptions.LongRunning);
+        }
+
+        public ClientStats GetStats(bool reset)
+        {
+            var histogramData = histogram.GetSnapshot(reset);
+            var secondsElapsed = wallClockStopwatch.GetElapsedSnapshot(reset).TotalSeconds;
+
+            // TODO: populate user time and system time
+            return new ClientStats
+            {
+                Latencies = histogramData,
+                TimeElapsed = secondsElapsed,
+                TimeUser = 0,
+                TimeSystem = 0
+            };
+        }
+
+        public async Task StopAsync()
+        {
+            stoppedCts.Cancel();
+            await runnerTask;
+            await channel.ShutdownAsync();
+        }
+
+        private void Run()
+        {
+            var request = new SimpleRequest
+            {
+                Payload = CreateZerosPayload(payloadSize)
+            };
+            var stopwatch = new Stopwatch();
+
+            while (!stoppedCts.Token.IsCancellationRequested)
+            {
+                stopwatch.Restart();
+                client.UnaryCall(request);
+                stopwatch.Stop();
+
+                // TODO: 1e9 needs to be in sync with C++ code
+                histogram.AddObservation(stopwatch.Elapsed.TotalSeconds * 1e9);
+            }
+        }
+
+        private static Payload CreateZerosPayload(int size)
+        {
+            return new Payload { Body = ByteString.CopyFrom(new byte[size]) };
+        }
+    }
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/Control.cs b/src/csharp/Grpc.IntegrationTesting/Control.cs
new file mode 100644
index 0000000000000000000000000000000000000000..101f6cce91ff9f8563eebf0346a17654c3a90e71
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/Control.cs
@@ -0,0 +1,2302 @@
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+// source: test/proto/benchmarks/control.proto
+#pragma warning disable 1591, 0612, 3021
+#region Designer generated code
+
+using pb = global::Google.Protobuf;
+using pbc = global::Google.Protobuf.Collections;
+using pbr = global::Google.Protobuf.Reflection;
+using scg = global::System.Collections.Generic;
+namespace Grpc.Testing {
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public static partial class Control {
+
+    #region Descriptor
+    public static pbr::FileDescriptor Descriptor {
+      get { return descriptor; }
+    }
+    private static pbr::FileDescriptor descriptor;
+
+    static Control() {
+      byte[] descriptorData = global::System.Convert.FromBase64String(
+          string.Concat(
+            "CiN0ZXN0L3Byb3RvL2JlbmNobWFya3MvY29udHJvbC5wcm90bxIMZ3JwYy50", 
+            "ZXN0aW5nGiR0ZXN0L3Byb3RvL2JlbmNobWFya3MvcGF5bG9hZHMucHJvdG8a", 
+            "IXRlc3QvcHJvdG8vYmVuY2htYXJrcy9zdGF0cy5wcm90byIlCg1Qb2lzc29u", 
+            "UGFyYW1zEhQKDG9mZmVyZWRfbG9hZBgBIAEoASJBCg1Vbmlmb3JtUGFyYW1z", 
+            "EhcKD2ludGVyYXJyaXZhbF9sbxgBIAEoARIXCg9pbnRlcmFycml2YWxfaGkY", 
+            "AiABKAEiKwoTRGV0ZXJtaW5pc3RpY1BhcmFtcxIUCgxvZmZlcmVkX2xvYWQY", 
+            "ASABKAEiOAoMUGFyZXRvUGFyYW1zEhkKEWludGVyYXJyaXZhbF9iYXNlGAEg", 
+            "ASgBEg0KBWFscGhhGAIgASgBIhIKEENsb3NlZExvb3BQYXJhbXMijgIKCkxv", 
+            "YWRQYXJhbXMSNQoLY2xvc2VkX2xvb3AYASABKAsyHi5ncnBjLnRlc3Rpbmcu", 
+            "Q2xvc2VkTG9vcFBhcmFtc0gAEi4KB3BvaXNzb24YAiABKAsyGy5ncnBjLnRl", 
+            "c3RpbmcuUG9pc3NvblBhcmFtc0gAEi4KB3VuaWZvcm0YAyABKAsyGy5ncnBj", 
+            "LnRlc3RpbmcuVW5pZm9ybVBhcmFtc0gAEjMKBmRldGVybRgEIAEoCzIhLmdy", 
+            "cGMudGVzdGluZy5EZXRlcm1pbmlzdGljUGFyYW1zSAASLAoGcGFyZXRvGAUg", 
+            "ASgLMhouZ3JwYy50ZXN0aW5nLlBhcmV0b1BhcmFtc0gAQgYKBGxvYWQiQwoO", 
+            "U2VjdXJpdHlQYXJhbXMSEwoLdXNlX3Rlc3RfY2EYASABKAgSHAoUc2VydmVy", 
+            "X2hvc3Rfb3ZlcnJpZGUYAiABKAki9gIKDENsaWVudENvbmZpZxIWCg5zZXJ2", 
+            "ZXJfdGFyZ2V0cxgBIAMoCRItCgtjbGllbnRfdHlwZRgCIAEoDjIYLmdycGMu", 
+            "dGVzdGluZy5DbGllbnRUeXBlEjUKD3NlY3VyaXR5X3BhcmFtcxgDIAEoCzIc", 
+            "LmdycGMudGVzdGluZy5TZWN1cml0eVBhcmFtcxIkChxvdXRzdGFuZGluZ19y", 
+            "cGNzX3Blcl9jaGFubmVsGAQgASgFEhcKD2NsaWVudF9jaGFubmVscxgFIAEo", 
+            "BRIcChRhc3luY19jbGllbnRfdGhyZWFkcxgHIAEoBRInCghycGNfdHlwZRgI", 
+            "IAEoDjIVLmdycGMudGVzdGluZy5ScGNUeXBlEi0KC2xvYWRfcGFyYW1zGAog", 
+            "ASgLMhguZ3JwYy50ZXN0aW5nLkxvYWRQYXJhbXMSMwoOcGF5bG9hZF9jb25m", 
+            "aWcYCyABKAsyGy5ncnBjLnRlc3RpbmcuUGF5bG9hZENvbmZpZyI4CgxDbGll", 
+            "bnRTdGF0dXMSKAoFc3RhdHMYASABKAsyGS5ncnBjLnRlc3RpbmcuQ2xpZW50", 
+            "U3RhdHMiFQoETWFyaxINCgVyZXNldBgBIAEoCCJoCgpDbGllbnRBcmdzEisK", 
+            "BXNldHVwGAEgASgLMhouZ3JwYy50ZXN0aW5nLkNsaWVudENvbmZpZ0gAEiIK", 
+            "BG1hcmsYAiABKAsyEi5ncnBjLnRlc3RpbmcuTWFya0gAQgkKB2FyZ3R5cGUi", 
+            "6QEKDFNlcnZlckNvbmZpZxItCgtzZXJ2ZXJfdHlwZRgBIAEoDjIYLmdycGMu", 
+            "dGVzdGluZy5TZXJ2ZXJUeXBlEjUKD3NlY3VyaXR5X3BhcmFtcxgCIAEoCzIc", 
+            "LmdycGMudGVzdGluZy5TZWN1cml0eVBhcmFtcxIMCgRwb3J0GAQgASgFEhwK", 
+            "FGFzeW5jX3NlcnZlcl90aHJlYWRzGAcgASgFEhIKCmNvcmVfbGltaXQYCCAB", 
+            "KAUSMwoOcGF5bG9hZF9jb25maWcYCSABKAsyGy5ncnBjLnRlc3RpbmcuUGF5", 
+            "bG9hZENvbmZpZyJoCgpTZXJ2ZXJBcmdzEisKBXNldHVwGAEgASgLMhouZ3Jw", 
+            "Yy50ZXN0aW5nLlNlcnZlckNvbmZpZ0gAEiIKBG1hcmsYAiABKAsyEi5ncnBj", 
+            "LnRlc3RpbmcuTWFya0gAQgkKB2FyZ3R5cGUiVQoMU2VydmVyU3RhdHVzEigK", 
+            "BXN0YXRzGAEgASgLMhkuZ3JwYy50ZXN0aW5nLlNlcnZlclN0YXRzEgwKBHBv", 
+            "cnQYAiABKAUSDQoFY29yZXMYAyABKAUqLwoKQ2xpZW50VHlwZRIPCgtTWU5D", 
+            "X0NMSUVOVBAAEhAKDEFTWU5DX0NMSUVOVBABKi8KClNlcnZlclR5cGUSDwoL", 
+            "U1lOQ19TRVJWRVIQABIQCgxBU1lOQ19TRVJWRVIQASojCgdScGNUeXBlEgkK", 
+            "BVVOQVJZEAASDQoJU1RSRUFNSU5HEAFiBnByb3RvMw=="));
+      descriptor = pbr::FileDescriptor.InternalBuildGeneratedFileFrom(descriptorData,
+          new pbr::FileDescriptor[] { global::Grpc.Testing.Payloads.Descriptor, global::Grpc.Testing.Stats.Descriptor, },
+          new pbr::GeneratedCodeInfo(new[] {typeof(global::Grpc.Testing.ClientType), typeof(global::Grpc.Testing.ServerType), typeof(global::Grpc.Testing.RpcType), }, new pbr::GeneratedCodeInfo[] {
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.PoissonParams), new[]{ "OfferedLoad" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.UniformParams), new[]{ "InterarrivalLo", "InterarrivalHi" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.DeterministicParams), new[]{ "OfferedLoad" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ParetoParams), new[]{ "InterarrivalBase", "Alpha" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ClosedLoopParams), null, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.LoadParams), new[]{ "ClosedLoop", "Poisson", "Uniform", "Determ", "Pareto" }, new[]{ "Load" }, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.SecurityParams), new[]{ "UseTestCa", "ServerHostOverride" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ClientConfig), new[]{ "ServerTargets", "ClientType", "SecurityParams", "OutstandingRpcsPerChannel", "ClientChannels", "AsyncClientThreads", "RpcType", "LoadParams", "PayloadConfig" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ClientStatus), new[]{ "Stats" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.Mark), new[]{ "Reset" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ClientArgs), new[]{ "Setup", "Mark" }, new[]{ "Argtype" }, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ServerConfig), new[]{ "ServerType", "SecurityParams", "Port", "AsyncServerThreads", "CoreLimit", "PayloadConfig" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ServerArgs), new[]{ "Setup", "Mark" }, new[]{ "Argtype" }, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ServerStatus), new[]{ "Stats", "Port", "Cores" }, null, null, null)
+          }));
+    }
+    #endregion
+
+  }
+  #region Enums
+  public enum ClientType {
+    SYNC_CLIENT = 0,
+    ASYNC_CLIENT = 1,
+  }
+
+  public enum ServerType {
+    SYNC_SERVER = 0,
+    ASYNC_SERVER = 1,
+  }
+
+  public enum RpcType {
+    UNARY = 0,
+    STREAMING = 1,
+  }
+
+  #endregion
+
+  #region Messages
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class PoissonParams : pb::IMessage<PoissonParams> {
+    private static readonly pb::MessageParser<PoissonParams> _parser = new pb::MessageParser<PoissonParams>(() => new PoissonParams());
+    public static pb::MessageParser<PoissonParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[0]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public PoissonParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public PoissonParams(PoissonParams other) : this() {
+      offeredLoad_ = other.offeredLoad_;
+    }
+
+    public PoissonParams Clone() {
+      return new PoissonParams(this);
+    }
+
+    public const int OfferedLoadFieldNumber = 1;
+    private double offeredLoad_;
+    public double OfferedLoad {
+      get { return offeredLoad_; }
+      set {
+        offeredLoad_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as PoissonParams);
+    }
+
+    public bool Equals(PoissonParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (OfferedLoad != other.OfferedLoad) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (OfferedLoad != 0D) hash ^= OfferedLoad.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (OfferedLoad != 0D) {
+        output.WriteRawTag(9);
+        output.WriteDouble(OfferedLoad);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (OfferedLoad != 0D) {
+        size += 1 + 8;
+      }
+      return size;
+    }
+
+    public void MergeFrom(PoissonParams other) {
+      if (other == null) {
+        return;
+      }
+      if (other.OfferedLoad != 0D) {
+        OfferedLoad = other.OfferedLoad;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 9: {
+            OfferedLoad = input.ReadDouble();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class UniformParams : pb::IMessage<UniformParams> {
+    private static readonly pb::MessageParser<UniformParams> _parser = new pb::MessageParser<UniformParams>(() => new UniformParams());
+    public static pb::MessageParser<UniformParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[1]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public UniformParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public UniformParams(UniformParams other) : this() {
+      interarrivalLo_ = other.interarrivalLo_;
+      interarrivalHi_ = other.interarrivalHi_;
+    }
+
+    public UniformParams Clone() {
+      return new UniformParams(this);
+    }
+
+    public const int InterarrivalLoFieldNumber = 1;
+    private double interarrivalLo_;
+    public double InterarrivalLo {
+      get { return interarrivalLo_; }
+      set {
+        interarrivalLo_ = value;
+      }
+    }
+
+    public const int InterarrivalHiFieldNumber = 2;
+    private double interarrivalHi_;
+    public double InterarrivalHi {
+      get { return interarrivalHi_; }
+      set {
+        interarrivalHi_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as UniformParams);
+    }
+
+    public bool Equals(UniformParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (InterarrivalLo != other.InterarrivalLo) return false;
+      if (InterarrivalHi != other.InterarrivalHi) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (InterarrivalLo != 0D) hash ^= InterarrivalLo.GetHashCode();
+      if (InterarrivalHi != 0D) hash ^= InterarrivalHi.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (InterarrivalLo != 0D) {
+        output.WriteRawTag(9);
+        output.WriteDouble(InterarrivalLo);
+      }
+      if (InterarrivalHi != 0D) {
+        output.WriteRawTag(17);
+        output.WriteDouble(InterarrivalHi);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (InterarrivalLo != 0D) {
+        size += 1 + 8;
+      }
+      if (InterarrivalHi != 0D) {
+        size += 1 + 8;
+      }
+      return size;
+    }
+
+    public void MergeFrom(UniformParams other) {
+      if (other == null) {
+        return;
+      }
+      if (other.InterarrivalLo != 0D) {
+        InterarrivalLo = other.InterarrivalLo;
+      }
+      if (other.InterarrivalHi != 0D) {
+        InterarrivalHi = other.InterarrivalHi;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 9: {
+            InterarrivalLo = input.ReadDouble();
+            break;
+          }
+          case 17: {
+            InterarrivalHi = input.ReadDouble();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class DeterministicParams : pb::IMessage<DeterministicParams> {
+    private static readonly pb::MessageParser<DeterministicParams> _parser = new pb::MessageParser<DeterministicParams>(() => new DeterministicParams());
+    public static pb::MessageParser<DeterministicParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[2]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public DeterministicParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public DeterministicParams(DeterministicParams other) : this() {
+      offeredLoad_ = other.offeredLoad_;
+    }
+
+    public DeterministicParams Clone() {
+      return new DeterministicParams(this);
+    }
+
+    public const int OfferedLoadFieldNumber = 1;
+    private double offeredLoad_;
+    public double OfferedLoad {
+      get { return offeredLoad_; }
+      set {
+        offeredLoad_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as DeterministicParams);
+    }
+
+    public bool Equals(DeterministicParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (OfferedLoad != other.OfferedLoad) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (OfferedLoad != 0D) hash ^= OfferedLoad.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (OfferedLoad != 0D) {
+        output.WriteRawTag(9);
+        output.WriteDouble(OfferedLoad);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (OfferedLoad != 0D) {
+        size += 1 + 8;
+      }
+      return size;
+    }
+
+    public void MergeFrom(DeterministicParams other) {
+      if (other == null) {
+        return;
+      }
+      if (other.OfferedLoad != 0D) {
+        OfferedLoad = other.OfferedLoad;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 9: {
+            OfferedLoad = input.ReadDouble();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ParetoParams : pb::IMessage<ParetoParams> {
+    private static readonly pb::MessageParser<ParetoParams> _parser = new pb::MessageParser<ParetoParams>(() => new ParetoParams());
+    public static pb::MessageParser<ParetoParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[3]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ParetoParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ParetoParams(ParetoParams other) : this() {
+      interarrivalBase_ = other.interarrivalBase_;
+      alpha_ = other.alpha_;
+    }
+
+    public ParetoParams Clone() {
+      return new ParetoParams(this);
+    }
+
+    public const int InterarrivalBaseFieldNumber = 1;
+    private double interarrivalBase_;
+    public double InterarrivalBase {
+      get { return interarrivalBase_; }
+      set {
+        interarrivalBase_ = value;
+      }
+    }
+
+    public const int AlphaFieldNumber = 2;
+    private double alpha_;
+    public double Alpha {
+      get { return alpha_; }
+      set {
+        alpha_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ParetoParams);
+    }
+
+    public bool Equals(ParetoParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (InterarrivalBase != other.InterarrivalBase) return false;
+      if (Alpha != other.Alpha) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (InterarrivalBase != 0D) hash ^= InterarrivalBase.GetHashCode();
+      if (Alpha != 0D) hash ^= Alpha.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (InterarrivalBase != 0D) {
+        output.WriteRawTag(9);
+        output.WriteDouble(InterarrivalBase);
+      }
+      if (Alpha != 0D) {
+        output.WriteRawTag(17);
+        output.WriteDouble(Alpha);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (InterarrivalBase != 0D) {
+        size += 1 + 8;
+      }
+      if (Alpha != 0D) {
+        size += 1 + 8;
+      }
+      return size;
+    }
+
+    public void MergeFrom(ParetoParams other) {
+      if (other == null) {
+        return;
+      }
+      if (other.InterarrivalBase != 0D) {
+        InterarrivalBase = other.InterarrivalBase;
+      }
+      if (other.Alpha != 0D) {
+        Alpha = other.Alpha;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 9: {
+            InterarrivalBase = input.ReadDouble();
+            break;
+          }
+          case 17: {
+            Alpha = input.ReadDouble();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ClosedLoopParams : pb::IMessage<ClosedLoopParams> {
+    private static readonly pb::MessageParser<ClosedLoopParams> _parser = new pb::MessageParser<ClosedLoopParams>(() => new ClosedLoopParams());
+    public static pb::MessageParser<ClosedLoopParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[4]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ClosedLoopParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ClosedLoopParams(ClosedLoopParams other) : this() {
+    }
+
+    public ClosedLoopParams Clone() {
+      return new ClosedLoopParams(this);
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ClosedLoopParams);
+    }
+
+    public bool Equals(ClosedLoopParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      return size;
+    }
+
+    public void MergeFrom(ClosedLoopParams other) {
+      if (other == null) {
+        return;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class LoadParams : pb::IMessage<LoadParams> {
+    private static readonly pb::MessageParser<LoadParams> _parser = new pb::MessageParser<LoadParams>(() => new LoadParams());
+    public static pb::MessageParser<LoadParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[5]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public LoadParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public LoadParams(LoadParams other) : this() {
+      switch (other.LoadCase) {
+        case LoadOneofCase.ClosedLoop:
+          ClosedLoop = other.ClosedLoop.Clone();
+          break;
+        case LoadOneofCase.Poisson:
+          Poisson = other.Poisson.Clone();
+          break;
+        case LoadOneofCase.Uniform:
+          Uniform = other.Uniform.Clone();
+          break;
+        case LoadOneofCase.Determ:
+          Determ = other.Determ.Clone();
+          break;
+        case LoadOneofCase.Pareto:
+          Pareto = other.Pareto.Clone();
+          break;
+      }
+
+    }
+
+    public LoadParams Clone() {
+      return new LoadParams(this);
+    }
+
+    public const int ClosedLoopFieldNumber = 1;
+    public global::Grpc.Testing.ClosedLoopParams ClosedLoop {
+      get { return loadCase_ == LoadOneofCase.ClosedLoop ? (global::Grpc.Testing.ClosedLoopParams) load_ : null; }
+      set {
+        load_ = value;
+        loadCase_ = value == null ? LoadOneofCase.None : LoadOneofCase.ClosedLoop;
+      }
+    }
+
+    public const int PoissonFieldNumber = 2;
+    public global::Grpc.Testing.PoissonParams Poisson {
+      get { return loadCase_ == LoadOneofCase.Poisson ? (global::Grpc.Testing.PoissonParams) load_ : null; }
+      set {
+        load_ = value;
+        loadCase_ = value == null ? LoadOneofCase.None : LoadOneofCase.Poisson;
+      }
+    }
+
+    public const int UniformFieldNumber = 3;
+    public global::Grpc.Testing.UniformParams Uniform {
+      get { return loadCase_ == LoadOneofCase.Uniform ? (global::Grpc.Testing.UniformParams) load_ : null; }
+      set {
+        load_ = value;
+        loadCase_ = value == null ? LoadOneofCase.None : LoadOneofCase.Uniform;
+      }
+    }
+
+    public const int DetermFieldNumber = 4;
+    public global::Grpc.Testing.DeterministicParams Determ {
+      get { return loadCase_ == LoadOneofCase.Determ ? (global::Grpc.Testing.DeterministicParams) load_ : null; }
+      set {
+        load_ = value;
+        loadCase_ = value == null ? LoadOneofCase.None : LoadOneofCase.Determ;
+      }
+    }
+
+    public const int ParetoFieldNumber = 5;
+    public global::Grpc.Testing.ParetoParams Pareto {
+      get { return loadCase_ == LoadOneofCase.Pareto ? (global::Grpc.Testing.ParetoParams) load_ : null; }
+      set {
+        load_ = value;
+        loadCase_ = value == null ? LoadOneofCase.None : LoadOneofCase.Pareto;
+      }
+    }
+
+    private object load_;
+    public enum LoadOneofCase {
+      None = 0,
+      ClosedLoop = 1,
+      Poisson = 2,
+      Uniform = 3,
+      Determ = 4,
+      Pareto = 5,
+    }
+    private LoadOneofCase loadCase_ = LoadOneofCase.None;
+    public LoadOneofCase LoadCase {
+      get { return loadCase_; }
+    }
+
+    public void ClearLoad() {
+      loadCase_ = LoadOneofCase.None;
+      load_ = null;
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as LoadParams);
+    }
+
+    public bool Equals(LoadParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (!object.Equals(ClosedLoop, other.ClosedLoop)) return false;
+      if (!object.Equals(Poisson, other.Poisson)) return false;
+      if (!object.Equals(Uniform, other.Uniform)) return false;
+      if (!object.Equals(Determ, other.Determ)) return false;
+      if (!object.Equals(Pareto, other.Pareto)) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (loadCase_ == LoadOneofCase.ClosedLoop) hash ^= ClosedLoop.GetHashCode();
+      if (loadCase_ == LoadOneofCase.Poisson) hash ^= Poisson.GetHashCode();
+      if (loadCase_ == LoadOneofCase.Uniform) hash ^= Uniform.GetHashCode();
+      if (loadCase_ == LoadOneofCase.Determ) hash ^= Determ.GetHashCode();
+      if (loadCase_ == LoadOneofCase.Pareto) hash ^= Pareto.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (loadCase_ == LoadOneofCase.ClosedLoop) {
+        output.WriteRawTag(10);
+        output.WriteMessage(ClosedLoop);
+      }
+      if (loadCase_ == LoadOneofCase.Poisson) {
+        output.WriteRawTag(18);
+        output.WriteMessage(Poisson);
+      }
+      if (loadCase_ == LoadOneofCase.Uniform) {
+        output.WriteRawTag(26);
+        output.WriteMessage(Uniform);
+      }
+      if (loadCase_ == LoadOneofCase.Determ) {
+        output.WriteRawTag(34);
+        output.WriteMessage(Determ);
+      }
+      if (loadCase_ == LoadOneofCase.Pareto) {
+        output.WriteRawTag(42);
+        output.WriteMessage(Pareto);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (loadCase_ == LoadOneofCase.ClosedLoop) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(ClosedLoop);
+      }
+      if (loadCase_ == LoadOneofCase.Poisson) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Poisson);
+      }
+      if (loadCase_ == LoadOneofCase.Uniform) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Uniform);
+      }
+      if (loadCase_ == LoadOneofCase.Determ) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Determ);
+      }
+      if (loadCase_ == LoadOneofCase.Pareto) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Pareto);
+      }
+      return size;
+    }
+
+    public void MergeFrom(LoadParams other) {
+      if (other == null) {
+        return;
+      }
+      switch (other.LoadCase) {
+        case LoadOneofCase.ClosedLoop:
+          ClosedLoop = other.ClosedLoop;
+          break;
+        case LoadOneofCase.Poisson:
+          Poisson = other.Poisson;
+          break;
+        case LoadOneofCase.Uniform:
+          Uniform = other.Uniform;
+          break;
+        case LoadOneofCase.Determ:
+          Determ = other.Determ;
+          break;
+        case LoadOneofCase.Pareto:
+          Pareto = other.Pareto;
+          break;
+      }
+
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 10: {
+            global::Grpc.Testing.ClosedLoopParams subBuilder = new global::Grpc.Testing.ClosedLoopParams();
+            if (loadCase_ == LoadOneofCase.ClosedLoop) {
+              subBuilder.MergeFrom(ClosedLoop);
+            }
+            input.ReadMessage(subBuilder);
+            ClosedLoop = subBuilder;
+            break;
+          }
+          case 18: {
+            global::Grpc.Testing.PoissonParams subBuilder = new global::Grpc.Testing.PoissonParams();
+            if (loadCase_ == LoadOneofCase.Poisson) {
+              subBuilder.MergeFrom(Poisson);
+            }
+            input.ReadMessage(subBuilder);
+            Poisson = subBuilder;
+            break;
+          }
+          case 26: {
+            global::Grpc.Testing.UniformParams subBuilder = new global::Grpc.Testing.UniformParams();
+            if (loadCase_ == LoadOneofCase.Uniform) {
+              subBuilder.MergeFrom(Uniform);
+            }
+            input.ReadMessage(subBuilder);
+            Uniform = subBuilder;
+            break;
+          }
+          case 34: {
+            global::Grpc.Testing.DeterministicParams subBuilder = new global::Grpc.Testing.DeterministicParams();
+            if (loadCase_ == LoadOneofCase.Determ) {
+              subBuilder.MergeFrom(Determ);
+            }
+            input.ReadMessage(subBuilder);
+            Determ = subBuilder;
+            break;
+          }
+          case 42: {
+            global::Grpc.Testing.ParetoParams subBuilder = new global::Grpc.Testing.ParetoParams();
+            if (loadCase_ == LoadOneofCase.Pareto) {
+              subBuilder.MergeFrom(Pareto);
+            }
+            input.ReadMessage(subBuilder);
+            Pareto = subBuilder;
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class SecurityParams : pb::IMessage<SecurityParams> {
+    private static readonly pb::MessageParser<SecurityParams> _parser = new pb::MessageParser<SecurityParams>(() => new SecurityParams());
+    public static pb::MessageParser<SecurityParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[6]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public SecurityParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public SecurityParams(SecurityParams other) : this() {
+      useTestCa_ = other.useTestCa_;
+      serverHostOverride_ = other.serverHostOverride_;
+    }
+
+    public SecurityParams Clone() {
+      return new SecurityParams(this);
+    }
+
+    public const int UseTestCaFieldNumber = 1;
+    private bool useTestCa_;
+    public bool UseTestCa {
+      get { return useTestCa_; }
+      set {
+        useTestCa_ = value;
+      }
+    }
+
+    public const int ServerHostOverrideFieldNumber = 2;
+    private string serverHostOverride_ = "";
+    public string ServerHostOverride {
+      get { return serverHostOverride_; }
+      set {
+        serverHostOverride_ = pb::Preconditions.CheckNotNull(value, "value");
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as SecurityParams);
+    }
+
+    public bool Equals(SecurityParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (UseTestCa != other.UseTestCa) return false;
+      if (ServerHostOverride != other.ServerHostOverride) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (UseTestCa != false) hash ^= UseTestCa.GetHashCode();
+      if (ServerHostOverride.Length != 0) hash ^= ServerHostOverride.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (UseTestCa != false) {
+        output.WriteRawTag(8);
+        output.WriteBool(UseTestCa);
+      }
+      if (ServerHostOverride.Length != 0) {
+        output.WriteRawTag(18);
+        output.WriteString(ServerHostOverride);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (UseTestCa != false) {
+        size += 1 + 1;
+      }
+      if (ServerHostOverride.Length != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeStringSize(ServerHostOverride);
+      }
+      return size;
+    }
+
+    public void MergeFrom(SecurityParams other) {
+      if (other == null) {
+        return;
+      }
+      if (other.UseTestCa != false) {
+        UseTestCa = other.UseTestCa;
+      }
+      if (other.ServerHostOverride.Length != 0) {
+        ServerHostOverride = other.ServerHostOverride;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 8: {
+            UseTestCa = input.ReadBool();
+            break;
+          }
+          case 18: {
+            ServerHostOverride = input.ReadString();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ClientConfig : pb::IMessage<ClientConfig> {
+    private static readonly pb::MessageParser<ClientConfig> _parser = new pb::MessageParser<ClientConfig>(() => new ClientConfig());
+    public static pb::MessageParser<ClientConfig> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[7]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ClientConfig() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ClientConfig(ClientConfig other) : this() {
+      serverTargets_ = other.serverTargets_.Clone();
+      clientType_ = other.clientType_;
+      SecurityParams = other.securityParams_ != null ? other.SecurityParams.Clone() : null;
+      outstandingRpcsPerChannel_ = other.outstandingRpcsPerChannel_;
+      clientChannels_ = other.clientChannels_;
+      asyncClientThreads_ = other.asyncClientThreads_;
+      rpcType_ = other.rpcType_;
+      LoadParams = other.loadParams_ != null ? other.LoadParams.Clone() : null;
+      PayloadConfig = other.payloadConfig_ != null ? other.PayloadConfig.Clone() : null;
+    }
+
+    public ClientConfig Clone() {
+      return new ClientConfig(this);
+    }
+
+    public const int ServerTargetsFieldNumber = 1;
+    private static readonly pb::FieldCodec<string> _repeated_serverTargets_codec
+        = pb::FieldCodec.ForString(10);
+    private readonly pbc::RepeatedField<string> serverTargets_ = new pbc::RepeatedField<string>();
+    public pbc::RepeatedField<string> ServerTargets {
+      get { return serverTargets_; }
+    }
+
+    public const int ClientTypeFieldNumber = 2;
+    private global::Grpc.Testing.ClientType clientType_ = global::Grpc.Testing.ClientType.SYNC_CLIENT;
+    public global::Grpc.Testing.ClientType ClientType {
+      get { return clientType_; }
+      set {
+        clientType_ = value;
+      }
+    }
+
+    public const int SecurityParamsFieldNumber = 3;
+    private global::Grpc.Testing.SecurityParams securityParams_;
+    public global::Grpc.Testing.SecurityParams SecurityParams {
+      get { return securityParams_; }
+      set {
+        securityParams_ = value;
+      }
+    }
+
+    public const int OutstandingRpcsPerChannelFieldNumber = 4;
+    private int outstandingRpcsPerChannel_;
+    public int OutstandingRpcsPerChannel {
+      get { return outstandingRpcsPerChannel_; }
+      set {
+        outstandingRpcsPerChannel_ = value;
+      }
+    }
+
+    public const int ClientChannelsFieldNumber = 5;
+    private int clientChannels_;
+    public int ClientChannels {
+      get { return clientChannels_; }
+      set {
+        clientChannels_ = value;
+      }
+    }
+
+    public const int AsyncClientThreadsFieldNumber = 7;
+    private int asyncClientThreads_;
+    public int AsyncClientThreads {
+      get { return asyncClientThreads_; }
+      set {
+        asyncClientThreads_ = value;
+      }
+    }
+
+    public const int RpcTypeFieldNumber = 8;
+    private global::Grpc.Testing.RpcType rpcType_ = global::Grpc.Testing.RpcType.UNARY;
+    public global::Grpc.Testing.RpcType RpcType {
+      get { return rpcType_; }
+      set {
+        rpcType_ = value;
+      }
+    }
+
+    public const int LoadParamsFieldNumber = 10;
+    private global::Grpc.Testing.LoadParams loadParams_;
+    public global::Grpc.Testing.LoadParams LoadParams {
+      get { return loadParams_; }
+      set {
+        loadParams_ = value;
+      }
+    }
+
+    public const int PayloadConfigFieldNumber = 11;
+    private global::Grpc.Testing.PayloadConfig payloadConfig_;
+    public global::Grpc.Testing.PayloadConfig PayloadConfig {
+      get { return payloadConfig_; }
+      set {
+        payloadConfig_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ClientConfig);
+    }
+
+    public bool Equals(ClientConfig other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if(!serverTargets_.Equals(other.serverTargets_)) return false;
+      if (ClientType != other.ClientType) return false;
+      if (!object.Equals(SecurityParams, other.SecurityParams)) return false;
+      if (OutstandingRpcsPerChannel != other.OutstandingRpcsPerChannel) return false;
+      if (ClientChannels != other.ClientChannels) return false;
+      if (AsyncClientThreads != other.AsyncClientThreads) return false;
+      if (RpcType != other.RpcType) return false;
+      if (!object.Equals(LoadParams, other.LoadParams)) return false;
+      if (!object.Equals(PayloadConfig, other.PayloadConfig)) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      hash ^= serverTargets_.GetHashCode();
+      if (ClientType != global::Grpc.Testing.ClientType.SYNC_CLIENT) hash ^= ClientType.GetHashCode();
+      if (securityParams_ != null) hash ^= SecurityParams.GetHashCode();
+      if (OutstandingRpcsPerChannel != 0) hash ^= OutstandingRpcsPerChannel.GetHashCode();
+      if (ClientChannels != 0) hash ^= ClientChannels.GetHashCode();
+      if (AsyncClientThreads != 0) hash ^= AsyncClientThreads.GetHashCode();
+      if (RpcType != global::Grpc.Testing.RpcType.UNARY) hash ^= RpcType.GetHashCode();
+      if (loadParams_ != null) hash ^= LoadParams.GetHashCode();
+      if (payloadConfig_ != null) hash ^= PayloadConfig.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      serverTargets_.WriteTo(output, _repeated_serverTargets_codec);
+      if (ClientType != global::Grpc.Testing.ClientType.SYNC_CLIENT) {
+        output.WriteRawTag(16);
+        output.WriteEnum((int) ClientType);
+      }
+      if (securityParams_ != null) {
+        output.WriteRawTag(26);
+        output.WriteMessage(SecurityParams);
+      }
+      if (OutstandingRpcsPerChannel != 0) {
+        output.WriteRawTag(32);
+        output.WriteInt32(OutstandingRpcsPerChannel);
+      }
+      if (ClientChannels != 0) {
+        output.WriteRawTag(40);
+        output.WriteInt32(ClientChannels);
+      }
+      if (AsyncClientThreads != 0) {
+        output.WriteRawTag(56);
+        output.WriteInt32(AsyncClientThreads);
+      }
+      if (RpcType != global::Grpc.Testing.RpcType.UNARY) {
+        output.WriteRawTag(64);
+        output.WriteEnum((int) RpcType);
+      }
+      if (loadParams_ != null) {
+        output.WriteRawTag(82);
+        output.WriteMessage(LoadParams);
+      }
+      if (payloadConfig_ != null) {
+        output.WriteRawTag(90);
+        output.WriteMessage(PayloadConfig);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      size += serverTargets_.CalculateSize(_repeated_serverTargets_codec);
+      if (ClientType != global::Grpc.Testing.ClientType.SYNC_CLIENT) {
+        size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) ClientType);
+      }
+      if (securityParams_ != null) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(SecurityParams);
+      }
+      if (OutstandingRpcsPerChannel != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(OutstandingRpcsPerChannel);
+      }
+      if (ClientChannels != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(ClientChannels);
+      }
+      if (AsyncClientThreads != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(AsyncClientThreads);
+      }
+      if (RpcType != global::Grpc.Testing.RpcType.UNARY) {
+        size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) RpcType);
+      }
+      if (loadParams_ != null) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(LoadParams);
+      }
+      if (payloadConfig_ != null) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(PayloadConfig);
+      }
+      return size;
+    }
+
+    public void MergeFrom(ClientConfig other) {
+      if (other == null) {
+        return;
+      }
+      serverTargets_.Add(other.serverTargets_);
+      if (other.ClientType != global::Grpc.Testing.ClientType.SYNC_CLIENT) {
+        ClientType = other.ClientType;
+      }
+      if (other.securityParams_ != null) {
+        if (securityParams_ == null) {
+          securityParams_ = new global::Grpc.Testing.SecurityParams();
+        }
+        SecurityParams.MergeFrom(other.SecurityParams);
+      }
+      if (other.OutstandingRpcsPerChannel != 0) {
+        OutstandingRpcsPerChannel = other.OutstandingRpcsPerChannel;
+      }
+      if (other.ClientChannels != 0) {
+        ClientChannels = other.ClientChannels;
+      }
+      if (other.AsyncClientThreads != 0) {
+        AsyncClientThreads = other.AsyncClientThreads;
+      }
+      if (other.RpcType != global::Grpc.Testing.RpcType.UNARY) {
+        RpcType = other.RpcType;
+      }
+      if (other.loadParams_ != null) {
+        if (loadParams_ == null) {
+          loadParams_ = new global::Grpc.Testing.LoadParams();
+        }
+        LoadParams.MergeFrom(other.LoadParams);
+      }
+      if (other.payloadConfig_ != null) {
+        if (payloadConfig_ == null) {
+          payloadConfig_ = new global::Grpc.Testing.PayloadConfig();
+        }
+        PayloadConfig.MergeFrom(other.PayloadConfig);
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 10: {
+            serverTargets_.AddEntriesFrom(input, _repeated_serverTargets_codec);
+            break;
+          }
+          case 16: {
+            clientType_ = (global::Grpc.Testing.ClientType) input.ReadEnum();
+            break;
+          }
+          case 26: {
+            if (securityParams_ == null) {
+              securityParams_ = new global::Grpc.Testing.SecurityParams();
+            }
+            input.ReadMessage(securityParams_);
+            break;
+          }
+          case 32: {
+            OutstandingRpcsPerChannel = input.ReadInt32();
+            break;
+          }
+          case 40: {
+            ClientChannels = input.ReadInt32();
+            break;
+          }
+          case 56: {
+            AsyncClientThreads = input.ReadInt32();
+            break;
+          }
+          case 64: {
+            rpcType_ = (global::Grpc.Testing.RpcType) input.ReadEnum();
+            break;
+          }
+          case 82: {
+            if (loadParams_ == null) {
+              loadParams_ = new global::Grpc.Testing.LoadParams();
+            }
+            input.ReadMessage(loadParams_);
+            break;
+          }
+          case 90: {
+            if (payloadConfig_ == null) {
+              payloadConfig_ = new global::Grpc.Testing.PayloadConfig();
+            }
+            input.ReadMessage(payloadConfig_);
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ClientStatus : pb::IMessage<ClientStatus> {
+    private static readonly pb::MessageParser<ClientStatus> _parser = new pb::MessageParser<ClientStatus>(() => new ClientStatus());
+    public static pb::MessageParser<ClientStatus> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[8]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ClientStatus() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ClientStatus(ClientStatus other) : this() {
+      Stats = other.stats_ != null ? other.Stats.Clone() : null;
+    }
+
+    public ClientStatus Clone() {
+      return new ClientStatus(this);
+    }
+
+    public const int StatsFieldNumber = 1;
+    private global::Grpc.Testing.ClientStats stats_;
+    public global::Grpc.Testing.ClientStats Stats {
+      get { return stats_; }
+      set {
+        stats_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ClientStatus);
+    }
+
+    public bool Equals(ClientStatus other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (!object.Equals(Stats, other.Stats)) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (stats_ != null) hash ^= Stats.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (stats_ != null) {
+        output.WriteRawTag(10);
+        output.WriteMessage(Stats);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (stats_ != null) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Stats);
+      }
+      return size;
+    }
+
+    public void MergeFrom(ClientStatus other) {
+      if (other == null) {
+        return;
+      }
+      if (other.stats_ != null) {
+        if (stats_ == null) {
+          stats_ = new global::Grpc.Testing.ClientStats();
+        }
+        Stats.MergeFrom(other.Stats);
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 10: {
+            if (stats_ == null) {
+              stats_ = new global::Grpc.Testing.ClientStats();
+            }
+            input.ReadMessage(stats_);
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class Mark : pb::IMessage<Mark> {
+    private static readonly pb::MessageParser<Mark> _parser = new pb::MessageParser<Mark>(() => new Mark());
+    public static pb::MessageParser<Mark> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[9]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public Mark() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public Mark(Mark other) : this() {
+      reset_ = other.reset_;
+    }
+
+    public Mark Clone() {
+      return new Mark(this);
+    }
+
+    public const int ResetFieldNumber = 1;
+    private bool reset_;
+    public bool Reset {
+      get { return reset_; }
+      set {
+        reset_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as Mark);
+    }
+
+    public bool Equals(Mark other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (Reset != other.Reset) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (Reset != false) hash ^= Reset.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (Reset != false) {
+        output.WriteRawTag(8);
+        output.WriteBool(Reset);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (Reset != false) {
+        size += 1 + 1;
+      }
+      return size;
+    }
+
+    public void MergeFrom(Mark other) {
+      if (other == null) {
+        return;
+      }
+      if (other.Reset != false) {
+        Reset = other.Reset;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 8: {
+            Reset = input.ReadBool();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ClientArgs : pb::IMessage<ClientArgs> {
+    private static readonly pb::MessageParser<ClientArgs> _parser = new pb::MessageParser<ClientArgs>(() => new ClientArgs());
+    public static pb::MessageParser<ClientArgs> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[10]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ClientArgs() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ClientArgs(ClientArgs other) : this() {
+      switch (other.ArgtypeCase) {
+        case ArgtypeOneofCase.Setup:
+          Setup = other.Setup.Clone();
+          break;
+        case ArgtypeOneofCase.Mark:
+          Mark = other.Mark.Clone();
+          break;
+      }
+
+    }
+
+    public ClientArgs Clone() {
+      return new ClientArgs(this);
+    }
+
+    public const int SetupFieldNumber = 1;
+    public global::Grpc.Testing.ClientConfig Setup {
+      get { return argtypeCase_ == ArgtypeOneofCase.Setup ? (global::Grpc.Testing.ClientConfig) argtype_ : null; }
+      set {
+        argtype_ = value;
+        argtypeCase_ = value == null ? ArgtypeOneofCase.None : ArgtypeOneofCase.Setup;
+      }
+    }
+
+    public const int MarkFieldNumber = 2;
+    public global::Grpc.Testing.Mark Mark {
+      get { return argtypeCase_ == ArgtypeOneofCase.Mark ? (global::Grpc.Testing.Mark) argtype_ : null; }
+      set {
+        argtype_ = value;
+        argtypeCase_ = value == null ? ArgtypeOneofCase.None : ArgtypeOneofCase.Mark;
+      }
+    }
+
+    private object argtype_;
+    public enum ArgtypeOneofCase {
+      None = 0,
+      Setup = 1,
+      Mark = 2,
+    }
+    private ArgtypeOneofCase argtypeCase_ = ArgtypeOneofCase.None;
+    public ArgtypeOneofCase ArgtypeCase {
+      get { return argtypeCase_; }
+    }
+
+    public void ClearArgtype() {
+      argtypeCase_ = ArgtypeOneofCase.None;
+      argtype_ = null;
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ClientArgs);
+    }
+
+    public bool Equals(ClientArgs other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (!object.Equals(Setup, other.Setup)) return false;
+      if (!object.Equals(Mark, other.Mark)) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (argtypeCase_ == ArgtypeOneofCase.Setup) hash ^= Setup.GetHashCode();
+      if (argtypeCase_ == ArgtypeOneofCase.Mark) hash ^= Mark.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (argtypeCase_ == ArgtypeOneofCase.Setup) {
+        output.WriteRawTag(10);
+        output.WriteMessage(Setup);
+      }
+      if (argtypeCase_ == ArgtypeOneofCase.Mark) {
+        output.WriteRawTag(18);
+        output.WriteMessage(Mark);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (argtypeCase_ == ArgtypeOneofCase.Setup) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Setup);
+      }
+      if (argtypeCase_ == ArgtypeOneofCase.Mark) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Mark);
+      }
+      return size;
+    }
+
+    public void MergeFrom(ClientArgs other) {
+      if (other == null) {
+        return;
+      }
+      switch (other.ArgtypeCase) {
+        case ArgtypeOneofCase.Setup:
+          Setup = other.Setup;
+          break;
+        case ArgtypeOneofCase.Mark:
+          Mark = other.Mark;
+          break;
+      }
+
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 10: {
+            global::Grpc.Testing.ClientConfig subBuilder = new global::Grpc.Testing.ClientConfig();
+            if (argtypeCase_ == ArgtypeOneofCase.Setup) {
+              subBuilder.MergeFrom(Setup);
+            }
+            input.ReadMessage(subBuilder);
+            Setup = subBuilder;
+            break;
+          }
+          case 18: {
+            global::Grpc.Testing.Mark subBuilder = new global::Grpc.Testing.Mark();
+            if (argtypeCase_ == ArgtypeOneofCase.Mark) {
+              subBuilder.MergeFrom(Mark);
+            }
+            input.ReadMessage(subBuilder);
+            Mark = subBuilder;
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ServerConfig : pb::IMessage<ServerConfig> {
+    private static readonly pb::MessageParser<ServerConfig> _parser = new pb::MessageParser<ServerConfig>(() => new ServerConfig());
+    public static pb::MessageParser<ServerConfig> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[11]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ServerConfig() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ServerConfig(ServerConfig other) : this() {
+      serverType_ = other.serverType_;
+      SecurityParams = other.securityParams_ != null ? other.SecurityParams.Clone() : null;
+      port_ = other.port_;
+      asyncServerThreads_ = other.asyncServerThreads_;
+      coreLimit_ = other.coreLimit_;
+      PayloadConfig = other.payloadConfig_ != null ? other.PayloadConfig.Clone() : null;
+    }
+
+    public ServerConfig Clone() {
+      return new ServerConfig(this);
+    }
+
+    public const int ServerTypeFieldNumber = 1;
+    private global::Grpc.Testing.ServerType serverType_ = global::Grpc.Testing.ServerType.SYNC_SERVER;
+    public global::Grpc.Testing.ServerType ServerType {
+      get { return serverType_; }
+      set {
+        serverType_ = value;
+      }
+    }
+
+    public const int SecurityParamsFieldNumber = 2;
+    private global::Grpc.Testing.SecurityParams securityParams_;
+    public global::Grpc.Testing.SecurityParams SecurityParams {
+      get { return securityParams_; }
+      set {
+        securityParams_ = value;
+      }
+    }
+
+    public const int PortFieldNumber = 4;
+    private int port_;
+    public int Port {
+      get { return port_; }
+      set {
+        port_ = value;
+      }
+    }
+
+    public const int AsyncServerThreadsFieldNumber = 7;
+    private int asyncServerThreads_;
+    public int AsyncServerThreads {
+      get { return asyncServerThreads_; }
+      set {
+        asyncServerThreads_ = value;
+      }
+    }
+
+    public const int CoreLimitFieldNumber = 8;
+    private int coreLimit_;
+    public int CoreLimit {
+      get { return coreLimit_; }
+      set {
+        coreLimit_ = value;
+      }
+    }
+
+    public const int PayloadConfigFieldNumber = 9;
+    private global::Grpc.Testing.PayloadConfig payloadConfig_;
+    public global::Grpc.Testing.PayloadConfig PayloadConfig {
+      get { return payloadConfig_; }
+      set {
+        payloadConfig_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ServerConfig);
+    }
+
+    public bool Equals(ServerConfig other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (ServerType != other.ServerType) return false;
+      if (!object.Equals(SecurityParams, other.SecurityParams)) return false;
+      if (Port != other.Port) return false;
+      if (AsyncServerThreads != other.AsyncServerThreads) return false;
+      if (CoreLimit != other.CoreLimit) return false;
+      if (!object.Equals(PayloadConfig, other.PayloadConfig)) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (ServerType != global::Grpc.Testing.ServerType.SYNC_SERVER) hash ^= ServerType.GetHashCode();
+      if (securityParams_ != null) hash ^= SecurityParams.GetHashCode();
+      if (Port != 0) hash ^= Port.GetHashCode();
+      if (AsyncServerThreads != 0) hash ^= AsyncServerThreads.GetHashCode();
+      if (CoreLimit != 0) hash ^= CoreLimit.GetHashCode();
+      if (payloadConfig_ != null) hash ^= PayloadConfig.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (ServerType != global::Grpc.Testing.ServerType.SYNC_SERVER) {
+        output.WriteRawTag(8);
+        output.WriteEnum((int) ServerType);
+      }
+      if (securityParams_ != null) {
+        output.WriteRawTag(18);
+        output.WriteMessage(SecurityParams);
+      }
+      if (Port != 0) {
+        output.WriteRawTag(32);
+        output.WriteInt32(Port);
+      }
+      if (AsyncServerThreads != 0) {
+        output.WriteRawTag(56);
+        output.WriteInt32(AsyncServerThreads);
+      }
+      if (CoreLimit != 0) {
+        output.WriteRawTag(64);
+        output.WriteInt32(CoreLimit);
+      }
+      if (payloadConfig_ != null) {
+        output.WriteRawTag(74);
+        output.WriteMessage(PayloadConfig);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (ServerType != global::Grpc.Testing.ServerType.SYNC_SERVER) {
+        size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) ServerType);
+      }
+      if (securityParams_ != null) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(SecurityParams);
+      }
+      if (Port != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(Port);
+      }
+      if (AsyncServerThreads != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(AsyncServerThreads);
+      }
+      if (CoreLimit != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(CoreLimit);
+      }
+      if (payloadConfig_ != null) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(PayloadConfig);
+      }
+      return size;
+    }
+
+    public void MergeFrom(ServerConfig other) {
+      if (other == null) {
+        return;
+      }
+      if (other.ServerType != global::Grpc.Testing.ServerType.SYNC_SERVER) {
+        ServerType = other.ServerType;
+      }
+      if (other.securityParams_ != null) {
+        if (securityParams_ == null) {
+          securityParams_ = new global::Grpc.Testing.SecurityParams();
+        }
+        SecurityParams.MergeFrom(other.SecurityParams);
+      }
+      if (other.Port != 0) {
+        Port = other.Port;
+      }
+      if (other.AsyncServerThreads != 0) {
+        AsyncServerThreads = other.AsyncServerThreads;
+      }
+      if (other.CoreLimit != 0) {
+        CoreLimit = other.CoreLimit;
+      }
+      if (other.payloadConfig_ != null) {
+        if (payloadConfig_ == null) {
+          payloadConfig_ = new global::Grpc.Testing.PayloadConfig();
+        }
+        PayloadConfig.MergeFrom(other.PayloadConfig);
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 8: {
+            serverType_ = (global::Grpc.Testing.ServerType) input.ReadEnum();
+            break;
+          }
+          case 18: {
+            if (securityParams_ == null) {
+              securityParams_ = new global::Grpc.Testing.SecurityParams();
+            }
+            input.ReadMessage(securityParams_);
+            break;
+          }
+          case 32: {
+            Port = input.ReadInt32();
+            break;
+          }
+          case 56: {
+            AsyncServerThreads = input.ReadInt32();
+            break;
+          }
+          case 64: {
+            CoreLimit = input.ReadInt32();
+            break;
+          }
+          case 74: {
+            if (payloadConfig_ == null) {
+              payloadConfig_ = new global::Grpc.Testing.PayloadConfig();
+            }
+            input.ReadMessage(payloadConfig_);
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ServerArgs : pb::IMessage<ServerArgs> {
+    private static readonly pb::MessageParser<ServerArgs> _parser = new pb::MessageParser<ServerArgs>(() => new ServerArgs());
+    public static pb::MessageParser<ServerArgs> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[12]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ServerArgs() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ServerArgs(ServerArgs other) : this() {
+      switch (other.ArgtypeCase) {
+        case ArgtypeOneofCase.Setup:
+          Setup = other.Setup.Clone();
+          break;
+        case ArgtypeOneofCase.Mark:
+          Mark = other.Mark.Clone();
+          break;
+      }
+
+    }
+
+    public ServerArgs Clone() {
+      return new ServerArgs(this);
+    }
+
+    public const int SetupFieldNumber = 1;
+    public global::Grpc.Testing.ServerConfig Setup {
+      get { return argtypeCase_ == ArgtypeOneofCase.Setup ? (global::Grpc.Testing.ServerConfig) argtype_ : null; }
+      set {
+        argtype_ = value;
+        argtypeCase_ = value == null ? ArgtypeOneofCase.None : ArgtypeOneofCase.Setup;
+      }
+    }
+
+    public const int MarkFieldNumber = 2;
+    public global::Grpc.Testing.Mark Mark {
+      get { return argtypeCase_ == ArgtypeOneofCase.Mark ? (global::Grpc.Testing.Mark) argtype_ : null; }
+      set {
+        argtype_ = value;
+        argtypeCase_ = value == null ? ArgtypeOneofCase.None : ArgtypeOneofCase.Mark;
+      }
+    }
+
+    private object argtype_;
+    public enum ArgtypeOneofCase {
+      None = 0,
+      Setup = 1,
+      Mark = 2,
+    }
+    private ArgtypeOneofCase argtypeCase_ = ArgtypeOneofCase.None;
+    public ArgtypeOneofCase ArgtypeCase {
+      get { return argtypeCase_; }
+    }
+
+    public void ClearArgtype() {
+      argtypeCase_ = ArgtypeOneofCase.None;
+      argtype_ = null;
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ServerArgs);
+    }
+
+    public bool Equals(ServerArgs other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (!object.Equals(Setup, other.Setup)) return false;
+      if (!object.Equals(Mark, other.Mark)) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (argtypeCase_ == ArgtypeOneofCase.Setup) hash ^= Setup.GetHashCode();
+      if (argtypeCase_ == ArgtypeOneofCase.Mark) hash ^= Mark.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (argtypeCase_ == ArgtypeOneofCase.Setup) {
+        output.WriteRawTag(10);
+        output.WriteMessage(Setup);
+      }
+      if (argtypeCase_ == ArgtypeOneofCase.Mark) {
+        output.WriteRawTag(18);
+        output.WriteMessage(Mark);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (argtypeCase_ == ArgtypeOneofCase.Setup) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Setup);
+      }
+      if (argtypeCase_ == ArgtypeOneofCase.Mark) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Mark);
+      }
+      return size;
+    }
+
+    public void MergeFrom(ServerArgs other) {
+      if (other == null) {
+        return;
+      }
+      switch (other.ArgtypeCase) {
+        case ArgtypeOneofCase.Setup:
+          Setup = other.Setup;
+          break;
+        case ArgtypeOneofCase.Mark:
+          Mark = other.Mark;
+          break;
+      }
+
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 10: {
+            global::Grpc.Testing.ServerConfig subBuilder = new global::Grpc.Testing.ServerConfig();
+            if (argtypeCase_ == ArgtypeOneofCase.Setup) {
+              subBuilder.MergeFrom(Setup);
+            }
+            input.ReadMessage(subBuilder);
+            Setup = subBuilder;
+            break;
+          }
+          case 18: {
+            global::Grpc.Testing.Mark subBuilder = new global::Grpc.Testing.Mark();
+            if (argtypeCase_ == ArgtypeOneofCase.Mark) {
+              subBuilder.MergeFrom(Mark);
+            }
+            input.ReadMessage(subBuilder);
+            Mark = subBuilder;
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ServerStatus : pb::IMessage<ServerStatus> {
+    private static readonly pb::MessageParser<ServerStatus> _parser = new pb::MessageParser<ServerStatus>(() => new ServerStatus());
+    public static pb::MessageParser<ServerStatus> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Control.Descriptor.MessageTypes[13]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ServerStatus() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ServerStatus(ServerStatus other) : this() {
+      Stats = other.stats_ != null ? other.Stats.Clone() : null;
+      port_ = other.port_;
+      cores_ = other.cores_;
+    }
+
+    public ServerStatus Clone() {
+      return new ServerStatus(this);
+    }
+
+    public const int StatsFieldNumber = 1;
+    private global::Grpc.Testing.ServerStats stats_;
+    public global::Grpc.Testing.ServerStats Stats {
+      get { return stats_; }
+      set {
+        stats_ = value;
+      }
+    }
+
+    public const int PortFieldNumber = 2;
+    private int port_;
+    public int Port {
+      get { return port_; }
+      set {
+        port_ = value;
+      }
+    }
+
+    public const int CoresFieldNumber = 3;
+    private int cores_;
+    public int Cores {
+      get { return cores_; }
+      set {
+        cores_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ServerStatus);
+    }
+
+    public bool Equals(ServerStatus other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (!object.Equals(Stats, other.Stats)) return false;
+      if (Port != other.Port) return false;
+      if (Cores != other.Cores) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (stats_ != null) hash ^= Stats.GetHashCode();
+      if (Port != 0) hash ^= Port.GetHashCode();
+      if (Cores != 0) hash ^= Cores.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (stats_ != null) {
+        output.WriteRawTag(10);
+        output.WriteMessage(Stats);
+      }
+      if (Port != 0) {
+        output.WriteRawTag(16);
+        output.WriteInt32(Port);
+      }
+      if (Cores != 0) {
+        output.WriteRawTag(24);
+        output.WriteInt32(Cores);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (stats_ != null) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Stats);
+      }
+      if (Port != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(Port);
+      }
+      if (Cores != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(Cores);
+      }
+      return size;
+    }
+
+    public void MergeFrom(ServerStatus other) {
+      if (other == null) {
+        return;
+      }
+      if (other.stats_ != null) {
+        if (stats_ == null) {
+          stats_ = new global::Grpc.Testing.ServerStats();
+        }
+        Stats.MergeFrom(other.Stats);
+      }
+      if (other.Port != 0) {
+        Port = other.Port;
+      }
+      if (other.Cores != 0) {
+        Cores = other.Cores;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 10: {
+            if (stats_ == null) {
+              stats_ = new global::Grpc.Testing.ServerStats();
+            }
+            input.ReadMessage(stats_);
+            break;
+          }
+          case 16: {
+            Port = input.ReadInt32();
+            break;
+          }
+          case 24: {
+            Cores = input.ReadInt32();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  #endregion
+
+}
+
+#endregion Designer generated code
diff --git a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj
index 2b7530573103cc7b64f65653c8010ced96ee284a..012de45524d014d3a9bc9323c60f9092eb20e227 100644
--- a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj
+++ b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj
@@ -38,55 +38,46 @@
     <AssemblyOriginatorKeyFile>C:\keys\Grpc.snk</AssemblyOriginatorKeyFile>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="BouncyCastle.Crypto, Version=1.7.4137.9688, Culture=neutral, PublicKeyToken=a4292a325f69b123, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll</HintPath>
-    </Reference>
     <Reference Include="CommandLine">
       <HintPath>..\packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll</HintPath>
     </Reference>
-    <Reference Include="Google.Apis.Auth, Version=1.9.3.19379, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
+    <Reference Include="nunit.framework">
+      <HintPath>..\packages\NUnit.2.6.4\lib\nunit.framework.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Interactive.Async">
+      <HintPath>..\packages\Ix-Async.1.2.3\lib\net45\System.Interactive.Async.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Net" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Net.Http.WebRequest" />
+    <Reference Include="BouncyCastle.Crypto">
+      <HintPath>..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll</HintPath>
+    </Reference>
+    <Reference Include="Google.Apis.Auth">
       <HintPath>..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.dll</HintPath>
     </Reference>
-    <Reference Include="Google.Apis.Auth.PlatformServices, Version=1.9.3.19383, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
+    <Reference Include="Google.Apis.Auth.PlatformServices">
       <HintPath>..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.PlatformServices.dll</HintPath>
     </Reference>
-    <Reference Include="Google.Apis.Core, Version=1.9.3.19379, Culture=neutral, PublicKeyToken=4b01fa6e34db77ab, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
+    <Reference Include="Google.Apis.Core">
       <HintPath>..\packages\Google.Apis.Core.1.9.3\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll</HintPath>
     </Reference>
-    <Reference Include="Google.Protobuf, Version=3.0.0.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
+    <Reference Include="Google.Protobuf">
       <HintPath>..\packages\Google.Protobuf.3.0.0-alpha4\lib\portable-net45+netcore45+wpa81+wp8\Google.Protobuf.dll</HintPath>
     </Reference>
-    <Reference Include="Microsoft.Threading.Tasks, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
+    <Reference Include="Microsoft.Threading.Tasks">
       <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll</HintPath>
     </Reference>
-    <Reference Include="Microsoft.Threading.Tasks.Extensions, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
+    <Reference Include="Microsoft.Threading.Tasks.Extensions">
       <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll</HintPath>
     </Reference>
-    <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop, Version=1.0.168.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
+    <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop">
       <HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath>
     </Reference>
-    <Reference Include="Newtonsoft.Json, Version=7.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
+    <Reference Include="Newtonsoft.Json">
       <HintPath>..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
     </Reference>
-    <Reference Include="nunit.framework">
-      <HintPath>..\packages\NUnit.2.6.4\lib\nunit.framework.dll</HintPath>
-    </Reference>
-    <Reference Include="System" />
-    <Reference Include="System.Interactive.Async">
-      <HintPath>..\packages\Ix-Async.1.2.3\lib\net45\System.Interactive.Async.dll</HintPath>
-    </Reference>
-    <Reference Include="System.Net" />
-    <Reference Include="System.Net.Http" />
-    <Reference Include="System.Net.Http.WebRequest" />
   </ItemGroup>
   <ItemGroup>
     <Compile Include="..\Grpc.Core\Version.cs">
@@ -104,6 +95,22 @@
     <Compile Include="TestGrpc.cs" />
     <Compile Include="SslCredentialsTest.cs" />
     <Compile Include="Test.cs" />
+    <Compile Include="IClientRunner.cs" />
+    <Compile Include="ClientRunners.cs" />
+    <Compile Include="IServerRunner.cs" />
+    <Compile Include="ServerRunners.cs" />
+    <Compile Include="RunnerClientServerTest.cs" />
+    <Compile Include="Control.cs" />
+    <Compile Include="Payloads.cs" />
+    <Compile Include="Services.cs" />
+    <Compile Include="ServicesGrpc.cs" />
+    <Compile Include="Stats.cs" />
+    <Compile Include="BenchmarkServiceImpl.cs" />
+    <Compile Include="Histogram.cs" />
+    <Compile Include="HistogramTest.cs" />
+    <Compile Include="WorkerServiceImpl.cs" />
+    <Compile Include="QpsWorker.cs" />
+    <Compile Include="WallClockStopwatch.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <ItemGroup>
diff --git a/src/csharp/Grpc.IntegrationTesting/Histogram.cs b/src/csharp/Grpc.IntegrationTesting/Histogram.cs
new file mode 100644
index 0000000000000000000000000000000000000000..7e7cb2c4de0d59fe6a971d0320072afa7504f4fe
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/Histogram.cs
@@ -0,0 +1,153 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+using Grpc.Testing;
+
+namespace Grpc.IntegrationTesting
+{
+    /// <summary>
+    /// Basic implementation of histogram based on grpc/support/histogram.h.
+    /// </summary>
+    public class Histogram
+    {
+        readonly object myLock = new object();
+        readonly double multiplier;
+        readonly double oneOnLogMultiplier;
+        readonly double maxPossible;
+        readonly uint[] buckets;
+
+        int count;
+        double sum;
+        double sumOfSquares;
+        double min;
+        double max;
+
+        public Histogram(double resolution, double maxPossible)
+        {
+            Grpc.Core.Utils.Preconditions.CheckArgument(resolution > 0);
+            Grpc.Core.Utils.Preconditions.CheckArgument(maxPossible > 0);
+            this.maxPossible = maxPossible;
+            this.multiplier = 1.0 + resolution;
+            this.oneOnLogMultiplier = 1.0 / Math.Log(1.0 + resolution);
+            this.buckets = new uint[FindBucket(maxPossible) + 1];
+
+            ResetUnsafe();
+        }
+
+        public void AddObservation(double value)
+        {
+            lock (myLock)
+            {
+                AddObservationUnsafe(value);    
+            }
+        }
+
+
+        /// <summary>
+        /// Gets snapshot of stats and reset 
+        /// </summary>
+        public HistogramData GetSnapshot(bool reset = false)
+        {
+            lock (myLock)
+            {
+                return GetSnapshotUnsafe(reset);    
+            }
+        }
+
+        /// <summary>
+        /// Finds bucket index to which given observation should go.
+        /// </summary>
+        private int FindBucket(double value)
+        {
+            value = Math.Max(value, 1.0);
+            value = Math.Min(value, this.maxPossible);
+            return (int)(Math.Log(value) * oneOnLogMultiplier);
+        }
+
+        private void AddObservationUnsafe(double value)
+        {
+            this.count++;
+            this.sum += value;
+            this.sumOfSquares += value * value;
+            this.min = Math.Min(this.min, value);
+            this.max = Math.Max(this.max, value);
+
+            this.buckets[FindBucket(value)]++;
+        }
+
+        private HistogramData GetSnapshotUnsafe(bool reset)
+        {
+            var data = new HistogramData
+            {
+                Count = count,
+                Sum = sum,
+                SumOfSquares = sumOfSquares,
+                MinSeen = min,
+                MaxSeen = max,
+                Bucket = { buckets }
+            };
+
+            if (reset)
+            {
+                ResetUnsafe();
+            }
+
+            return data;
+        }
+
+        private void ResetUnsafe()
+        {
+            this.count = 0;
+            this.sum = 0;
+            this.sumOfSquares = 0;
+            this.min = double.PositiveInfinity;
+            this.max = double.NegativeInfinity;
+            for (int i = 0; i < this.buckets.Length; i++)
+            {
+                this.buckets[i] = 0;
+            }
+        }
+    }
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/HistogramTest.cs b/src/csharp/Grpc.IntegrationTesting/HistogramTest.cs
new file mode 100644
index 0000000000000000000000000000000000000000..fa160cbd15b2161c9ce58ca4161fb206dc6057da
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/HistogramTest.cs
@@ -0,0 +1,104 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using Grpc.Testing;
+using NUnit.Framework;
+
+namespace Grpc.IntegrationTesting
+{
+    public class HistogramTest
+    {
+        [Test]
+        public void Simple()
+        {
+            var hist = new Histogram(0.01, 60e9);
+            hist.AddObservation(10000);
+            hist.AddObservation(10000);
+            hist.AddObservation(11000);
+            hist.AddObservation(11000);
+
+            var data = hist.GetSnapshot();
+
+            Assert.AreEqual(4, data.Count);
+            Assert.AreEqual(42000.0, data.Sum, 1e-6);
+            Assert.AreEqual(10000, data.MinSeen);
+            Assert.AreEqual(11000, data.MaxSeen);
+            Assert.AreEqual(2.0*10000*10000 + 2.0*11000*11000, data.SumOfSquares, 1e-6);
+
+            // 1.01^925 < 10000 < 1.01^926
+            Assert.AreEqual(2, data.Bucket[925]);
+            Assert.AreEqual(2, data.Bucket[935]);
+        }
+
+        [Test]
+        public void ExtremeObservations()
+        {
+            var hist = new Histogram(0.01, 60e9);
+            hist.AddObservation(-0.5);  // should be in the first bucket
+            hist.AddObservation(1e12);  // should be in the last bucket 
+
+            var data = hist.GetSnapshot();
+            Assert.AreEqual(1, data.Bucket[0]);
+            Assert.AreEqual(1, data.Bucket[data.Bucket.Count - 1]);
+        }
+
+        [Test]
+        public void Reset()
+        {
+            var hist = new Histogram(0.01, 60e9);
+            hist.AddObservation(10000);
+            hist.AddObservation(11000);
+
+            var data = hist.GetSnapshot(true);  // snapshot contains data before reset
+            Assert.AreEqual(2, data.Count);
+            Assert.AreEqual(10000, data.MinSeen);
+            Assert.AreEqual(11000, data.MaxSeen);
+
+            data = hist.GetSnapshot();  // snapshot contains state after reset
+            Assert.AreEqual(0, data.Count);
+            Assert.AreEqual(double.PositiveInfinity, data.MinSeen);
+            Assert.AreEqual(double.NegativeInfinity, data.MaxSeen);
+            Assert.AreEqual(0, data.Sum);
+            Assert.AreEqual(0, data.SumOfSquares);
+            CollectionAssert.AreEqual(new uint[data.Bucket.Count], data.Bucket); 
+        }
+    }
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/IClientRunner.cs b/src/csharp/Grpc.IntegrationTesting/IClientRunner.cs
new file mode 100644
index 0000000000000000000000000000000000000000..39b94f1e6d1b9e020879562ff11ee6494b13acc0
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/IClientRunner.cs
@@ -0,0 +1,67 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+using Grpc.Testing;
+
+namespace Grpc.IntegrationTesting
+{
+    /// <summary>
+    /// Abstract client runner.
+    /// </summary>
+    public interface IClientRunner
+    {
+        /// <summary>
+        /// Gets stats snapshot.
+        /// </summary>
+        /// <returns>The stats.</returns>
+        ClientStats GetStats(bool reset);
+
+        /// <summary>
+        /// Asynchronously stops the client.
+        /// </summary>
+        /// <returns>Task that finishes when client has come to a full stop.</returns>
+        Task StopAsync();
+    }
+        
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/IServerRunner.cs b/src/csharp/Grpc.IntegrationTesting/IServerRunner.cs
new file mode 100644
index 0000000000000000000000000000000000000000..53a62fbf1cc4923e50bc2d3a7a32e60da0fb3dd9
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/IServerRunner.cs
@@ -0,0 +1,72 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+using Grpc.Testing;
+
+namespace Grpc.IntegrationTesting
+{
+    /// <summary>
+    /// Abstract server runner.
+    /// </summary>
+    public interface IServerRunner
+    {
+        /// <summary>
+        /// Port on which the server is listening.
+        /// </summary>
+        int BoundPort { get; }
+        
+        /// <summary>
+        /// Gets server stats.
+        /// </summary>
+        /// <returns>The stats.</returns>
+        ServerStats GetStats(bool reset);
+
+        /// <summary>
+        /// Asynchronously stops the server.
+        /// </summary>
+        /// <returns>Task that finishes when server has shutdown.</returns>
+        Task StopAsync();
+    }
+        
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/Payloads.cs b/src/csharp/Grpc.IntegrationTesting/Payloads.cs
new file mode 100644
index 0000000000000000000000000000000000000000..a37dd9a685cf46583e1f79986ea8c6ce4013c6b4
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/Payloads.cs
@@ -0,0 +1,580 @@
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+// source: test/proto/benchmarks/payloads.proto
+#pragma warning disable 1591, 0612, 3021
+#region Designer generated code
+
+using pb = global::Google.Protobuf;
+using pbc = global::Google.Protobuf.Collections;
+using pbr = global::Google.Protobuf.Reflection;
+using scg = global::System.Collections.Generic;
+namespace Grpc.Testing {
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public static partial class Payloads {
+
+    #region Descriptor
+    public static pbr::FileDescriptor Descriptor {
+      get { return descriptor; }
+    }
+    private static pbr::FileDescriptor descriptor;
+
+    static Payloads() {
+      byte[] descriptorData = global::System.Convert.FromBase64String(
+          string.Concat(
+            "CiR0ZXN0L3Byb3RvL2JlbmNobWFya3MvcGF5bG9hZHMucHJvdG8SDGdycGMu", 
+            "dGVzdGluZyI3ChBCeXRlQnVmZmVyUGFyYW1zEhAKCHJlcV9zaXplGAEgASgF", 
+            "EhEKCXJlc3Bfc2l6ZRgCIAEoBSI4ChFTaW1wbGVQcm90b1BhcmFtcxIQCghy", 
+            "ZXFfc2l6ZRgBIAEoBRIRCglyZXNwX3NpemUYAiABKAUiFAoSQ29tcGxleFBy", 
+            "b3RvUGFyYW1zIsoBCg1QYXlsb2FkQ29uZmlnEjgKDmJ5dGVidWZfcGFyYW1z", 
+            "GAEgASgLMh4uZ3JwYy50ZXN0aW5nLkJ5dGVCdWZmZXJQYXJhbXNIABI4Cg1z", 
+            "aW1wbGVfcGFyYW1zGAIgASgLMh8uZ3JwYy50ZXN0aW5nLlNpbXBsZVByb3Rv", 
+            "UGFyYW1zSAASOgoOY29tcGxleF9wYXJhbXMYAyABKAsyIC5ncnBjLnRlc3Rp", 
+            "bmcuQ29tcGxleFByb3RvUGFyYW1zSABCCQoHcGF5bG9hZGIGcHJvdG8z"));
+      descriptor = pbr::FileDescriptor.InternalBuildGeneratedFileFrom(descriptorData,
+          new pbr::FileDescriptor[] { },
+          new pbr::GeneratedCodeInfo(null, new pbr::GeneratedCodeInfo[] {
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ByteBufferParams), new[]{ "ReqSize", "RespSize" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.SimpleProtoParams), new[]{ "ReqSize", "RespSize" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ComplexProtoParams), null, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.PayloadConfig), new[]{ "BytebufParams", "SimpleParams", "ComplexParams" }, new[]{ "Payload" }, null, null)
+          }));
+    }
+    #endregion
+
+  }
+  #region Messages
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ByteBufferParams : pb::IMessage<ByteBufferParams> {
+    private static readonly pb::MessageParser<ByteBufferParams> _parser = new pb::MessageParser<ByteBufferParams>(() => new ByteBufferParams());
+    public static pb::MessageParser<ByteBufferParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Payloads.Descriptor.MessageTypes[0]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ByteBufferParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ByteBufferParams(ByteBufferParams other) : this() {
+      reqSize_ = other.reqSize_;
+      respSize_ = other.respSize_;
+    }
+
+    public ByteBufferParams Clone() {
+      return new ByteBufferParams(this);
+    }
+
+    public const int ReqSizeFieldNumber = 1;
+    private int reqSize_;
+    public int ReqSize {
+      get { return reqSize_; }
+      set {
+        reqSize_ = value;
+      }
+    }
+
+    public const int RespSizeFieldNumber = 2;
+    private int respSize_;
+    public int RespSize {
+      get { return respSize_; }
+      set {
+        respSize_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ByteBufferParams);
+    }
+
+    public bool Equals(ByteBufferParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (ReqSize != other.ReqSize) return false;
+      if (RespSize != other.RespSize) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (ReqSize != 0) hash ^= ReqSize.GetHashCode();
+      if (RespSize != 0) hash ^= RespSize.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (ReqSize != 0) {
+        output.WriteRawTag(8);
+        output.WriteInt32(ReqSize);
+      }
+      if (RespSize != 0) {
+        output.WriteRawTag(16);
+        output.WriteInt32(RespSize);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (ReqSize != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(ReqSize);
+      }
+      if (RespSize != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(RespSize);
+      }
+      return size;
+    }
+
+    public void MergeFrom(ByteBufferParams other) {
+      if (other == null) {
+        return;
+      }
+      if (other.ReqSize != 0) {
+        ReqSize = other.ReqSize;
+      }
+      if (other.RespSize != 0) {
+        RespSize = other.RespSize;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 8: {
+            ReqSize = input.ReadInt32();
+            break;
+          }
+          case 16: {
+            RespSize = input.ReadInt32();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class SimpleProtoParams : pb::IMessage<SimpleProtoParams> {
+    private static readonly pb::MessageParser<SimpleProtoParams> _parser = new pb::MessageParser<SimpleProtoParams>(() => new SimpleProtoParams());
+    public static pb::MessageParser<SimpleProtoParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Payloads.Descriptor.MessageTypes[1]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public SimpleProtoParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public SimpleProtoParams(SimpleProtoParams other) : this() {
+      reqSize_ = other.reqSize_;
+      respSize_ = other.respSize_;
+    }
+
+    public SimpleProtoParams Clone() {
+      return new SimpleProtoParams(this);
+    }
+
+    public const int ReqSizeFieldNumber = 1;
+    private int reqSize_;
+    public int ReqSize {
+      get { return reqSize_; }
+      set {
+        reqSize_ = value;
+      }
+    }
+
+    public const int RespSizeFieldNumber = 2;
+    private int respSize_;
+    public int RespSize {
+      get { return respSize_; }
+      set {
+        respSize_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as SimpleProtoParams);
+    }
+
+    public bool Equals(SimpleProtoParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (ReqSize != other.ReqSize) return false;
+      if (RespSize != other.RespSize) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (ReqSize != 0) hash ^= ReqSize.GetHashCode();
+      if (RespSize != 0) hash ^= RespSize.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (ReqSize != 0) {
+        output.WriteRawTag(8);
+        output.WriteInt32(ReqSize);
+      }
+      if (RespSize != 0) {
+        output.WriteRawTag(16);
+        output.WriteInt32(RespSize);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (ReqSize != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(ReqSize);
+      }
+      if (RespSize != 0) {
+        size += 1 + pb::CodedOutputStream.ComputeInt32Size(RespSize);
+      }
+      return size;
+    }
+
+    public void MergeFrom(SimpleProtoParams other) {
+      if (other == null) {
+        return;
+      }
+      if (other.ReqSize != 0) {
+        ReqSize = other.ReqSize;
+      }
+      if (other.RespSize != 0) {
+        RespSize = other.RespSize;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 8: {
+            ReqSize = input.ReadInt32();
+            break;
+          }
+          case 16: {
+            RespSize = input.ReadInt32();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ComplexProtoParams : pb::IMessage<ComplexProtoParams> {
+    private static readonly pb::MessageParser<ComplexProtoParams> _parser = new pb::MessageParser<ComplexProtoParams>(() => new ComplexProtoParams());
+    public static pb::MessageParser<ComplexProtoParams> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Payloads.Descriptor.MessageTypes[2]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ComplexProtoParams() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ComplexProtoParams(ComplexProtoParams other) : this() {
+    }
+
+    public ComplexProtoParams Clone() {
+      return new ComplexProtoParams(this);
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ComplexProtoParams);
+    }
+
+    public bool Equals(ComplexProtoParams other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      return size;
+    }
+
+    public void MergeFrom(ComplexProtoParams other) {
+      if (other == null) {
+        return;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class PayloadConfig : pb::IMessage<PayloadConfig> {
+    private static readonly pb::MessageParser<PayloadConfig> _parser = new pb::MessageParser<PayloadConfig>(() => new PayloadConfig());
+    public static pb::MessageParser<PayloadConfig> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Payloads.Descriptor.MessageTypes[3]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public PayloadConfig() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public PayloadConfig(PayloadConfig other) : this() {
+      switch (other.PayloadCase) {
+        case PayloadOneofCase.BytebufParams:
+          BytebufParams = other.BytebufParams.Clone();
+          break;
+        case PayloadOneofCase.SimpleParams:
+          SimpleParams = other.SimpleParams.Clone();
+          break;
+        case PayloadOneofCase.ComplexParams:
+          ComplexParams = other.ComplexParams.Clone();
+          break;
+      }
+
+    }
+
+    public PayloadConfig Clone() {
+      return new PayloadConfig(this);
+    }
+
+    public const int BytebufParamsFieldNumber = 1;
+    public global::Grpc.Testing.ByteBufferParams BytebufParams {
+      get { return payloadCase_ == PayloadOneofCase.BytebufParams ? (global::Grpc.Testing.ByteBufferParams) payload_ : null; }
+      set {
+        payload_ = value;
+        payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.BytebufParams;
+      }
+    }
+
+    public const int SimpleParamsFieldNumber = 2;
+    public global::Grpc.Testing.SimpleProtoParams SimpleParams {
+      get { return payloadCase_ == PayloadOneofCase.SimpleParams ? (global::Grpc.Testing.SimpleProtoParams) payload_ : null; }
+      set {
+        payload_ = value;
+        payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.SimpleParams;
+      }
+    }
+
+    public const int ComplexParamsFieldNumber = 3;
+    public global::Grpc.Testing.ComplexProtoParams ComplexParams {
+      get { return payloadCase_ == PayloadOneofCase.ComplexParams ? (global::Grpc.Testing.ComplexProtoParams) payload_ : null; }
+      set {
+        payload_ = value;
+        payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.ComplexParams;
+      }
+    }
+
+    private object payload_;
+    public enum PayloadOneofCase {
+      None = 0,
+      BytebufParams = 1,
+      SimpleParams = 2,
+      ComplexParams = 3,
+    }
+    private PayloadOneofCase payloadCase_ = PayloadOneofCase.None;
+    public PayloadOneofCase PayloadCase {
+      get { return payloadCase_; }
+    }
+
+    public void ClearPayload() {
+      payloadCase_ = PayloadOneofCase.None;
+      payload_ = null;
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as PayloadConfig);
+    }
+
+    public bool Equals(PayloadConfig other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (!object.Equals(BytebufParams, other.BytebufParams)) return false;
+      if (!object.Equals(SimpleParams, other.SimpleParams)) return false;
+      if (!object.Equals(ComplexParams, other.ComplexParams)) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (payloadCase_ == PayloadOneofCase.BytebufParams) hash ^= BytebufParams.GetHashCode();
+      if (payloadCase_ == PayloadOneofCase.SimpleParams) hash ^= SimpleParams.GetHashCode();
+      if (payloadCase_ == PayloadOneofCase.ComplexParams) hash ^= ComplexParams.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (payloadCase_ == PayloadOneofCase.BytebufParams) {
+        output.WriteRawTag(10);
+        output.WriteMessage(BytebufParams);
+      }
+      if (payloadCase_ == PayloadOneofCase.SimpleParams) {
+        output.WriteRawTag(18);
+        output.WriteMessage(SimpleParams);
+      }
+      if (payloadCase_ == PayloadOneofCase.ComplexParams) {
+        output.WriteRawTag(26);
+        output.WriteMessage(ComplexParams);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (payloadCase_ == PayloadOneofCase.BytebufParams) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(BytebufParams);
+      }
+      if (payloadCase_ == PayloadOneofCase.SimpleParams) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(SimpleParams);
+      }
+      if (payloadCase_ == PayloadOneofCase.ComplexParams) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(ComplexParams);
+      }
+      return size;
+    }
+
+    public void MergeFrom(PayloadConfig other) {
+      if (other == null) {
+        return;
+      }
+      switch (other.PayloadCase) {
+        case PayloadOneofCase.BytebufParams:
+          BytebufParams = other.BytebufParams;
+          break;
+        case PayloadOneofCase.SimpleParams:
+          SimpleParams = other.SimpleParams;
+          break;
+        case PayloadOneofCase.ComplexParams:
+          ComplexParams = other.ComplexParams;
+          break;
+      }
+
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 10: {
+            global::Grpc.Testing.ByteBufferParams subBuilder = new global::Grpc.Testing.ByteBufferParams();
+            if (payloadCase_ == PayloadOneofCase.BytebufParams) {
+              subBuilder.MergeFrom(BytebufParams);
+            }
+            input.ReadMessage(subBuilder);
+            BytebufParams = subBuilder;
+            break;
+          }
+          case 18: {
+            global::Grpc.Testing.SimpleProtoParams subBuilder = new global::Grpc.Testing.SimpleProtoParams();
+            if (payloadCase_ == PayloadOneofCase.SimpleParams) {
+              subBuilder.MergeFrom(SimpleParams);
+            }
+            input.ReadMessage(subBuilder);
+            SimpleParams = subBuilder;
+            break;
+          }
+          case 26: {
+            global::Grpc.Testing.ComplexProtoParams subBuilder = new global::Grpc.Testing.ComplexProtoParams();
+            if (payloadCase_ == PayloadOneofCase.ComplexParams) {
+              subBuilder.MergeFrom(ComplexParams);
+            }
+            input.ReadMessage(subBuilder);
+            ComplexParams = subBuilder;
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  #endregion
+
+}
+
+#endregion Designer generated code
diff --git a/src/csharp/Grpc.IntegrationTesting/QpsWorker.cs b/src/csharp/Grpc.IntegrationTesting/QpsWorker.cs
new file mode 100644
index 0000000000000000000000000000000000000000..686b48434537ceef3967d121f0e27d989ad9f7cb
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/QpsWorker.cs
@@ -0,0 +1,108 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+using CommandLine;
+using CommandLine.Text;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using Grpc.Testing;
+using NUnit.Framework;
+
+namespace Grpc.IntegrationTesting
+{
+    public class QpsWorker
+    {
+        private class ServerOptions
+        {
+            [Option("driver_port", DefaultValue = 0)]
+            public int DriverPort { get; set; }
+
+            [HelpOption]
+            public string GetUsage()
+            {
+                var help = new HelpText
+                {
+                    Heading = "gRPC C# performance testing worker",
+                    AddDashesToOption = true
+                };
+                help.AddPreOptionsLine("Usage:");
+                help.AddOptions(this);
+                return help;
+            }
+        }
+
+        ServerOptions options;
+
+        private QpsWorker(ServerOptions options)
+        {
+            this.options = options;
+        }
+
+        public static void Run(string[] args)
+        {
+            var options = new ServerOptions();
+            if (!Parser.Default.ParseArguments(args, options))
+            {
+                Environment.Exit(1);
+            }
+
+            var workerServer = new QpsWorker(options);
+            workerServer.Run();
+        }
+
+        private void Run()
+        {
+            string host = "0.0.0.0";
+            int port = options.DriverPort;
+
+            var server = new Server
+            {
+                Services = { WorkerService.BindService(new WorkerServiceImpl()) },
+                Ports = { new ServerPort(host, options.DriverPort, ServerCredentials.Insecure )}
+            };
+            int boundPort = server.Ports.Single().BoundPort;
+            Console.WriteLine("Running qps worker server on " + string.Format("{0}:{1}", host, boundPort));
+            server.Start();
+
+            server.ShutdownTask.Wait();
+        }
+    }
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/RunnerClientServerTest.cs b/src/csharp/Grpc.IntegrationTesting/RunnerClientServerTest.cs
new file mode 100644
index 0000000000000000000000000000000000000000..33e0f2471966f5320c74d2fccd8e4737d84fffb9
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/RunnerClientServerTest.cs
@@ -0,0 +1,111 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using Grpc.Testing;
+using NUnit.Framework;
+
+namespace Grpc.IntegrationTesting
+{
+    /// <summary>
+    /// Runs performance tests in-process.
+    /// </summary>
+    public class RunnerClientServerTest
+    {
+        const string Host = "localhost";
+        IServerRunner serverRunner;
+
+        [TestFixtureSetUp]
+        public void Init()
+        {
+            var serverConfig = new ServerConfig
+            {
+                ServerType = ServerType.ASYNC_SERVER,
+                PayloadConfig = new PayloadConfig
+                {
+                    SimpleParams = new SimpleProtoParams
+                    {
+                        RespSize = 100
+                    }
+                }
+            };
+            serverRunner = ServerRunners.CreateStarted(serverConfig);
+        }
+
+        [TestFixtureTearDown]
+        public void Cleanup()
+        {
+            serverRunner.StopAsync().Wait();
+        }
+
+        // Test attribute commented out to prevent running as part of the default test suite.
+        //[Test]
+        //[Category("Performance")]
+        public async Task ClientServerRunner()
+        {
+            var config = new ClientConfig
+            {
+                ServerTargets = { string.Format("{0}:{1}", Host, serverRunner.BoundPort) },
+                RpcType = RpcType.UNARY,
+                LoadParams = new LoadParams { ClosedLoop = new ClosedLoopParams() },
+                PayloadConfig = new PayloadConfig
+                {
+                    SimpleParams = new SimpleProtoParams
+                    {
+                        ReqSize = 100
+                    }
+                }
+            };
+
+            var runner = ClientRunners.CreateStarted(config);
+
+            System.Console.WriteLine("Warming up");
+            await Task.Delay(3000);
+            runner.GetStats(true);  // throw away warm-up data
+
+            System.Console.WriteLine("Benchmarking");
+            await Task.Delay(3000);
+            var stats = runner.GetStats(true);
+            await runner.StopAsync();
+
+            System.Console.WriteLine(stats);
+            System.Console.WriteLine("avg micros/call " + (long) ((stats.Latencies.Sum / stats.Latencies.Count) * 1000000));
+        }
+    }
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/ServerRunners.cs b/src/csharp/Grpc.IntegrationTesting/ServerRunners.cs
new file mode 100644
index 0000000000000000000000000000000000000000..a60d60259bc30a5b23a12be0dcfdd91b2fe3a871
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/ServerRunners.cs
@@ -0,0 +1,124 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+using Grpc.Testing;
+
+namespace Grpc.IntegrationTesting
+{
+    /// <summary>
+    /// Helper methods to start server runners for performance testing.
+    /// </summary>
+    public static class ServerRunners
+    {
+        /// <summary>
+        /// Creates a started server runner.
+        /// </summary>
+        public static IServerRunner CreateStarted(ServerConfig config)
+        {
+            Grpc.Core.Utils.Preconditions.CheckArgument(config.ServerType == ServerType.ASYNC_SERVER);
+            var credentials = config.SecurityParams != null ? TestCredentials.CreateSslServerCredentials() : ServerCredentials.Insecure;
+
+            // TODO: qps_driver needs to setup payload properly...
+            int responseSize = config.PayloadConfig != null ? config.PayloadConfig.SimpleParams.RespSize : 0;
+            var server = new Server
+            {
+                Services = { BenchmarkService.BindService(new BenchmarkServiceImpl(responseSize)) },
+                Ports = { new ServerPort("0.0.0.0", config.Port, credentials) }
+            };
+
+            server.Start();
+            return new ServerRunnerImpl(server);
+        }
+    }
+
+    /// <summary>
+    /// Server runner.
+    /// </summary>
+    public class ServerRunnerImpl : IServerRunner
+    {
+        readonly Server server;
+        readonly WallClockStopwatch wallClockStopwatch = new WallClockStopwatch();
+
+        public ServerRunnerImpl(Server server)
+        {
+            this.server = Grpc.Core.Utils.Preconditions.CheckNotNull(server);
+        }
+
+        public int BoundPort
+        {
+            get
+            {
+                return server.Ports.Single().BoundPort;
+            }
+        }
+
+        /// <summary>
+        /// Gets server stats.
+        /// </summary>
+        /// <returns>The stats.</returns>
+        public ServerStats GetStats(bool reset)
+        {
+            var secondsElapsed = wallClockStopwatch.GetElapsedSnapshot(reset).TotalSeconds;
+
+            // TODO: populate user time and system time
+            return new ServerStats
+            {
+                TimeElapsed = secondsElapsed,
+                TimeUser = 0,
+                TimeSystem = 0
+            };
+        }
+
+        /// <summary>
+        /// Asynchronously stops the server.
+        /// </summary>
+        /// <returns>Task that finishes when server has shutdown.</returns>
+        public Task StopAsync()
+        {
+            return server.ShutdownAsync();
+        }
+    }
+        
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/Services.cs b/src/csharp/Grpc.IntegrationTesting/Services.cs
new file mode 100644
index 0000000000000000000000000000000000000000..b648da6734f9994151aa0961617c513fdedc11a7
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/Services.cs
@@ -0,0 +1,44 @@
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+// source: test/proto/benchmarks/services.proto
+#pragma warning disable 1591, 0612, 3021
+#region Designer generated code
+
+using pb = global::Google.Protobuf;
+using pbc = global::Google.Protobuf.Collections;
+using pbr = global::Google.Protobuf.Reflection;
+using scg = global::System.Collections.Generic;
+namespace Grpc.Testing {
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public static partial class Services {
+
+    #region Descriptor
+    public static pbr::FileDescriptor Descriptor {
+      get { return descriptor; }
+    }
+    private static pbr::FileDescriptor descriptor;
+
+    static Services() {
+      byte[] descriptorData = global::System.Convert.FromBase64String(
+          string.Concat(
+            "CiR0ZXN0L3Byb3RvL2JlbmNobWFya3Mvc2VydmljZXMucHJvdG8SDGdycGMu", 
+            "dGVzdGluZxoZdGVzdC9wcm90by9tZXNzYWdlcy5wcm90bxojdGVzdC9wcm90", 
+            "by9iZW5jaG1hcmtzL2NvbnRyb2wucHJvdG8yqgEKEEJlbmNobWFya1NlcnZp", 
+            "Y2USRgoJVW5hcnlDYWxsEhsuZ3JwYy50ZXN0aW5nLlNpbXBsZVJlcXVlc3Qa", 
+            "HC5ncnBjLnRlc3RpbmcuU2ltcGxlUmVzcG9uc2USTgoNU3RyZWFtaW5nQ2Fs", 
+            "bBIbLmdycGMudGVzdGluZy5TaW1wbGVSZXF1ZXN0GhwuZ3JwYy50ZXN0aW5n", 
+            "LlNpbXBsZVJlc3BvbnNlKAEwATKdAQoNV29ya2VyU2VydmljZRJFCglSdW5T", 
+            "ZXJ2ZXISGC5ncnBjLnRlc3RpbmcuU2VydmVyQXJncxoaLmdycGMudGVzdGlu", 
+            "Zy5TZXJ2ZXJTdGF0dXMoATABEkUKCVJ1bkNsaWVudBIYLmdycGMudGVzdGlu", 
+            "Zy5DbGllbnRBcmdzGhouZ3JwYy50ZXN0aW5nLkNsaWVudFN0YXR1cygBMAFi", 
+            "BnByb3RvMw=="));
+      descriptor = pbr::FileDescriptor.InternalBuildGeneratedFileFrom(descriptorData,
+          new pbr::FileDescriptor[] { global::Grpc.Testing.Messages.Descriptor, global::Grpc.Testing.Control.Descriptor, },
+          new pbr::GeneratedCodeInfo(null, null));
+    }
+    #endregion
+
+  }
+}
+
+#endregion Designer generated code
diff --git a/src/csharp/Grpc.IntegrationTesting/ServicesGrpc.cs b/src/csharp/Grpc.IntegrationTesting/ServicesGrpc.cs
new file mode 100644
index 0000000000000000000000000000000000000000..ce388c6d5c6529275635d0882c6d922053d820c8
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/ServicesGrpc.cs
@@ -0,0 +1,198 @@
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+// source: test/proto/benchmarks/services.proto
+#region Designer generated code
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Grpc.Core;
+
+namespace Grpc.Testing {
+  public static class BenchmarkService
+  {
+    static readonly string __ServiceName = "grpc.testing.BenchmarkService";
+
+    static readonly Marshaller<global::Grpc.Testing.SimpleRequest> __Marshaller_SimpleRequest = Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::Grpc.Testing.SimpleRequest.Parser.ParseFrom);
+    static readonly Marshaller<global::Grpc.Testing.SimpleResponse> __Marshaller_SimpleResponse = Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::Grpc.Testing.SimpleResponse.Parser.ParseFrom);
+
+    static readonly Method<global::Grpc.Testing.SimpleRequest, global::Grpc.Testing.SimpleResponse> __Method_UnaryCall = new Method<global::Grpc.Testing.SimpleRequest, global::Grpc.Testing.SimpleResponse>(
+        MethodType.Unary,
+        __ServiceName,
+        "UnaryCall",
+        __Marshaller_SimpleRequest,
+        __Marshaller_SimpleResponse);
+
+    static readonly Method<global::Grpc.Testing.SimpleRequest, global::Grpc.Testing.SimpleResponse> __Method_StreamingCall = new Method<global::Grpc.Testing.SimpleRequest, global::Grpc.Testing.SimpleResponse>(
+        MethodType.DuplexStreaming,
+        __ServiceName,
+        "StreamingCall",
+        __Marshaller_SimpleRequest,
+        __Marshaller_SimpleResponse);
+
+    // service descriptor
+    public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor
+    {
+      get { return global::Grpc.Testing.Services.Descriptor.Services[0]; }
+    }
+
+    // client interface
+    public interface IBenchmarkServiceClient
+    {
+      global::Grpc.Testing.SimpleResponse UnaryCall(global::Grpc.Testing.SimpleRequest request, Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken));
+      global::Grpc.Testing.SimpleResponse UnaryCall(global::Grpc.Testing.SimpleRequest request, CallOptions options);
+      AsyncUnaryCall<global::Grpc.Testing.SimpleResponse> UnaryCallAsync(global::Grpc.Testing.SimpleRequest request, Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken));
+      AsyncUnaryCall<global::Grpc.Testing.SimpleResponse> UnaryCallAsync(global::Grpc.Testing.SimpleRequest request, CallOptions options);
+      AsyncDuplexStreamingCall<global::Grpc.Testing.SimpleRequest, global::Grpc.Testing.SimpleResponse> StreamingCall(Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken));
+      AsyncDuplexStreamingCall<global::Grpc.Testing.SimpleRequest, global::Grpc.Testing.SimpleResponse> StreamingCall(CallOptions options);
+    }
+
+    // server-side interface
+    public interface IBenchmarkService
+    {
+      Task<global::Grpc.Testing.SimpleResponse> UnaryCall(global::Grpc.Testing.SimpleRequest request, ServerCallContext context);
+      Task StreamingCall(IAsyncStreamReader<global::Grpc.Testing.SimpleRequest> requestStream, IServerStreamWriter<global::Grpc.Testing.SimpleResponse> responseStream, ServerCallContext context);
+    }
+
+    // client stub
+    public class BenchmarkServiceClient : ClientBase, IBenchmarkServiceClient
+    {
+      public BenchmarkServiceClient(Channel channel) : base(channel)
+      {
+      }
+      public global::Grpc.Testing.SimpleResponse UnaryCall(global::Grpc.Testing.SimpleRequest request, Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken))
+      {
+        var call = CreateCall(__Method_UnaryCall, new CallOptions(headers, deadline, cancellationToken));
+        return Calls.BlockingUnaryCall(call, request);
+      }
+      public global::Grpc.Testing.SimpleResponse UnaryCall(global::Grpc.Testing.SimpleRequest request, CallOptions options)
+      {
+        var call = CreateCall(__Method_UnaryCall, options);
+        return Calls.BlockingUnaryCall(call, request);
+      }
+      public AsyncUnaryCall<global::Grpc.Testing.SimpleResponse> UnaryCallAsync(global::Grpc.Testing.SimpleRequest request, Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken))
+      {
+        var call = CreateCall(__Method_UnaryCall, new CallOptions(headers, deadline, cancellationToken));
+        return Calls.AsyncUnaryCall(call, request);
+      }
+      public AsyncUnaryCall<global::Grpc.Testing.SimpleResponse> UnaryCallAsync(global::Grpc.Testing.SimpleRequest request, CallOptions options)
+      {
+        var call = CreateCall(__Method_UnaryCall, options);
+        return Calls.AsyncUnaryCall(call, request);
+      }
+      public AsyncDuplexStreamingCall<global::Grpc.Testing.SimpleRequest, global::Grpc.Testing.SimpleResponse> StreamingCall(Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken))
+      {
+        var call = CreateCall(__Method_StreamingCall, new CallOptions(headers, deadline, cancellationToken));
+        return Calls.AsyncDuplexStreamingCall(call);
+      }
+      public AsyncDuplexStreamingCall<global::Grpc.Testing.SimpleRequest, global::Grpc.Testing.SimpleResponse> StreamingCall(CallOptions options)
+      {
+        var call = CreateCall(__Method_StreamingCall, options);
+        return Calls.AsyncDuplexStreamingCall(call);
+      }
+    }
+
+    // creates service definition that can be registered with a server
+    public static ServerServiceDefinition BindService(IBenchmarkService serviceImpl)
+    {
+      return ServerServiceDefinition.CreateBuilder(__ServiceName)
+          .AddMethod(__Method_UnaryCall, serviceImpl.UnaryCall)
+          .AddMethod(__Method_StreamingCall, serviceImpl.StreamingCall).Build();
+    }
+
+    // creates a new client
+    public static BenchmarkServiceClient NewClient(Channel channel)
+    {
+      return new BenchmarkServiceClient(channel);
+    }
+
+  }
+  public static class WorkerService
+  {
+    static readonly string __ServiceName = "grpc.testing.WorkerService";
+
+    static readonly Marshaller<global::Grpc.Testing.ServerArgs> __Marshaller_ServerArgs = Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::Grpc.Testing.ServerArgs.Parser.ParseFrom);
+    static readonly Marshaller<global::Grpc.Testing.ServerStatus> __Marshaller_ServerStatus = Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::Grpc.Testing.ServerStatus.Parser.ParseFrom);
+    static readonly Marshaller<global::Grpc.Testing.ClientArgs> __Marshaller_ClientArgs = Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::Grpc.Testing.ClientArgs.Parser.ParseFrom);
+    static readonly Marshaller<global::Grpc.Testing.ClientStatus> __Marshaller_ClientStatus = Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::Grpc.Testing.ClientStatus.Parser.ParseFrom);
+
+    static readonly Method<global::Grpc.Testing.ServerArgs, global::Grpc.Testing.ServerStatus> __Method_RunServer = new Method<global::Grpc.Testing.ServerArgs, global::Grpc.Testing.ServerStatus>(
+        MethodType.DuplexStreaming,
+        __ServiceName,
+        "RunServer",
+        __Marshaller_ServerArgs,
+        __Marshaller_ServerStatus);
+
+    static readonly Method<global::Grpc.Testing.ClientArgs, global::Grpc.Testing.ClientStatus> __Method_RunClient = new Method<global::Grpc.Testing.ClientArgs, global::Grpc.Testing.ClientStatus>(
+        MethodType.DuplexStreaming,
+        __ServiceName,
+        "RunClient",
+        __Marshaller_ClientArgs,
+        __Marshaller_ClientStatus);
+
+    // service descriptor
+    public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor
+    {
+      get { return global::Grpc.Testing.Services.Descriptor.Services[1]; }
+    }
+
+    // client interface
+    public interface IWorkerServiceClient
+    {
+      AsyncDuplexStreamingCall<global::Grpc.Testing.ServerArgs, global::Grpc.Testing.ServerStatus> RunServer(Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken));
+      AsyncDuplexStreamingCall<global::Grpc.Testing.ServerArgs, global::Grpc.Testing.ServerStatus> RunServer(CallOptions options);
+      AsyncDuplexStreamingCall<global::Grpc.Testing.ClientArgs, global::Grpc.Testing.ClientStatus> RunClient(Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken));
+      AsyncDuplexStreamingCall<global::Grpc.Testing.ClientArgs, global::Grpc.Testing.ClientStatus> RunClient(CallOptions options);
+    }
+
+    // server-side interface
+    public interface IWorkerService
+    {
+      Task RunServer(IAsyncStreamReader<global::Grpc.Testing.ServerArgs> requestStream, IServerStreamWriter<global::Grpc.Testing.ServerStatus> responseStream, ServerCallContext context);
+      Task RunClient(IAsyncStreamReader<global::Grpc.Testing.ClientArgs> requestStream, IServerStreamWriter<global::Grpc.Testing.ClientStatus> responseStream, ServerCallContext context);
+    }
+
+    // client stub
+    public class WorkerServiceClient : ClientBase, IWorkerServiceClient
+    {
+      public WorkerServiceClient(Channel channel) : base(channel)
+      {
+      }
+      public AsyncDuplexStreamingCall<global::Grpc.Testing.ServerArgs, global::Grpc.Testing.ServerStatus> RunServer(Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken))
+      {
+        var call = CreateCall(__Method_RunServer, new CallOptions(headers, deadline, cancellationToken));
+        return Calls.AsyncDuplexStreamingCall(call);
+      }
+      public AsyncDuplexStreamingCall<global::Grpc.Testing.ServerArgs, global::Grpc.Testing.ServerStatus> RunServer(CallOptions options)
+      {
+        var call = CreateCall(__Method_RunServer, options);
+        return Calls.AsyncDuplexStreamingCall(call);
+      }
+      public AsyncDuplexStreamingCall<global::Grpc.Testing.ClientArgs, global::Grpc.Testing.ClientStatus> RunClient(Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken))
+      {
+        var call = CreateCall(__Method_RunClient, new CallOptions(headers, deadline, cancellationToken));
+        return Calls.AsyncDuplexStreamingCall(call);
+      }
+      public AsyncDuplexStreamingCall<global::Grpc.Testing.ClientArgs, global::Grpc.Testing.ClientStatus> RunClient(CallOptions options)
+      {
+        var call = CreateCall(__Method_RunClient, options);
+        return Calls.AsyncDuplexStreamingCall(call);
+      }
+    }
+
+    // creates service definition that can be registered with a server
+    public static ServerServiceDefinition BindService(IWorkerService serviceImpl)
+    {
+      return ServerServiceDefinition.CreateBuilder(__ServiceName)
+          .AddMethod(__Method_RunServer, serviceImpl.RunServer)
+          .AddMethod(__Method_RunClient, serviceImpl.RunClient).Build();
+    }
+
+    // creates a new client
+    public static WorkerServiceClient NewClient(Channel channel)
+    {
+      return new WorkerServiceClient(channel);
+    }
+
+  }
+}
+#endregion
diff --git a/src/csharp/Grpc.IntegrationTesting/Settings.StyleCop b/src/csharp/Grpc.IntegrationTesting/Settings.StyleCop
index fb99cd4af1cd6f61557b16a2cedca158b689b7c3..746f2ef5ce15a855b404ca5acef4ce75651c56fb 100644
--- a/src/csharp/Grpc.IntegrationTesting/Settings.StyleCop
+++ b/src/csharp/Grpc.IntegrationTesting/Settings.StyleCop
@@ -1,7 +1,10 @@
 <StyleCopSettings Version="105">
   <SourceFileList>
-    <SourceFile>Messages.cs</SourceFile>
     <SourceFile>Empty.cs</SourceFile>
+    <SourceFile>Control.cs</SourceFile>
+    <SourceFile>Messages.cs</SourceFile>
+    <SourceFile>Payloads.cs</SourceFile>
+    <SourceFile>Stats.cs</SourceFile>
     <Settings>
     <GlobalSettings>
       <BooleanProperty Name="RulesEnabledByDefault">False</BooleanProperty>
diff --git a/src/csharp/Grpc.IntegrationTesting/Stats.cs b/src/csharp/Grpc.IntegrationTesting/Stats.cs
new file mode 100644
index 0000000000000000000000000000000000000000..8165e926884d4ff6751cb16822f0b0e000d2fc96
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/Stats.cs
@@ -0,0 +1,614 @@
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+// source: test/proto/benchmarks/stats.proto
+#pragma warning disable 1591, 0612, 3021
+#region Designer generated code
+
+using pb = global::Google.Protobuf;
+using pbc = global::Google.Protobuf.Collections;
+using pbr = global::Google.Protobuf.Reflection;
+using scg = global::System.Collections.Generic;
+namespace Grpc.Testing {
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public static partial class Stats {
+
+    #region Descriptor
+    public static pbr::FileDescriptor Descriptor {
+      get { return descriptor; }
+    }
+    private static pbr::FileDescriptor descriptor;
+
+    static Stats() {
+      byte[] descriptorData = global::System.Convert.FromBase64String(
+          string.Concat(
+            "CiF0ZXN0L3Byb3RvL2JlbmNobWFya3Mvc3RhdHMucHJvdG8SDGdycGMudGVz", 
+            "dGluZyJLCgtTZXJ2ZXJTdGF0cxIUCgx0aW1lX2VsYXBzZWQYASABKAESEQoJ", 
+            "dGltZV91c2VyGAIgASgBEhMKC3RpbWVfc3lzdGVtGAMgASgBIncKDUhpc3Rv", 
+            "Z3JhbURhdGESDgoGYnVja2V0GAEgAygNEhAKCG1pbl9zZWVuGAIgASgBEhAK", 
+            "CG1heF9zZWVuGAMgASgBEgsKA3N1bRgEIAEoARIWCg5zdW1fb2Zfc3F1YXJl", 
+            "cxgFIAEoARINCgVjb3VudBgGIAEoASJ7CgtDbGllbnRTdGF0cxIuCglsYXRl", 
+            "bmNpZXMYASABKAsyGy5ncnBjLnRlc3RpbmcuSGlzdG9ncmFtRGF0YRIUCgx0", 
+            "aW1lX2VsYXBzZWQYAiABKAESEQoJdGltZV91c2VyGAMgASgBEhMKC3RpbWVf", 
+            "c3lzdGVtGAQgASgBYgZwcm90bzM="));
+      descriptor = pbr::FileDescriptor.InternalBuildGeneratedFileFrom(descriptorData,
+          new pbr::FileDescriptor[] { },
+          new pbr::GeneratedCodeInfo(null, new pbr::GeneratedCodeInfo[] {
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ServerStats), new[]{ "TimeElapsed", "TimeUser", "TimeSystem" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.HistogramData), new[]{ "Bucket", "MinSeen", "MaxSeen", "Sum", "SumOfSquares", "Count" }, null, null, null),
+            new pbr::GeneratedCodeInfo(typeof(global::Grpc.Testing.ClientStats), new[]{ "Latencies", "TimeElapsed", "TimeUser", "TimeSystem" }, null, null, null)
+          }));
+    }
+    #endregion
+
+  }
+  #region Messages
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ServerStats : pb::IMessage<ServerStats> {
+    private static readonly pb::MessageParser<ServerStats> _parser = new pb::MessageParser<ServerStats>(() => new ServerStats());
+    public static pb::MessageParser<ServerStats> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Stats.Descriptor.MessageTypes[0]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ServerStats() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ServerStats(ServerStats other) : this() {
+      timeElapsed_ = other.timeElapsed_;
+      timeUser_ = other.timeUser_;
+      timeSystem_ = other.timeSystem_;
+    }
+
+    public ServerStats Clone() {
+      return new ServerStats(this);
+    }
+
+    public const int TimeElapsedFieldNumber = 1;
+    private double timeElapsed_;
+    public double TimeElapsed {
+      get { return timeElapsed_; }
+      set {
+        timeElapsed_ = value;
+      }
+    }
+
+    public const int TimeUserFieldNumber = 2;
+    private double timeUser_;
+    public double TimeUser {
+      get { return timeUser_; }
+      set {
+        timeUser_ = value;
+      }
+    }
+
+    public const int TimeSystemFieldNumber = 3;
+    private double timeSystem_;
+    public double TimeSystem {
+      get { return timeSystem_; }
+      set {
+        timeSystem_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ServerStats);
+    }
+
+    public bool Equals(ServerStats other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (TimeElapsed != other.TimeElapsed) return false;
+      if (TimeUser != other.TimeUser) return false;
+      if (TimeSystem != other.TimeSystem) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (TimeElapsed != 0D) hash ^= TimeElapsed.GetHashCode();
+      if (TimeUser != 0D) hash ^= TimeUser.GetHashCode();
+      if (TimeSystem != 0D) hash ^= TimeSystem.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (TimeElapsed != 0D) {
+        output.WriteRawTag(9);
+        output.WriteDouble(TimeElapsed);
+      }
+      if (TimeUser != 0D) {
+        output.WriteRawTag(17);
+        output.WriteDouble(TimeUser);
+      }
+      if (TimeSystem != 0D) {
+        output.WriteRawTag(25);
+        output.WriteDouble(TimeSystem);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (TimeElapsed != 0D) {
+        size += 1 + 8;
+      }
+      if (TimeUser != 0D) {
+        size += 1 + 8;
+      }
+      if (TimeSystem != 0D) {
+        size += 1 + 8;
+      }
+      return size;
+    }
+
+    public void MergeFrom(ServerStats other) {
+      if (other == null) {
+        return;
+      }
+      if (other.TimeElapsed != 0D) {
+        TimeElapsed = other.TimeElapsed;
+      }
+      if (other.TimeUser != 0D) {
+        TimeUser = other.TimeUser;
+      }
+      if (other.TimeSystem != 0D) {
+        TimeSystem = other.TimeSystem;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 9: {
+            TimeElapsed = input.ReadDouble();
+            break;
+          }
+          case 17: {
+            TimeUser = input.ReadDouble();
+            break;
+          }
+          case 25: {
+            TimeSystem = input.ReadDouble();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class HistogramData : pb::IMessage<HistogramData> {
+    private static readonly pb::MessageParser<HistogramData> _parser = new pb::MessageParser<HistogramData>(() => new HistogramData());
+    public static pb::MessageParser<HistogramData> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Stats.Descriptor.MessageTypes[1]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public HistogramData() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public HistogramData(HistogramData other) : this() {
+      bucket_ = other.bucket_.Clone();
+      minSeen_ = other.minSeen_;
+      maxSeen_ = other.maxSeen_;
+      sum_ = other.sum_;
+      sumOfSquares_ = other.sumOfSquares_;
+      count_ = other.count_;
+    }
+
+    public HistogramData Clone() {
+      return new HistogramData(this);
+    }
+
+    public const int BucketFieldNumber = 1;
+    private static readonly pb::FieldCodec<uint> _repeated_bucket_codec
+        = pb::FieldCodec.ForUInt32(10);
+    private readonly pbc::RepeatedField<uint> bucket_ = new pbc::RepeatedField<uint>();
+    public pbc::RepeatedField<uint> Bucket {
+      get { return bucket_; }
+    }
+
+    public const int MinSeenFieldNumber = 2;
+    private double minSeen_;
+    public double MinSeen {
+      get { return minSeen_; }
+      set {
+        minSeen_ = value;
+      }
+    }
+
+    public const int MaxSeenFieldNumber = 3;
+    private double maxSeen_;
+    public double MaxSeen {
+      get { return maxSeen_; }
+      set {
+        maxSeen_ = value;
+      }
+    }
+
+    public const int SumFieldNumber = 4;
+    private double sum_;
+    public double Sum {
+      get { return sum_; }
+      set {
+        sum_ = value;
+      }
+    }
+
+    public const int SumOfSquaresFieldNumber = 5;
+    private double sumOfSquares_;
+    public double SumOfSquares {
+      get { return sumOfSquares_; }
+      set {
+        sumOfSquares_ = value;
+      }
+    }
+
+    public const int CountFieldNumber = 6;
+    private double count_;
+    public double Count {
+      get { return count_; }
+      set {
+        count_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as HistogramData);
+    }
+
+    public bool Equals(HistogramData other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if(!bucket_.Equals(other.bucket_)) return false;
+      if (MinSeen != other.MinSeen) return false;
+      if (MaxSeen != other.MaxSeen) return false;
+      if (Sum != other.Sum) return false;
+      if (SumOfSquares != other.SumOfSquares) return false;
+      if (Count != other.Count) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      hash ^= bucket_.GetHashCode();
+      if (MinSeen != 0D) hash ^= MinSeen.GetHashCode();
+      if (MaxSeen != 0D) hash ^= MaxSeen.GetHashCode();
+      if (Sum != 0D) hash ^= Sum.GetHashCode();
+      if (SumOfSquares != 0D) hash ^= SumOfSquares.GetHashCode();
+      if (Count != 0D) hash ^= Count.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      bucket_.WriteTo(output, _repeated_bucket_codec);
+      if (MinSeen != 0D) {
+        output.WriteRawTag(17);
+        output.WriteDouble(MinSeen);
+      }
+      if (MaxSeen != 0D) {
+        output.WriteRawTag(25);
+        output.WriteDouble(MaxSeen);
+      }
+      if (Sum != 0D) {
+        output.WriteRawTag(33);
+        output.WriteDouble(Sum);
+      }
+      if (SumOfSquares != 0D) {
+        output.WriteRawTag(41);
+        output.WriteDouble(SumOfSquares);
+      }
+      if (Count != 0D) {
+        output.WriteRawTag(49);
+        output.WriteDouble(Count);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      size += bucket_.CalculateSize(_repeated_bucket_codec);
+      if (MinSeen != 0D) {
+        size += 1 + 8;
+      }
+      if (MaxSeen != 0D) {
+        size += 1 + 8;
+      }
+      if (Sum != 0D) {
+        size += 1 + 8;
+      }
+      if (SumOfSquares != 0D) {
+        size += 1 + 8;
+      }
+      if (Count != 0D) {
+        size += 1 + 8;
+      }
+      return size;
+    }
+
+    public void MergeFrom(HistogramData other) {
+      if (other == null) {
+        return;
+      }
+      bucket_.Add(other.bucket_);
+      if (other.MinSeen != 0D) {
+        MinSeen = other.MinSeen;
+      }
+      if (other.MaxSeen != 0D) {
+        MaxSeen = other.MaxSeen;
+      }
+      if (other.Sum != 0D) {
+        Sum = other.Sum;
+      }
+      if (other.SumOfSquares != 0D) {
+        SumOfSquares = other.SumOfSquares;
+      }
+      if (other.Count != 0D) {
+        Count = other.Count;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 10:
+          case 8: {
+            bucket_.AddEntriesFrom(input, _repeated_bucket_codec);
+            break;
+          }
+          case 17: {
+            MinSeen = input.ReadDouble();
+            break;
+          }
+          case 25: {
+            MaxSeen = input.ReadDouble();
+            break;
+          }
+          case 33: {
+            Sum = input.ReadDouble();
+            break;
+          }
+          case 41: {
+            SumOfSquares = input.ReadDouble();
+            break;
+          }
+          case 49: {
+            Count = input.ReadDouble();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+  public sealed partial class ClientStats : pb::IMessage<ClientStats> {
+    private static readonly pb::MessageParser<ClientStats> _parser = new pb::MessageParser<ClientStats>(() => new ClientStats());
+    public static pb::MessageParser<ClientStats> Parser { get { return _parser; } }
+
+    public static pbr::MessageDescriptor Descriptor {
+      get { return global::Grpc.Testing.Stats.Descriptor.MessageTypes[2]; }
+    }
+
+    pbr::MessageDescriptor pb::IMessage.Descriptor {
+      get { return Descriptor; }
+    }
+
+    public ClientStats() {
+      OnConstruction();
+    }
+
+    partial void OnConstruction();
+
+    public ClientStats(ClientStats other) : this() {
+      Latencies = other.latencies_ != null ? other.Latencies.Clone() : null;
+      timeElapsed_ = other.timeElapsed_;
+      timeUser_ = other.timeUser_;
+      timeSystem_ = other.timeSystem_;
+    }
+
+    public ClientStats Clone() {
+      return new ClientStats(this);
+    }
+
+    public const int LatenciesFieldNumber = 1;
+    private global::Grpc.Testing.HistogramData latencies_;
+    public global::Grpc.Testing.HistogramData Latencies {
+      get { return latencies_; }
+      set {
+        latencies_ = value;
+      }
+    }
+
+    public const int TimeElapsedFieldNumber = 2;
+    private double timeElapsed_;
+    public double TimeElapsed {
+      get { return timeElapsed_; }
+      set {
+        timeElapsed_ = value;
+      }
+    }
+
+    public const int TimeUserFieldNumber = 3;
+    private double timeUser_;
+    public double TimeUser {
+      get { return timeUser_; }
+      set {
+        timeUser_ = value;
+      }
+    }
+
+    public const int TimeSystemFieldNumber = 4;
+    private double timeSystem_;
+    public double TimeSystem {
+      get { return timeSystem_; }
+      set {
+        timeSystem_ = value;
+      }
+    }
+
+    public override bool Equals(object other) {
+      return Equals(other as ClientStats);
+    }
+
+    public bool Equals(ClientStats other) {
+      if (ReferenceEquals(other, null)) {
+        return false;
+      }
+      if (ReferenceEquals(other, this)) {
+        return true;
+      }
+      if (!object.Equals(Latencies, other.Latencies)) return false;
+      if (TimeElapsed != other.TimeElapsed) return false;
+      if (TimeUser != other.TimeUser) return false;
+      if (TimeSystem != other.TimeSystem) return false;
+      return true;
+    }
+
+    public override int GetHashCode() {
+      int hash = 1;
+      if (latencies_ != null) hash ^= Latencies.GetHashCode();
+      if (TimeElapsed != 0D) hash ^= TimeElapsed.GetHashCode();
+      if (TimeUser != 0D) hash ^= TimeUser.GetHashCode();
+      if (TimeSystem != 0D) hash ^= TimeSystem.GetHashCode();
+      return hash;
+    }
+
+    public override string ToString() {
+      return pb::JsonFormatter.Default.Format(this);
+    }
+
+    public void WriteTo(pb::CodedOutputStream output) {
+      if (latencies_ != null) {
+        output.WriteRawTag(10);
+        output.WriteMessage(Latencies);
+      }
+      if (TimeElapsed != 0D) {
+        output.WriteRawTag(17);
+        output.WriteDouble(TimeElapsed);
+      }
+      if (TimeUser != 0D) {
+        output.WriteRawTag(25);
+        output.WriteDouble(TimeUser);
+      }
+      if (TimeSystem != 0D) {
+        output.WriteRawTag(33);
+        output.WriteDouble(TimeSystem);
+      }
+    }
+
+    public int CalculateSize() {
+      int size = 0;
+      if (latencies_ != null) {
+        size += 1 + pb::CodedOutputStream.ComputeMessageSize(Latencies);
+      }
+      if (TimeElapsed != 0D) {
+        size += 1 + 8;
+      }
+      if (TimeUser != 0D) {
+        size += 1 + 8;
+      }
+      if (TimeSystem != 0D) {
+        size += 1 + 8;
+      }
+      return size;
+    }
+
+    public void MergeFrom(ClientStats other) {
+      if (other == null) {
+        return;
+      }
+      if (other.latencies_ != null) {
+        if (latencies_ == null) {
+          latencies_ = new global::Grpc.Testing.HistogramData();
+        }
+        Latencies.MergeFrom(other.Latencies);
+      }
+      if (other.TimeElapsed != 0D) {
+        TimeElapsed = other.TimeElapsed;
+      }
+      if (other.TimeUser != 0D) {
+        TimeUser = other.TimeUser;
+      }
+      if (other.TimeSystem != 0D) {
+        TimeSystem = other.TimeSystem;
+      }
+    }
+
+    public void MergeFrom(pb::CodedInputStream input) {
+      uint tag;
+      while ((tag = input.ReadTag()) != 0) {
+        switch(tag) {
+          default:
+            input.SkipLastField();
+            break;
+          case 10: {
+            if (latencies_ == null) {
+              latencies_ = new global::Grpc.Testing.HistogramData();
+            }
+            input.ReadMessage(latencies_);
+            break;
+          }
+          case 17: {
+            TimeElapsed = input.ReadDouble();
+            break;
+          }
+          case 25: {
+            TimeUser = input.ReadDouble();
+            break;
+          }
+          case 33: {
+            TimeSystem = input.ReadDouble();
+            break;
+          }
+        }
+      }
+    }
+
+  }
+
+  #endregion
+
+}
+
+#endregion Designer generated code
diff --git a/src/csharp/Grpc.IntegrationTesting/WallClockStopwatch.cs b/src/csharp/Grpc.IntegrationTesting/WallClockStopwatch.cs
new file mode 100644
index 0000000000000000000000000000000000000000..e44ae2a5ff364ad172b42cbc26372e342f2db4de
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/WallClockStopwatch.cs
@@ -0,0 +1,78 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+using Grpc.Testing;
+
+namespace Grpc.IntegrationTesting
+{
+    /// <summary>
+    /// Snapshottable wall clock stopwatch.
+    /// </summary>
+    public class WallClockStopwatch
+    {
+        long startTicks;
+
+        public WallClockStopwatch()
+        {
+            this.startTicks = DateTime.UtcNow.Ticks;
+        }
+
+        public TimeSpan GetElapsedSnapshot(bool reset)
+        {
+            var utcNow = DateTime.UtcNow;
+
+            long oldStartTicks;
+            if (reset)
+            {
+                oldStartTicks = Interlocked.Exchange(ref this.startTicks, utcNow.Ticks);
+            }
+            else
+            {
+                oldStartTicks = this.startTicks;
+            }
+            return utcNow - new DateTime(oldStartTicks, DateTimeKind.Utc);
+        }
+    }
+}
diff --git a/src/csharp/Grpc.IntegrationTesting/WorkerServiceImpl.cs b/src/csharp/Grpc.IntegrationTesting/WorkerServiceImpl.cs
new file mode 100644
index 0000000000000000000000000000000000000000..bb2918bf463dc0e527d24b2cfd7766c285957d70
--- /dev/null
+++ b/src/csharp/Grpc.IntegrationTesting/WorkerServiceImpl.cs
@@ -0,0 +1,96 @@
+#region Copyright notice and license
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Grpc.Core;
+using Grpc.Core.Utils;
+using Grpc.IntegrationTesting;
+
+namespace Grpc.Testing
+{
+    /// <summary>
+    /// Implementation of WorkerService server
+    /// </summary>
+    public class WorkerServiceImpl : WorkerService.IWorkerService
+    {
+        public async Task RunServer(IAsyncStreamReader<ServerArgs> requestStream, IServerStreamWriter<ServerStatus> responseStream, ServerCallContext context)
+        {
+            Grpc.Core.Utils.Preconditions.CheckState(await requestStream.MoveNext());
+            var serverConfig = requestStream.Current.Setup;
+            var runner = ServerRunners.CreateStarted(serverConfig);
+
+            await responseStream.WriteAsync(new ServerStatus
+            {
+                Stats = runner.GetStats(false),
+                Port = runner.BoundPort,
+                Cores = 0,  // TODO: set number of cores
+            });
+                
+            while (await requestStream.MoveNext())
+            {
+                var reset = requestStream.Current.Mark.Reset;
+                await responseStream.WriteAsync(new ServerStatus
+                {
+                    Stats = runner.GetStats(reset)
+                });
+            }
+            await runner.StopAsync();
+        }
+
+        public async Task RunClient(IAsyncStreamReader<ClientArgs> requestStream, IServerStreamWriter<ClientStatus> responseStream, ServerCallContext context)
+        {
+            Grpc.Core.Utils.Preconditions.CheckState(await requestStream.MoveNext());
+            var clientConfig = requestStream.Current.Setup;
+            var runner = ClientRunners.CreateStarted(clientConfig);
+
+            await responseStream.WriteAsync(new ClientStatus
+            {
+                Stats = runner.GetStats(false)
+            });
+
+            while (await requestStream.MoveNext())
+            {
+                var reset = requestStream.Current.Mark.Reset;
+                await responseStream.WriteAsync(new ClientStatus
+                {
+                    Stats = runner.GetStats(reset)
+                });
+            }
+            await runner.StopAsync();
+        }
+    }
+}
diff --git a/src/csharp/Grpc.sln b/src/csharp/Grpc.sln
index f19f29c6a27ccebfbdf884dde6c8d6c2040a1c63..8ff35e8c0d0193f35ad390314f467e16d8cec859 100644
--- a/src/csharp/Grpc.sln
+++ b/src/csharp/Grpc.sln
@@ -1,6 +1,6 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 2013
+# Visual Studio 2012
 VisualStudioVersion = 12.0.31101.0
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grpc.Examples", "Grpc.Examples\Grpc.Examples.csproj", "{7DC1433E-3225-42C7-B7EA-546D56E27A4B}"
@@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grpc.HealthCheck", "Grpc.He
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grpc.HealthCheck.Tests", "Grpc.HealthCheck.Tests\Grpc.HealthCheck.Tests.csproj", "{F8C6D937-C44B-4EE3-A431-B0FBAEACE47D}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grpc.IntegrationTesting.QpsWorker", "Grpc.IntegrationTesting.QpsWorker\Grpc.IntegrationTesting.QpsWorker.csproj", "{B82B7DFE-7F7B-40EF-B3D6-064FF2B01294}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -39,72 +41,78 @@ Global
 		ReleaseSigned|Any CPU = ReleaseSigned|Any CPU
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
-		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.Release|Any CPU.Build.0 = Release|Any CPU
-		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
-		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
-		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.Release|Any CPU.Build.0 = Release|Any CPU
-		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
-		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
-		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.Release|Any CPU.Build.0 = Release|Any CPU
-		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
-		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
 		{143B1C29-C442-4BE0-BF3F-A8F92288AC9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{143B1C29-C442-4BE0-BF3F-A8F92288AC9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{143B1C29-C442-4BE0-BF3F-A8F92288AC9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{143B1C29-C442-4BE0-BF3F-A8F92288AC9F}.Release|Any CPU.Build.0 = Release|Any CPU
 		{143B1C29-C442-4BE0-BF3F-A8F92288AC9F}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
 		{143B1C29-C442-4BE0-BF3F-A8F92288AC9F}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
-		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.Release|Any CPU.Build.0 = Release|Any CPU
-		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
-		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
-		{C61154BA-DD4A-4838-8420-0162A28925E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{C61154BA-DD4A-4838-8420-0162A28925E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{C61154BA-DD4A-4838-8420-0162A28925E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{C61154BA-DD4A-4838-8420-0162A28925E0}.Release|Any CPU.Build.0 = Release|Any CPU
-		{C61154BA-DD4A-4838-8420-0162A28925E0}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
-		{C61154BA-DD4A-4838-8420-0162A28925E0}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
 		{3D166931-BA2D-416E-95A3-D36E8F6E90B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{3D166931-BA2D-416E-95A3-D36E8F6E90B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{3D166931-BA2D-416E-95A3-D36E8F6E90B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{3D166931-BA2D-416E-95A3-D36E8F6E90B9}.Release|Any CPU.Build.0 = Release|Any CPU
 		{3D166931-BA2D-416E-95A3-D36E8F6E90B9}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
 		{3D166931-BA2D-416E-95A3-D36E8F6E90B9}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
+		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
+		{61ECB8EE-0C96-4F8E-B187-8E4D227417C0}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
+		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
+		{7DC1433E-3225-42C7-B7EA-546D56E27A4B}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
+		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.Release|Any CPU.Build.0 = Release|Any CPU
+		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
+		{86EC5CB4-4EA2-40A2-8057-86542A0353BB}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
 		{A654F3B8-E859-4E6A-B30D-227527DBEF0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{A654F3B8-E859-4E6A-B30D-227527DBEF0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{A654F3B8-E859-4E6A-B30D-227527DBEF0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{A654F3B8-E859-4E6A-B30D-227527DBEF0D}.Release|Any CPU.Build.0 = Release|Any CPU
 		{A654F3B8-E859-4E6A-B30D-227527DBEF0D}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
 		{A654F3B8-E859-4E6A-B30D-227527DBEF0D}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
-		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.Release|Any CPU.Build.0 = Release|Any CPU
-		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
-		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
-		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.Release|Any CPU.Build.0 = Release|Any CPU
-		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
-		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
 		{AA5E328A-8835-49D7-98ED-C29F2B3049F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{AA5E328A-8835-49D7-98ED-C29F2B3049F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{AA5E328A-8835-49D7-98ED-C29F2B3049F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{AA5E328A-8835-49D7-98ED-C29F2B3049F0}.Release|Any CPU.Build.0 = Release|Any CPU
 		{AA5E328A-8835-49D7-98ED-C29F2B3049F0}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
 		{AA5E328A-8835-49D7-98ED-C29F2B3049F0}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
+		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
+		{AE21D0EE-9A2C-4C15-AB7F-5224EED5B0EA}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
+		{B82B7DFE-7F7B-40EF-B3D6-064FF2B01294}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B82B7DFE-7F7B-40EF-B3D6-064FF2B01294}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B82B7DFE-7F7B-40EF-B3D6-064FF2B01294}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B82B7DFE-7F7B-40EF-B3D6-064FF2B01294}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B82B7DFE-7F7B-40EF-B3D6-064FF2B01294}.ReleaseSigned|Any CPU.ActiveCfg = Release|Any CPU
+		{B82B7DFE-7F7B-40EF-B3D6-064FF2B01294}.ReleaseSigned|Any CPU.Build.0 = Release|Any CPU
+		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.Release|Any CPU.Build.0 = Release|Any CPU
+		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
+		{BF62FE08-373A-43D6-9D73-41CAA38B7011}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
+		{C61154BA-DD4A-4838-8420-0162A28925E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C61154BA-DD4A-4838-8420-0162A28925E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C61154BA-DD4A-4838-8420-0162A28925E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C61154BA-DD4A-4838-8420-0162A28925E0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C61154BA-DD4A-4838-8420-0162A28925E0}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
+		{C61154BA-DD4A-4838-8420-0162A28925E0}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
+		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.Release|Any CPU.Build.0 = Release|Any CPU
+		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
+		{CCC4440E-49F7-4790-B0AF-FEABB0837AE7}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
 		{F8C6D937-C44B-4EE3-A431-B0FBAEACE47D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{F8C6D937-C44B-4EE3-A431-B0FBAEACE47D}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{F8C6D937-C44B-4EE3-A431-B0FBAEACE47D}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -112,6 +120,8 @@ Global
 		{F8C6D937-C44B-4EE3-A431-B0FBAEACE47D}.ReleaseSigned|Any CPU.ActiveCfg = ReleaseSigned|Any CPU
 		{F8C6D937-C44B-4EE3-A431-B0FBAEACE47D}.ReleaseSigned|Any CPU.Build.0 = ReleaseSigned|Any CPU
 	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 	EndGlobalSection
diff --git a/src/csharp/generate_proto_csharp.sh b/src/csharp/generate_proto_csharp.sh
index f879e074aa55a21b69dfe6c5f31e29fb34fdff6d..92348d13942e4f36219eb8fba90011eb39ca57b6 100755
--- a/src/csharp/generate_proto_csharp.sh
+++ b/src/csharp/generate_proto_csharp.sh
@@ -35,14 +35,14 @@ cd $(dirname $0)
 PROTOC=../../bins/opt/protobuf/protoc
 PLUGIN=protoc-gen-grpc=../../bins/opt/grpc_csharp_plugin
 EXAMPLES_DIR=Grpc.Examples
-INTEROP_DIR=Grpc.IntegrationTesting
+TESTING_DIR=Grpc.IntegrationTesting
 HEALTHCHECK_DIR=Grpc.HealthCheck
 
 $PROTOC --plugin=$PLUGIN --csharp_out=$EXAMPLES_DIR --grpc_out=$EXAMPLES_DIR \
     -I $EXAMPLES_DIR/proto $EXAMPLES_DIR/proto/math.proto
 
-$PROTOC --plugin=$PLUGIN --csharp_out=$INTEROP_DIR --grpc_out=$INTEROP_DIR \
-    -I ../.. ../../test/proto/*.proto
+$PROTOC --plugin=$PLUGIN --csharp_out=$TESTING_DIR --grpc_out=$TESTING_DIR \
+    -I ../.. ../../test/proto/*.proto ../../test/proto/benchmarks/*.proto
 
 $PROTOC --plugin=$PLUGIN --csharp_out=$HEALTHCHECK_DIR --grpc_out=$HEALTHCHECK_DIR \
     -I $HEALTHCHECK_DIR/proto $HEALTHCHECK_DIR/proto/health.proto