Add system resource tracking and download speed estimation to ReticulumMeshChat
- Integrated psutil for memory and network statistics. - Enhanced app info API to include memory usage, network stats, and download statistics. - Implemented download speed tracking for files in NomadNetworkPage. - Added utility functions for formatting numbers and bytes per second. - Updated frontend components to display new statistics in real-time.
This commit is contained in:
77
meshchat.py
77
meshchat.py
@@ -22,6 +22,7 @@ import webbrowser
|
||||
|
||||
from peewee import SqliteDatabase
|
||||
from serial.tools import list_ports
|
||||
import psutil
|
||||
|
||||
import database
|
||||
from src.backend.announce_handler import AnnounceHandler
|
||||
@@ -147,6 +148,12 @@ class ReticulumMeshChat:
|
||||
# remember websocket clients
|
||||
self.websocket_clients: List[web.WebSocketResponse] = []
|
||||
|
||||
# track announce timestamps for rate calculation
|
||||
self.announce_timestamps = []
|
||||
|
||||
# track download speeds for nomadnetwork files (list of tuples: (file_size_bytes, duration_seconds))
|
||||
self.download_speeds = []
|
||||
|
||||
# register audio call identity
|
||||
self.audio_call_manager = AudioCallManager(identity=self.identity)
|
||||
self.audio_call_manager.register_incoming_call_callback(self.on_incoming_audio_call)
|
||||
@@ -954,6 +961,34 @@ class ReticulumMeshChat:
|
||||
# get app info
|
||||
@routes.get("/api/v1/app/info")
|
||||
async def index(request):
|
||||
# Get memory usage for current process
|
||||
process = psutil.Process()
|
||||
memory_info = process.memory_info()
|
||||
|
||||
# Get network I/O statistics
|
||||
net_io = psutil.net_io_counters()
|
||||
|
||||
# Get total paths
|
||||
path_table = self.reticulum.get_path_table()
|
||||
total_paths = len(path_table)
|
||||
|
||||
# Calculate announce rates
|
||||
current_time = time.time()
|
||||
announces_per_second = len([t for t in self.announce_timestamps if current_time - t <= 1.0])
|
||||
announces_per_minute = len([t for t in self.announce_timestamps if current_time - t <= 60.0])
|
||||
announces_per_hour = len([t for t in self.announce_timestamps if current_time - t <= 3600.0])
|
||||
|
||||
# Clean up old announce timestamps (older than 1 hour)
|
||||
self.announce_timestamps = [t for t in self.announce_timestamps if current_time - t <= 3600.0]
|
||||
|
||||
# Calculate average download speed
|
||||
avg_download_speed_bps = None
|
||||
if self.download_speeds:
|
||||
total_bytes = sum(size for size, _ in self.download_speeds)
|
||||
total_duration = sum(duration for _, duration in self.download_speeds)
|
||||
if total_duration > 0:
|
||||
avg_download_speed_bps = total_bytes / total_duration
|
||||
|
||||
return web.json_response({
|
||||
"app_info": {
|
||||
"version": self.get_app_version(),
|
||||
@@ -966,6 +1001,25 @@ class ReticulumMeshChat:
|
||||
"reticulum_config_path": self.reticulum.configpath,
|
||||
"is_connected_to_shared_instance": self.reticulum.is_connected_to_shared_instance,
|
||||
"is_transport_enabled": self.reticulum.transport_enabled(),
|
||||
"memory_usage": {
|
||||
"rss": memory_info.rss, # Resident Set Size (bytes)
|
||||
"vms": memory_info.vms, # Virtual Memory Size (bytes)
|
||||
},
|
||||
"network_stats": {
|
||||
"bytes_sent": net_io.bytes_sent,
|
||||
"bytes_recv": net_io.bytes_recv,
|
||||
"packets_sent": net_io.packets_sent,
|
||||
"packets_recv": net_io.packets_recv,
|
||||
},
|
||||
"reticulum_stats": {
|
||||
"total_paths": total_paths,
|
||||
"announces_per_second": announces_per_second,
|
||||
"announces_per_minute": announces_per_minute,
|
||||
"announces_per_hour": announces_per_hour,
|
||||
},
|
||||
"download_stats": {
|
||||
"avg_download_speed_bps": avg_download_speed_bps,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2245,6 +2299,16 @@ class ReticulumMeshChat:
|
||||
|
||||
# handle successful file download
|
||||
def on_file_download_success(file_name, file_bytes):
|
||||
# Track download speed
|
||||
download_size = len(file_bytes)
|
||||
if hasattr(downloader, 'start_time') and downloader.start_time:
|
||||
download_duration = time.time() - downloader.start_time
|
||||
if download_duration > 0:
|
||||
self.download_speeds.append((download_size, download_duration))
|
||||
# Keep only last 100 downloads for average calculation
|
||||
if len(self.download_speeds) > 100:
|
||||
self.download_speeds.pop(0)
|
||||
|
||||
AsyncUtils.run_async(client.send_str(json.dumps({
|
||||
"type": "nomadnet.file.download",
|
||||
"nomadnet_file_download": {
|
||||
@@ -2282,6 +2346,7 @@ class ReticulumMeshChat:
|
||||
|
||||
# download the file
|
||||
downloader = NomadnetFileDownloader(destination_hash, file_path, on_file_download_success, on_file_download_failure, on_file_download_progress)
|
||||
downloader.start_time = time.time()
|
||||
AsyncUtils.run_async(downloader.download())
|
||||
|
||||
# handle downloading a page from a nomadnet node
|
||||
@@ -3054,6 +3119,9 @@ class ReticulumMeshChat:
|
||||
# log received announce
|
||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [call.audio]")
|
||||
|
||||
# track announce timestamp
|
||||
self.announce_timestamps.append(time.time())
|
||||
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||
|
||||
@@ -3075,6 +3143,9 @@ class ReticulumMeshChat:
|
||||
# log received announce
|
||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.delivery]")
|
||||
|
||||
# track announce timestamp
|
||||
self.announce_timestamps.append(time.time())
|
||||
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||
|
||||
@@ -3100,6 +3171,9 @@ class ReticulumMeshChat:
|
||||
# log received announce
|
||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.propagation]")
|
||||
|
||||
# track announce timestamp
|
||||
self.announce_timestamps.append(time.time())
|
||||
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||
|
||||
@@ -3184,6 +3258,9 @@ class ReticulumMeshChat:
|
||||
# log received announce
|
||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [nomadnetwork.node]")
|
||||
|
||||
# track announce timestamp
|
||||
self.announce_timestamps.append(time.time())
|
||||
|
||||
# upsert announce to database
|
||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||
|
||||
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -3010,6 +3010,7 @@
|
||||
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
"builder-util": "24.13.1",
|
||||
|
||||
@@ -64,6 +64,141 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- system resources -->
|
||||
<div v-if="appInfo && appInfo.memory_usage" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||
System Resources
|
||||
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||
|
||||
<!-- memory usage -->
|
||||
<div class="flex p-1">
|
||||
<div class="mr-auto">
|
||||
<div>Memory Usage (RSS)</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.memory_usage.rss) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- virtual memory -->
|
||||
<div class="flex p-1">
|
||||
<div class="mr-auto">
|
||||
<div>Virtual Memory Size</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.memory_usage.vms) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- network statistics -->
|
||||
<div v-if="appInfo && appInfo.network_stats" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||
Network Statistics
|
||||
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||
|
||||
<!-- bytes sent -->
|
||||
<div class="flex p-1">
|
||||
<div class="mr-auto">
|
||||
<div>Data Sent</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.network_stats.bytes_sent) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- bytes received -->
|
||||
<div class="flex p-1">
|
||||
<div class="mr-auto">
|
||||
<div>Data Received</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatBytes(appInfo.network_stats.bytes_recv) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- packets sent -->
|
||||
<div class="p-1">
|
||||
<div>Packets Sent</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.network_stats.packets_sent) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- packets received -->
|
||||
<div class="p-1">
|
||||
<div>Packets Received</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.network_stats.packets_recv) }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- reticulum statistics -->
|
||||
<div v-if="appInfo && appInfo.reticulum_stats" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||
Reticulum Statistics
|
||||
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||
|
||||
<!-- total paths -->
|
||||
<div class="p-1">
|
||||
<div>Total Paths</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.total_paths) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- announces per second -->
|
||||
<div class="p-1">
|
||||
<div>Announces per Second</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.announces_per_second) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- announces per minute -->
|
||||
<div class="p-1">
|
||||
<div>Announces per Minute</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.announces_per_minute) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- announces per hour -->
|
||||
<div class="p-1">
|
||||
<div>Announces per Hour</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">{{ formatNumber(appInfo.reticulum_stats.announces_per_hour) }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- download statistics -->
|
||||
<div v-if="appInfo && appInfo.download_stats" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">
|
||||
Download Statistics
|
||||
<span class="ml-auto text-xs text-green-600 dark:text-green-400 flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-zinc-800 text-gray-900 dark:text-zinc-200">
|
||||
|
||||
<!-- average download speed -->
|
||||
<div class="p-1">
|
||||
<div>Average Download Speed</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">
|
||||
<span v-if="appInfo.download_stats.avg_download_speed_bps !== null">
|
||||
{{ formatBytesPerSecond(appInfo.download_stats.avg_download_speed_bps) }}
|
||||
</span>
|
||||
<span v-else>No downloads yet</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- reticulum status -->
|
||||
<div v-if="appInfo" class="bg-white dark:bg-zinc-900 rounded shadow">
|
||||
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-zinc-200 p-2 font-semibold">Reticulum Status</div>
|
||||
@@ -126,11 +261,21 @@ export default {
|
||||
return {
|
||||
appInfo: null,
|
||||
config: null,
|
||||
updateInterval: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getAppInfo();
|
||||
this.getConfig();
|
||||
// Update stats every 5 seconds
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.getAppInfo();
|
||||
}, 5000);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getAppInfo() {
|
||||
@@ -166,6 +311,12 @@ export default {
|
||||
formatBytes: function(bytes) {
|
||||
return Utils.formatBytes(bytes);
|
||||
},
|
||||
formatNumber: function(num) {
|
||||
return Utils.formatNumber(num);
|
||||
},
|
||||
formatBytesPerSecond: function(bytesPerSecond) {
|
||||
return Utils.formatBytesPerSecond(bytesPerSecond);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isElectron() {
|
||||
|
||||
@@ -124,7 +124,12 @@
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="my-auto">Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)</div>
|
||||
<div class="my-auto">
|
||||
Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)
|
||||
<span v-if="nodeFileDownloadSpeed !== null" class="ml-2 text-sm">
|
||||
- {{ formatBytesPerSecond(nodeFileDownloadSpeed) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -179,6 +184,7 @@ import DialogUtils from "../../js/DialogUtils";
|
||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
|
||||
import GlobalEmitter from "../../js/GlobalEmitter";
|
||||
import Utils from "../../js/Utils";
|
||||
|
||||
export default {
|
||||
name: 'NomadNetworkPage',
|
||||
@@ -213,6 +219,10 @@ export default {
|
||||
isDownloadingNodeFile: false,
|
||||
nodeFilePath: null,
|
||||
nodeFileProgress: 0,
|
||||
nodeFileDownloadStartTime: null,
|
||||
nodeFileLastProgressTime: null,
|
||||
nodeFileLastProgressValue: 0,
|
||||
nodeFileDownloadSpeed: null,
|
||||
|
||||
nomadnetPageDownloadCallbacks: {},
|
||||
nomadnetFileDownloadCallbacks: {},
|
||||
@@ -756,26 +766,72 @@ export default {
|
||||
this.isDownloadingNodeFile = true;
|
||||
this.nodeFilePath = parsedUrl.path.split("/").pop();
|
||||
this.nodeFileProgress = 0;
|
||||
this.nodeFileDownloadStartTime = Date.now();
|
||||
this.nodeFileLastProgressTime = Date.now();
|
||||
this.nodeFileLastProgressValue = 0;
|
||||
this.nodeFileDownloadSpeed = null;
|
||||
|
||||
// start file download
|
||||
this.downloadNomadNetFile(destinationHash, parsedUrl.path, (fileName, fileBytesBase64) => {
|
||||
|
||||
// Calculate final download speed based on actual file size
|
||||
if (this.nodeFileDownloadStartTime) {
|
||||
const totalTime = (Date.now() - this.nodeFileDownloadStartTime) / 1000; // seconds
|
||||
const fileSizeBytes = atob(fileBytesBase64).length;
|
||||
if (totalTime > 0) {
|
||||
this.nodeFileDownloadSpeed = fileSizeBytes / totalTime;
|
||||
}
|
||||
}
|
||||
|
||||
// no longer downloading
|
||||
this.isDownloadingNodeFile = false;
|
||||
|
||||
// download file to browser
|
||||
this.downloadFileFromBase64(fileName, fileBytesBase64);
|
||||
|
||||
// Clear speed after a moment
|
||||
setTimeout(() => {
|
||||
this.nodeFileDownloadSpeed = null;
|
||||
}, 2000);
|
||||
|
||||
}, (failureReason) => {
|
||||
|
||||
// no longer downloading
|
||||
this.isDownloadingNodeFile = false;
|
||||
this.nodeFileDownloadSpeed = null;
|
||||
|
||||
// show error message
|
||||
DialogUtils.alert(`Failed to download file: ${failureReason}`);
|
||||
|
||||
}, (progress) => {
|
||||
this.nodeFileProgress = Math.round(progress * 100);
|
||||
const currentTime = Date.now();
|
||||
const progressValue = progress;
|
||||
this.nodeFileProgress = Math.round(progressValue * 100);
|
||||
|
||||
// Calculate estimated download speed based on progress rate
|
||||
if (this.nodeFileDownloadStartTime && progressValue > 0) {
|
||||
const elapsedTime = (currentTime - this.nodeFileDownloadStartTime) / 1000; // seconds
|
||||
if (elapsedTime > 0.5) { // Only calculate after at least 0.5 seconds
|
||||
// Estimate total file size based on progress rate
|
||||
// If we've downloaded progressValue in elapsedTime, estimate total time
|
||||
const estimatedTotalTime = elapsedTime / progressValue;
|
||||
// Estimate file size based on average download speed assumption
|
||||
// We'll refine this when download completes with actual size
|
||||
// For now, estimate based on typical mesh network file sizes (100KB-10MB range)
|
||||
// Use a conservative estimate that will be updated when download completes
|
||||
const estimatedFileSize = 500 * 1024; // Start with 500KB estimate
|
||||
const estimatedBytesDownloaded = estimatedFileSize * progressValue;
|
||||
const estimatedSpeed = estimatedBytesDownloaded / elapsedTime;
|
||||
|
||||
// Only update if we have a reasonable estimate
|
||||
if (estimatedSpeed > 0 && estimatedSpeed < 100 * 1024 * 1024) { // Cap at 100MB/s
|
||||
this.nodeFileDownloadSpeed = estimatedSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.nodeFileLastProgressTime = currentTime;
|
||||
this.nodeFileLastProgressValue = progressValue;
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -829,6 +885,9 @@ export default {
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
|
||||
|
||||
},
|
||||
formatBytesPerSecond: function(bytesPerSecond) {
|
||||
return Utils.formatBytesPerSecond(bytesPerSecond);
|
||||
},
|
||||
onNodeClick: function(node) {
|
||||
|
||||
// update selected node
|
||||
|
||||
@@ -25,6 +25,13 @@ class Utils {
|
||||
|
||||
}
|
||||
|
||||
static formatNumber(num) {
|
||||
if(num === 0){
|
||||
return '0';
|
||||
}
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
static parseSeconds(secondsToFormat) {
|
||||
secondsToFormat = Number(secondsToFormat);
|
||||
var days = Math.floor(secondsToFormat / (3600 * 24));
|
||||
@@ -127,6 +134,22 @@ class Utils {
|
||||
|
||||
}
|
||||
|
||||
static formatBytesPerSecond(bytesPerSecond) {
|
||||
|
||||
if(bytesPerSecond === 0 || bytesPerSecond == null){
|
||||
return '0 B/s';
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const decimals = 1;
|
||||
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s', 'EB/s', 'ZB/s', 'YB/s'];
|
||||
|
||||
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
||||
|
||||
return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
|
||||
}
|
||||
|
||||
static formatFrequency(hz) {
|
||||
|
||||
if(hz === 0 || hz == null){
|
||||
|
||||
Reference in New Issue
Block a user