void PrintStats()
    {
        // Print out all the aggregate stats
        for (var i = 0; i < qosServers?.Length; ++i)
        {
            var ipAndPort = qosServers[i].ToString();

            if (m_Stats.TryGetWeightedAverage(ipAndPort, out var result))
            {
                m_Stats.TryGetAllResults(ipAndPort, out var allResults);

                // NOTE:  You probably don't want Linq in your game, but it's convenient here to filter out the invalid results.
                Debug.Log($"Weighted average QoS report for {ipAndPort}: " +
                          $"Latency: {result.LatencyMs}ms, " +
                          $"Packet Loss: {result.PacketLoss * 100.0f:F1}%, " +
                          $"All Results: {string.Join(", ", allResults.Select(x => x.IsValid() ? x.LatencyMs : 0))}");
            }
            else
            {
                Debug.Log($"No results for {ipAndPort}.");
            }
        }

#if USE_QOSCONNECTOR || USE_MMCONNECTOR
        // Print out what would be returned through the QosConnector
        QosTicketInfo results = QosConnector.Instance.Execute();
        if (results.QosResults?.Count > 0)
        {
            Debug.Log("QosTicketInfo coming from the QosConnector:\n" + JsonUtility.ToJson(results, true));
        }
#endif
    }
        /// <summary>
        ///     Initialize a new MatchmakingRequest object
        /// </summary>
        /// <param name="client">
        ///     An already existing MatchmakingClient to use as the client for the request
        /// </param>
        /// <param name="request">
        ///     The ticket data to send with the matchmaking request.
        ///     Data will be copied internally on construction and treated as immutable.
        /// </param>
        /// <param name="timeoutMs">
        ///     The amount of time to wait (in ms) before aborting an incomplete MatchmakingRequest after it has been sent.
        ///     Match requests that time out on the client side will immediately be set to completed and stop listening for a match
        ///     assignment.
        /// </param>
        public MatchmakingRequest(MatchmakingClient client, CreateTicketRequest request, uint timeoutMs = 0)
        {
            m_Client = client
                       ?? throw new ArgumentNullException(nameof(client), logPre + $"Matchmaking {nameof(client)} must not be null");

            if (request == null)
            {
                throw new ArgumentNullException(nameof(request), logPre + $"{nameof(request)} must be a non-null, valid {nameof(CreateTicketRequest)} object");
            }

            // Try to immediately create and store the protobuf version of the CreateTicketRequest
            //  This allows us to fail fast, and also copies the data to prevent it from being mutable
            //  This may cause exceptions inside the protobuf code, which is fine since we're in the constructor
            var createTicketRequest = new Protobuf.CreateTicketRequest();


            string key = nameof(QosTicketInfo).ToLower();

            if (request.Properties != null && !request.Properties.ContainsKey(key))
            {
                QosTicketInfo results = QosConnector.Instance.Execute();
                if (results?.QosResults?.Count > 0)
                {
                    request.Properties = request.Properties ?? new Dictionary <string, string>();
                    request.Properties.Add(key, JsonUtility.ToJson(results));
                }
            }

            // Only set properties if not null
            // Request properties have to be massaged to be protobuf ByteString compatible
            if (request.Properties != null)
            {
                foreach (var kvp in request.Properties)
                {
                    var keyToLower = kvp.Key.ToLower();

#if UNITY_EDITOR || DEVELOPMENT_BUILD
                    if (!kvp.Key.Equals(keyToLower))
                    {
                        Debug.LogWarning(logPre + $"Ticket property with key {kvp.Key} must be all lowercase; changing in-place.");
                    }
#endif
                    createTicketRequest.Properties.Add(keyToLower, ByteString.CopyFrom(Encoding.UTF8.GetBytes(kvp.Value)));
                }
            }

            // Only add attributes if they exist
            if (request?.Attributes?.Count > 0)
            {
                createTicketRequest.Attributes.Add(request.Attributes);
            }

            m_CreateTicketRequest = createTicketRequest;

            State = MatchmakingRequestState.NotStarted;

            m_MatchRequestTimeoutMs = timeoutMs;
        }