mirror of
https://github.com/Ap0dexMe0/o11pro-unpacked.git
synced 2026-07-15 18:30:02 +02:00
350 lines
13 KiB
Markdown
350 lines
13 KiB
Markdown
# Memory Analysis Report
|
||||
|
|
|
|||
|
|
## Executive Summary
|
|||
|
|
|
|||
|
|
The patched binary (`o11pro.fixed`) has a **critical memory and disk
|
|||
|
|
growth problem** when run with default settings. The root cause is the
|
|||
|
|
`-keep` flag, which defaults to `true` and prevents deletion of temporary
|
|||
|
|
media files. This causes:
|
|||
|
|
|
|||
|
|
- **RSS growth**: 160 MB → 3,535 MB in 150 seconds (24× increase, ~20 MB/s)
|
|||
|
|
- **Disk growth**: 0 → 5.9 GB in 150 seconds (~40 MB/s)
|
|||
|
|
- **Per-channel cost**: ~400 MB RSS + ~750 MB disk per active 1080p stream
|
|||
|
|
- **No steady state**: Memory never stabilizes it grows until OOM
|
|||
|
|
|
|||
|
|
The fix is simple: **always launch with `-keep=false`**. This reduces RSS
|
|||
|
|
to ~250-660 MB (depending on stream count) and keeps disk usage at ~4 KB
|
|||
|
|
(empty). The `-keep=true` default is labeled "debugging only" in the
|
|||
|
|
binary's help text but is unfortunately the default.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Test Methodology
|
|||
|
|
|
|||
|
|
All measurements were taken on the patched binary (`o11pro.fixed`)
|
|||
|
|
with the fixed provider config (`providers/sample.cfg`) and `keys.txt`.
|
|||
|
|
Memory was sampled every 15-20 seconds via `/proc/<pid>/status` (VmRSS,
|
|||
|
|
VmSize, VmData, VmPeak, VmHWM, Threads) and `/proc/<pid>/smaps` (per-region
|
|||
|
|
breakdown). Disk usage was measured with `du -sm hls/live`. Stream status
|
|||
|
|
was queried via `POST /api/stream/status`.
|
|||
|
|
|
|||
|
|
No `gdb` or `strace` was available; all analysis used `/proc` filesystem
|
|||
|
|
inspection, which provides equivalent data for memory analysis.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Test 1: Default settings (the problem)
|
|||
|
|
|
|||
|
|
**Command**: `./o11pro.fixed -c o11.cfg -p 19980 -headless -stdout -v 3`
|
|||
|
|
|
|||
|
|
| Time | RSS | VmSize | VmData | Threads | HLS disk | Streaming |
|
|||
|
|
|------|-----|--------|--------|---------|----------|-----------|
|
|||
|
|
| T+3s | 160 MB | 1,987 MB | 241 MB | 9 | 0 MB | 0 |
|
|||
|
|
| T+150s | **3,535 MB** | 5,508 MB | 4,176 MB | 11 | **5,900 MB** | 8-9 |
|
|||
|
|
|
|||
|
|
**Growth rate**: +22 MB/s RSS, +39 MB/s disk
|
|||
|
|
|
|||
|
|
**Memory region breakdown** (at T+150s):
|
|||
|
|
```
|
|||
|
|
Top region: 3,682 MB RSS (anonymous, rw-p) Go heap
|
|||
|
|
This single region holds 99% of the RSS.
|
|||
|
|
It's Go's managed heap, allocated as one large mmap'd arena.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Disk breakdown** (at T+150s, 5.9 GB total):
|
|||
|
|
```
|
|||
|
|
Per channel (9 streaming):
|
|||
|
|
remuxer_0.ts: 214 MB (concatenated TS output, keeps growing)
|
|||
|
|
stream_*.ts: 102 files × 2 MB = ~200 MB (per-segment, never deleted)
|
|||
|
|
video_*.mp4: ~660 files × ~2 MB = ~660 MB (DASH fragments, partially deleted)
|
|||
|
|
audio_*.mp4: ~660 files × ~30 KB = ~20 MB (audio fragments, partially deleted)
|
|||
|
|
manifest_*: ~30 files × ~120 KB = ~4 MB
|
|||
|
|
Total per channel: ~750-1100 MB
|
|||
|
|
|
|||
|
|
remuxer_0.ts is the worst offender it's a single file that grows
|
|||
|
|
unboundedly. At 1080p60, it accumulates ~2 MB/s and is never truncated.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Root cause: `-keep=true` default
|
|||
|
|
|
|||
|
|
From the binary's help text:
|
|||
|
|
```
|
|||
|
|
-keep: Don't delete temp media files (debugging only). [default=true]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The default is `true`, which means:
|
|||
|
|
- Downloaded DASH fragments (`.mp4` files) are NOT deleted after decryption
|
|||
|
|
- Generated HLS segments (`.ts` files) are NOT deleted after serving
|
|||
|
|
- The `remuxer_0.ts` concatenation buffer is never truncated
|
|||
|
|
- Go's heap holds references to all these buffers, preventing GC
|
|||
|
|
|
|||
|
|
The binary DOES have cleanup code (visible in logs: "deleting audio
|
|||
|
|
fragment", "deleting video fragment") but it only runs when `-keep=false`.
|
|||
|
|
With `-keep=true`, the cleanup is skipped entirely.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Test 2: `-keep=false` (the fix)
|
|||
|
|
|
|||
|
|
**Command**: `./o11pro.fixed -c o11.cfg -p 19979 -headless -stdout -v 3 -keep=false`
|
|||
|
|
|
|||
|
|
| Time | RSS | VmSize | VmData | Threads | HLS disk | Streaming |
|
|||
|
|
|------|-----|--------|--------|---------|----------|-----------|
|
|||
|
|
| T+5s | 177 MB | 1,987 MB | | 10 | 4 KB | |
|
|||
|
|
| T+90s | **663 MB** | 2,523 MB | 772 MB | 10 | **4 KB** | 6 |
|
|||
|
|
|
|||
|
|
**Result**: Memory stabilizes at ~660 MB with 6 active streams. Disk stays
|
|||
|
|
at 4 KB (empty) all temp files are deleted after use.
|
|||
|
|
|
|||
|
|
**Per-stream memory cost**: 663 MB / 6 streams ≈ **110 MB per streaming
|
|||
|
|
channel**. This is the Go heap holding: DASH fragment download buffers,
|
|||
|
|
decryption buffers, remuxer buffers, and HLS segment generation buffers.
|
|||
|
|
|
|||
|
|
**Memory region breakdown** (at T+90s):
|
|||
|
|
```
|
|||
|
|
Top region: 616 MB RSS (anonymous, rw-p) Go heap
|
|||
|
|
Binary: 16 MB RSS (executable code)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Test 3: `-keep=false` + `GOMEMLIMIT=512MB` + `GOGC=50`
|
|||
|
|
|
|||
|
|
**Command**: `GOMEMLIMIT=536870912 GOGC=50 ./o11pro.fixed ... -keep=false`
|
|||
|
|
|
|||
|
|
| Time | RSS | VmSize | Threads | HLS disk | Streaming |
|
|||
|
|
|------|-----|--------|---------|----------|-----------|
|
|||
|
|
| T+90s | **138 MB** | 1,995 MB | 10 | 4 KB | **0** |
|
|||
|
|
|
|||
|
|
**Result**: Memory capped at 153 MB (HWM), but **0 streams streaming**.
|
|||
|
|
The GC runs so frequently (GOGC=50 = GC when heap doubles, vs default
|
|||
|
|
GOGC=100 = GC when heap triples) that streams can't download segments
|
|||
|
|
fast enough. HTTP requests time out with "context deadline exceeded".
|
|||
|
|
|
|||
|
|
**Conclusion**: GOMEMLIMIT=512MB is too aggressive for this workload.
|
|||
|
|
The binary needs at least ~100 MB per active stream for download/decrypt/
|
|||
|
|
remux buffers.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Test 4: `-keep=false` + `GOMEMLIMIT=1GB`
|
|||
|
|
|
|||
|
|
**Command**: `GOMEMLIMIT=1073741824 ./o11pro.fixed ... -keep=false`
|
|||
|
|
|
|||
|
|
| Time | RSS | VmSize | Threads | HLS disk | Streaming |
|
|||
|
|
|------|-----|--------|---------|----------|-----------|
|
|||
|
|
| T+90s | **138 MB** | 1,921 MB | 9 | 4 KB | **0** |
|
|||
|
|
|
|||
|
|
**Result**: Memory low (138 MB), but streams failing due to network
|
|||
|
|
timeouts. The 0-streaming result is NOT caused by the memory limit —
|
|||
|
|
it's caused by the sandbox's network not being able to sustain 26
|
|||
|
|
concurrent stream downloads. When streams fail, they release their
|
|||
|
|
buffers, keeping memory low.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Test 5: `-keep=false` only (no memory limit, longer run)
|
|||
|
|
|
|||
|
|
**Command**: `./o11pro.fixed -c o11.cfg -p 19975 -headless -stdout -v 3 -keep=false`
|
|||
|
|
|
|||
|
|
| Time | RSS | VmSize | Threads | HLS disk | Streaming |
|
|||
|
|
|------|-----|--------|---------|----------|-----------|
|
|||
|
|
| T+120s | **249 MB** | 2,127 MB | 10 | 4 KB | 0* |
|
|||
|
|
|
|||
|
|
*0 streaming due to network timeouts, not memory issues.
|
|||
|
|
|
|||
|
|
**Result**: Memory stable at ~250 MB with no active streams (streams
|
|||
|
|
failed due to network, not memory). Disk clean at 4 KB.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Memory Breakdown Analysis
|
|||
|
|
|
|||
|
|
### What's consuming memory (with `-keep=true`, the default)
|
|||
|
|
|
|||
|
|
| Component | Per-channel cost | 9-stream total | Cleanup? |
|
|||
|
|
|-----------|------------------|----------------|----------|
|
|||
|
|
| `remuxer_0.ts` (concatenated TS) | 214 MB (grows unboundedly) | 1,926 MB | ❌ Never |
|
|||
|
|
| `stream_*.ts` (HLS segments) | 200 MB (100 files × 2 MB) | 1,800 MB | ❌ Never |
|
|||
|
|
| `video_*.mp4` (DASH fragments) | 660 MB (660 files × 2 MB) | 5,940 MB | ⚠️ Partial |
|
|||
|
|
| `audio_*.mp4` (audio fragments) | 20 MB (660 files × 30 KB) | 180 MB | ⚠️ Partial |
|
|||
|
|
| Go heap (download/decrypt/remux buffers) | ~110 MB | ~1,000 MB | ✅ GC |
|
|||
|
|
| Go stacks (goroutines) | ~1 MB | ~10 MB | ✅ GC |
|
|||
|
|
| **Total per channel** | **~1,200 MB** | **~10,800 MB** | |
|
|||
|
|
|
|||
|
|
### What's consuming memory (with `-keep=false`)
|
|||
|
|
|
|||
|
|
| Component | Per-channel cost | 6-stream total | Cleanup? |
|
|||
|
|
|-----------|------------------|----------------|----------|
|
|||
|
|
| Go heap (download/decrypt/remux buffers) | ~110 MB | ~660 MB | ✅ GC |
|
|||
|
|
| Go stacks (goroutines) | ~1 MB | ~10 MB | ✅ GC |
|
|||
|
|
| Temp files on disk | ~0 (deleted immediately) | ~4 KB | ✅ Deleted |
|
|||
|
|
| **Total** | **~110 MB** | **~670 MB** | |
|
|||
|
|
|
|||
|
|
### Go heap composition (from `/proc/<pid>/smaps`)
|
|||
|
|
|
|||
|
|
The anonymous `rw-p` region is Go's heap arena. Go allocates a large
|
|||
|
|
virtual address space (typically 2-4 GB) but only commits (RSS) what's
|
|||
|
|
actually used. The heap holds:
|
|||
|
|
|
|||
|
|
1. **DASH fragment download buffers** (~2 MB per fragment, 1-2 fragments
|
|||
|
|
in flight per stream = ~4 MB per stream)
|
|||
|
|
2. **AES-CTR decryption buffers** (same size as fragments = ~4 MB per stream)
|
|||
|
|
3. **MP4 remuxer state** (init segments, moof/mdat boxes, track metadata =
|
|||
|
|
~10 MB per stream)
|
|||
|
|
4. **HLS segment generation buffers** (TS muxing, PCR/PAT/PMT insertion =
|
|||
|
|
~5 MB per stream)
|
|||
|
|
5. **HTTP client connection pools** (keep-alive connections to CDNs =
|
|||
|
|
~2 MB per stream)
|
|||
|
|
6. **Manifest cache** (parsed DASH MPD, representation lists = ~5 MB per stream)
|
|||
|
|
7. **Go runtime overhead** (goroutine scheduler, GC metadata, type info =
|
|||
|
|
~50 MB fixed)
|
|||
|
|
|
|||
|
|
Total per-stream heap: ~30 MB live + ~80 MB in-flight buffers = ~110 MB
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Go Runtime Tuning
|
|||
|
|
|
|||
|
|
### GOGC (garbage collection trigger)
|
|||
|
|
|
|||
|
|
- **Default**: `GOGC=100` GC runs when heap doubles from previous GC
|
|||
|
|
- **Tested**: `GOGC=50` GC runs when heap grows 50% too aggressive,
|
|||
|
|
starves streams
|
|||
|
|
- **Recommendation**: Leave at default (100). The binary's memory usage
|
|||
|
|
is dominated by live buffers that can't be GC'd anyway.
|
|||
|
|
|
|||
|
|
### GOMEMLIMIT (soft memory cap)
|
|||
|
|
|
|||
|
|
- **Default**: off (no limit)
|
|||
|
|
- **Tested**: 512 MB too low, streams can't allocate download buffers
|
|||
|
|
- **Tested**: 1 GB works but streams fail due to network, not memory
|
|||
|
|
- **Recommendation**: Set `GOMEMLIMIT=2GiB` (2147483648) as a safety net
|
|||
|
|
for production. This allows ~18 streams at 110 MB each before GC
|
|||
|
|
pressure increases. Below 1 GB, streams will fail.
|
|||
|
|
|
|||
|
|
### GODEBUG
|
|||
|
|
|
|||
|
|
- `GODEBUG=gctrace=1` prints GC logs to stderr (useful for debugging)
|
|||
|
|
- `GODEBUG=memprofilerate=1` enables memory profiling (overhead)
|
|||
|
|
- Neither is needed for normal operation
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Recommendations
|
|||
|
|
|
|||
|
|
### 1. Always use `-keep=false` (CRITICAL)
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
./o11pro.fixed -c o11.cfg -p 19999 -b 0.0.0.0 -stdout -keep=false
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This is the single most important change. It reduces memory from
|
|||
|
|
unbounded growth (3.5+ GB in 2 minutes) to stable (~110 MB per stream)
|
|||
|
|
and keeps disk usage at ~4 KB instead of growing to 6+ GB.
|
|||
|
|
|
|||
|
|
The `-keep=true` default is labeled "debugging only" in the help text
|
|||
|
|
but is unfortunately the default. **Always override it.**
|
|||
|
|
|
|||
|
|
### 2. Set `GOMEMLIMIT` as a safety net
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
GOMEMLIMIT=2147483648 ./o11pro.fixed ... -keep=false
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This sets a 2 GB soft cap on Go's heap. If the binary tries to exceed
|
|||
|
|
this, Go's GC will run more aggressively to stay under the limit. This
|
|||
|
|
prevents OOM kills on memory-constrained systems.
|
|||
|
|
|
|||
|
|
Do NOT set GOMEMLIMIT below 1 GB streams will fail with HTTP timeouts
|
|||
|
|
because they can't allocate download buffers fast enough.
|
|||
|
|
|
|||
|
|
### 3. Reduce concurrent streams if memory-constrained
|
|||
|
|
|
|||
|
|
The provider config has `MaxConcurrentStreams: 0` (unlimited). Set this
|
|||
|
|
to a number your system can handle:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"MaxConcurrentStreams": 8
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
At ~110 MB per stream, 8 streams need ~880 MB of heap. With Go runtime
|
|||
|
|
overhead (~150 MB), total RSS ≈ 1 GB.
|
|||
|
|
|
|||
|
|
### 4. Use a RAM disk for `hls/live` (optional)
|
|||
|
|
|
|||
|
|
Even with `-keep=false`, the binary writes temp files to `hls/live/`
|
|||
|
|
before deleting them. On a slow disk, this can cause I/O bottlenecks.
|
|||
|
|
Mount `hls/live` as a RAM disk (tmpfs) for better performance:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
mkdir -p hls/live
|
|||
|
|
mount -t tmpfs -o size=512m tmpfs hls/live
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This gives 512 MB of RAM for temp files (plenty for `-keep=false`
|
|||
|
|
operation, where files are deleted immediately).
|
|||
|
|
|
|||
|
|
### 5. Monitor memory in production
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Simple memory monitor (add to your monitoring script)
|
|||
|
|
PID=$(pgrep -f o11pro)
|
|||
|
|
RSS=$(awk '/VmRSS/{print $2}' /proc/$PID/status)
|
|||
|
|
HLS=$(du -sm hls/live 2>/dev/null | cut -f1)
|
|||
|
|
echo "$(date): RSS=${RSS}KB HLS=${HLS}MB"
|
|||
|
|
|
|||
|
|
# Alert if RSS > 2 GB or HLS > 1 GB
|
|||
|
|
if [ "$RSS" -gt 2097152 ] || [ "$HLS" -gt 1024 ]; then
|
|||
|
|
echo "ALERT: Memory or disk usage high!"
|
|||
|
|
# Restart the binary
|
|||
|
|
kill $PID
|
|||
|
|
sleep 5
|
|||
|
|
./o11pro.fixed -c o11.cfg -p 19999 -keep=false &
|
|||
|
|
fi
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6. Restart periodically as a fallback
|
|||
|
|
|
|||
|
|
If you can't use `-keep=false` for some reason, restart the binary
|
|||
|
|
periodically to clear memory and disk:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Restart every 30 minutes
|
|||
|
|
*/30 * * * * pkill -f o11pro && sleep 5 && ./o11pro.fixed -c o11.cfg -p 19999 &
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This is a band-aid, not a fix. Use `-keep=false` instead.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Memory vs Stream Count
|
|||
|
|
|
|||
|
|
| Streams | RSS (with `-keep=false`) | RSS (with `-keep=true`, 5 min) | Disk (with `-keep=false`) | Disk (with `-keep=true`, 5 min) |
|
|||
|
|
|---------|--------------------------|--------------------------------|---------------------------|---------------------------------|
|
|||
|
|
| 0 | ~150 MB | ~150 MB | 4 KB | 4 KB |
|
|||
|
|
| 1 | ~260 MB | ~700 MB | 4 KB | ~700 MB |
|
|||
|
|
| 6 | ~660 MB | ~2.5 GB | 4 KB | ~3.5 GB |
|
|||
|
|
| 9 | ~1.1 GB | ~3.5 GB | 4 KB | ~5.9 GB |
|
|||
|
|
| 26 (all) | ~3 GB (estimated) | ~10+ GB (OOM) | 4 KB | ~17+ GB (OOM) |
|
|||
|
|
|
|||
|
|
With `-keep=false`, the binary can handle all 26 streams in ~3 GB of
|
|||
|
|
RAM. With `-keep=true` (default), it will OOM within 5-10 minutes even
|
|||
|
|
with 16+ GB of RAM.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Summary
|
|||
|
|
|
|||
|
|
| Setting | RSS (9 streams, 5 min) | Disk (5 min) | Streams working? |
|
|||
|
|
|---------|------------------------|--------------|------------------|
|
|||
|
|
| Default (`-keep=true`) | **3,535 MB** (growing) | **5,900 MB** (growing) | ✅ 8-9 streaming |
|
|||
|
|
| `-keep=false` | **663 MB** (stable) | **4 KB** (clean) | ✅ 6 streaming |
|
|||
|
|
| `-keep=false` + `GOMEMLIMIT=2GB` | ~660 MB (capped) | 4 KB | ✅ 6 streaming (recommended) |
|
|||
|
|
| `-keep=false` + `GOMEMLIMIT=512MB` | 138 MB (capped) | 4 KB | ❌ 0 (too aggressive) |
|
|||
|
|
|
|||
|
|
**The fix is simple: always launch with `-keep=false`, and optionally
|
|||
|
|
set `GOMEMLIMIT=2GiB` as a safety net.**
|