Skip to content

Commit 364fba6

Browse files
authored
Measure RSS and CPU for the samples (#213)
1 parent 743073b commit 364fba6

File tree

4 files changed

+488
-17
lines changed

4 files changed

+488
-17
lines changed

.github/workflows/ci.yml

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -186,17 +186,25 @@ jobs:
186186
distribution: 'adopt'
187187
cache: maven
188188

189+
- name: Set up Python 3.12
190+
if: ${{ runner.os == 'macOS' || runner.os == 'linux' }}
191+
uses: actions/setup-python@v5
192+
with:
193+
python-version: "3.12"
194+
195+
- name: Install Python dependencies
196+
if: ${{ runner.os == 'macOS' || runner.os == 'linux' }}
197+
working-directory: scripts/python
198+
run: |
199+
python -m pip install --upgrade pip
200+
pip install -r requirements.txt
201+
189202
- name: Download JNI Library
190203
uses: actions/download-artifact@v4
191204
with:
192205
name: jni-library-${{ matrix.platform.name }}
193206
path: jni/
194207

195-
- name: Install dependencies (MacOS)
196-
if: ${{ runner.os == 'macOS' }}
197-
run: |
198-
brew install coreutils # For `gtimeout`
199-
200208
- name: Build with Maven
201209
run: mvn clean compile assembly:single
202210

@@ -206,7 +214,7 @@ jobs:
206214
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
207215
aws-region: ${{ secrets.AWS_REGION }}
208216

209-
- name: Create the stream (DemoAppCachedInfo)
217+
- name: Create the stream (DemoAppMain)
210218
# This sample needs a pre-created stream: `${NAME}`
211219
if: ${{ matrix.sample == 'DemoAppMain' }}
212220
working-directory: scripts
@@ -238,24 +246,35 @@ jobs:
238246
fi
239247
240248
echo "Using JAR file: $JAR_FILE"
241-
242-
# Use gtimeout on macOS and timeout on Linux
243-
if [ "${{ runner.os }}" == "macOS" ]; then
244-
TIMEOUT_CMD="gtimeout"
245-
else
246-
TIMEOUT_CMD="timeout"
247-
fi
248-
249-
$TIMEOUT_CMD 30s java -classpath "$JAR_FILE" \
249+
java -classpath "$JAR_FILE" \
250250
-Daws.accessKeyId=${AWS_ACCESS_KEY_ID} \
251251
-Daws.secretKey=${AWS_SECRET_ACCESS_KEY} \
252252
-Daws.sessionToken=${AWS_SESSION_TOKEN} \
253253
-Djava.library.path=${JNI_FOLDER} \
254254
-Dkvs-stream=$STREAM_NAME \
255255
-Dlog4j.configurationFile=log4j2.xml \
256-
com.amazonaws.kinesisvideo.demoapp.${{ matrix.sample }}
257-
256+
com.amazonaws.kinesisvideo.demoapp.${{ matrix.sample }} &
257+
JAVA_PID=$!
258+
259+
python ./scripts/python/capture_rss_and_cpu.py $JAVA_PID &
260+
MONITOR_PID=$!
261+
262+
# Wait for maximum 30 seconds
263+
TIMEOUT=30
264+
for ((i=1; i<=TIMEOUT; i++)); do
265+
if ! kill -0 $JAVA_PID 2>/dev/null; then
266+
break
267+
fi
268+
sleep 1
269+
if [ $i -eq $TIMEOUT ]; then
270+
# Time's up, kill the process
271+
kill -9 $JAVA_PID
272+
fi
273+
done
274+
275+
wait $JAVA_PID
258276
EXIT_CODE=$?
277+
wait $MONITOR_PID
259278
260279
set -e
261280
@@ -299,3 +318,22 @@ jobs:
299318
300319
echo Process exited with code: %EXIT_CODE%
301320
exit /b %EXIT_CODE%
321+
322+
- name: Generate memory and CPU graph (Mac and Linux)
323+
if: ${{ runner.os == 'macOS' || runner.os == 'linux' }}
324+
run: |
325+
DATA_FILE=$(find . -name 'process_*.txt' | head -n 1)
326+
COMMIT_HASH=$(git rev-parse --short HEAD)
327+
python ./scripts/python/plot_rss_and_cpu.py "$DATA_FILE" \
328+
--title "Memory and CPU Usage (${{ matrix.sample }} @ ${COMMIT_HASH})\n${{ matrix.platform.os }}, Java ${{ matrix.java }}" \
329+
--output "${{ env.STREAM_NAME }}-mem-cpu.png"
330+
331+
shell: bash
332+
333+
- name: Upload memory and CPU graph (Mac and Linux)
334+
if: ${{ runner.os == 'macOS' || runner.os == 'linux' }}
335+
uses: actions/upload-artifact@v4
336+
with:
337+
name: ${{ env.STREAM_NAME }}-mem-cpu.png
338+
path: ${{ env.STREAM_NAME }}-mem-cpu.png
339+
retention-days: 7
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Process Monitor Script
5+
6+
This script monitors CPU and RAM usage of a specified process using its PID.
7+
Metrics are recorded to a CSV file until the process terminates.
8+
"""
9+
10+
import argparse
11+
import csv
12+
import os
13+
import sys
14+
import time
15+
16+
from datetime import datetime
17+
from psutil import Process, NoSuchProcess, pid_exists
18+
19+
20+
class ProcessMonitor:
21+
"""Class to monitor process metrics including CPU and RAM usage."""
22+
23+
def __init__(self, pid: int, interval: float = 0.1):
24+
"""
25+
Initialize the ProcessMonitor.
26+
27+
Args:
28+
pid (int): Process ID to monitor
29+
interval (float): Sampling interval in seconds
30+
"""
31+
self.pid = pid
32+
self.interval = interval
33+
self.output_file = f'process_{pid}_metrics.txt'
34+
35+
def get_process_metrics(self) -> tuple[str, float, float] | None:
36+
"""
37+
Collect current process metrics.
38+
39+
Returns:
40+
tuple: (timestamp, memory_mb, cpu_percent) or None if process not found
41+
"""
42+
try:
43+
process = Process(self.pid)
44+
cpu_percent = process.cpu_percent(interval=0.1)
45+
memory_kb = process.memory_info().rss / 1024
46+
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
47+
48+
return timestamp, memory_kb, cpu_percent
49+
50+
except NoSuchProcess:
51+
print(f"Process with PID {self.pid} no longer exists")
52+
return None
53+
except Exception as e:
54+
print(f"Error: {e}")
55+
return None
56+
57+
def write_to_csv(self, data: tuple[str, float, float]) -> None:
58+
"""
59+
Write metrics to CSV file.
60+
61+
Args:
62+
data (tuple): (timestamp, memory_kb, cpu_percent)
63+
"""
64+
file_exists = os.path.isfile(self.output_file)
65+
66+
with open(self.output_file, 'a', newline='', encoding='utf-8') as csvfile:
67+
writer = csv.writer(csvfile)
68+
69+
if not file_exists:
70+
writer.writerow(['Timestamp', 'RAM (KB)', 'CPU (%)'])
71+
72+
writer.writerow(data)
73+
74+
def record_metrics_until_process_ends(self) -> None:
75+
"""Start monitoring the process and recording metrics."""
76+
print(f"Starting monitoring of PID {self.pid}")
77+
print(f"Writing data to {self.output_file}")
78+
79+
try:
80+
while True:
81+
metrics = self.get_process_metrics()
82+
83+
if metrics is None:
84+
print("Process monitoring ended")
85+
break
86+
87+
self.write_to_csv(metrics)
88+
89+
time.sleep(self.interval)
90+
91+
except KeyboardInterrupt:
92+
print("\nInterrupted by user")
93+
except Exception as e:
94+
print(f"Error: {e}")
95+
96+
97+
def parse_arguments() -> argparse.Namespace:
98+
"""
99+
Parse command line arguments.
100+
101+
Returns:
102+
argparse.Namespace: Parsed command line arguments
103+
"""
104+
parser = argparse.ArgumentParser(
105+
description='Monitor CPU and RAM usage of a process.',
106+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
107+
)
108+
109+
parser.add_argument(
110+
'pid',
111+
type=int,
112+
help='Process ID to monitor'
113+
)
114+
115+
parser.add_argument(
116+
'-i', '--interval',
117+
type=float,
118+
default=0.1,
119+
help='Sampling interval in seconds'
120+
)
121+
122+
return parser.parse_args()
123+
124+
125+
def validate_args(args: argparse.Namespace) -> None:
126+
"""
127+
Checks the validity of arguments.
128+
129+
Raises an error if any of the args are invalid.
130+
"""
131+
# Validate PID
132+
if not pid_exists(args.pid):
133+
raise ValueError(f"Error: Process with PID {args.pid} does not exist")
134+
135+
# Validate interval
136+
if args.interval <= 0:
137+
raise ValueError("Error: Interval must be greater than 0")
138+
139+
140+
def main() -> int:
141+
"""
142+
Main function to run the process monitor.
143+
144+
Returns:
145+
int: Exit code (0 for success, 1 for error)
146+
"""
147+
try:
148+
args = parse_arguments()
149+
150+
validate_args(args)
151+
152+
monitor = ProcessMonitor(args.pid, args.interval)
153+
monitor.record_metrics_until_process_ends()
154+
return 0
155+
156+
except Exception as e:
157+
print(f"Error: {e}")
158+
return 1
159+
160+
161+
if __name__ == "__main__":
162+
sys.exit(main())

0 commit comments

Comments
 (0)