nakaolab oct19
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
UE Count Plotter
|
||||
----------------
|
||||
Parses log lines like:
|
||||
[2025-09-08 21:33:40.789676] UE RNTI=44512 ...
|
||||
Groups entries into 1-second slots (by wall-clock time) and plots the number of
|
||||
unique UEs (by RNTI) per slot for the entire file.
|
||||
|
||||
Usage:
|
||||
python ue_count_plot.py /path/to/log.txt [--save output.png] [--no-show]
|
||||
|
||||
Notes:
|
||||
- Uses matplotlib for a single-line plot.
|
||||
- Does not set any colors or styles.
|
||||
- For very large logs, this streams line-by-line with O(N) memory in the number
|
||||
of distinct seconds encountered (each holds a set of RNTIs).
|
||||
"""
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict, OrderedDict
|
||||
from typing import Dict, Set, Iterable, Tuple
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
|
||||
|
||||
TIMESTAMP_RE = re.compile(r"\[(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)\]")
|
||||
RNTI_RE = re.compile(r"\bRNTI=(?P<rnti>\d+)\b")
|
||||
|
||||
|
||||
def parse_lines(lines: Iterable[str]) -> Dict[dt.datetime, Set[int]]:
|
||||
"""
|
||||
Parse lines and bucket unique RNTIs per second.
|
||||
|
||||
Returns:
|
||||
dict mapping second (datetime truncated to seconds) -> set of RNTIs
|
||||
"""
|
||||
buckets: Dict[dt.datetime, Set[int]] = defaultdict(set)
|
||||
|
||||
for line in lines:
|
||||
ts_m = TIMESTAMP_RE.search(line)
|
||||
rnti_m = RNTI_RE.search(line)
|
||||
if not ts_m or not rnti_m:
|
||||
# Skip lines that don't fit the schema
|
||||
continue
|
||||
|
||||
ts_str = ts_m.group("ts")
|
||||
# Parse timestamp with or without fractional seconds
|
||||
# Accept both "%Y-%m-%d %H:%M:%S" and with "%f"
|
||||
if "." in ts_str:
|
||||
ts = dt.datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S.%f")
|
||||
else:
|
||||
ts = dt.datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Truncate to whole-second wall time
|
||||
ts_minute = ts.replace(second=0, microsecond=0)
|
||||
rnti = int(rnti_m.group("rnti"))
|
||||
buckets[ts_minute].add(rnti)
|
||||
return buckets
|
||||
|
||||
|
||||
def sort_and_fill(buckets: Dict[dt.datetime, Set[int]]) -> Tuple[list, list]:
|
||||
"""
|
||||
Sorts buckets by time and fills missing seconds with zero counts.
|
||||
|
||||
Returns:
|
||||
times (list[datetime]), counts (list[int])
|
||||
"""
|
||||
if not buckets:
|
||||
return [], []
|
||||
|
||||
# Sort keys
|
||||
sorted_times = sorted(buckets.keys())
|
||||
start = sorted_times[0]
|
||||
end = sorted_times[-1]
|
||||
|
||||
all_times = []
|
||||
counts = []
|
||||
|
||||
cur = start
|
||||
one_sec = dt.timedelta(minutes=1)
|
||||
while cur <= end:
|
||||
all_times.append(cur)
|
||||
counts.append(len(buckets.get(cur, set())))
|
||||
cur += one_sec
|
||||
|
||||
return all_times, counts
|
||||
|
||||
|
||||
def plot_counts(times: list, counts: list, title: str = "Unique UEs per minute"):
|
||||
"""
|
||||
Render a simple matplotlib line plot (no specific colors/styles set).
|
||||
"""
|
||||
fig, ax = plt.subplots(figsize=(10, 4))
|
||||
ax.plot(times, counts, linestyle="-")
|
||||
ax.set_title(title)
|
||||
ax.set_xlabel("Time (s)")
|
||||
ax.set_ylabel("Unique UEs")
|
||||
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
|
||||
fig.autofmt_xdate()
|
||||
plt.tight_layout()
|
||||
return fig, ax
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Plot unique UEs (by RNTI) per minute from a log file.")
|
||||
parser.add_argument("--save", help="Optional path to save the plot image (e.g., output.png)")
|
||||
parser.add_argument("--no-show", action="store_true", help="Do not display the plot window")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read lines
|
||||
try:
|
||||
lines = open("artifacts/mac_stats_20250909_231855.log", "r", encoding="utf-8")
|
||||
except OSError as e:
|
||||
print(f"Error opening {args.logfile}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
buckets = parse_lines(lines)
|
||||
if lines is not sys.stdin:
|
||||
lines.close()
|
||||
|
||||
times, counts = sort_and_fill(buckets)
|
||||
if not times:
|
||||
print("No valid entries found to plot.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
fig, _ = plot_counts(times, counts)
|
||||
|
||||
if args.save:
|
||||
try:
|
||||
fig.savefig(args.save, dpi=150, bbox_inches="tight")
|
||||
print(f"Saved plot to: {args.save}")
|
||||
except Exception as e:
|
||||
print(f"Failed to save figure: {e}", file=sys.stderr)
|
||||
|
||||
if not args.no_show:
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user