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:
19
fetch.py
19
fetch.py
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user