Skip to content

Commit 7925d37

Browse files
adding connection and audio latency tests
1 parent 5a61a2e commit 7925d37

3 files changed

Lines changed: 690 additions & 806 deletions

File tree

src/tests/common/test_common.h

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
/*
2+
* Copyright 2025 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <algorithm>
20+
#include <atomic>
21+
#include <chrono>
22+
#include <cstdlib>
23+
#include <gtest/gtest.h>
24+
#include <iomanip>
25+
#include <iostream>
26+
#include <livekit/livekit.h>
27+
#include <map>
28+
#include <mutex>
29+
#include <numeric>
30+
#include <string>
31+
#include <thread>
32+
#include <vector>
33+
34+
namespace livekit {
35+
namespace test {
36+
37+
using namespace std::chrono_literals;
38+
39+
// =============================================================================
40+
// Common Constants
41+
// =============================================================================
42+
43+
// Default number of test iterations for connection tests
44+
constexpr int kDefaultTestIterations = 10;
45+
46+
// Default stress test duration in seconds
47+
constexpr int kDefaultStressDurationSeconds = 600; // 10 minutes
48+
49+
// =============================================================================
50+
// Common Test Configuration
51+
// =============================================================================
52+
53+
/**
54+
* Common test configuration loaded from environment variables.
55+
*
56+
* Environment variables:
57+
* LIVEKIT_URL - WebSocket URL of the LiveKit server
58+
* LIVEKIT_CALLER_TOKEN - Token for the caller/sender participant
59+
* LIVEKIT_RECEIVER_TOKEN - Token for the receiver participant
60+
* TEST_ITERATIONS - Number of iterations for iterative tests (default:
61+
* 10) STRESS_DURATION_SECONDS - Duration for stress tests in seconds (default:
62+
* 600) STRESS_CALLER_THREADS - Number of caller threads for stress tests
63+
* (default: 4)
64+
*/
65+
struct TestConfig {
66+
std::string url;
67+
std::string caller_token;
68+
std::string receiver_token;
69+
int test_iterations;
70+
int stress_duration_seconds;
71+
int num_caller_threads;
72+
bool available = false;
73+
74+
static TestConfig fromEnv() {
75+
TestConfig config;
76+
const char *url = std::getenv("LIVEKIT_URL");
77+
const char *caller_token = std::getenv("LIVEKIT_CALLER_TOKEN");
78+
const char *receiver_token = std::getenv("LIVEKIT_RECEIVER_TOKEN");
79+
const char *iterations_env = std::getenv("TEST_ITERATIONS");
80+
const char *duration_env = std::getenv("STRESS_DURATION_SECONDS");
81+
const char *threads_env = std::getenv("STRESS_CALLER_THREADS");
82+
83+
if (url && caller_token && receiver_token) {
84+
config.url = url;
85+
config.caller_token = caller_token;
86+
config.receiver_token = receiver_token;
87+
config.available = true;
88+
}
89+
90+
config.test_iterations =
91+
iterations_env ? std::atoi(iterations_env) : kDefaultTestIterations;
92+
config.stress_duration_seconds =
93+
duration_env ? std::atoi(duration_env) : kDefaultStressDurationSeconds;
94+
config.num_caller_threads = threads_env ? std::atoi(threads_env) : 4;
95+
96+
return config;
97+
}
98+
};
99+
100+
// =============================================================================
101+
// Utility Functions
102+
// =============================================================================
103+
104+
/// Get current timestamp in microseconds
105+
inline uint64_t getTimestampUs() {
106+
return std::chrono::duration_cast<std::chrono::microseconds>(
107+
std::chrono::system_clock::now().time_since_epoch())
108+
.count();
109+
}
110+
111+
/// Wait for a remote participant to appear in the room
112+
inline bool waitForParticipant(Room *room, const std::string &identity,
113+
std::chrono::milliseconds timeout) {
114+
auto start = std::chrono::steady_clock::now();
115+
while (std::chrono::steady_clock::now() - start < timeout) {
116+
if (room->remoteParticipant(identity) != nullptr) {
117+
return true;
118+
}
119+
std::this_thread::sleep_for(100ms);
120+
}
121+
return false;
122+
}
123+
124+
// =============================================================================
125+
// Statistics Collection
126+
// =============================================================================
127+
128+
/**
129+
* Thread-safe latency statistics collector.
130+
* Records latency measurements and provides summary statistics.
131+
*/
132+
class LatencyStats {
133+
public:
134+
void addMeasurement(double latency_ms) {
135+
std::lock_guard<std::mutex> lock(mutex_);
136+
measurements_.push_back(latency_ms);
137+
}
138+
139+
void printStats(const std::string &title) const {
140+
std::lock_guard<std::mutex> lock(mutex_);
141+
142+
if (measurements_.empty()) {
143+
std::cout << "\n" << title << ": No measurements collected" << std::endl;
144+
return;
145+
}
146+
147+
std::vector<double> sorted = measurements_;
148+
std::sort(sorted.begin(), sorted.end());
149+
150+
double sum = std::accumulate(sorted.begin(), sorted.end(), 0.0);
151+
double avg = sum / sorted.size();
152+
double min = sorted.front();
153+
double max = sorted.back();
154+
double p50 = getPercentile(sorted, 50);
155+
double p95 = getPercentile(sorted, 95);
156+
double p99 = getPercentile(sorted, 99);
157+
158+
std::cout << "\n========================================" << std::endl;
159+
std::cout << " " << title << std::endl;
160+
std::cout << "========================================" << std::endl;
161+
std::cout << "Samples: " << sorted.size() << std::endl;
162+
std::cout << std::fixed << std::setprecision(2);
163+
std::cout << "Min: " << min << " ms" << std::endl;
164+
std::cout << "Avg: " << avg << " ms" << std::endl;
165+
std::cout << "P50: " << p50 << " ms" << std::endl;
166+
std::cout << "P95: " << p95 << " ms" << std::endl;
167+
std::cout << "P99: " << p99 << " ms" << std::endl;
168+
std::cout << "Max: " << max << " ms" << std::endl;
169+
std::cout << "========================================\n" << std::endl;
170+
}
171+
172+
size_t count() const {
173+
std::lock_guard<std::mutex> lock(mutex_);
174+
return measurements_.size();
175+
}
176+
177+
void clear() {
178+
std::lock_guard<std::mutex> lock(mutex_);
179+
measurements_.clear();
180+
}
181+
182+
private:
183+
static double getPercentile(const std::vector<double> &sorted,
184+
int percentile) {
185+
if (sorted.empty())
186+
return 0.0;
187+
size_t index = (sorted.size() * percentile) / 100;
188+
if (index >= sorted.size())
189+
index = sorted.size() - 1;
190+
return sorted[index];
191+
}
192+
193+
mutable std::mutex mutex_;
194+
std::vector<double> measurements_;
195+
};
196+
197+
/**
198+
* Extended statistics collector for stress tests.
199+
* Tracks success/failure counts, bytes transferred, and error breakdown.
200+
*/
201+
class StressTestStats {
202+
public:
203+
void recordCall(bool success, double latency_ms, size_t payload_size = 0) {
204+
std::lock_guard<std::mutex> lock(mutex_);
205+
total_calls_++;
206+
if (success) {
207+
successful_calls_++;
208+
latencies_.push_back(latency_ms);
209+
total_bytes_ += payload_size;
210+
} else {
211+
failed_calls_++;
212+
}
213+
}
214+
215+
void recordError(const std::string &error_type) {
216+
std::lock_guard<std::mutex> lock(mutex_);
217+
error_counts_[error_type]++;
218+
}
219+
220+
void printStats(const std::string &title = "Stress Test Statistics") const {
221+
std::lock_guard<std::mutex> lock(mutex_);
222+
223+
std::cout << "\n========================================" << std::endl;
224+
std::cout << " " << title << std::endl;
225+
std::cout << "========================================" << std::endl;
226+
std::cout << "Total calls: " << total_calls_ << std::endl;
227+
std::cout << "Successful: " << successful_calls_ << std::endl;
228+
std::cout << "Failed: " << failed_calls_ << std::endl;
229+
std::cout << "Success rate: " << std::fixed << std::setprecision(2)
230+
<< (total_calls_ > 0 ? (100.0 * successful_calls_ / total_calls_)
231+
: 0.0)
232+
<< "%" << std::endl;
233+
std::cout << "Total bytes: " << total_bytes_ << " ("
234+
<< (total_bytes_ / (1024.0 * 1024.0)) << " MB)" << std::endl;
235+
236+
if (!latencies_.empty()) {
237+
std::vector<double> sorted_latencies = latencies_;
238+
std::sort(sorted_latencies.begin(), sorted_latencies.end());
239+
240+
double sum = std::accumulate(sorted_latencies.begin(),
241+
sorted_latencies.end(), 0.0);
242+
double avg = sum / sorted_latencies.size();
243+
double min = sorted_latencies.front();
244+
double max = sorted_latencies.back();
245+
double p50 = sorted_latencies[sorted_latencies.size() * 50 / 100];
246+
double p95 = sorted_latencies[sorted_latencies.size() * 95 / 100];
247+
double p99 = sorted_latencies[sorted_latencies.size() * 99 / 100];
248+
249+
std::cout << "\nLatency (ms):" << std::endl;
250+
std::cout << " Min: " << min << std::endl;
251+
std::cout << " Avg: " << avg << std::endl;
252+
std::cout << " P50: " << p50 << std::endl;
253+
std::cout << " P95: " << p95 << std::endl;
254+
std::cout << " P99: " << p99 << std::endl;
255+
std::cout << " Max: " << max << std::endl;
256+
}
257+
258+
if (!error_counts_.empty()) {
259+
std::cout << "\nError breakdown:" << std::endl;
260+
for (const auto &pair : error_counts_) {
261+
std::cout << " " << pair.first << ": " << pair.second << std::endl;
262+
}
263+
}
264+
265+
std::cout << "========================================\n" << std::endl;
266+
}
267+
268+
int totalCalls() const {
269+
std::lock_guard<std::mutex> lock(mutex_);
270+
return total_calls_;
271+
}
272+
273+
int successfulCalls() const {
274+
std::lock_guard<std::mutex> lock(mutex_);
275+
return successful_calls_;
276+
}
277+
278+
int failedCalls() const {
279+
std::lock_guard<std::mutex> lock(mutex_);
280+
return failed_calls_;
281+
}
282+
283+
private:
284+
mutable std::mutex mutex_;
285+
int total_calls_ = 0;
286+
int successful_calls_ = 0;
287+
int failed_calls_ = 0;
288+
size_t total_bytes_ = 0;
289+
std::vector<double> latencies_;
290+
std::map<std::string, int> error_counts_;
291+
};
292+
293+
// =============================================================================
294+
// Base Test Fixture
295+
// =============================================================================
296+
297+
/**
298+
* Base test fixture that handles SDK initialization and configuration loading.
299+
*/
300+
class LiveKitTestBase : public ::testing::Test {
301+
protected:
302+
void SetUp() override {
303+
livekit::initialize(livekit::LogSink::kConsole);
304+
config_ = TestConfig::fromEnv();
305+
}
306+
307+
void TearDown() override { livekit::shutdown(); }
308+
309+
/// Skip the test if the required environment variables are not set
310+
void skipIfNotConfigured() {
311+
if (!config_.available) {
312+
GTEST_SKIP() << "LIVEKIT_URL, LIVEKIT_CALLER_TOKEN, and "
313+
"LIVEKIT_RECEIVER_TOKEN not set";
314+
}
315+
}
316+
317+
TestConfig config_;
318+
};
319+
320+
} // namespace test
321+
} // namespace livekit

0 commit comments

Comments
 (0)