/// <summary> /// Add a result to the weighted rolling average history. /// </summary> /// <param name="ipAndPort">IP:Port string to identify where the results go</param> /// <param name="result">The result to add to the history. This result becomes the current result.</param> public void AddResult(string ipAndPort, QosResult result) { if (result.PacketLoss + Mathf.Epsilon < 0.0f || result.PacketLoss - Mathf.Epsilon > 1.0f) { throw new ArgumentOutOfRangeException(nameof(result), "PacketLoss must be in the range [0.0..1.0]"); } m_ResultsLock.EnterWriteLock(); try { WeightedMovingAverage wma = null; if (!m_Results.TryGetValue(ipAndPort, out wma)) { // Tracking new server wma = new WeightedMovingAverage(m_NumResults, m_Weight); } wma.AddResult(new QosStatsResult((int)result.AverageLatencyMs, result.PacketLoss)); m_Results[ipAndPort] = wma; } finally { m_ResultsLock.ExitWriteLock(); } }
private bool TryCopyResult(ulong id, ref QosResult result) { var rc = TryFindFirstMatchingResult(id, out var r); if (rc == true) { result = r; } return(rc); }
private bool TryFindFirstMatchingResult(ulong id, out QosResult r) { for (var i = 0; i < m_QosServers.Length; ++i) { if (m_QosServers[i].Id == id) { r = QosResults[i]; return(true); } } r = new QosResult(); return(false); }
public void Execute() { if (m_QosServers.Length == 0) { return; // Nothing to do. } m_JobExpireTimeUtc = DateTime.UtcNow.AddMilliseconds(TimeoutMs); // Create the local socket int errorcode = 0; (m_Socket, errorcode) = CreateAndBindSocket(); if (m_Socket == -1 || errorcode != 0) { // Can't run the job Debug.LogError("Failed to create and bind the local socket for QoS Check"); } else { m_Identifier = (ushort)new Random().Next(ushort.MinValue, ushort.MaxValue); for (int i = 0; i < m_QosServers.Length; ++i) { QosResult result = QosResults[i]; InternalQosServer server = m_QosServers[i]; if (QosHelper.ExpiredUtc(m_JobExpireTimeUtc)) { Debug.LogWarning($"Ran out of time to finish remaining QoS Check for endpoint {i}."); break; } // If we've already visited this server, just copy those results here. if (QosServerVisited(server.Id)) { if (TryCopyResult(server.Id, ref result) == false) { Debug.LogError($"Visited server must have a previous result available"); break; } } else if (DateTime.UtcNow > server.BackoffUntilUtc) // Only contact this server if we are allowed { // For each iteration of the loop, give the remaining endpoints an equal fraction of the remaining // overall job time. For example if there are five endpoints that each get 1000ms (5000ms total), // and the first endpoint finishes in 200ms, the remaining endpoints will get 1200ms to complete // (4800ms remaining / 4 endpoints = 1200ms/endpoint). double allottedTimeMs = QosHelper.RemainingUtc(m_JobExpireTimeUtc).TotalMilliseconds / (m_QosServers.Length - i); DateTime startTimeUtc = DateTime.UtcNow; DateTime expireTimeUtc = DateTime.UtcNow.AddMilliseconds(allottedTimeMs); #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.Log($"QoS Check {i} gets {(expireTimeUtc - DateTime.UtcNow).TotalMilliseconds:F0}ms to complete."); #endif ++m_Identifier; int err = SendQosRequests(server.RemoteEndpoint, m_Identifier, expireTimeUtc, ref result); if (err != 0) { Debug.LogError($"Error {err} sending QoS requests. Will attempt to receive responses anyway."); } err = RecvQosResponses(server.RemoteEndpoint, m_Identifier, expireTimeUtc, ref result); if (err != 0) { Debug.LogError($"Error {err} receiving QoS responses. Will attempt to continue anyway."); } Debug.Log($"Received {result.ResponsesReceived}/{result.RequestsSent} responses from endpoint {i} in {(DateTime.UtcNow - startTimeUtc).TotalMilliseconds:F0}ms"); // Mark this server as visited SetQosServerVisited(server.Id); } else { Debug.LogWarning($"Did not contact endpoint {i} due to backoff restrictions."); } // Save the result (even if we didn't contact the server) QosResults[i] = result; } } NativeBindings.network_close(ref m_Socket, ref errorcode); }
/// <summary> /// Receive QoS responses from the given endpoint /// </summary> /// <param name="remoteEndpoint">Where to expect responses to come from</param> /// <param name="identifier">Identifier that will accompany a valid response</param> /// <param name="expireTimeUtc">How long to wait for responses</param> /// <param name="result">Results from the receive side of the check (packets received, latency, packet loss)</param> /// <returns> /// errorcode - the last error code (if any). 0 means no error. /// </returns> private int RecvQosResponses(NetworkEndPoint remoteEndpoint, ushort identifier, DateTime expireTimeUtc, ref QosResult result) { if (result.RequestsSent == 0) { return(0); // Not expecting any responses } NativeArray <int> responseLatency = new NativeArray <int>((int)result.RequestsSent, Allocator.Temp); for (int i = 0; i < responseLatency.Length; ++i) { responseLatency[i] = -1; } QosResponse response = new QosResponse(); int errorcode = 0; while (result.ResponsesReceived < result.RequestsSent && !QosHelper.ExpiredUtc(expireTimeUtc)) { errorcode = 0; int received = 0; (received, errorcode) = response.Recv(m_Socket, remoteEndpoint, expireTimeUtc); if (received == -1) { Debug.LogError($"Invalid or no response received (errorcode = {errorcode})"); } else if (!response.Verify()) { Debug.LogWarning("Ignoring invalid QosResponse"); ++result.InvalidResponses; } else if (response.Identifier != identifier) { Debug.LogWarning($"Identifier 0x{response.Identifier:X4} != expected identifier 0x{identifier:X4}; ignoring..."); ++result.InvalidResponses; } else { if (response.Sequence >= result.RequestsSent) // Sequence can't be more than number of requests sent { Debug.LogWarning($"Ignoring response with sequence {response.Sequence} that is higher than max sequence expected"); ++result.InvalidResponses; } else if (responseLatency[response.Sequence] == -1) { responseLatency[response.Sequence] = response.LatencyMs; ++result.ResponsesReceived; } else { Debug.Log($"Duplicate response {response.Sequence} received for QosCheck identifier 0x{response.Identifier:X4}"); ++result.DuplicateResponses; } // Determine if we've had flow control applied to us. If so, save the most significant result based // on the unit count. In this version, both Ban and Throttle have the same result: client back-off. var fc = response.ParseFlowControl(); if (fc.type != FcType.None && fc.units > result.FcUnits) { result.FcType = fc.type; result.FcUnits = fc.units; } } } // Calculate average latency and log results result.AverageLatencyMs = 0; if (result.ResponsesReceived > 0) { uint validResponses = 0; for (int i = 0, length = responseLatency.Length; i < length; i++) { var latency = responseLatency[i]; if (latency >= 0) { result.AverageLatencyMs += (uint)latency; validResponses++; #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.Log($"Received response {i} for QosCheck identifier 0x{identifier:X4} with latency {latency}ms"); #endif } #if UNITY_EDITOR || DEVELOPMENT_BUILD else { Debug.LogWarning($"Failed to receive response {i} for QosCheck identifier 0x{identifier:X4}"); } #endif } result.AverageLatencyMs /= validResponses; } result.UpdatePacketLoss(); responseLatency.Dispose(); return(errorcode); }
/// <summary> /// Send QoS requests to the given endpoint. /// </summary> /// <param name="remoteEndpoint">Server that QoS requests should be sent to</param> /// <param name="expireTime">When to stop trying to send requests</param> /// <param name="result">Results from the send side of the check (packets sent)</param> /// <returns> /// errorcode - the last error code generated (if any). 0 indicates no error. /// </returns> private int SendQosRequests(NetworkEndPoint remoteEndpoint, ushort identifier, DateTime expireTime, ref QosResult result) { QosRequest request = new QosRequest { Title = m_TitleBytesUtf8.ToArray(), Identifier = identifier }; #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.Log($"Identifier for this QoS check is: 0x{request.Identifier:X4}"); #endif // Send all requests. result.RequestsSent = 0; while (result.RequestsSent < RequestsPerEndpoint && !QosHelper.ExpiredUtc(expireTime)) { request.Timestamp = (ulong)(DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond); request.Sequence = (byte)result.RequestsSent; int errorcode = 0; int sent = 0; (sent, errorcode) = request.Send(m_Socket, ref remoteEndpoint, expireTime); if (errorcode != 0) { Debug.LogError($"Send returned error code {errorcode}, can't continue"); return(errorcode); } else if (sent != request.Length) { Debug.LogWarning($"Sent {sent} of {request.Length} bytes, ignoring this request"); ++result.InvalidRequests; } else { ++result.RequestsSent; } } return(0); }