mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
432 lines
12 KiB
Rust
432 lines
12 KiB
Rust
//! Concurrency tests for RtmpStreamHandler cache lock optimization.
|
|
//!
|
|
//! These tests verify:
|
|
//! - High concurrent access to video/audio/metadata caches
|
|
//! - No deadlocks under contention
|
|
//! - Correctness of parallel frame saving
|
|
|
|
#![allow(clippy::unwrap_used)]
|
|
use parking_lot::RwLock;
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
|
|
// Import the actual SplitCache from the library
|
|
use bytes::BytesMut;
|
|
use synctv_xiu::rtmp::cache::SplitCache;
|
|
use synctv_xiu::streamhub::define::FrameData;
|
|
|
|
const SPLIT_LOCK_READ_WRITE_ITERATIONS: usize = 1000;
|
|
const CONTENTION_ITERATIONS: usize = 5000;
|
|
const CONTENTION_THREADS: usize = 16;
|
|
const SPLIT_CACHE_CONCURRENT_SAVES_ITERATIONS: usize = 100;
|
|
const HIGH_CONTENTION_THREADS: usize = 8;
|
|
const HIGH_CONTENTION_ITERATIONS: usize = 200;
|
|
|
|
fn usize_to_u8(value: usize) -> u8 {
|
|
u8::try_from(value).expect("test value should fit in u8")
|
|
}
|
|
|
|
fn usize_to_u32(value: usize) -> u32 {
|
|
u32::try_from(value).expect("test value should fit in u32")
|
|
}
|
|
|
|
fn expect_metadata_frame(frame: Option<FrameData>) -> (u32, bytes::Bytes) {
|
|
match frame {
|
|
Some(FrameData::MetaData { timestamp, data }) => (timestamp, data),
|
|
other => panic!("expected metadata frame, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn touch_sim_cache_reads(cache: &SplitCacheSim) {
|
|
drop(cache.get_video());
|
|
drop(cache.get_audio());
|
|
}
|
|
|
|
fn touch_split_cache_reads(cache: &SplitCache) {
|
|
drop(cache.get_metadata());
|
|
drop(cache.get_video_seq());
|
|
drop(cache.get_audio_seq());
|
|
}
|
|
|
|
// Simulated Split Lock Architecture (for performance baseline)
|
|
|
|
/// Simulated split cache structure for benchmarking
|
|
struct SplitCacheSim {
|
|
video_seq: RwLock<(Vec<u8>, u32)>,
|
|
audio_seq: RwLock<(Vec<u8>, u32)>,
|
|
metadata: RwLock<(Vec<u8>, u32)>,
|
|
}
|
|
|
|
impl SplitCacheSim {
|
|
const fn new() -> Self {
|
|
Self {
|
|
video_seq: RwLock::new((Vec::new(), 0)),
|
|
audio_seq: RwLock::new((Vec::new(), 0)),
|
|
metadata: RwLock::new((Vec::new(), 0)),
|
|
}
|
|
}
|
|
|
|
fn save_video(&self, data: &[u8], ts: u32) {
|
|
let mut guard = self.video_seq.write();
|
|
*guard = (data.to_vec(), ts);
|
|
}
|
|
|
|
fn save_audio(&self, data: &[u8], ts: u32) {
|
|
let mut guard = self.audio_seq.write();
|
|
*guard = (data.to_vec(), ts);
|
|
}
|
|
|
|
fn save_metadata(&self, data: &[u8], ts: u32) {
|
|
let mut guard = self.metadata.write();
|
|
*guard = (data.to_vec(), ts);
|
|
}
|
|
|
|
fn get_video(&self) -> (Vec<u8>, u32) {
|
|
self.video_seq.read().clone()
|
|
}
|
|
|
|
fn get_audio(&self) -> (Vec<u8>, u32) {
|
|
self.audio_seq.read().clone()
|
|
}
|
|
}
|
|
|
|
/// Test concurrent read/write with split locks
|
|
#[test]
|
|
fn test_split_lock_concurrent_read_write() {
|
|
let cache = Arc::new(SplitCacheSim::new());
|
|
|
|
// Writer thread
|
|
let writer_cache = Arc::clone(&cache);
|
|
let writer = thread::spawn(move || {
|
|
for i in 0..SPLIT_LOCK_READ_WRITE_ITERATIONS {
|
|
let data = vec![1u8; 64];
|
|
writer_cache.save_video(&data, usize_to_u32(i));
|
|
}
|
|
});
|
|
|
|
// Reader threads
|
|
let reader_caches: Vec<_> = (0..4).map(|_| Arc::clone(&cache)).collect();
|
|
let readers: Vec<_> = reader_caches
|
|
.into_iter()
|
|
.map(|c| {
|
|
thread::spawn(move || {
|
|
let mut last_ts = 0u32;
|
|
for _ in 0..SPLIT_LOCK_READ_WRITE_ITERATIONS {
|
|
let (_, ts) = c.get_video();
|
|
// Timestamps should be monotonically increasing
|
|
// (or equal due to race conditions, which is fine)
|
|
assert!(ts >= last_ts || ts == 0);
|
|
last_ts = ts;
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
writer.join().unwrap();
|
|
for r in readers {
|
|
r.join().unwrap();
|
|
}
|
|
}
|
|
|
|
/// Test no deadlock under heavy contention
|
|
#[test]
|
|
fn test_no_deadlock_under_contention() {
|
|
let cache = Arc::new(SplitCacheSim::new());
|
|
|
|
let handles: Vec<_> = (0..CONTENTION_THREADS)
|
|
.map(|tid| {
|
|
let cache = Arc::clone(&cache);
|
|
thread::spawn(move || {
|
|
for i in 0..CONTENTION_ITERATIONS {
|
|
let data = vec![usize_to_u8(tid); 32];
|
|
// Interleave operations to increase contention
|
|
let ts = usize_to_u32(i);
|
|
cache.save_video(&data, ts);
|
|
cache.save_audio(&data, ts);
|
|
cache.save_metadata(&data, ts);
|
|
touch_sim_cache_reads(&cache);
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
// Set a timeout - if there's a deadlock, this will hang
|
|
let result = thread::spawn(move || {
|
|
for h in handles {
|
|
h.join().unwrap();
|
|
}
|
|
});
|
|
|
|
// Should complete within reasonable time
|
|
// 16 threads * 5000 iterations * 5 ops = 400,000 operations
|
|
// Even with contention, should complete in < 5 seconds
|
|
let timeout = Duration::from_secs(10);
|
|
let start = Instant::now();
|
|
loop {
|
|
if result.is_finished() {
|
|
break;
|
|
}
|
|
assert!(
|
|
start.elapsed() <= timeout,
|
|
"Deadlock detected: test did not complete within {timeout:?}"
|
|
);
|
|
thread::sleep(Duration::from_millis(100));
|
|
}
|
|
}
|
|
|
|
// Correctness Tests
|
|
|
|
/// Test that split cache maintains data integrity
|
|
#[test]
|
|
fn test_split_cache_data_integrity() {
|
|
let cache = Arc::new(SplitCacheSim::new());
|
|
|
|
let writer = Arc::clone(&cache);
|
|
let h1 = thread::spawn(move || {
|
|
for i in 0..100u32 {
|
|
let data = vec![0xAA; i as usize + 1];
|
|
writer.save_video(&data, i);
|
|
}
|
|
});
|
|
|
|
let writer = Arc::clone(&cache);
|
|
let h2 = thread::spawn(move || {
|
|
for i in 0..100u32 {
|
|
let data = vec![0xBB; i as usize + 1];
|
|
writer.save_audio(&data, i);
|
|
}
|
|
});
|
|
|
|
h1.join().unwrap();
|
|
h2.join().unwrap();
|
|
|
|
// Final state should have last written values
|
|
let (video_data, video_ts) = cache.get_video();
|
|
assert_eq!(video_data.len(), 100);
|
|
assert!(video_data.iter().all(|&b| b == 0xAA));
|
|
assert_eq!(video_ts, 99);
|
|
|
|
let (audio_data, audio_ts) = cache.get_audio();
|
|
assert_eq!(audio_data.len(), 100);
|
|
assert!(audio_data.iter().all(|&b| b == 0xBB));
|
|
assert_eq!(audio_ts, 99);
|
|
}
|
|
|
|
/// Test parking_lot RwLock read/write behavior
|
|
#[test]
|
|
fn test_parking_lot_rwlock_basic() {
|
|
let lock = RwLock::new(0u32);
|
|
|
|
// Multiple readers
|
|
{
|
|
let r1 = lock.read();
|
|
let r2 = lock.read();
|
|
assert_eq!(*r1, 0);
|
|
assert_eq!(*r2, 0);
|
|
}
|
|
|
|
// Writer
|
|
{
|
|
let mut w = lock.write();
|
|
*w = 42;
|
|
}
|
|
|
|
// Verify write
|
|
{
|
|
let r = lock.read();
|
|
assert_eq!(*r, 42);
|
|
}
|
|
}
|
|
|
|
/// Test RwLock upgrade/downgrade semantics
|
|
#[test]
|
|
fn test_rwlock_read_write_alternation() {
|
|
let cache = SplitCacheSim::new();
|
|
|
|
// Write then read
|
|
cache.save_video(&[1, 2, 3], 100);
|
|
let (data, ts) = cache.get_video();
|
|
assert_eq!(data, vec![1, 2, 3]);
|
|
assert_eq!(ts, 100);
|
|
|
|
// Overwrite
|
|
cache.save_video(&[4, 5, 6], 200);
|
|
let (data, ts) = cache.get_video();
|
|
assert_eq!(data, vec![4, 5, 6]);
|
|
assert_eq!(ts, 200);
|
|
}
|
|
|
|
// Real SplitCache Tests
|
|
|
|
/// Test SplitCache metadata operations
|
|
#[test]
|
|
fn test_split_cache_metadata() {
|
|
let cache = SplitCache::new(5, None, None);
|
|
|
|
// Initially no metadata
|
|
assert!(cache.get_metadata().is_none());
|
|
|
|
// Save valid RTMP metadata (AMF0 format: "onMetaData" + ECMA array)
|
|
// This is a simplified metadata: string marker + length + "onMetaData"
|
|
let mut data = BytesMut::new();
|
|
data.extend_from_slice(&[0x02]); // AMF0 string marker
|
|
data.extend_from_slice(&[0x00, 0x0a]); // length 10
|
|
data.extend_from_slice(b"onMetaData");
|
|
let data = data.freeze();
|
|
cache.save_metadata(&data, 1000);
|
|
|
|
// Retrieve metadata
|
|
let (timestamp, _) = expect_metadata_frame(cache.get_metadata());
|
|
assert_eq!(timestamp, 1000);
|
|
}
|
|
|
|
/// Test SplitCache video sequence header operations
|
|
#[test]
|
|
fn test_split_cache_video_seq() {
|
|
let cache = SplitCache::new(5, None, None);
|
|
|
|
// Initially no video sequence
|
|
assert!(cache.get_video_seq().is_none());
|
|
|
|
// Note: This is a minimal test - real H264 sequence headers are more complex
|
|
let data = BytesMut::new().freeze();
|
|
assert!(
|
|
cache.save_video_data(&data, 0).is_err(),
|
|
"empty video data should fail header parsing"
|
|
);
|
|
|
|
// After saving video data, video_seq should still be None for non-sequence data
|
|
assert!(cache.get_video_seq().is_none());
|
|
}
|
|
|
|
/// Test SplitCache audio sequence header operations
|
|
#[test]
|
|
fn test_split_cache_audio_seq() {
|
|
let cache = SplitCache::new(5, None, None);
|
|
|
|
// Initially no audio sequence
|
|
assert!(cache.get_audio_seq().is_none());
|
|
|
|
let data = BytesMut::new().freeze();
|
|
assert!(
|
|
cache.save_audio_data(&data, 0).is_err(),
|
|
"empty audio data should fail header parsing"
|
|
);
|
|
|
|
// After saving audio data, audio_seq should still be None for non-sequence data
|
|
assert!(cache.get_audio_seq().is_none());
|
|
}
|
|
|
|
/// Test SplitCache GOP operations
|
|
#[test]
|
|
fn test_split_cache_gops() {
|
|
let cache = SplitCache::new(3, None, None);
|
|
|
|
// Initially no GOPs (disabled when gop_num is 0)
|
|
let cache_disabled = SplitCache::new(0, None, None);
|
|
assert!(cache_disabled.get_gops_data().is_none());
|
|
|
|
// With GOP enabled, should return some data even if empty
|
|
let gops = cache.get_gops_data();
|
|
assert!(gops.is_some());
|
|
}
|
|
|
|
/// Test concurrent SplitCache video/audio saves
|
|
#[test]
|
|
fn test_split_cache_concurrent_saves() {
|
|
let cache = Arc::new(SplitCache::new(5, None, None));
|
|
|
|
// Writer threads
|
|
let cache_video = Arc::clone(&cache);
|
|
let h1 = thread::spawn(move || {
|
|
for i in 0..SPLIT_CACHE_CONCURRENT_SAVES_ITERATIONS {
|
|
let mut data = BytesMut::new();
|
|
data.extend_from_slice(&[usize_to_u8(i); 64]);
|
|
let data = data.freeze();
|
|
let _result = cache_video.save_video_data(&data, usize_to_u32(i));
|
|
}
|
|
});
|
|
|
|
let cache_audio = Arc::clone(&cache);
|
|
let h2 = thread::spawn(move || {
|
|
for i in 0..SPLIT_CACHE_CONCURRENT_SAVES_ITERATIONS {
|
|
let mut data = BytesMut::new();
|
|
data.extend_from_slice(&[usize_to_u8(i); 64]);
|
|
let data = data.freeze();
|
|
let _result = cache_audio.save_audio_data(&data, usize_to_u32(i));
|
|
}
|
|
});
|
|
|
|
// Reader thread - should not block writers
|
|
let cache_reader = Arc::clone(&cache);
|
|
let h3 = thread::spawn(move || {
|
|
for _ in 0..SPLIT_CACHE_CONCURRENT_SAVES_ITERATIONS {
|
|
touch_split_cache_reads(&cache_reader);
|
|
}
|
|
});
|
|
|
|
h1.join().unwrap();
|
|
h2.join().unwrap();
|
|
h3.join().unwrap();
|
|
|
|
// Verify GOPs have data
|
|
let gops = cache.get_gops_data();
|
|
assert!(gops.is_some());
|
|
// Should have frames from both video and audio
|
|
let total_frames: usize = gops
|
|
.unwrap()
|
|
.iter()
|
|
.map(synctv_xiu::rtmp::cache::gop::Gop::len)
|
|
.sum();
|
|
assert!(total_frames > 0, "Should have saved frames");
|
|
}
|
|
|
|
/// Test SplitCache under high contention
|
|
#[test]
|
|
fn test_split_cache_high_contention() {
|
|
let cache = Arc::new(SplitCache::new(10, None, None));
|
|
|
|
let handles: Vec<_> = (0..HIGH_CONTENTION_THREADS)
|
|
.map(|tid| {
|
|
let cache = Arc::clone(&cache);
|
|
thread::spawn(move || {
|
|
for i in 0..HIGH_CONTENTION_ITERATIONS {
|
|
let mut data = BytesMut::new();
|
|
data.extend_from_slice(&[usize_to_u8(tid), usize_to_u8(i)]);
|
|
let data = data.freeze();
|
|
let ts = usize_to_u32(i);
|
|
|
|
let _result = match tid % 3 {
|
|
0 => cache.save_video_data(&data, ts),
|
|
1 => cache.save_audio_data(&data, ts),
|
|
_ => {
|
|
cache.save_metadata(&data, ts);
|
|
Ok(())
|
|
}
|
|
};
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
// Set a timeout - if there's a deadlock, this will hang
|
|
let timeout = Duration::from_secs(10);
|
|
let start = Instant::now();
|
|
|
|
for h in handles {
|
|
while !h.is_finished() {
|
|
assert!(
|
|
start.elapsed() <= timeout,
|
|
"Deadlock detected in SplitCache"
|
|
);
|
|
thread::sleep(Duration::from_millis(10));
|
|
}
|
|
h.join().unwrap();
|
|
}
|
|
|
|
touch_split_cache_reads(&cache);
|
|
let gops = cache.get_gops_data();
|
|
assert!(gops.is_some());
|
|
}
|