Skip to content

Commit 3936b34

Browse files
feat: some new stats
1 parent 2a7de8a commit 3936b34

15 files changed

Lines changed: 543 additions & 149 deletions

File tree

src-tauri/src/commands/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ pub use simulation::{
1515
};
1616
pub use stats::{
1717
get_all_drones_statistics, get_drone_statistics, get_global_statistics, get_host_stats,
18-
get_network_infos, get_new_messages, get_node_info, get_overview_metrics, get_drone_metrics
18+
get_network_infos, get_new_messages, get_node_info, get_overview_metrics, get_drone_metrics, get_host_metrics
1919
};

src-tauri/src/commands/stats.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use serde_json::json;
66
use serde_json::Value;
77
use std::collections::HashMap;
88
use std::sync::Arc;
9+
use std::time::Duration;
910
use tauri::State;
1011
use wg_2024::controller::DroneEvent;
1112
use wg_2024::network::NodeId;
@@ -117,6 +118,10 @@ pub fn get_new_messages(
117118
let total_messages = state.received_messages.len();
118119
let start_index = total_messages.saturating_sub(max_messages).max(last_id);
119120

121+
if start_index >= total_messages {
122+
return Vec::new();
123+
}
124+
120125
state.received_messages[start_index..]
121126
.iter()
122127
.enumerate()
@@ -418,4 +423,36 @@ pub fn get_drone_metrics(
418423
.get(&node_id)
419424
.cloned()
420425
.ok_or_else(|| NetworkError::NodeNotFound(node_id.to_string()))
421-
}
426+
}
427+
428+
#[derive(Serialize, Deserialize)]
429+
pub struct HostStats {
430+
latencies: Vec<Duration>,
431+
number_of_fragment_sent: u64,
432+
}
433+
434+
#[tauri::command]
435+
pub fn get_host_metrics(
436+
state: State<Arc<Mutex<NetworkState>>>,
437+
node_id: NodeId,
438+
) -> Result<HostStats, NetworkError> {
439+
let state = state.lock();
440+
let latencies = state
441+
.metrics
442+
.host_metrics
443+
.get(&node_id)
444+
.map(|m| m.latencies.clone())
445+
.ok_or_else(|| NetworkError::NodeNotFound(node_id.to_string()))?;
446+
447+
let number_of_fragment_sent = state
448+
.metrics
449+
.host_metrics
450+
.get(&node_id)
451+
.map(|m| m.number_of_fragments_sent())
452+
.ok_or_else(|| NetworkError::NodeNotFound(node_id.to_string()))?;
453+
454+
Ok(HostStats {
455+
latencies,
456+
number_of_fragment_sent,
457+
})
458+
}

src-tauri/src/lib.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ use crate::commands::{
88
add_neighbor, config_remove_edge, config_remove_node, crash_command, delete_history_config,
99
get_all_drones_statistics, get_config, get_default_configs, get_default_configs_dir,
1010
get_discovery_interval, get_drone_metrics, get_drone_statistics, get_global_statistics,
11-
get_graph, get_history_configs, get_history_dir, get_host_stats, get_network_infos,
12-
get_network_nodes, get_network_status, get_new_messages, get_node_info, get_overview_metrics,
13-
get_strict_mode, load_config, remove_neighbor, send_packet, send_set_pdr_command,
14-
set_discovery_interval, set_strict_mode, start_network, start_repeated_sending, stop_network,
15-
stop_repeated_sending,
11+
get_graph, get_history_configs, get_history_dir, get_host_metrics, get_host_stats,
12+
get_network_infos, get_network_nodes, get_network_status, get_new_messages, get_node_info,
13+
get_overview_metrics, get_strict_mode, load_config, remove_neighbor, send_packet,
14+
send_set_pdr_command, set_discovery_interval, set_strict_mode, start_network,
15+
start_repeated_sending, stop_network, stop_repeated_sending,
1616
};
1717
use crate::listener::Listener;
1818
use crate::network::state::NetworkState;
@@ -101,6 +101,7 @@ pub fn run() {
101101
get_host_stats,
102102
get_overview_metrics,
103103
get_drone_metrics,
104+
get_host_metrics,
104105
])
105106
.run(tauri::generate_context!())
106107
.expect("error while running tauri application");

