渲染优化
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 siteThemeQuery = null; // For media query cleanup
|
||||||
let siteThemeHandler = null;
|
let siteThemeHandler = null;
|
||||||
let backgroundIntervals = []; // To track setIntervals
|
let backgroundIntervals = []; // To track setIntervals
|
||||||
|
let lastMapDataHash = ''; // Cache for map rendering optimization
|
||||||
|
|
||||||
// Load sort state from localStorage or use default
|
// Load sort state from localStorage or use default
|
||||||
let currentSort = { column: 'up', direction: 'desc' };
|
let currentSort = { column: 'up', direction: 'desc' };
|
||||||
@@ -882,12 +883,46 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function updateMap2D(servers) {
|
// Debounced version of the map update to avoid rapid succession calls
|
||||||
if (!myMap2D) return;
|
// Optimized map update with change detection and performance tuning
|
||||||
|
const updateMap2D = debounce(function (servers) {
|
||||||
|
if (!myMap2D || !servers) return;
|
||||||
|
|
||||||
// Shift longitude for Pacific-centered view
|
// Shift longitude for Pacific-centered view
|
||||||
const shiftLng = (lng) => (lng < -20 ? lng + 360 : lng);
|
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
|
const geoData = servers
|
||||||
.filter(s => s.lat && s.lng)
|
.filter(s => s.lat && s.lng)
|
||||||
.map(s => ({
|
.map(s => ({
|
||||||
@@ -901,9 +936,10 @@
|
|||||||
netTx: s.netTx
|
netTx: s.netTx
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Combine all series
|
// Start with the scatter series
|
||||||
const finalSeries = [
|
const finalSeries = [
|
||||||
{
|
{
|
||||||
|
id: 'nodes-scatter',
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
coordinateSystem: 'geo',
|
coordinateSystem: 'geo',
|
||||||
geoIndex: 0,
|
geoIndex: 0,
|
||||||
@@ -914,11 +950,12 @@
|
|||||||
shadowColor: 'rgba(6, 182, 212, 0.5)'
|
shadowColor: 'rgba(6, 182, 212, 0.5)'
|
||||||
},
|
},
|
||||||
data: geoData,
|
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) {
|
if (currentLatencies && currentLatencies.length > 0) {
|
||||||
const countryCoords = {
|
const countryCoords = {
|
||||||
'china': [116.4074, 39.9042],
|
'china': [116.4074, 39.9042],
|
||||||
@@ -950,130 +987,114 @@
|
|||||||
'seoul': [126.9780, 37.5665]
|
'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 getShiftedCoords = (name) => {
|
||||||
const c = countryCoords[(name || '').toLowerCase().trim()];
|
const lower = (name || '').toLowerCase().trim();
|
||||||
if (c) return [shiftLng(c[0]), c[1]];
|
if (countryCoords[lower]) return [shiftLng(countryCoords[lower][0]), countryCoords[lower][1]];
|
||||||
const raw = getCoords(name);
|
const sCoord = serverCoords.get(lower);
|
||||||
if (raw) return [shiftLng(raw[0]), raw[1]];
|
if (sCoord) return sCoord;
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Group latency routes by path to handle overlap visually
|
|
||||||
const routeGroups = {};
|
const routeGroups = {};
|
||||||
currentLatencies.forEach(route => {
|
currentLatencies.forEach(route => {
|
||||||
const start = getShiftedCoords(route.source);
|
const start = getShiftedCoords(route.source);
|
||||||
const end = getShiftedCoords(route.dest);
|
const end = getShiftedCoords(route.dest);
|
||||||
if (start && end) {
|
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 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] = [];
|
if (!routeGroups[key]) routeGroups[key] = [];
|
||||||
routeGroups[key].push({ route, start, end });
|
routeGroups[key].push({ route, start, end });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const styleGroupedSeries = new Map();
|
||||||
|
|
||||||
Object.keys(routeGroups).forEach(key => {
|
Object.keys(routeGroups).forEach(key => {
|
||||||
const routes = routeGroups[key];
|
const routesInGroup = routeGroups[key];
|
||||||
const count = routes.length;
|
const count = routesInGroup.length;
|
||||||
|
|
||||||
routes.forEach((item, i) => {
|
routesInGroup.forEach((item, i) => {
|
||||||
const { route, start, end } = item;
|
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 lowS = (route.source || '').toLowerCase().trim();
|
||||||
const lowD = (route.dest || '').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 names = [lowS, lowD].sort();
|
||||||
const routeHash = names.join('').split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
const routeHash = names.join('').split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||||
|
|
||||||
let finalCurve = 0;
|
let finalCurve = 0.12;
|
||||||
if (isSeattleJapan) {
|
if (isSeattleJapan) {
|
||||||
// Special rule: Seattle ↔ Japan route curves upward (North)
|
finalCurve = (lowS === 'seattle' || lowS === 'us seattle') ? -0.2 : 0.2;
|
||||||
// Based on user feedback that previous signs resulted in a downward curve,
|
} else if (count > 1) {
|
||||||
// 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
|
|
||||||
const magnitude = 0.12 + Math.floor(i / 2) * 0.12;
|
const magnitude = 0.12 + Math.floor(i / 2) * 0.12;
|
||||||
const spread = (i % 2 === 0) ? magnitude : -magnitude;
|
const spread = (i % 2 === 0) ? magnitude : -magnitude;
|
||||||
finalCurve = isForward ? spread : -spread;
|
finalCurve = isForward ? spread : -spread;
|
||||||
}
|
}
|
||||||
|
|
||||||
finalSeries.push({
|
const period = 3 + ((routeHash + i) % 5);
|
||||||
type: 'lines',
|
const styleKey = `${finalCurve.toFixed(2)}_${period}`;
|
||||||
coordinateSystem: 'geo',
|
|
||||||
zlevel: 2,
|
if (!styleGroupedSeries.has(styleKey)) {
|
||||||
latencyLine: true,
|
styleGroupedSeries.set(styleKey, {
|
||||||
effect: {
|
id: 'latency-group-' + styleKey,
|
||||||
show: true,
|
type: 'lines',
|
||||||
period: 3 + ((routeHash + i) % 5), // Deterministic random period: 3-7s to vary start/speed
|
coordinateSystem: 'geo',
|
||||||
trailLength: 0.1,
|
zlevel: 2,
|
||||||
color: 'rgba(99, 102, 241, 0.8)',
|
effect: {
|
||||||
symbol: 'arrow',
|
show: true,
|
||||||
symbolSize: 6
|
period: period,
|
||||||
},
|
trailLength: 0.1,
|
||||||
lineStyle: {
|
color: 'rgba(99, 102, 241, 0.8)',
|
||||||
color: 'rgba(99, 102, 241, 0.3)',
|
symbol: 'arrow',
|
||||||
width: 2,
|
symbolSize: 6
|
||||||
curveness: finalCurve
|
},
|
||||||
},
|
lineStyle: {
|
||||||
tooltip: {
|
color: 'rgba(99, 102, 241, 0.3)',
|
||||||
formatter: () => {
|
width: 2,
|
||||||
const latVal = (route.latency !== null && route.latency !== undefined) ? `${route.latency.toFixed(2)} ms` : '测量中...';
|
curveness: finalCurve
|
||||||
return `
|
},
|
||||||
<div style="padding: 4px;">
|
progressive: 50, // Progressive rendering for GPU offload
|
||||||
<div style="font-weight: 700;">${route.source} ↔ ${route.dest}</div>
|
animation: false,
|
||||||
<div style="font-size: 0.75rem; color: var(--accent-indigo); margin-top: 4px;">延时: ${latVal}</div>
|
tooltip: {
|
||||||
</div>
|
formatter: (params) => {
|
||||||
`;
|
const r = params.data.meta;
|
||||||
}
|
if (!r) return '';
|
||||||
},
|
const latVal = (r.latency !== null && r.latency !== undefined) ? `${r.latency.toFixed(2)} ms` : '测量中...';
|
||||||
data: [{
|
return `
|
||||||
fromName: route.source,
|
<div style="padding: 4px;">
|
||||||
toName: route.dest,
|
<div style="font-weight: 700;">${r.source} ↔ ${r.dest}</div>
|
||||||
coords: [start, end]
|
<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({
|
myMap2D.setOption({ series: finalSeries }, { replaceMerge: ['series'] });
|
||||||
series: finalSeries
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update footer stats
|
|
||||||
if (dom.globeTotalNodes) dom.globeTotalNodes.textContent = geoData.length;
|
if (dom.globeTotalNodes) dom.globeTotalNodes.textContent = geoData.length;
|
||||||
if (dom.globeTotalRegions) {
|
if (dom.globeTotalRegions) {
|
||||||
const regions = new Set(geoData.map(d => d.country)).size;
|
dom.globeTotalRegions.textContent = new Set(geoData.map(d => d.country)).size;
|
||||||
dom.globeTotalRegions.textContent = regions;
|
|
||||||
}
|
}
|
||||||
}
|
}, 800);
|
||||||
|
|
||||||
// ---- Update Dashboard ----
|
// ---- Update Dashboard ----
|
||||||
function updateDashboard(data) {
|
function updateDashboard(data) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class AreaChart {
|
|||||||
|
|
||||||
this.prevMaxVal = 0;
|
this.prevMaxVal = 0;
|
||||||
this.currentMaxVal = 0;
|
this.currentMaxVal = 0;
|
||||||
|
this.lastDataHash = ''; // Fingerprint for optimization
|
||||||
|
|
||||||
// Use debounced resize for performance and safety
|
// Use debounced resize for performance and safety
|
||||||
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
|
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
|
||||||
@@ -40,6 +41,14 @@ class AreaChart {
|
|||||||
setData(data) {
|
setData(data) {
|
||||||
if (!data || !data.timestamps) return;
|
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
|
// Store old data for smooth transition before updating this.data
|
||||||
// Only clone if there is data to clone; otherwise use empty set
|
// Only clone if there is data to clone; otherwise use empty set
|
||||||
if (this.data && this.data.timestamps && this.data.timestamps.length > 0) {
|
if (this.data && this.data.timestamps && this.data.timestamps.length > 0) {
|
||||||
@@ -335,6 +344,7 @@ class MetricChart {
|
|||||||
|
|
||||||
this.prevMaxVal = 0;
|
this.prevMaxVal = 0;
|
||||||
this.currentMaxVal = 0;
|
this.currentMaxVal = 0;
|
||||||
|
this.lastDataHash = ''; // Fingerprint for optimization
|
||||||
|
|
||||||
// Use debounced resize for performance and safety
|
// Use debounced resize for performance and safety
|
||||||
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
|
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
|
||||||
@@ -358,6 +368,15 @@ class MetricChart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
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) {
|
if (this.data && this.data.values && this.data.values.length > 0) {
|
||||||
this.prevData = JSON.parse(JSON.stringify(this.data));
|
this.prevData = JSON.parse(JSON.stringify(this.data));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user