Skip to content
This repository was archived by the owner on Sep 10, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions loadtest/scripts/collect-server-baseline.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "usage: $0 OUTPUT_DIR DURATION_SECONDS [INTERVAL_SECONDS]" >&2
if [[ $# -lt 2 || $# -gt 4 ]]; then
echo "usage: $0 OUTPUT_DIR DURATION_SECONDS [INTERVAL_SECONDS] [ENABLE_PY_SPY]" >&2
exit 2
fi

output_dir=$1
duration_seconds=$2
interval_seconds=${3:-1}
enable_py_spy=${4:-true}

if [[ "$enable_py_spy" != "true" && "$enable_py_spy" != "false" ]]; then
echo "ENABLE_PY_SPY must be true or false" >&2
exit 2
fi

mkdir -p "$output_dir"

Expand Down Expand Up @@ -43,8 +49,11 @@ echo "timestamp,active_sessions,queue_size,received_real,processed_real,dropped_

pidstat -h -r -u -p "$server_pid" "$interval_seconds" "$duration_seconds" > "$output_dir/pidstat.txt" &
pidstat_pid=$!
/opt/inno-live-venv/bin/py-spy record --pid "$server_pid" --duration "$duration_seconds" --rate 50 --format speedscope --output "$output_dir/py-spy-speedscope.json" &
py_spy_pid=$!
py_spy_pid=""
if [[ "$enable_py_spy" == "true" ]]; then
/opt/inno-live-venv/bin/py-spy record --pid "$server_pid" --duration "$duration_seconds" --rate 50 --format speedscope --output "$output_dir/py-spy-speedscope.json" &
py_spy_pid=$!
fi

samples=$((duration_seconds / interval_seconds))
for ((sample = 0; sample < samples; sample++)); do
Expand All @@ -65,7 +74,9 @@ for ((sample = 0; sample < samples; sample++)); do
done

wait "$pidstat_pid"
wait "$py_spy_pid"
if [[ -n "$py_spy_pid" ]]; then
wait "$py_spy_pid"
fi
curl --fail --silent http://127.0.0.1:8000/health > "$output_dir/health-final.json"
curl --fail --silent http://127.0.0.1:8000/sessions > "$output_dir/sessions-final.json"
curl --fail --silent http://127.0.0.1:8000/metrics > "$output_dir/metrics-final.prom"
141 changes: 141 additions & 0 deletions loadtest/windows/collect-server-metrics.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
param(
[Parameter(Mandatory = $true)]
[string]$OutputDirectory,

[int]$DurationSeconds = 360,
[int]$IntervalSeconds = 1,
[int]$WaitTimeoutSeconds = 600,
[ValidateSet("bypass", "fixed_delay", "real")]
[string]$Mode = "fixed_delay",
[string]$BaseUrl = "http://127.0.0.1:8000"
)

$ErrorActionPreference = "Stop"

function Get-MetricsText {
(Invoke-WebRequest -UseBasicParsing -Uri "$BaseUrl/metrics" -TimeoutSec 5).Content
}

function Get-MetricValue {
param(
[string]$Text,
[string]$Name,
[string]$Labels = "",
[switch]$ZeroIfMissing
)

$escapedName = [regex]::Escape($Name)
$escapedLabels = [regex]::Escape($Labels)
$pattern = if ($Labels) {
"(?m)^${escapedName}\{${escapedLabels}\} ([0-9.eE+-]+)$"
} else {
"(?m)^${escapedName} ([0-9.eE+-]+)$"
}

$match = [regex]::Match($Text, $pattern)
if ($match.Success) {
return [double]$match.Groups[1].Value
}
if ($ZeroIfMissing) {
return 0.0
}

$metric = if ($Labels) { "${Name}{${Labels}}" } else { $Name }
throw "Required Prometheus metric not found: $metric"
}

function Assert-MetricFamily {
param(
[string]$Text,
[string]$Name
)

$escapedName = [regex]::Escape($Name)
if (-not [regex]::IsMatch($Text, "(?m)^# TYPE ${escapedName} ")) {
throw "Required Prometheus metric family not found: $Name"
}
}

New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
$initialMetricsPath = Join-Path $OutputDirectory "metrics-initial.prom"
$finalMetricsPath = Join-Path $OutputDirectory "metrics-final.prom"
$samplesPath = Join-Path $OutputDirectory "app.csv"
$modeLabels = "mode=`"$Mode`""

$initialMetrics = Get-MetricsText
$requiredMetricFamilies = @(
"innolive_active_sessions",
"innolive_processing_queue_size",
"innolive_frame_received_total",
"innolive_frame_processed_total",
"innolive_frame_dropped_total",
"innolive_frame_failures_total",
"process_cpu_seconds_total",
"process_resident_memory_bytes"
)
foreach ($metricName in $requiredMetricFamilies) {
Assert-MetricFamily -Text $initialMetrics -Name $metricName
}
$initialMetrics | Set-Content -Encoding utf8 -Path $initialMetricsPath
(Invoke-WebRequest -UseBasicParsing -Uri "$BaseUrl/health" -TimeoutSec 5).Content |
Set-Content -Encoding utf8 -Path (Join-Path $OutputDirectory "health-initial.json")

"timestamp,active_sessions,queue_size,received_$Mode,processed_$Mode,dropped_$Mode,failures_$Mode,process_cpu_seconds,process_rss_bytes" |
Set-Content -Encoding utf8 -Path $samplesPath

Write-Host "Waiting for an active WebRTC session (timeout: ${WaitTimeoutSeconds}s)"
$deadline = (Get-Date).AddSeconds($WaitTimeoutSeconds)
do {
$metrics = Get-MetricsText
$active = Get-MetricValue -Text $metrics -Name "innolive_active_sessions"
if ($active -gt 0) {
break
}
if ((Get-Date) -ge $deadline) {
throw "Timed out waiting for an active WebRTC session"
}
Start-Sleep -Seconds 1
} while ($true)

Write-Host "Active session detected; collecting for ${DurationSeconds}s"
$metricDeadline = (Get-Date).AddSeconds(10)
do {
$metrics = Get-MetricsText
try {
$null = Get-MetricValue -Text $metrics -Name "innolive_frame_received_total" -Labels $modeLabels
$null = Get-MetricValue -Text $metrics -Name "innolive_frame_processed_total" -Labels $modeLabels
break
} catch {
if ((Get-Date) -ge $metricDeadline) {
throw
}
Start-Sleep -Seconds 1
}
} while ($true)

$end = (Get-Date).AddSeconds($DurationSeconds)
do {
$metrics = Get-MetricsText
$timestamp = (Get-Date).ToString("o")
$active = Get-MetricValue -Text $metrics -Name "innolive_active_sessions"
$queue = Get-MetricValue -Text $metrics -Name "innolive_processing_queue_size"
$received = Get-MetricValue -Text $metrics -Name "innolive_frame_received_total" -Labels $modeLabels
$processed = Get-MetricValue -Text $metrics -Name "innolive_frame_processed_total" -Labels $modeLabels
$dropped = Get-MetricValue -Text $metrics -Name "innolive_frame_dropped_total" -Labels $modeLabels -ZeroIfMissing
$failures = Get-MetricValue -Text $metrics -Name "innolive_frame_failures_total" -Labels $modeLabels -ZeroIfMissing
$cpu = Get-MetricValue -Text $metrics -Name "process_cpu_seconds_total"
$rss = Get-MetricValue -Text $metrics -Name "process_resident_memory_bytes"

"$timestamp,$active,$queue,$received,$processed,$dropped,$failures,$cpu,$rss" |
Add-Content -Encoding utf8 -Path $samplesPath
Start-Sleep -Seconds $IntervalSeconds
} while ((Get-Date) -lt $end)

$finalMetrics = Get-MetricsText
$finalMetrics | Set-Content -Encoding utf8 -Path $finalMetricsPath
(Invoke-WebRequest -UseBasicParsing -Uri "$BaseUrl/health" -TimeoutSec 5).Content |
Set-Content -Encoding utf8 -Path (Join-Path $OutputDirectory "health-final.json")
(Invoke-WebRequest -UseBasicParsing -Uri "$BaseUrl/sessions" -TimeoutSec 5).Content |
Set-Content -Encoding utf8 -Path (Join-Path $OutputDirectory "sessions-final.json")

Write-Host "Server metrics saved to $OutputDirectory"
49 changes: 49 additions & 0 deletions test-docs/results/fixed-delay-20ms/1-session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Python fixed_delay 20ms — 1세션

## 대표 상세 실행 정보

- 클라이언트 원본: `baseline-fixed-delay-20ms-5m.json` / `.csv`
- 원본 검증값: [artifact-manifest.md](artifact-manifest.md)
- 시작: 2026-07-16 22:29:54 KST
- 종료: 2026-07-16 22:34:59 KST
- 전체 실행 시간: 305.48초
- 입력: `720p30.ivf` (1280×720, VP8, 30FPS)
- 안정 구간: 최초 30초 이후 표본
- 결과: **대표 실행 통합 기준 통과**

## 전체 반복 실행 요약

| 실행 | 시작 | 평균 수신 FPS | 평균 프레임 차이 | RTT p95 최대 | 최종 연결 | 클라이언트 판정 |
| --- | --- | ---: | ---: | ---: | ---: | --- |
| `baseline-fixed-delay-20ms-5m` | 07-16 22:29:54 | 29.61 | 1.31% | 32.71ms | 1/1 | 통과 |
| `fixed-delay-20ms-stateful-1-after-disconnect-5m` | 07-17 00:08:03 | 29.75 | 0.77% | 18.95ms | 1/1 | 통과 |

- 전체 성공률: 2/2 (100%)
- 성공 실행 평균 수신 FPS: 29.68
- 성공 실행 평균 프레임 차이: 1.04%
- 실행별 RTT p95 최댓값 평균: 25.83ms

## 클라이언트 처리량

| 클라이언트 | 평균 수신 FPS | 수신 FPS p5 | 0 제외 최소 FPS | 0 표본 | 송신 프레임 | 수신 프레임 | 프레임 차이 | 손실 RTP 패킷 | RTP 손실률 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 29.61 | 28.20 | 24.20 | 0 | 8,999 | 8,881 | 1.31% | 0 | 0.000% |

## 클라이언트 네트워크 및 연결 상태

| 클라이언트 | jitter 평균/p95/최대 | RTT 평균/p95/최대 | 최종 PeerConnection / ICE |
| ---: | ---: | ---: | --- |
| 1 | 3.41 / 5.84 / 20.55ms | 15.88 / 32.71 / 150.99ms | `connected` / `connected` |

## 서버 수집 결과

연결 설정과 종료 정리를 포함해 324초 동안 서버 지표를 수집했습니다. 수신·처리
프레임은 각각 8,882개, stale drop은 110개(1.24%), 처리 오류는 0건이었고 처리 큐
최댓값은 0이었습니다. 전체 수집 구간의 CPU 사용량은 약 0.74코어, 종료 시 RSS는
811.19MiB였으며 최종 세션 목록은 비어 있었습니다. 연결 설정과 종료가 포함된 전체
구간 FPS는 최초 30초를 제외한 합격 판정에는 사용하지 않았습니다.

## 판정

평균 수신 FPS가 27을 넘고 프레임 차이가 10% 미만이었습니다. 연결이 끝까지
유지됐으며 서버 처리 오류와 큐 증가도 없었습니다.
50 changes: 50 additions & 0 deletions test-docs/results/fixed-delay-20ms/2-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Python fixed_delay 20ms — 2세션

## 대표 상세 실행 정보

- 클라이언트 원본: `fixed-delay-20ms-2-clients-5m.json` / `.csv`
- 원본 검증값: [artifact-manifest.md](artifact-manifest.md)
- 시작: 2026-07-16 22:39:35 KST
- 종료: 2026-07-16 22:44:46 KST
- 전체 실행 시간: 310.63초
- 입력: `720p30.ivf` (1280×720, VP8, 30FPS)
- 안정 구간: 최초 30초 이후 표본
- 결과: **통합 기준 통과**

## 전체 반복 실행 요약

| 실행 | 시작 | 평균 수신 FPS | 최저 세션 평균 FPS | 평균/최대 프레임 차이 | RTT p95 최대 | 최종 연결 | 클라이언트 판정 |
| --- | --- | ---: | ---: | ---: | ---: | ---: | --- |
| `fixed-delay-20ms-2-clients-5m` | 07-16 22:39:35 | 29.57 | 29.56 | 1.48% / 1.53% | 34.93ms | 2/2 | 통과 |

- 전체 성공률: 1/1 (100%)
- 성공 실행 평균 수신 FPS: 29.57
- 성공 실행 평균 프레임 차이: 1.48%
- 반복 실행이 1건이므로 반복성은 아직 확정하지 않습니다.

## 클라이언트 처리량

| 클라이언트 | 평균 수신 FPS | 수신 FPS p5 | 0 제외 최소 FPS | 0 표본 | 송신 프레임 | 수신 프레임 | 프레임 차이 | 손실 RTP 패킷 | RTP 손실률 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 29.57 | 28.80 | 26.60 | 0 | 9,158 | 9,027 | 1.43% | 0 | 0.000% |
| 2 | 29.56 | 27.40 | 26.40 | 0 | 8,999 | 8,861 | 1.53% | 0 | 0.000% |

## 클라이언트 네트워크 및 연결 상태

| 클라이언트 | jitter 평균/p95/최대 | RTT 평균/p95/최대 | 최종 PeerConnection / ICE |
| ---: | ---: | ---: | --- |
| 1 | 3.68 / 9.48 / 13.24ms | 14.01 / 34.93 / 81.63ms | `connected` / `connected` |
| 2 | 4.09 / 12.51 / 23.98ms | 11.69 / 26.09 / 165.37ms | `connected` / `connected` |

## 서버 수집 결과

326초 동안 서버 지표를 수집했습니다. 서버 수신 17,890프레임, 처리 17,889프레임,
stale drop 256개(1.43%), 처리 오류 0건, 처리 큐 최댓값 0을 기록했습니다. 전체 수집
구간의 CPU 사용량은 약 1.06코어였고 종료 시 RSS는 932.81MiB였습니다. 최종 세션
목록은 비어 있었습니다. 연결 설정과 종료가 포함되므로 전체 구간 FPS는 참고값으로만
사용합니다.

## 판정

두 세션 모두 평균 수신 27FPS를 넘고 연결을 유지했으며 최대 프레임 차이는 1.53%로
10% 기준을 충분히 만족했습니다.
56 changes: 56 additions & 0 deletions test-docs/results/fixed-delay-20ms/4-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Python fixed_delay 20ms — 4세션

## 대표 상세 실행 정보

- 클라이언트 원본: `fixed-delay-20ms-4-clients-5m.json` / `.csv`
- 원본 검증값: [artifact-manifest.md](artifact-manifest.md)
- 시작: 2026-07-16 22:49:14 KST
- 종료: 2026-07-16 22:54:35 KST
- 전체 실행 시간: 321.27초
- 입력: `720p30.ivf` (1280×720, VP8, 30FPS)
- 안정 구간: 최초 30초 이후 표본
- 결과: **대표 실행 통합 기준 통과**

## 전체 반복 실행 요약

| 실행 | 시작 | 평균 수신 FPS | 최저 세션 평균 FPS | 평균/최대 프레임 차이 | RTT p95 최대 | 최종 연결 | 클라이언트 판정 |
| --- | --- | ---: | ---: | ---: | ---: | ---: | --- |
| `fixed-delay-20ms-4-clients-5m` | 07-16 22:49:14 | 29.06 | 28.91 | 3.18% / 3.66% | 96.76ms | 4/4 | 통과 |
| `fixed-delay-20ms-stateful-4-after-disconnect-5m` | 07-17 00:16:09 | 29.62 | 29.48 | 1.25% / 1.69% | 58.98ms | 4/4 | 통과 |

- 전체 성공률: 2/2 (100%)
- 성공 실행 평균 수신 FPS: 29.34
- 성공 실행 평균 프레임 차이: 2.22%
- 실행별 최대 프레임 차이 평균: 2.67%
- 실행별 RTT p95 최댓값 평균: 77.87ms

## 클라이언트 처리량

| 클라이언트 | 평균 수신 FPS | 수신 FPS p5 | 0 제외 최소 FPS | 0 표본 | 송신 프레임 | 수신 프레임 | 프레임 차이 | 손실 RTP 패킷 | RTP 손실률 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 29.18 | 28.20 | 26.40 | 0 | 9,477 | 9,212 | 2.80% | 0 | 0.000% |
| 2 | 29.14 | 27.60 | 25.60 | 0 | 9,318 | 9,045 | 2.93% | 0 | 0.000% |
| 3 | 29.00 | 26.80 | 24.80 | 0 | 9,159 | 8,852 | 3.35% | 0 | 0.000% |
| 4 | 28.91 | 26.60 | 22.60 | 0 | 8,999 | 8,670 | 3.66% | 0 | 0.000% |

## 클라이언트 네트워크 및 연결 상태

| 클라이언트 | jitter 평균/p95/최대 | RTT 평균/p95/최대 | 최종 PeerConnection / ICE |
| ---: | ---: | ---: | --- |
| 1 | 4.46 / 10.54 / 17.32ms | 21.53 / 59.62 / 196.69ms | `connected` / `connected` |
| 2 | 4.03 / 8.41 / 15.97ms | 21.29 / 96.76 / 111.40ms | `connected` / `connected` |
| 3 | 4.60 / 9.51 / 16.89ms | 24.17 / 92.93 / 244.00ms | `connected` / `connected` |
| 4 | 4.39 / 8.54 / 12.47ms | 20.30 / 66.82 / 153.63ms | `connected` / `connected` |

## 서버 수집 결과

362초 동안 서버 지표를 수집했습니다. 서버 수신·처리 프레임은 각각 35,783개,
stale drop은 1,141개(3.19%), 처리 오류는 0건이었고 처리 큐 최댓값은 0이었습니다.
전체 수집 구간의 CPU 사용량은 약 1.83코어, 종료 시 RSS는 1,129.31MiB였습니다.
최종 세션 목록은 비어 있었습니다. 연결 설정과 종료가 포함된 전체 구간 FPS는 안정
구간 판정값이 아닌 참고값입니다.

## 판정

네 세션 모두 평균 수신 27FPS를 넘었습니다. 최대 프레임 차이는 3.66%였고 모든
PeerConnection과 ICE 상태가 `connected`로 유지됐습니다.
49 changes: 49 additions & 0 deletions test-docs/results/fixed-delay-20ms/5-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Python fixed_delay 20ms — 5세션

## 시험 정보

- 클라이언트 원본: `fixed-delay-20ms-stateful-5-boundary-5m.json` / `.csv`
- 원본 검증값: [artifact-manifest.md](artifact-manifest.md)
- 시작: 2026-07-17 00:30:55 KST
- 종료: 2026-07-17 00:36:21 KST
- 전체 실행 시간: 326.49초
- 입력: `720p30.ivf` (1280×720, VP8, 30FPS)
- 안정 구간: 최초 30초 이후 표본
- 결과: **실패**

## 전체 반복 실행 요약

| 실행 | 평균 수신 FPS | 최저 세션 평균 FPS | 평균/최대 프레임 차이 | RTT p95 최대 | 최종 연결 | 클라이언트 판정 |
| --- | ---: | ---: | ---: | ---: | ---: | --- |
| `fixed-delay-20ms-stateful-5-boundary-5m` | 23.49 | 20.46 | 20.84% / 30.03% | 1,404.51ms | 4/5 | 실패 |

- 전체 성공률: 0/1 (0%)
- 성공 실행 평균: 성공 실행이 없어 계산하지 않음
- 단일 실패 실행만 있으므로 서버의 5세션 고정 용량 경계로 확정하지 않음

## 클라이언트 처리량

| 클라이언트 | 평균 수신 FPS | 수신 FPS p5 | 0 제외 최소 FPS | 0 표본 | 송신 프레임 | 수신 프레임 | 프레임 차이 | 손실 RTP 패킷 | RTP 손실률 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 24.62 | 7.40 | 5.60 | 0 | 9,635 | 7,920 | 17.80% | 2 | 0.011% |
| 2 | 20.46 | 0.00 | 4.00 | 14 | 9,476 | 6,630 | 30.03% | 0 | 0.000% |
| 3 | 23.93 | 6.60 | 4.60 | 0 | 9,318 | 7,461 | 19.93% | 5 | 0.027% |
| 4 | 24.39 | 7.60 | 5.80 | 0 | 9,159 | 7,479 | 18.34% | 5 | 0.028% |
| 5 | 24.06 | 6.20 | 5.00 | 1 | 8,999 | 7,371 | 18.09% | 3 | 0.017% |

## 클라이언트 네트워크 및 연결 상태

| 클라이언트 | jitter 평균/p95/최대 | RTT 평균/p95/최대 | 최종 PeerConnection / ICE |
| ---: | ---: | ---: | --- |
| 1 | 5.96 / 16.13 / 21.44ms | 246.80 / 1,350.62 / 1,478.47ms | `connected` / `connected` |
| 2 | 6.31 / 11.78 / 17.42ms | 421.75 / 1,304.28 / 1,473.51ms | `failed` / `failed` |
| 3 | 5.47 / 15.27 / 23.32ms | 266.83 / 1,394.26 / 1,698.52ms | `connected` / `connected` |
| 4 | 6.05 / 16.26 / 28.17ms | 249.54 / 1,404.51 / 1,587.83ms | `connected` / `connected` |
| 5 | 5.74 / 16.25 / 19.25ms | 240.66 / 1,389.36 / 1,421.40ms | `connected` / `connected` |

## 판정

모든 세션의 평균 FPS와 프레임 차이가 기준을 위반했고 RTT도 초 단위로 증가했습니다.
이후 같은 서버에서 6·7·8세션이 통과했으므로 이 결과는 서버의 단조로운 용량 한계가
아니라 당시 공인 UDP/NAT 경로 이상 사례로 분류합니다. 실패 결과 자체는 전체 반복
성공률과 네트워크 이상 이력에 포함합니다.
Loading
Loading