Merge pull request #2956 from HB9HIL/dxcluster_improvement

Dxcluster improvement
This commit is contained in:
Fabian Berg
2026-02-16 08:39:52 +01:00
committed by GitHub
6 changed files with 113 additions and 165 deletions

View File

@@ -892,6 +892,19 @@ $config['max_login_attempts'] = 3;
$config['enable_dxcluster_file_cache_band'] = false;
$config['enable_dxcluster_file_cache_worked'] = false;
/*
|--------------------------------------------------------------------------
| DXCluster Refresh Time
|--------------------------------------------------------------------------
| This defines the how often the DXCluster spots are refreshed in seconds. Default is 30 seconds.
| Be careful with this and do not set it too low because depending on how many QSOs a user has it
| can cause a lot of load on the server. Also consider enabling a proper caching (file caches are
| not recommended for very large installations) to reduce the load on the server.
|--------------------------------------------------------------------------
*/
$config['dxcluster_refresh_time'] = 30;
/*
|--------------------------------------------------------------------------
| Internal tools

View File

@@ -60,18 +60,20 @@ class Bandmap extends CI_Controller {
}
switch ($pageData['custom_date_format']) {
case "d/m/y": $pageData['custom_date_format'] = 'DD/MM/YY'; break;
case "d/m/Y": $pageData['custom_date_format'] = 'DD/MM/YYYY'; break;
case "m/d/y": $pageData['custom_date_format'] = 'MM/DD/YY'; break;
case "m/d/Y": $pageData['custom_date_format'] = 'MM/DD/YYYY'; break;
case "d.m.Y": $pageData['custom_date_format'] = 'DD.MM.YYYY'; break;
case "y/m/d": $pageData['custom_date_format'] = 'YY/MM/DD'; break;
case "Y-m-d": $pageData['custom_date_format'] = 'YYYY-MM-DD'; break;
case "M d, Y": $pageData['custom_date_format'] = 'MMM DD, YYYY'; break;
case "M d, y": $pageData['custom_date_format'] = 'MMM DD, YY'; break;
default: $pageData['custom_date_format'] = 'DD/MM/YYYY';
case "d/m/y": $pageData['custom_date_format'] = 'DD/MM/YY'; break;
case "d/m/Y": $pageData['custom_date_format'] = 'DD/MM/YYYY'; break;
case "m/d/y": $pageData['custom_date_format'] = 'MM/DD/YY'; break;
case "m/d/Y": $pageData['custom_date_format'] = 'MM/DD/YYYY'; break;
case "d.m.Y": $pageData['custom_date_format'] = 'DD.MM.YYYY'; break;
case "y/m/d": $pageData['custom_date_format'] = 'YY/MM/DD'; break;
case "Y-m-d": $pageData['custom_date_format'] = 'YYYY-MM-DD'; break;
case "M d, Y": $pageData['custom_date_format'] = 'MMM DD, YYYY'; break;
case "M d, y": $pageData['custom_date_format'] = 'MMM DD, YY'; break;
default: $pageData['custom_date_format'] = 'DD/MM/YYYY';
}
$data['dxcluster_refresh_time'] = $this->config->item('dxcluster_refresh_time') ?? 30;
$data['page_title'] = __("DXCluster");
$this->load->view('interface_assets/header', $data);
$this->load->view('bandmap/list',$pageData);

View File

@@ -2761,16 +2761,6 @@ class Logbook_model extends CI_Model {
// Load cache driver for file caching
$cache_enabled = $this->config->item('enable_dxcluster_file_cache_worked') === true;
// Gets already loaded in dxcluster controller
//
// if ($cache_enabled && !isset($this->cache)) {
// $this->load->driver('cache', [
// 'adapter' => $this->config->item('cache_adapter') ?? 'file',
// 'backup' => $this->config->item('cache_backup') ?? 'file',
// 'key_prefix' => $this->config->item('cache_key_prefix') ?? ''
// ]);
// }
// Cache TTL in seconds (15 minutes = 900 seconds)
$cache_ttl = 900;
@@ -2826,7 +2816,7 @@ class Logbook_model extends CI_Model {
$cache_key = "{$logbook_ids_key}|call|{$callsign}";
// Check in-memory cache first
if (!isset($this->spot_status_cache[$cache_key])) {
if (!array_key_exists($cache_key, $this->spot_status_cache)) {
// Check file cache
if ($cache_enabled) {
$file_cache_key = $this->dxclustercache->get_worked_call_key($logbook_ids_key, $callsign);
@@ -2845,7 +2835,7 @@ class Logbook_model extends CI_Model {
foreach (array_keys($dxccs) as $dxcc) {
$cache_key = "{$logbook_ids_key}|dxcc|{$dxcc}";
if (!isset($this->spot_status_cache[$cache_key])) {
if (!array_key_exists($cache_key, $this->spot_status_cache)) {
if ($cache_enabled) {
$file_cache_key = $this->dxclustercache->get_worked_dxcc_key($logbook_ids_key, $dxcc);
$cached_data = $this->cache->get($file_cache_key);
@@ -2861,7 +2851,7 @@ class Logbook_model extends CI_Model {
foreach (array_keys($continents) as $cont) {
$cache_key = "{$logbook_ids_key}|cont|{$cont}";
if (!isset($this->spot_status_cache[$cache_key])) {
if (!array_key_exists($cache_key, $this->spot_status_cache)) {
if ($cache_enabled) {
$file_cache_key = $this->dxclustercache->get_worked_cont_key($logbook_ids_key, $cont);
$cached_data = $this->cache->get($file_cache_key);
@@ -2893,84 +2883,48 @@ class Logbook_model extends CI_Model {
$dxccs_array = array_keys($dxccs_to_query);
$continents_array = array_keys($continents_to_query);
// Split into two queries for performance: worked (faster) and confirmed (pre-filtered)
$worked_queries = [];
$confirmed_queries = [];
$worked_bind_params = [];
$confirmed_bind_params = [];
// OPTIMIZATION: Use ONE query instead of two (worked + confirmed)
$combined_queries = [];
$bind_params = [];
if (!empty($callsigns_array)) {
$callsigns_placeholders = implode(',', array_fill(0, count($callsigns_array), '?'));
// Query 1: Get all worked combinations
// Index: idx_HRD_COL_CALL_station_id (station_id, COL_CALL, COL_TIME_ON)
$worked_queries[] = "
SELECT 'call' as type, COL_CALL as identifier, COL_BAND as band, COL_MODE as mode
// Single query with conditional aggregation for worked AND confirmed
$combined_queries[] = "
SELECT 'call' as type, COL_CALL as identifier, COL_BAND as band, COL_MODE as mode, 1 as worked, MAX(CASE WHEN ({$qsl_where}) THEN 1 ELSE 0 END) as confirmed
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_COL_CALL_station_id)
WHERE station_id IN ({$station_ids_placeholders})
AND COL_CALL IN ({$callsigns_placeholders})
GROUP BY COL_CALL, COL_BAND, COL_MODE
";
$worked_bind_params = array_merge($worked_bind_params, $logbooks_locations_array, $callsigns_array);
// Query 2: Get only confirmed combinations (pre-filtered by QSL status)
$confirmed_queries[] = "
SELECT 'call' as type, COL_CALL as identifier, COL_BAND as band, COL_MODE as mode
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_COL_CALL_station_id)
WHERE station_id IN ({$station_ids_placeholders})
AND COL_CALL IN ({$callsigns_placeholders})
AND ({$qsl_where})
GROUP BY COL_CALL, COL_BAND, COL_MODE
";
$confirmed_bind_params = array_merge($confirmed_bind_params, $logbooks_locations_array, $callsigns_array);
$bind_params = array_merge($bind_params, $logbooks_locations_array, $callsigns_array);
}
if (!empty($dxccs_array)) {
$dxccs_placeholders = implode(',', array_fill(0, count($dxccs_array), '?'));
// Index: idx_HRD_COL_DXCC_station_id (station_id, COL_DXCC, COL_TIME_ON)
$worked_queries[] = "
SELECT 'dxcc' as type, COL_DXCC as identifier, COL_BAND as band, COL_MODE as mode
$combined_queries[] = "
SELECT 'dxcc' as type, COL_DXCC as identifier, COL_BAND as band, COL_MODE as mode, 1 as worked, MAX(CASE WHEN ({$qsl_where}) THEN 1 ELSE 0 END) as confirmed
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_COL_DXCC_station_id)
WHERE station_id IN ({$station_ids_placeholders})
AND COL_DXCC IN ({$dxccs_placeholders})
GROUP BY COL_DXCC, COL_BAND, COL_MODE
";
$worked_bind_params = array_merge($worked_bind_params, $logbooks_locations_array, $dxccs_array);
$confirmed_queries[] = "
SELECT 'dxcc' as type, COL_DXCC as identifier, COL_BAND as band, COL_MODE as mode
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_COL_DXCC_station_id)
WHERE station_id IN ({$station_ids_placeholders})
AND COL_DXCC IN ({$dxccs_placeholders})
AND ({$qsl_where})
GROUP BY COL_DXCC, COL_BAND, COL_MODE
";
$confirmed_bind_params = array_merge($confirmed_bind_params, $logbooks_locations_array, $dxccs_array);
$bind_params = array_merge($bind_params, $logbooks_locations_array, $dxccs_array);
}
if (!empty($continents_array)) {
$continents_placeholders = implode(',', array_fill(0, count($continents_array), '?'));
// No specific index for COL_CONT - let MySQL optimizer choose
$worked_queries[] = "
SELECT 'cont' as type, COL_CONT as identifier, COL_BAND as band, COL_MODE as mode
$combined_queries[] = "
SELECT 'cont' as type, COL_CONT as identifier, COL_BAND as band, COL_MODE as mode, 1 as worked, MAX(CASE WHEN ({$qsl_where}) THEN 1 ELSE 0 END) as confirmed
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_station_id)
WHERE station_id IN ({$station_ids_placeholders})
AND COL_CONT IN ({$continents_placeholders})
GROUP BY COL_CONT, COL_BAND, COL_MODE
";
$worked_bind_params = array_merge($worked_bind_params, $logbooks_locations_array, $continents_array);
$confirmed_queries[] = "
SELECT 'cont' as type, COL_CONT as identifier, COL_BAND as band, COL_MODE as mode
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_station_id)
WHERE station_id IN ({$station_ids_placeholders})
AND COL_CONT IN ({$continents_placeholders})
AND ({$qsl_where})
GROUP BY COL_CONT, COL_BAND, COL_MODE
";
$confirmed_bind_params = array_merge($confirmed_bind_params, $logbooks_locations_array, $continents_array);
$bind_params = array_merge($bind_params, $logbooks_locations_array, $continents_array);
}
if (empty($worked_queries)) {
if (empty($combined_queries)) {
// Nothing to query, use cached data
foreach ($spots_by_callsign as $callsign => $callsign_spots) {
foreach ($callsign_spots as $spot) {
@@ -2980,134 +2934,92 @@ class Logbook_model extends CI_Model {
return $statuses;
}
// Execute worked query (faster - no QSL filter)
$worked_sql = implode(' UNION ALL ', $worked_queries);
$worked_query = $this->db->query($worked_sql, $worked_bind_params);
$worked_results = $worked_query->result_array();
// Execute confirmed query (only scans confirmed QSOs)
$confirmed_sql = implode(' UNION ALL ', $confirmed_queries);
$confirmed_query = $this->db->query($confirmed_sql, $confirmed_bind_params);
$confirmed_results = $confirmed_query->result_array();
$combined_sql = implode(' UNION ALL ', $combined_queries);
$query = $this->db->query($combined_sql, $bind_params);
$results = $query->result_array();
// Build comprehensive cache structure: identifier => [band|mode => status]
// This allows reusing data for ALL spots with same callsign/dxcc/continent
$call_data = []; // callsign => [band|mode => ['worked' => bool, 'confirmed' => bool]]
$dxcc_data = []; // dxcc => [band|mode => ['worked' => bool, 'confirmed' => bool]]
$cont_data = []; // continent => [band|mode => ['worked' => bool, 'confirmed' => bool]]
// Pre-allocate arrays to avoid repeated checks
$call_data = [];
$dxcc_data = [];
$cont_data = [];
// Process worked results first (mark as worked, not confirmed)
foreach ($worked_results as $row) {
// Pre-build mode mapping lookup table to avoid repeated function calls
$mode_cache = [];
// Process ALL results in one pass (worked AND confirmed combined)
foreach ($results as $row) {
$identifier = $row['identifier'];
$band = $row['band'];
$logbook_mode = $row['mode'];
$worked = (bool)$row['worked'];
$confirmed = (bool)$row['confirmed'];
// Convert logbook mode to spot mode category (phone/cw/digi)
$qrgmode = @$this->Modes->get_qrgmode_from_mode($logbook_mode);
$qrgmode_lower = strtolower($qrgmode ?? '');
// Check mode cache first to avoid redundant conversions
if (!isset($mode_cache[$logbook_mode])) {
// Convert logbook mode to spot mode category (phone/cw/digi)
$qrgmode = @$this->Modes->get_qrgmode_from_mode($logbook_mode);
$qrgmode_lower = strtolower($qrgmode ?? '');
// Check if qrgmode is valid (phone/cw/data/digi), otherwise use fallback
if (!empty($qrgmode) && in_array($qrgmode_lower, ['phone', 'cw', 'data', 'digi'])) {
$mode_category = $qrgmode_lower;
if ($mode_category === 'data') {
$mode_category = 'digi';
}
} else {
// Fallback to hardcoded mapping
$logbook_mode_upper = strtoupper($logbook_mode ?? '');
if (in_array($logbook_mode_upper, ['SSB', 'FM', 'AM', 'PHONE'])) {
$mode_category = 'phone';
} elseif (in_array($logbook_mode_upper, ['CW'])) {
$mode_category = 'cw';
// Check if qrgmode is valid (phone/cw/data/digi), otherwise use fallback
if (!empty($qrgmode) && in_array($qrgmode_lower, ['phone', 'cw', 'data', 'digi'])) {
$mode_cache[$logbook_mode] = ($qrgmode_lower === 'data') ? 'digi' : $qrgmode_lower;
} else {
$mode_category = 'digi';
// Fallback to hardcoded mapping
$logbook_mode_upper = strtoupper($logbook_mode);
if (in_array($logbook_mode_upper, ['SSB', 'FM', 'AM', 'PHONE'])) {
$mode_cache[$logbook_mode] = 'phone';
} elseif ($logbook_mode_upper === 'CW') {
$mode_cache[$logbook_mode] = 'cw';
} else {
$mode_cache[$logbook_mode] = 'digi';
}
}
}
$mode_category = $mode_cache[$logbook_mode];
$band_mode_key = $band . '|' . $mode_category;
// Store in appropriate data structure
if ($row['type'] === 'call') {
if (!isset($call_data[$identifier])) {
$call_data[$identifier] = [];
}
$call_data[$identifier][$band_mode_key] = [
'worked' => true,
'confirmed' => false
'worked' => $worked,
'confirmed' => $confirmed
];
} elseif ($row['type'] === 'dxcc') {
if (!isset($dxcc_data[$identifier])) {
$dxcc_data[$identifier] = [];
}
$dxcc_data[$identifier][$band_mode_key] = [
'worked' => true,
'confirmed' => false
'worked' => $worked,
'confirmed' => $confirmed
];
} elseif ($row['type'] === 'cont') {
if (!isset($cont_data[$identifier])) {
$cont_data[$identifier] = [];
}
$cont_data[$identifier][$band_mode_key] = [
'worked' => true,
'confirmed' => false
'worked' => $worked,
'confirmed' => $confirmed
];
}
}
// Now overlay confirmed results (update confirmed flag to true)
foreach ($confirmed_results as $row) {
$identifier = $row['identifier'];
$band = $row['band'];
$logbook_mode = $row['mode'];
// Convert logbook mode to spot mode category (phone/cw/digi)
$qrgmode = @$this->Modes->get_qrgmode_from_mode($logbook_mode);
$qrgmode_lower = strtolower($qrgmode ?? '');
// Check if qrgmode is valid (phone/cw/data/digi), otherwise use fallback
if (!empty($qrgmode) && in_array($qrgmode_lower, ['phone', 'cw', 'data', 'digi'])) {
$mode_category = $qrgmode_lower;
if ($mode_category === 'data') {
$mode_category = 'digi';
}
} else {
// Fallback to hardcoded mapping
$logbook_mode_upper = strtoupper($logbook_mode ?? '');
if (in_array($logbook_mode_upper, ['SSB', 'FM', 'AM', 'PHONE'])) {
$mode_category = 'phone';
} elseif (in_array($logbook_mode_upper, ['CW'])) {
$mode_category = 'cw';
} else {
$mode_category = 'digi';
}
}
$band_mode_key = $band . '|' . $mode_category;
if ($row['type'] === 'call') {
if (isset($call_data[$identifier][$band_mode_key])) {
$call_data[$identifier][$band_mode_key]['confirmed'] = true;
}
} elseif ($row['type'] === 'dxcc') {
if (isset($dxcc_data[$identifier][$band_mode_key])) {
$dxcc_data[$identifier][$band_mode_key]['confirmed'] = true;
}
} elseif ($row['type'] === 'cont') {
if (isset($cont_data[$identifier][$band_mode_key])) {
$cont_data[$identifier][$band_mode_key]['confirmed'] = true;
}
}
}
// Cache the complete data for each callsign/dxcc/continent (both in-memory and file)
// OPTIMIZATION: Batch file cache writes if enabled to reduce I/O operations
$file_cache_batch = [];
// Store worked items with their band/mode data
foreach ($call_data as $callsign => $data) {
$cache_key = "{$logbook_ids_key}|call|{$callsign}";
$this->spot_status_cache[$cache_key] = $data;
// Save to file cache for 15 minutes
if ($cache_enabled) {
$file_cache_key = $this->dxclustercache->get_worked_call_key($logbook_ids_key, $callsign);
$this->cache->save($file_cache_key, $data, $cache_ttl);
$file_cache_batch[$file_cache_key] = $data;
}
}
foreach ($dxcc_data as $dxcc => $data) {
@@ -3116,7 +3028,7 @@ class Logbook_model extends CI_Model {
if ($cache_enabled) {
$file_cache_key = $this->dxclustercache->get_worked_dxcc_key($logbook_ids_key, $dxcc);
$this->cache->save($file_cache_key, $data, $cache_ttl);
$file_cache_batch[$file_cache_key] = $data;
}
}
foreach ($cont_data as $cont => $data) {
@@ -3125,9 +3037,11 @@ class Logbook_model extends CI_Model {
if ($cache_enabled) {
$file_cache_key = $this->dxclustercache->get_worked_cont_key($logbook_ids_key, $cont);
$this->cache->save($file_cache_key, $data, $cache_ttl);
$file_cache_batch[$file_cache_key] = $data;
}
} // Cache NOT WORKED items (negative results) - store empty arrays
}
// Cache NOT WORKED items (negative results) - store empty arrays
// This prevents redundant database queries for callsigns/dxccs/continents not in logbook
foreach ($callsigns_array as $callsign) {
if (!isset($call_data[$callsign])) {
@@ -3136,7 +3050,7 @@ class Logbook_model extends CI_Model {
if ($cache_enabled) {
$file_cache_key = $this->dxclustercache->get_worked_call_key($logbook_ids_key, $callsign);
$this->cache->save($file_cache_key, [], $cache_ttl);
$file_cache_batch[$file_cache_key] = [];
}
}
}
@@ -3147,7 +3061,7 @@ class Logbook_model extends CI_Model {
if ($cache_enabled) {
$file_cache_key = $this->dxclustercache->get_worked_dxcc_key($logbook_ids_key, $dxcc);
$this->cache->save($file_cache_key, [], $cache_ttl);
$file_cache_batch[$file_cache_key] = [];
}
}
}
@@ -3158,9 +3072,15 @@ class Logbook_model extends CI_Model {
if ($cache_enabled) {
$file_cache_key = $this->dxclustercache->get_worked_cont_key($logbook_ids_key, $cont);
$this->cache->save($file_cache_key, [], $cache_ttl);
$file_cache_batch[$file_cache_key] = [];
}
}
}
if ($cache_enabled && !empty($file_cache_batch)) {
foreach ($file_cache_batch as $key => $data) {
$this->cache->save($key, $data, $cache_ttl);
}
} // Now map all spots to their status using cached data (query results + previously cached)
foreach ($spots_by_callsign as $callsign => $callsign_spots) {
foreach ($callsign_spots as $spot) {

View File

@@ -4,6 +4,7 @@
var cat_timeout_interval = "<?php echo $this->optionslib->get_option('cat_timeout_interval'); ?>";
var dxcluster_maxage = <?php echo $this->optionslib->get_option('dxcluster_maxage') ?? 60; ?>;
var custom_date_format = "<?php echo $custom_date_format ?>";
var dxcluster_refresh_time = <?php echo $dxcluster_refresh_time; ?>;
// Detect OS for proper keyboard shortcuts
var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;

View File

@@ -15,7 +15,7 @@
// CONFIGURATION & CONSTANTS
// ========================================
const SPOT_REFRESH_INTERVAL = 60; // Auto-refresh interval in seconds
const SPOT_REFRESH_INTERVAL = dxcluster_refresh_time; // Auto-refresh interval in seconds, defined via config.php (default: 30s)
const QSO_SEND_DEBOUNCE_MS = 3000; // Debounce for sending callsign to QSO form (milliseconds)
// Mode display capitalization lookup (API returns lowercase)

View File

@@ -892,6 +892,18 @@ $config['enable_eqsl_massdownload'] = false;
$config['enable_dxcluster_file_cache_band'] = false;
$config['enable_dxcluster_file_cache_worked'] = false;
/*
|--------------------------------------------------------------------------
| DXCluster Refresh Time
|--------------------------------------------------------------------------
| This defines the how often the DXCluster spots are refreshed in seconds. Default is 30 seconds.
| Be careful with this and do not set it too low because depending on how many QSOs a user has it
| can cause a lot of load on the server. Also consider enabling a proper caching (file caches are
| not recommended for very large installations) to reduce the load on the server.
|--------------------------------------------------------------------------
*/
$config['dxcluster_refresh_time'] = 30;
/*
|--------------------------------------------------------------------------
| Internal tools