working to analyze timing issues

This commit is contained in:
2025-07-25 15:52:16 -04:00
parent 70c2a1b9d3
commit 4c6e23bff8
31 changed files with 3197 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
"""
TUI Panel components
"""
from .flow_list import FlowListPanel
from .detail_panel import DetailPanel
from .timeline import TimelinePanel
__all__ = ['FlowListPanel', 'DetailPanel', 'TimelinePanel']

View File

@@ -0,0 +1,177 @@
"""
Right panel - Flow details with frame type table
"""
from typing import List, Optional, Tuple
import curses
from ...models import FlowStats, FrameTypeStats
class DetailPanel:
"""Right panel showing detailed flow information"""
def draw(self, stdscr, x_offset: int, y_offset: int, width: int,
flows_list: List[FlowStats], selected_flow: int, max_height: Optional[int] = None):
"""Draw detailed information panel for selected flow or frame type"""
if not flows_list:
stdscr.addstr(y_offset, x_offset, "No flows available")
return
# Get the selected flow and frame type
flow, selected_frame_type = self._get_selected_flow_and_frame_type(flows_list, selected_flow)
if not flow:
stdscr.addstr(y_offset, x_offset, "No flow selected")
return
if max_height is None:
height, _ = stdscr.getmaxyx()
max_lines = height - y_offset - 2
else:
max_lines = y_offset + max_height
try:
# ALWAYS show flow details first
stdscr.addstr(y_offset, x_offset, f"FLOW DETAILS: {flow.src_ip} -> {flow.dst_ip}", curses.A_BOLD)
y_offset += 2
stdscr.addstr(y_offset, x_offset, f"Packets: {flow.frame_count} | Bytes: {flow.total_bytes:,}")
y_offset += 1
# Frame types table
if flow.frame_types and y_offset < max_lines:
y_offset += 1
stdscr.addstr(y_offset, x_offset, "Frame Types:", curses.A_BOLD)
y_offset += 1
# Table header
header = f"{'Type':<12} {'#Pkts':<6} {'Bytes':<8} {'Avg ΔT':<8} {'2σ Out':<6}"
stdscr.addstr(y_offset, x_offset, header, curses.A_UNDERLINE)
y_offset += 1
sorted_frame_types = sorted(flow.frame_types.items(), key=lambda x: x[1].count, reverse=True)
for frame_type, ft_stats in sorted_frame_types:
if y_offset >= max_lines:
break
avg_str = f"{ft_stats.avg_inter_arrival:.3f}s" if ft_stats.avg_inter_arrival > 0 else "N/A"
bytes_str = f"{ft_stats.total_bytes:,}" if ft_stats.total_bytes < 10000 else f"{ft_stats.total_bytes/1000:.1f}K"
outliers_count = len(ft_stats.outlier_details) if ft_stats.outlier_details else 0
# Truncate frame type name if too long
type_name = frame_type[:11] if len(frame_type) > 11 else frame_type
ft_line = f"{type_name:<12} {ft_stats.count:<6} {bytes_str:<8} {avg_str:<8} {outliers_count:<6}"
stdscr.addstr(y_offset, x_offset, ft_line)
y_offset += 1
# Timing statistics
if y_offset < max_lines:
y_offset += 1
stdscr.addstr(y_offset, x_offset, "Timing:", curses.A_BOLD)
y_offset += 1
if flow.avg_inter_arrival > 0:
stdscr.addstr(y_offset, x_offset + 2, f"Avg: {flow.avg_inter_arrival:.6f}s")
y_offset += 1
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset + 2, f"Std: {flow.std_inter_arrival:.6f}s")
y_offset += 1
else:
stdscr.addstr(y_offset, x_offset + 2, "No timing data")
y_offset += 1
# Display outlier frame details for each frame type
if flow.frame_types and y_offset < max_lines:
outlier_frame_types = [(frame_type, ft_stats) for frame_type, ft_stats in flow.frame_types.items()
if ft_stats.outlier_details]
if outlier_frame_types:
y_offset += 1
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset, "Outlier Frames:", curses.A_BOLD)
y_offset += 1
for frame_type, ft_stats in outlier_frame_types:
if y_offset >= max_lines:
break
# Display frame type header
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset + 2, f"{frame_type}:", curses.A_UNDERLINE)
y_offset += 1
# Display outlier details as individual table rows in format "frame# | deltaT"
for frame_num, frame_inter_arrival_time in ft_stats.outlier_details:
if y_offset >= max_lines:
break
outlier_line = f"{frame_num} | {frame_inter_arrival_time:.3f}s"
stdscr.addstr(y_offset, x_offset + 4, outlier_line)
y_offset += 1
# If a frame type is selected, show additional frame type specific details
if selected_frame_type and selected_frame_type in flow.frame_types and y_offset < max_lines:
ft_stats = flow.frame_types[selected_frame_type]
# Add separator
y_offset += 2
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset, "" * min(width-2, 40))
y_offset += 1
# Frame type specific header
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset, f"FRAME TYPE: {selected_frame_type}", curses.A_BOLD)
y_offset += 2
# Frame type specific info
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset, f"Count: {ft_stats.count}")
y_offset += 1
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset, f"Bytes: {ft_stats.total_bytes:,}")
y_offset += 1
# Frame type timing
if y_offset < max_lines:
y_offset += 1
stdscr.addstr(y_offset, x_offset, "Timing:", curses.A_BOLD)
y_offset += 1
if ft_stats.avg_inter_arrival > 0:
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset + 2, f"Avg: {ft_stats.avg_inter_arrival:.6f}s")
y_offset += 1
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset + 2, f"Std: {ft_stats.std_inter_arrival:.6f}s")
y_offset += 1
else:
if y_offset < max_lines:
stdscr.addstr(y_offset, x_offset + 2, "No timing data")
y_offset += 1
except curses.error:
# Ignore curses errors from writing outside screen bounds
pass
def _get_selected_flow_and_frame_type(self, flows_list: List[FlowStats],
selected_flow: int) -> Tuple[Optional[FlowStats], Optional[str]]:
"""Get the currently selected flow and frame type based on selection index"""
current_item = 0
for flow in flows_list:
if current_item == selected_flow:
return flow, None # Selected the main flow
current_item += 1
# Check frame types for this flow
if flow.frame_types:
sorted_frame_types = sorted(flow.frame_types.items(), key=lambda x: x[1].count, reverse=True)
for frame_type, ft_stats in sorted_frame_types:
if current_item == selected_flow:
return flow, frame_type # Selected a frame type
current_item += 1
# Fallback to first flow if selection is out of bounds
return flows_list[0] if flows_list else None, None