src-tauri/src/network/metrics.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ impl HostMetrics {
150150
self.packet_type_counts.values().sum()
151151
}
152152

153+
pub fn number_of_fragments_sent(&self) -> u64 {
154+
*self
155+
.packet_type_counts
156+
.get(&PacketTypeLabel::MsgFragment)
157+
.unwrap_or(&0)
158+
}
159+
153160
pub fn number_of_messages_sent(&self) -> u64 {
154161
self.latencies.len() as u64
155162
}

src/components/NetworkInfos.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
33
import { invoke } from "@tauri-apps/api/core";
44
import { LaptopIcon, ServerIcon } from "lucide-react";
55
import { PiDrone } from "react-icons/pi";
6+
import { useSimulation } from "@/components/SimulationContext.tsx";
67

78
interface NodeInfo {
89
node_id: number;
@@ -20,6 +21,7 @@ const NetworkInfos = () => {
2021
const [networkData, setNetworkData] = useState<NodeInfo[]>([]);
2122
const [loading, setLoading] = useState<boolean>(true);
2223
const [error, setError] = useState<string | null>(null);
24+
const { pollingInterval } = useSimulation();
2325

2426
const fetchNetworkInfos = async () => {
2527
try {
@@ -35,7 +37,7 @@ const NetworkInfos = () => {
3537

3638
useEffect(() => {
3739
fetchNetworkInfos();
38-
const interval = setInterval(fetchNetworkInfos, 5000);
40+
const interval = setInterval(fetchNetworkInfos, pollingInterval);
3941
return () => clearInterval(interval);
4042
}, []);
4143

src/components/NodeDetails.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ interface NodeDetailsProps {
4848
const NodeDetails = ({ nodeId, onClose, refreshGraph }: NodeDetailsProps) => {
4949
const [nodeData, setNodeData] = useState<NodeInfo | null>(null);
5050
const [newNeighbor, setNewNeighbor] = useState<string>("");
51-
const { clientUrl, serverUrl } = useSimulation();
51+
const { clientUrl, serverUrl, pollingInterval } = useSimulation();
5252

5353
// 📌 Funzione per recuperare i dettagli di un nodo
5454
const fetchNodeDetails = useCallback(async () => {
@@ -63,7 +63,7 @@ const NodeDetails = ({ nodeId, onClose, refreshGraph }: NodeDetailsProps) => {
6363

6464
useEffect(() => {
6565
fetchNodeDetails();
66-
const interval = setInterval(fetchNodeDetails, 5000);
66+
const interval = setInterval(fetchNodeDetails, pollingInterval);
6767
return () => clearInterval(interval);
6868
}, [fetchNodeDetails]);
6969

src/components/SimulationContext.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const SimulationContext = createContext<{
1616
setClientUrl: (url: string) => void;
1717
serverUrl: string;
1818
setServerUrl: (url: string) => void;
19+
pollingInterval: number;
20+
setPollingInterval: (interval: number) => void;
1921
}>({
2022
status: "Init",
2123
isLoading: true,
@@ -30,7 +32,10 @@ const SimulationContext = createContext<{
3032
},
3133
serverUrl: "http://127.0.0.1:8080",
3234
setServerUrl: () => {
33-
}
35+
},
36+
pollingInterval: 5000,
37+
setPollingInterval: () => {
38+
},
3439
});
3540

3641
// Custom hook to access the simulation context
@@ -41,6 +46,7 @@ export const SimulationProvider = ({ children }: { children: React.ReactNode })
4146
const [isLoading, setIsLoading] = useState<boolean>(true);
4247
const [clientUrl, setClientUrl] = useState<string>(localStorage.getItem("clientUrl") || "http://localhost:7373");
4348
const [serverUrl, setServerUrl] = useState<string>(localStorage.getItem("serverUrl") || "http://127.0.0.1:8080");
49+
const [pollingInterval, setPollingInterval] = useState<number>(Number(localStorage.getItem("pollingInterval")) || 5000);
4450

4551
useEffect(() => {
4652
localStorage.setItem("clientUrl", clientUrl);
@@ -50,6 +56,9 @@ export const SimulationProvider = ({ children }: { children: React.ReactNode })
5056
localStorage.setItem("serverUrl", serverUrl);
5157
}, [serverUrl]);
5258

59+
useEffect(() => {
60+
localStorage.setItem("pollingInterval", pollingInterval.toString());
61+
}, [pollingInterval]);
5362

5463
useEffect(() => {
5564
const fetchStatus = async () => {
@@ -116,7 +125,9 @@ export const SimulationProvider = ({ children }: { children: React.ReactNode })
116125
clientUrl,
117126
setClientUrl,
118127
serverUrl,
119-
setServerUrl
128+
setServerUrl,
129+
pollingInterval,
130+
setPollingInterval,
120131
} }
121132
>
122133
{ children }

src/components/graphs/Heatmap.tsx

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { useEffect, useRef, useState } from "react";
2+
import cytoscape from "cytoscape";
3+
4+
interface GraphEdge {
5+
source: string;
6+
target: string;
7+
weight: number;
8+
}
9+
10+
interface HeatmapGraphProps {
11+
heatmap: Record<string, number>;
12+
nodeTypes: Record<string, "Drone" | "Client" | "Server">;
13+
}
14+
15+
const getCssVariableAsRGB = (variable: string) => {
16+
const hsl = getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
17+
return hslToRgb(hsl);
18+
};
19+
20+
const hslToRgb = (hsl: string) => {
21+
const match = hsl.match(/(\d+),?\s*(\d+)%?,?\s*(\d+)%?/);
22+
if (!match) return "rgb(0, 0, 0)"; // Valore di default in caso di errore
23+
24+
let [h, s, l] = match.slice(1, 4).map(Number);
25+
s /= 100;
26+
l /= 100;
27+
28+
const k = (n: number) => (n + h / 30) % 12;
29+
const a = s * Math.min(l, 1 - l);
30+
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
31+
32+
return `rgb(${ Math.round(f(0) * 255) }, ${ Math.round(f(8) * 255) }, ${ Math.round(f(4) * 255) })`;
33+
};
34+
35+
const HeatmapGraph = ({ heatmap, nodeTypes }: HeatmapGraphProps) => {
36+
const cyRef = useRef<HTMLDivElement>(null);
37+
const cyInstance = useRef<cytoscape.Core | null>(null);
38+
const [prevGraph, setPrevGraph] = useState<{ nodes: Set<string>; edges: string[] } | null>(null);
39+
40+
const nodes = new Set<string>();
41+
const edgesMap = new Map<string, GraphEdge>();
42+
43+
Object.entries(heatmap).forEach(([key, weight]) => {
44+
let [src, dest] = key.split(",");
45+
if (!src || !dest) return;
46+
47+
if (src > dest) [src, dest] = [dest, src];
48+
49+
nodes.add(src);
50+
nodes.add(dest);
51+
52+
const edgeKey = `${ src }-${ dest }`;
53+
if (edgesMap.has(edgeKey)) {
54+
edgesMap.get(edgeKey)!.weight += weight;
55+
} else {
56+
edgesMap.set(edgeKey, { source: src, target: dest, weight });
57+
}
58+
});
59+
60+
const edges = Array.from(edgesMap.values());
61+
62+
const maxWeight = Math.max(...edges.map((e) => e.weight), 1);
63+
const minWeight = Math.min(...edges.map((e) => e.weight), maxWeight);
64+
65+
useEffect(() => {
66+
if (!cyRef.current) return;
67+
68+
const primaryColor = getCssVariableAsRGB("--primary");
69+
const newGraph = {
70+
nodes: new Set(nodes),
71+
edges: edges.map((e) => `${ e.source }-${ e.target }`),
72+
};
73+
74+
if (
75+
prevGraph &&
76+
newGraph.nodes.size === prevGraph.nodes.size &&
77+
newGraph.edges.length === prevGraph.edges.length &&
78+
newGraph.edges.every((edge) => prevGraph.edges.includes(edge))
79+
) {
80+
edges.forEach((edge) => {
81+
const cyEdge = cyInstance.current?.edges(`[source="${ edge.source }"][target="${ edge.target }"]`);
82+
if (cyEdge) {
83+
cyEdge.style({
84+
width: normalizeEdgeWeight(edge.weight, minWeight, maxWeight),
85+
"line-color": primaryColor,
86+
});
87+
}
88+
});
89+
return;
90+
}
91+
92+
cyInstance.current?.destroy();
93+
94+
cyInstance.current = cytoscape({
95+
container: cyRef.current,
96+
elements: [
97+
...Array.from(nodes).map((id) => ({
98+
data: { id, label: id, type: nodeTypes[id] || "Client" },
99+
})),
100+
...edges.map((edge) => ({
101+
data: { source: edge.source, target: edge.target, weight: edge.weight },
102+
})),
103+
],
104+
style: [
105+
{
106+
selector: "node",
107+
style: {
108+
label: "data(label)",
109+
"text-valign": "center",
110+
"text-halign": "center",
111+
"background-color": (ele) => getNodeColor(ele.data("type")),
112+
"border-width": 2,
113+
"border-color": (ele) => getNodeBorderColor(ele.data("type")),
114+
color: "#000",
115+
"font-size": "12px",
116+
"font-weight": "bold",
117+
width: 24,
118+
height: 24,
119+
shape: "ellipse",
120+
"events": "no"
121+
},
122+
},
123+
{
124+
selector: "edge",
125+
style: {
126+
"line-color": primaryColor,
127+
width: (ele: cytoscape.NodeSingular) => normalizeEdgeWeight(ele.data("weight"), minWeight, maxWeight),
128+
"curve-style": "bezier",
129+
"events": "no"
130+
},
131+
},
132+
],
133+
layout: {
134+
name: "cose",
135+
fit: true,
136+
padding: 40,
137+
},
138+
userZoomingEnabled: false,
139+
userPanningEnabled: false,
140+
boxSelectionEnabled: false,
141+
});
142+
143+
setPrevGraph(newGraph);
144+
}, [heatmap, nodeTypes]);
145+
146+
const normalizeEdgeWeight = (weight: number, minW: number, maxW: number): number => {
147+
if (maxW === minW) return 3;
148+
return 1 + ((weight - minW) / (maxW - minW)) * 5; // Normalizza tra 1 e 6
149+
};
150+
151+
const getNodeColor = (type: string): string => {
152+
const colors: Record<"Server" | "Drone" | "Client", string> = {
153+
Server: "#FEFAF4",
154+
Drone: "#F5FAFA",
155+
Client: "#F9FBF6",
156+
};
157+
// QUi tutti sono Client
158+
return colors[type as keyof typeof colors] || "#ddd";
159+
};
160+
161+
// 🎨 **Colori dei bordi**
162+
const getNodeBorderColor = (type: string): string => {
163+
const borderColors: Record<"Server" | "Drone" | "Client", string> = {
164+
Server: "#EDCB95",
165+
Drone: "#9ACDC8",
166+
Client: "#C3D59D",
167+
};
168+
return borderColors[type as keyof typeof borderColors] || "#aaa";
169+
};
170+
171+
return <div ref={ cyRef } className="w-full h-[400px]"></div>;
172+
};
173+
174+
export default HeatmapGraph;

0 commit comments

Comments
 (0)