渲染优化
This commit is contained in:
201
public/js/app.js
201
public/js/app.js
@@ -137,6 +137,7 @@
|
||||
let siteThemeQuery = null; // For media query cleanup
|
||||
let siteThemeHandler = null;
|
||||
let backgroundIntervals = []; // To track setIntervals
|
||||
let lastMapDataHash = ''; // Cache for map rendering optimization
|
||||
|
||||
// Load sort state from localStorage or use default
|
||||
let currentSort = { column: 'up', direction: 'desc' };
|
||||
@@ -882,12 +883,46 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
function updateMap2D(servers) {
|
||||
if (!myMap2D) return;
|
||||
// Debounced version of the map update to avoid rapid succession calls
|
||||
// Optimized map update with change detection and performance tuning
|
||||
const updateMap2D = debounce(function (servers) {
|
||||
if (!myMap2D || !servers) return;
|
||||
|
||||
// Shift longitude for Pacific-centered view
|
||||
const shiftLng = (lng) => (lng < -20 ? lng + 360 : lng);
|
||||
|
||||
// Create a data fingerprint to avoid redundant renders
|
||||
// We only care about geographical positions and latency connectivity for the map
|
||||
const dataFingerprint = JSON.stringify({
|
||||
serverCount: servers.length,
|
||||
latencyCount: currentLatencies.length,
|
||||
// Sample critical connectivity data (first/last items)
|
||||
lHash: currentLatencies.length > 0 ? (currentLatencies[0].id + currentLatencies[currentLatencies.length-1].id) : '',
|
||||
// Include theme to ensure theme toggle still triggers redraw
|
||||
theme: document.documentElement.classList.contains('light-theme') ? 'light' : 'dark'
|
||||
});
|
||||
|
||||
if (dataFingerprint === lastMapDataHash) {
|
||||
return;
|
||||
}
|
||||
lastMapDataHash = dataFingerprint;
|
||||
|
||||
// 1. Pre-build coord lookup for O(1) route lookup
|
||||
const serverCoords = new Map();
|
||||
servers.forEach(s => {
|
||||
if (s.lat && s.lng) {
|
||||
const coords = [shiftLng(s.lng), s.lat];
|
||||
const city = (s.city || '').toLowerCase().trim();
|
||||
const country = (s.country || '').toLowerCase().trim();
|
||||
const countryName = (s.countryName || '').toLowerCase().trim();
|
||||
|
||||
if (city) serverCoords.set(city, coords);
|
||||
if (country) serverCoords.set(country, coords);
|
||||
if (countryName) serverCoords.set(countryName, coords);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Prepare geoData for scatter points
|
||||
const geoData = servers
|
||||
.filter(s => s.lat && s.lng)
|
||||
.map(s => ({
|
||||
@@ -901,9 +936,10 @@
|
||||
netTx: s.netTx
|
||||
}));
|
||||
|
||||
// Combine all series
|
||||
// Start with the scatter series
|
||||
const finalSeries = [
|
||||
{
|
||||
id: 'nodes-scatter',
|
||||
type: 'scatter',
|
||||
coordinateSystem: 'geo',
|
||||
geoIndex: 0,
|
||||
@@ -914,11 +950,12 @@
|
||||
shadowColor: 'rgba(6, 182, 212, 0.5)'
|
||||
},
|
||||
data: geoData,
|
||||
zlevel: 1
|
||||
zlevel: 1,
|
||||
animation: false // Performance optimization
|
||||
}
|
||||
];
|
||||
|
||||
// Add latency routes if configured
|
||||
// 3. Process latency routes with grouping
|
||||
if (currentLatencies && currentLatencies.length > 0) {
|
||||
const countryCoords = {
|
||||
'china': [116.4074, 39.9042],
|
||||
@@ -950,130 +987,114 @@
|
||||
'seoul': [126.9780, 37.5665]
|
||||
};
|
||||
|
||||
const getCoords = (name) => {
|
||||
const lowerName = (name || '').toLowerCase().trim();
|
||||
if (countryCoords[lowerName]) return countryCoords[lowerName];
|
||||
|
||||
const s = servers.find(sv =>
|
||||
(sv.countryName || '').toLowerCase() === lowerName ||
|
||||
(sv.country || '').toLowerCase() === lowerName ||
|
||||
(sv.city || '').toLowerCase() === lowerName
|
||||
);
|
||||
if (s && s.lng && s.lat) return [shiftLng(s.lng), s.lat];
|
||||
return null;
|
||||
};
|
||||
|
||||
const getShiftedCoords = (name) => {
|
||||
const c = countryCoords[(name || '').toLowerCase().trim()];
|
||||
if (c) return [shiftLng(c[0]), c[1]];
|
||||
const raw = getCoords(name);
|
||||
if (raw) return [shiftLng(raw[0]), raw[1]];
|
||||
const lower = (name || '').toLowerCase().trim();
|
||||
if (countryCoords[lower]) return [shiftLng(countryCoords[lower][0]), countryCoords[lower][1]];
|
||||
const sCoord = serverCoords.get(lower);
|
||||
if (sCoord) return sCoord;
|
||||
return null;
|
||||
};
|
||||
|
||||
// Group latency routes by path to handle overlap visually
|
||||
const routeGroups = {};
|
||||
currentLatencies.forEach(route => {
|
||||
const start = getShiftedCoords(route.source);
|
||||
const end = getShiftedCoords(route.dest);
|
||||
if (start && end) {
|
||||
// Canonical points for grouping (independent of direction)
|
||||
// Sort by longitude then latitude
|
||||
const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
||||
const key = `${pts[0][0].toFixed(4)},${pts[0][1].toFixed(4)}_${pts[1][0].toFixed(4)},${pts[1][1].toFixed(4)}`;
|
||||
const key = `${pts[0][0].toFixed(2)},${pts[0][1].toFixed(2)}_${pts[1][0].toFixed(2)},${pts[1][1].toFixed(2)}`;
|
||||
if (!routeGroups[key]) routeGroups[key] = [];
|
||||
routeGroups[key].push({ route, start, end });
|
||||
}
|
||||
});
|
||||
|
||||
const styleGroupedSeries = new Map();
|
||||
|
||||
Object.keys(routeGroups).forEach(key => {
|
||||
const routes = routeGroups[key];
|
||||
const count = routes.length;
|
||||
const routesInGroup = routeGroups[key];
|
||||
const count = routesInGroup.length;
|
||||
|
||||
routes.forEach((item, i) => {
|
||||
routesInGroup.forEach((item, i) => {
|
||||
const { route, start, end } = item;
|
||||
|
||||
// Identify if the route is in the "canonical" direction
|
||||
const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
||||
const isForward = (start === pts[0]);
|
||||
|
||||
// Normalization and special routing rules
|
||||
const lowS = (route.source || '').toLowerCase().trim();
|
||||
const lowD = (route.dest || '').toLowerCase().trim();
|
||||
const isSeattleJapan = ( (lowS === 'seattle' || lowS === 'us seattle') && lowD === 'japan' ) ||
|
||||
( (lowD === 'seattle' || lowD === 'us seattle') && lowS === 'japan' );
|
||||
|
||||
const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
||||
const isForward = (start === pts[0]);
|
||||
const isSeattleJapan = ((lowS === 'seattle' || lowS === 'us seattle') && lowD === 'japan') ||
|
||||
((lowD === 'seattle' || lowD === 'us seattle') && lowS === 'japan');
|
||||
|
||||
// Canonical hash for deterministic randomization (period, etc)
|
||||
const names = [lowS, lowD].sort();
|
||||
const routeHash = names.join('').split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
|
||||
let finalCurve = 0;
|
||||
let finalCurve = 0.12;
|
||||
if (isSeattleJapan) {
|
||||
// Special rule: Seattle ↔ Japan route curves upward (North)
|
||||
// Based on user feedback that previous signs resulted in a downward curve,
|
||||
// we flip them to ensure the intended upward arc.
|
||||
const isWestbound = (lowS === 'seattle' || lowS === 'us seattle');
|
||||
finalCurve = isWestbound ? -0.2 : 0.2;
|
||||
} else if (count === 1) {
|
||||
// Subtle consistent curve for single routes to maintain parallelism
|
||||
finalCurve = 0.12;
|
||||
} else {
|
||||
// Symmetrical fan-out for multiple lines between the same points
|
||||
finalCurve = (lowS === 'seattle' || lowS === 'us seattle') ? -0.2 : 0.2;
|
||||
} else if (count > 1) {
|
||||
const magnitude = 0.12 + Math.floor(i / 2) * 0.12;
|
||||
const spread = (i % 2 === 0) ? magnitude : -magnitude;
|
||||
finalCurve = isForward ? spread : -spread;
|
||||
}
|
||||
|
||||
finalSeries.push({
|
||||
type: 'lines',
|
||||
coordinateSystem: 'geo',
|
||||
zlevel: 2,
|
||||
latencyLine: true,
|
||||
effect: {
|
||||
show: true,
|
||||
period: 3 + ((routeHash + i) % 5), // Deterministic random period: 3-7s to vary start/speed
|
||||
trailLength: 0.1,
|
||||
color: 'rgba(99, 102, 241, 0.8)',
|
||||
symbol: 'arrow',
|
||||
symbolSize: 6
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgba(99, 102, 241, 0.3)',
|
||||
width: 2,
|
||||
curveness: finalCurve
|
||||
},
|
||||
tooltip: {
|
||||
formatter: () => {
|
||||
const latVal = (route.latency !== null && route.latency !== undefined) ? `${route.latency.toFixed(2)} ms` : '测量中...';
|
||||
return `
|
||||
<div style="padding: 4px;">
|
||||
<div style="font-weight: 700;">${route.source} ↔ ${route.dest}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--accent-indigo); margin-top: 4px;">延时: ${latVal}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
data: [{
|
||||
fromName: route.source,
|
||||
toName: route.dest,
|
||||
coords: [start, end]
|
||||
}]
|
||||
const period = 3 + ((routeHash + i) % 5);
|
||||
const styleKey = `${finalCurve.toFixed(2)}_${period}`;
|
||||
|
||||
if (!styleGroupedSeries.has(styleKey)) {
|
||||
styleGroupedSeries.set(styleKey, {
|
||||
id: 'latency-group-' + styleKey,
|
||||
type: 'lines',
|
||||
coordinateSystem: 'geo',
|
||||
zlevel: 2,
|
||||
effect: {
|
||||
show: true,
|
||||
period: period,
|
||||
trailLength: 0.1,
|
||||
color: 'rgba(99, 102, 241, 0.8)',
|
||||
symbol: 'arrow',
|
||||
symbolSize: 6
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgba(99, 102, 241, 0.3)',
|
||||
width: 2,
|
||||
curveness: finalCurve
|
||||
},
|
||||
progressive: 50, // Progressive rendering for GPU offload
|
||||
animation: false,
|
||||
tooltip: {
|
||||
formatter: (params) => {
|
||||
const r = params.data.meta;
|
||||
if (!r) return '';
|
||||
const latVal = (r.latency !== null && r.latency !== undefined) ? `${r.latency.toFixed(2)} ms` : '测量中...';
|
||||
return `
|
||||
<div style="padding: 4px;">
|
||||
<div style="font-weight: 700;">${r.source} ↔ ${r.dest}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--accent-indigo); margin-top: 4px;">延时: ${latVal}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
styleGroupedSeries.get(styleKey).data.push({
|
||||
fromName: route.source,
|
||||
toName: route.dest,
|
||||
coords: [start, end],
|
||||
meta: route
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
styleGroupedSeries.forEach(s => finalSeries.push(s));
|
||||
}
|
||||
|
||||
myMap2D.setOption({
|
||||
series: finalSeries
|
||||
});
|
||||
myMap2D.setOption({ series: finalSeries }, { replaceMerge: ['series'] });
|
||||
|
||||
// Update footer stats
|
||||
if (dom.globeTotalNodes) dom.globeTotalNodes.textContent = geoData.length;
|
||||
if (dom.globeTotalRegions) {
|
||||
const regions = new Set(geoData.map(d => d.country)).size;
|
||||
dom.globeTotalRegions.textContent = regions;
|
||||
dom.globeTotalRegions.textContent = new Set(geoData.map(d => d.country)).size;
|
||||
}
|
||||
}
|
||||
}, 800);
|
||||
|
||||
// ---- Update Dashboard ----
|
||||
function updateDashboard(data) {
|
||||
|
||||
@@ -18,6 +18,7 @@ class AreaChart {
|
||||
|
||||
this.prevMaxVal = 0;
|
||||
this.currentMaxVal = 0;
|
||||
this.lastDataHash = ''; // Fingerprint for optimization
|
||||
|
||||
// Use debounced resize for performance and safety
|
||||
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
|
||||
@@ -40,6 +41,14 @@ class AreaChart {
|
||||
setData(data) {
|
||||
if (!data || !data.timestamps) return;
|
||||
|
||||
// 1. Data Fingerprinting: Skip redundant updates to save GPU/CPU
|
||||
const fingerprint = data.timestamps.length + '_' +
|
||||
(data.rx.length > 0 ? data.rx[data.rx.length - 1] : 0) + '_' +
|
||||
(data.tx.length > 0 ? data.tx[data.tx.length - 1] : 0);
|
||||
|
||||
if (fingerprint === this.lastDataHash) return;
|
||||
this.lastDataHash = fingerprint;
|
||||
|
||||
// Store old data for smooth transition before updating this.data
|
||||
// Only clone if there is data to clone; otherwise use empty set
|
||||
if (this.data && this.data.timestamps && this.data.timestamps.length > 0) {
|
||||
@@ -335,6 +344,7 @@ class MetricChart {
|
||||
|
||||
this.prevMaxVal = 0;
|
||||
this.currentMaxVal = 0;
|
||||
this.lastDataHash = ''; // Fingerprint for optimization
|
||||
|
||||
// Use debounced resize for performance and safety
|
||||
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
|
||||
@@ -358,6 +368,15 @@ class MetricChart {
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
if (!data || !data.timestamps) return;
|
||||
|
||||
// 1. Simple fingerprinting to avoid constant re-animation of same data
|
||||
const lastVal = data.values && data.values.length > 0 ? data.values[data.values.length - 1] : 0;
|
||||
const fingerprint = data.timestamps.length + '_' + lastVal + '_' + (data.series ? 's' : 'v');
|
||||
|
||||
if (fingerprint === this.lastDataHash) return;
|
||||
this.lastDataHash = fingerprint;
|
||||
|
||||
if (this.data && this.data.values && this.data.values.length > 0) {
|
||||
this.prevData = JSON.parse(JSON.stringify(this.data));
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user