View File

@@ -0,0 +1,127 @@
"""
Left panel - Flow list with frame type breakdowns
"""
from typing import List, Optional
import curses
from ...models import FlowStats
class FlowListPanel:
"""Left panel showing flows and frame type breakdowns"""
def __init__(self):
self.selected_item = 0
self.scroll_offset = 0
def draw(self, stdscr, x_offset: int, y_offset: int, width: int, height: int,
flows_list: List[FlowStats], selected_flow: int):
"""Draw the flow list panel"""
# Draw flows table header
stdscr.addstr(y_offset, x_offset, "FLOWS:", curses.A_BOLD)
headers = f"{'Source IP':15} {'Dest IP':15} {'Pkts':5} {'Protocol':18} {'ΔT Avg':10} {'Out':4}"
stdscr.addstr(y_offset + 1, x_offset, headers[:width-1], curses.A_UNDERLINE)
# Calculate scrolling parameters
start_row = y_offset + 2
max_rows = height - 3 # Account for header and title
total_items = self._get_total_display_items(flows_list)
# Calculate scroll offset to keep selected item visible
scroll_offset = self._calculate_scroll_offset(selected_flow, max_rows, total_items)
# Draw flows list with frame type breakdowns
current_row = start_row
display_item = 0 # Track selectable items (flows + frame types)
visible_items = 0 # Track items actually drawn
for flow_idx, flow in enumerate(flows_list):
# Check if main flow line should be displayed
if display_item >= scroll_offset and visible_items < max_rows:
# Draw main flow line
protocol_str = self._get_protocol_display(flow)
avg_time = f"{flow.avg_inter_arrival:.3f}s" if flow.avg_inter_arrival > 0 else "N/A"
line = f"{flow.src_ip:15} {flow.dst_ip:15} {flow.frame_count:5} {protocol_str:18} {avg_time:10} {'':4}"
if display_item == selected_flow:
stdscr.addstr(current_row, x_offset, line[:width-1], curses.A_REVERSE)
else:
stdscr.addstr(current_row, x_offset, line[:width-1], curses.A_BOLD)
current_row += 1
visible_items += 1
display_item += 1
# Draw frame type breakdowns for this flow
if flow.frame_types:
sorted_frame_types = sorted(flow.frame_types.items(), key=lambda x: x[1].count, reverse=True)
for frame_type, ft_stats in sorted_frame_types:
if display_item >= scroll_offset and visible_items < max_rows:
# Calculate frame type timing display
ft_avg = f"{ft_stats.avg_inter_arrival:.3f}s" if ft_stats.avg_inter_arrival > 0 else "N/A"
outlier_count = len(ft_stats.outlier_details) if ft_stats.outlier_details else 0
# Create frame type line aligned with columns
ft_line = f"{'':15} {'':15} {ft_stats.count:5} {frame_type:18} {ft_avg:10} {outlier_count:4}"
if display_item == selected_flow:
stdscr.addstr(current_row, x_offset, ft_line[:width-1], curses.A_REVERSE)
else:
stdscr.addstr(current_row, x_offset, ft_line[:width-1])
current_row += 1
visible_items += 1
display_item += 1
def _get_protocol_display(self, flow: FlowStats) -> str:
"""Get display string for flow protocols"""
if flow.detected_protocol_types:
# Prioritize specialized protocols
specialized = {'CHAPTER10', 'PTP', 'IENA'}
found_specialized = flow.detected_protocol_types & specialized
if found_specialized:
return list(found_specialized)[0]
# Use first detected protocol type
return list(flow.detected_protocol_types)[0]
# Fallback to basic protocols
if flow.protocols:
return list(flow.protocols)[0]
return "Unknown"
def _get_total_display_items(self, flows_list: List[FlowStats]) -> int:
"""Calculate total number of selectable items (flows + frame types)"""
total = 0
for flow in flows_list:
total += 1 # Flow itself
total += len(flow.frame_types) # Frame types under this flow
return total
def _calculate_scroll_offset(self, selected_item: int, max_visible: int, total_items: int) -> int:
"""Calculate scroll offset to keep selected item visible"""
if total_items <= max_visible:
return 0 # No scrolling needed
# Keep selected item in the middle third of visible area when possible
middle_position = max_visible // 3
# Calculate ideal scroll offset
scroll_offset = max(0, selected_item - middle_position)
# Ensure we don't scroll past the end
max_scroll = max(0, total_items - max_visible)
scroll_offset = min(scroll_offset, max_scroll)
return scroll_offset
def get_total_display_items(self, flows_list: List[FlowStats]) -> int:
"""Public method to get total display items"""
return self._get_total_display_items(flows_list)

