diff --git a/fetch.py b/fetch.py index fc5439f..32af3e2 100644 --- a/fetch.py +++ b/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 - if proxy in _proxy_valid_cache: - return _proxy_valid_cache[proxy] + 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 diff --git a/static/dashboard.js b/static/dashboard.js index 2a44846..4e87ddd 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -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 - renderLineChart('rateChart', d.rate_history, '#58a6ff', d.peak_rate * 1.1); - renderLineChart('srChart', d.success_rate_history, '#3fb950', 100); - renderHistogram('latencyHisto', d.latency_histogram); + // 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]; }); - 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); + 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 += '
'; @@ -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 += '' + code + '' + fmt(cnt) + ''; chtml += '' + pctVal + '%
'; }); - renderDoughnutChart('countryPie', cLabels, cValues, cColors); + if (activeTab === 'overview' || activeTab === 'db') { + renderDoughnutChart('countryPie', cLabels, cValues, cColors); + } $('countryLegend').innerHTML = chtml; } else { $('countryLegend').innerHTML = '
No data
'; @@ -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();