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 peewee import SqliteDatabase
|
||||||
from serial.tools import list_ports
|
from serial.tools import list_ports
|
||||||
|
import psutil
|
||||||
|
|
||||||
import database
|
import database
|
||||||
from src.backend.announce_handler import AnnounceHandler
|
from src.backend.announce_handler import AnnounceHandler
|
||||||
@@ -147,6 +148,12 @@ class ReticulumMeshChat:
|
|||||||
# remember websocket clients
|
# remember websocket clients
|
||||||
self.websocket_clients: List[web.WebSocketResponse] = []
|
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
|
# register audio call identity
|
||||||
self.audio_call_manager = AudioCallManager(identity=self.identity)
|
self.audio_call_manager = AudioCallManager(identity=self.identity)
|
||||||
self.audio_call_manager.register_incoming_call_callback(self.on_incoming_audio_call)
|
self.audio_call_manager.register_incoming_call_callback(self.on_incoming_audio_call)
|
||||||
@@ -954,6 +961,34 @@ class ReticulumMeshChat:
|
|||||||
# get app info
|
# get app info
|
||||||
@routes.get("/api/v1/app/info")
|
@routes.get("/api/v1/app/info")
|
||||||
async def index(request):
|
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({
|
return web.json_response({
|
||||||
"app_info": {
|
"app_info": {
|
||||||
"version": self.get_app_version(),
|
"version": self.get_app_version(),
|
||||||
@@ -966,6 +1001,25 @@ class ReticulumMeshChat:
|
|||||||
"reticulum_config_path": self.reticulum.configpath,
|
"reticulum_config_path": self.reticulum.configpath,
|
||||||
"is_connected_to_shared_instance": self.reticulum.is_connected_to_shared_instance,
|
"is_connected_to_shared_instance": self.reticulum.is_connected_to_shared_instance,
|
||||||
"is_transport_enabled": self.reticulum.transport_enabled(),
|
"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
|
# handle successful file download
|
||||||
def on_file_download_success(file_name, file_bytes):
|
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({
|
AsyncUtils.run_async(client.send_str(json.dumps({
|
||||||
"type": "nomadnet.file.download",
|
"type": "nomadnet.file.download",
|
||||||
"nomadnet_file_download": {
|
"nomadnet_file_download": {
|
||||||
@@ -2282,6 +2346,7 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# download the file
|
# download the file
|
||||||
downloader = NomadnetFileDownloader(destination_hash, file_path, on_file_download_success, on_file_download_failure, on_file_download_progress)
|
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())
|
AsyncUtils.run_async(downloader.download())
|
||||||
|
|
||||||
# handle downloading a page from a nomadnet node
|
# handle downloading a page from a nomadnet node
|
||||||
@@ -3054,6 +3119,9 @@ class ReticulumMeshChat:
|
|||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [call.audio]")
|
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
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
@@ -3075,6 +3143,9 @@ class ReticulumMeshChat:
|
|||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.delivery]")
|
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
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
@@ -3100,6 +3171,9 @@ class ReticulumMeshChat:
|
|||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.propagation]")
|
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
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
@@ -3184,6 +3258,9 @@ class ReticulumMeshChat:
|
|||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [nomadnetwork.node]")
|
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
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
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==",
|
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"builder-util": "24.13.1",
|
"builder-util": "24.13.1",
|
||||||
|
|||||||
@@ -64,6 +64,141 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- reticulum status -->
|
||||||
<div v-if="appInfo" class="bg-white dark:bg-zinc-900 rounded shadow">
|
<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>
|
<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 {
|
return {
|
||||||
appInfo: null,
|
appInfo: null,
|
||||||
config: null,
|
config: null,
|
||||||
|
updateInterval: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getAppInfo();
|
this.getAppInfo();
|
||||||
this.getConfig();
|
this.getConfig();
|
||||||
|
// Update stats every 5 seconds
|
||||||
|
this.updateInterval = setInterval(() => {
|
||||||
|
this.getAppInfo();
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.updateInterval) {
|
||||||
|
clearInterval(this.updateInterval);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async getAppInfo() {
|
async getAppInfo() {
|
||||||
@@ -166,6 +311,12 @@ export default {
|
|||||||
formatBytes: function(bytes) {
|
formatBytes: function(bytes) {
|
||||||
return Utils.formatBytes(bytes);
|
return Utils.formatBytes(bytes);
|
||||||
},
|
},
|
||||||
|
formatNumber: function(num) {
|
||||||
|
return Utils.formatNumber(num);
|
||||||
|
},
|
||||||
|
formatBytesPerSecond: function(bytesPerSecond) {
|
||||||
|
return Utils.formatBytesPerSecond(bytesPerSecond);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isElectron() {
|
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>
|
<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>
|
</svg>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,6 +184,7 @@ import DialogUtils from "../../js/DialogUtils";
|
|||||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||||
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
|
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
|
||||||
import GlobalEmitter from "../../js/GlobalEmitter";
|
import GlobalEmitter from "../../js/GlobalEmitter";
|
||||||
|
import Utils from "../../js/Utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NomadNetworkPage',
|
name: 'NomadNetworkPage',
|
||||||
@@ -213,6 +219,10 @@ export default {
|
|||||||
isDownloadingNodeFile: false,
|
isDownloadingNodeFile: false,
|
||||||
nodeFilePath: null,
|
nodeFilePath: null,
|
||||||
nodeFileProgress: 0,
|
nodeFileProgress: 0,
|
||||||
|
nodeFileDownloadStartTime: null,
|
||||||
|
nodeFileLastProgressTime: null,
|
||||||
|
nodeFileLastProgressValue: 0,
|
||||||
|
nodeFileDownloadSpeed: null,
|
||||||
|
|
||||||
nomadnetPageDownloadCallbacks: {},
|
nomadnetPageDownloadCallbacks: {},
|
||||||
nomadnetFileDownloadCallbacks: {},
|
nomadnetFileDownloadCallbacks: {},
|
||||||
@@ -756,26 +766,72 @@ export default {
|
|||||||
this.isDownloadingNodeFile = true;
|
this.isDownloadingNodeFile = true;
|
||||||
this.nodeFilePath = parsedUrl.path.split("/").pop();
|
this.nodeFilePath = parsedUrl.path.split("/").pop();
|
||||||
this.nodeFileProgress = 0;
|
this.nodeFileProgress = 0;
|
||||||
|
this.nodeFileDownloadStartTime = Date.now();
|
||||||
|
this.nodeFileLastProgressTime = Date.now();
|
||||||
|
this.nodeFileLastProgressValue = 0;
|
||||||
|
this.nodeFileDownloadSpeed = null;
|
||||||
|
|
||||||
// start file download
|
// start file download
|
||||||
this.downloadNomadNetFile(destinationHash, parsedUrl.path, (fileName, fileBytesBase64) => {
|
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
|
// no longer downloading
|
||||||
this.isDownloadingNodeFile = false;
|
this.isDownloadingNodeFile = false;
|
||||||
|
|
||||||
// download file to browser
|
// download file to browser
|
||||||
this.downloadFileFromBase64(fileName, fileBytesBase64);
|
this.downloadFileFromBase64(fileName, fileBytesBase64);
|
||||||
|
|
||||||
|
// Clear speed after a moment
|
||||||
|
setTimeout(() => {
|
||||||
|
this.nodeFileDownloadSpeed = null;
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
}, (failureReason) => {
|
}, (failureReason) => {
|
||||||
|
|
||||||
// no longer downloading
|
// no longer downloading
|
||||||
this.isDownloadingNodeFile = false;
|
this.isDownloadingNodeFile = false;
|
||||||
|
this.nodeFileDownloadSpeed = null;
|
||||||
|
|
||||||
// show error message
|
// show error message
|
||||||
DialogUtils.alert(`Failed to download file: ${failureReason}`);
|
DialogUtils.alert(`Failed to download file: ${failureReason}`);
|
||||||
|
|
||||||
}, (progress) => {
|
}, (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;
|
return;
|
||||||
@@ -829,6 +885,9 @@ export default {
|
|||||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
|
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
formatBytesPerSecond: function(bytesPerSecond) {
|
||||||
|
return Utils.formatBytesPerSecond(bytesPerSecond);
|
||||||
|
},
|
||||||
onNodeClick: function(node) {
|
onNodeClick: function(node) {
|
||||||
|
|
||||||
// update selected node
|
// update selected node
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ class Utils {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static formatNumber(num) {
|
||||||
|
if(num === 0){
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return num.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
static parseSeconds(secondsToFormat) {
|
static parseSeconds(secondsToFormat) {
|
||||||
secondsToFormat = Number(secondsToFormat);
|
secondsToFormat = Number(secondsToFormat);
|
||||||
var days = Math.floor(secondsToFormat / (3600 * 24));
|
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) {
|
static formatFrequency(hz) {
|
||||||
|
|
||||||
if(hz === 0 || hz == null){
|
if(hz === 0 || hz == null){
|
||||||
|
|||||||
Reference in New Issue
Block a user