View File

@@ -0,0 +1,269 @@
"""
Bottom panel - Timeline visualization
"""
from typing import List, Tuple, Optional
import curses
from ...models import FlowStats, FrameTypeStats
class TimelinePanel:
"""Bottom panel for timeline visualization"""
def draw(self, stdscr, x_offset: int, y_offset: int, width: int, height: int,
flows_list: List[FlowStats], selected_flow: int):
"""Draw timeline visualization panel for selected flow or frame type"""
if not flows_list or height < 5:
return
# Get the selected flow and frame type
flow, selected_frame_type = self._get_selected_flow_and_frame_type(flows_list, selected_flow)
if not flow:
return
try:
# Panel header
stdscr.addstr(y_offset, x_offset, "TIMING VISUALIZATION", curses.A_BOLD)
if selected_frame_type:
stdscr.addstr(y_offset + 1, x_offset, f"Flow: {flow.src_ip} -> {flow.dst_ip} | Frame Type: {selected_frame_type}")
else:
stdscr.addstr(y_offset + 1, x_offset, f"Flow: {flow.src_ip} -> {flow.dst_ip} | All Frames")
# Get the appropriate data for timeline
if selected_frame_type and selected_frame_type in flow.frame_types:
# Use frame type specific data
ft_stats = flow.frame_types[selected_frame_type]
if len(ft_stats.inter_arrival_times) < 2:
stdscr.addstr(y_offset + 2, x_offset, f"Insufficient data for {selected_frame_type} timeline")
return
deviations = self._calculate_frame_type_deviations(ft_stats)
timeline_flow = ft_stats # Use frame type stats for timeline
else:
# Use overall flow data
if len(flow.inter_arrival_times) < 2:
stdscr.addstr(y_offset + 2, x_offset, "Insufficient data for timeline")
return
deviations = self._calculate_frame_deviations(flow)
timeline_flow = flow # Use overall flow stats for timeline
if not deviations:
stdscr.addstr(y_offset + 2, x_offset, "No timing data available")
return
# Timeline dimensions
timeline_width = width - 10 # Leave space for labels
timeline_height = height - 6 # Leave space for header, labels, and time scale
timeline_y = y_offset + 3
timeline_x = x_offset + 5
# Draw timeline
self._draw_ascii_timeline(stdscr, timeline_x, timeline_y, timeline_width,
timeline_height, deviations, timeline_flow)
except curses.error:
# Ignore curses errors from writing outside screen bounds
pass
def _get_selected_flow_and_frame_type(self, flows_list: List[FlowStats],
selected_flow: int) -> Tuple[Optional[FlowStats], Optional[str]]:
"""Get the currently selected flow and frame type based on selection index"""
current_item = 0
for flow in flows_list:
if current_item == selected_flow:
return flow, None # Selected the main flow
current_item += 1
# Check frame types for this flow
if flow.frame_types:
sorted_frame_types = sorted(flow.frame_types.items(), key=lambda x: x[1].count, reverse=True)
for frame_type, ft_stats in sorted_frame_types:
if current_item == selected_flow:
return flow, frame_type # Selected a frame type
current_item += 1
# Fallback to first flow if selection is out of bounds
return flows_list[0] if flows_list else None, None
def _calculate_frame_deviations(self, flow: FlowStats) -> List[Tuple[int, float]]:
"""Calculate frame deviations from average inter-arrival time"""
if len(flow.inter_arrival_times) < 1 or flow.avg_inter_arrival == 0:
return []
deviations = []
# Each inter_arrival_time[i] is between frame[i] and frame[i+1]
for i, inter_time in enumerate(flow.inter_arrival_times):
if i + 1 < len(flow.frame_numbers):
frame_num = flow.frame_numbers[i + 1] # The frame that this inter-arrival time leads to
deviation = inter_time - flow.avg_inter_arrival
deviations.append((frame_num, deviation))
return deviations
def _calculate_frame_type_deviations(self, ft_stats: FrameTypeStats) -> List[Tuple[int, float]]:
"""Calculate frame deviations for a specific frame type"""
if len(ft_stats.inter_arrival_times) < 1 or ft_stats.avg_inter_arrival == 0:
return []
deviations = []
# Each inter_arrival_time[i] is between frame[i] and frame[i+1]
for i, inter_time in enumerate(ft_stats.inter_arrival_times):
if i + 1 < len(ft_stats.frame_numbers):
frame_num = ft_stats.frame_numbers[i + 1] # The frame that this inter-arrival time leads to
deviation = inter_time - ft_stats.avg_inter_arrival
deviations.append((frame_num, deviation))
return deviations
def _draw_ascii_timeline(self, stdscr, x_offset: int, y_offset: int, width: int, height: int,
deviations: List[Tuple[int, float]], flow):
"""Draw ASCII timeline chart"""
if not deviations or width < 10 or height < 3:
return
# Find min/max deviations for scaling
deviation_values = [dev for _, dev in deviations]
max_deviation = max(abs(min(deviation_values)), max(deviation_values))
if max_deviation == 0:
max_deviation = 0.001 # Avoid division by zero
# Calculate center line
center_y = y_offset + height // 2
# Draw center line (represents average timing)
center_line = "" * width
stdscr.addstr(center_y, x_offset, center_line)
# Add center line label
if x_offset > 4:
stdscr.addstr(center_y, x_offset - 4, "AVG")
# Scale factor for vertical positioning
scale_factor = (height // 2) / max_deviation
# Always scale to use the entire width
# Calculate the time span of the data
if len(flow.timestamps) < 2:
return
start_time = flow.timestamps[0]
end_time = flow.timestamps[-1]
time_span = end_time - start_time
if time_span <= 0:
return
# Create a mapping from deviation frame numbers to actual timestamps
frame_to_timestamp = {}
for i, (frame_num, deviation) in enumerate(deviations):
if i < len(flow.timestamps):
frame_to_timestamp[frame_num] = flow.timestamps[i]
# Plot points across entire width
for x in range(width):
# Calculate which timestamp this x position represents
time_ratio = x / (width - 1) if width > 1 else 0
target_time = start_time + (time_ratio * time_span)
# Find the closest deviation to this time
closest_deviation = None
min_time_diff = float('inf')
for frame_num, deviation in deviations:
# Use the correct timestamp mapping
if frame_num in frame_to_timestamp:
frame_time = frame_to_timestamp[frame_num]
time_diff = abs(frame_time - target_time)
if time_diff < min_time_diff:
min_time_diff = time_diff
closest_deviation = deviation
if closest_deviation is not None:
# Calculate vertical position
y_pos = center_y - int(closest_deviation * scale_factor)
y_pos = max(y_offset, min(y_offset + height - 1, y_pos))
# Choose character based on deviation magnitude
char = self._get_timeline_char(closest_deviation, flow.avg_inter_arrival)
# Draw the point
try:
stdscr.addstr(y_pos, x_offset + x, char)
except curses.error:
pass
# Draw scale labels and timeline info
self._draw_timeline_labels(stdscr, x_offset, y_offset, width, height,
max_deviation, deviations, flow, time_span)
def _get_timeline_char(self, deviation: float, avg_time: float) -> str:
"""Get character representation for timeline point based on deviation"""
if abs(deviation) < avg_time * 0.1: # Within 10% of average
return "·"
elif abs(deviation) < avg_time * 0.5: # Within 50% of average
return "" if deviation > 0 else ""
else: # Significant deviation (outlier)
return "" if deviation > 0 else ""
def _draw_timeline_labels(self, stdscr, x_offset: int, y_offset: int, width: int, height: int,
max_deviation: float, deviations: List[Tuple[int, float]],
flow, time_span: float):
"""Draw timeline labels and summary information"""
# Draw scale labels
if height >= 5:
# Top label (positive deviation)
top_dev = max_deviation
if x_offset > 4:
stdscr.addstr(y_offset, x_offset - 4, f"+{top_dev:.2f}s")
# Bottom label (negative deviation)
bottom_dev = -max_deviation
if x_offset > 4:
stdscr.addstr(y_offset + height - 1, x_offset - 4, f"{bottom_dev:.2f}s")
# Timeline info with time scale above summary
info_y = y_offset + height + 1
if info_y < y_offset + height + 3: # Make sure we have space for two lines
total_frames = len(deviations)
# First line: Time scale
relative_start = 0.0
relative_end = time_span
relative_middle = time_span / 2
# Format time scale labels
start_label = f"{relative_start:.1f}s"
middle_label = f"{relative_middle:.1f}s"
end_label = f"{relative_end:.1f}s"
# Draw time scale labels at left, middle, right
stdscr.addstr(info_y, x_offset, start_label)
# Middle label
middle_x = x_offset + width // 2 - len(middle_label) // 2
if middle_x > x_offset + len(start_label) + 1 and middle_x + len(middle_label) < x_offset + width - len(end_label) - 1:
stdscr.addstr(info_y, middle_x, middle_label)
# Right label
end_x = x_offset + width - len(end_label)
if end_x > x_offset + len(start_label) + 1:
stdscr.addstr(info_y, end_x, end_label)
# Second line: Frame count and deviation range
summary_y = info_y + 1
if summary_y < y_offset + height + 3:
left_info = f"Frames: {total_frames} | Range: ±{max_deviation:.3f}s"
stdscr.addstr(summary_y, x_offset, left_info)
# Right side outliers count with 2σ threshold
threshold_2sigma = flow.avg_inter_arrival + (2 * flow.std_inter_arrival)
outliers_info = f"Outliers: {len(flow.outlier_frames)} (>2σ: {threshold_2sigma:.4f}s)"
outliers_x = x_offset + width - len(outliers_info)
if outliers_x > x_offset + len(left_info) + 2: # Make sure there's space
stdscr.addstr(summary_y, outliers_x, outliers_info)