dashboard: add keyboard shortcuts and optimize polling

- fetch.py: convert proxy validation cache to LRU with OrderedDict
  - thread-safe lock, move_to_end() on hits, evict oldest when full
- dashboard.js: add keyboard shortcuts (r=refresh, 1-9=tabs, t=theme, p=pause)
- dashboard.js: skip chart rendering for inactive tabs (reduces CPU)
This commit is contained in:
Username
2025-12-28 16:52:52 +01:00
parent 18ae73bfb8
commit e758ce7178
2 changed files with 88 additions and 20 deletions

View File

@@ -1,6 +1,7 @@
import re, random, time, string
import json
import threading
from collections import OrderedDict
import rocksock
import network_stats
from http2 import RsHttp, _parse_url
@@ -9,9 +10,11 @@ from misc import _log, tor_proxy_url
config = None
# Cache for is_usable_proxy() - avoids repeated validation of same proxy strings
_proxy_valid_cache = {}
# LRU cache for is_usable_proxy() - avoids repeated validation of same proxy strings
# Uses OrderedDict to maintain insertion order; oldest entries evicted when full
_proxy_valid_cache = OrderedDict()
_proxy_valid_cache_max = 10000
_proxy_valid_cache_lock = threading.Lock()
class FetchSession(object):
@@ -603,16 +606,20 @@ def is_usable_proxy(proxy):
- Invalid port (0, >65535)
- Private/reserved ranges
Results are cached to avoid repeated validation of the same proxy strings.
Results are cached using LRU eviction to avoid repeated validation.
"""
# Check cache first
with _proxy_valid_cache_lock:
if proxy in _proxy_valid_cache:
# Move to end (most recently used)
_proxy_valid_cache.move_to_end(proxy)
return _proxy_valid_cache[proxy]
result = _validate_proxy(proxy)
# Cache result (with size limit)
if len(_proxy_valid_cache) < _proxy_valid_cache_max:
with _proxy_valid_cache_lock:
# Evict oldest entries if at capacity
while len(_proxy_valid_cache) >= _proxy_valid_cache_max:
_proxy_valid_cache.popitem(last=False)
_proxy_valid_cache[proxy] = result
return result

View File

@@ -9,6 +9,17 @@ var uplotCharts = {};
// Chart.js instances (persistent)
var chartJsInstances = {};
// Get currently active tab ID
function getActiveTab() {
var activeBtn = document.querySelector('.tab-btn.active');
return activeBtn ? activeBtn.dataset.tab : 'overview';
}
// Check if a specific tab is currently active
function isTabActive(tabId) {
return getActiveTab() === tabId;
}
// Tab switching
function initTabs() {
$$('.tab-btn').forEach(function(btn) {
@@ -331,10 +342,15 @@ function update(d) {
// System
$('uptime').textContent = fmtTime(d.uptime_seconds);
// Charts
// Charts - only render for active tabs to reduce CPU usage
var activeTab = getActiveTab();
if (activeTab === 'overview' || activeTab === 'perf') {
renderLineChart('rateChart', d.rate_history, '#58a6ff', d.peak_rate * 1.1);
renderLineChart('srChart', d.success_rate_history, '#3fb950', 100);
}
if (activeTab === 'perf') {
renderHistogram('latencyHisto', d.latency_histogram);
}
// Protocol breakdown
var ps = d.proto_stats || {};
@@ -347,9 +363,9 @@ function update(d) {
rateEl.className = 'proto-rate ' + (s.success_rate < 20 ? 'tag-err' : s.success_rate < 50 ? 'tag-warn' : 'tag-ok');
});
// Results pie (Chart.js doughnut)
// Results pie (Chart.js doughnut) - only on overview tab
var passed = d.passed || 0, failed = d.failed || 0, total = passed + failed;
if (total > 0) {
if (activeTab === 'overview' && total > 0) {
renderDoughnutChart('resultsPie', ['Passed', 'Failed'], [passed, failed], ['#3fb950', '#f85149']);
}
$('passedLeg').textContent = fmt(passed);
@@ -357,13 +373,15 @@ function update(d) {
$('failedLeg').textContent = fmt(failed);
$('failedPct').textContent = pct(failed, total) + '%';
// Failures breakdown (Chart.js doughnut)
// Failures breakdown (Chart.js doughnut) - only on overview tab
var fhtml = '', failColors = ['#f85149','#db6d28','#d29922','#58a6ff','#a371f7','#39c5cf','#db61a2','#7d8590'];
if (d.failures && Object.keys(d.failures).length > 0) {
var cats = Object.keys(d.failures).sort(function(a,b) { return d.failures[b] - d.failures[a]; });
if (activeTab === 'overview') {
var failVals = cats.map(function(c) { return d.failures[c]; });
var failCols = cats.map(function(_, i) { return failColors[i % failColors.length]; });
renderDoughnutChart('failPie', cats, failVals, failCols);
}
cats.forEach(function(cat, i) {
var n = d.failures[cat], col = failColors[i % failColors.length];
fhtml += '<div class="legend-item"><div class="legend-dot" style="background:' + col + '"></div>';
@@ -377,7 +395,7 @@ function update(d) {
// Leaderboards (session data)
renderLeaderboard('topAsns', d.top_asns_session, 'asn', 'count');
// Country pie chart (Chart.js doughnut)
// Country pie chart (Chart.js doughnut) - only on overview/db tabs
var countryColors = ['#58a6ff','#3fb950','#d29922','#f85149','#a371f7','#39c5cf','#db61a2','#db6d28','#7ee787','#7d8590'];
if (d.db && d.db.top_countries && d.db.top_countries.length > 0) {
var countries = d.db.top_countries.slice(0, 8);
@@ -394,7 +412,9 @@ function update(d) {
chtml += '<span class="legend-name">' + code + '</span><span class="legend-val">' + fmt(cnt) + '</span>';
chtml += '<span class="sub" style="margin-left:4px">' + pctVal + '%</span></div>';
});
if (activeTab === 'overview' || activeTab === 'db') {
renderDoughnutChart('countryPie', cLabels, cValues, cColors);
}
$('countryLegend').innerHTML = chtml;
} else {
$('countryLegend').innerHTML = '<div style="color:var(--dim);font-size:11px">No data</div>';
@@ -707,6 +727,47 @@ function handleVisibilityChange() {
document.addEventListener('visibilitychange', handleVisibilityChange);
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Ignore if typing in input/textarea
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
var key = e.key.toLowerCase();
// r = refresh
if (key === 'r' && !e.ctrlKey && !e.metaKey) {
fetchStats();
return;
}
// 1-9 = switch tabs
if (key >= '1' && key <= '9') {
var tabs = document.querySelectorAll('.tab-btn');
var idx = parseInt(key) - 1;
if (tabs[idx]) {
tabs[idx].click();
}
return;
}
// t = cycle theme
if (key === 't' && !e.ctrlKey && !e.metaKey) {
var btn = document.getElementById('themeToggle');
if (btn) btn.click();
return;
}
// p = toggle polling pause
if (key === 'p' && !e.ctrlKey && !e.metaKey) {
if (pollInterval) {
stopPolling();
} else {
startPolling();
}
return;
}
});
// Initial start
if (!document.hidden) {
startPolling();