Archives par mot-clé : QScope.org

Nouvelle statistique dans QScope 2.15.2

QScope Historical chart QSOs per band over years - XV4Y (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2015/10/HistoricalQSOsBands NULL.png)Ces derniers jours j’ai pris le temps de travailler un peu sur QScope, mon application en ligne de statistiques pour logs radioamateurs. Tout d’abord j’ai tenu compte des besoins exprimés par les utilisateurs (en particulier les DXers) et j’ai rajouté une nouvelle statistique appelée Historical. Cette dernière permet d’avoir une vue de l’évolution du nombre de QSO par an réparties par modes ou par bandes. Le nombre de DXCC est aussi disponible, mais il s’agit de slots c’est à dire que la somme des DXCC par bande sera supérieure au chiffre global des DXCC. Ces informations sont disponibles sous forme de graphique ou de chiffre bruts. Un dessin valant mieux qu’un long discours, le graphe en illustration vous donnera une bonne idée de la statistique.

J’ai aussi apporté au fil de l’eau quelques améliorations mineures. La plus importante étant que maintenant QScope peut détecter automatiquement s’il y a besoin d’ajouter les informations géographiques au log (DXCC, Zones, Continent) basées sur les Country-Files de Jim AD1C. La présentation de la liste des logs publics a aussi été améliorée pour faciliter la visibilité d’un grand nombre de logs.

Pour finir, l’outil de calcul des résultats du DX Marathon (http://dxmarathon NULL.qscope NULL.org) a aussi vu quelques modifications. Il existe maintenant deux listes de Invalid Calls et  (http://www NULL.dxmarathon NULL.com/helpfulhints/2015/HelpfulHints2015 NULL.htm)Bad Spots (http://www NULL.dxmarathon NULL.com/helpfulhints/2015/HelpfulHints2015 NULL.htm). Dans le premier cas le QSO est purement et simplement retiré du décompte et l’utilisateur en est informé, dans le deuxième cas ce QSO douteux est remonté auprès de l’utilisateur mais gardé pour le décompte. Les listes sont mises à jour à chaque fois que John K9EL m’en informe.

Bibliothèque PHP de traitement de fichiers logs ADIF v3

Logo QScope.org (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/08/logo_qscope_b NULL.png)Ceci est une nouvelle version de la bibliothèque PHP que j’avais précédemment publié (http://xv4y NULL.radioclub NULL.asia/2013/10/15/bibliotheque-php-de-traitement-de-fichiers-logs-adif-v2/). Elle accepte maintenant conforme à la norme ADIF v3 et accepte de manière transparente les fichiers au format traditionnels ADI ou au nouveau format ADX (XML). C’est la librairie que j’utilise pour mon site d’analyse de log en ligne QScope.org.

Ce code ne gagnerait pas un concours de beauté, mais il fonctionne. J’avoue que les tests avec le format XML ont été limités à l’exemple donné avec la norme ADIF v3, car je n’ai pas de logiciel capable de générer de tels fichiers. Aucune comparaison de performance n’a été faite entre les deux formats, mais je soupçonne que le format traditionnel est un peu plus rapide à importer, même si la limite viendra plus rapidement de la base de données que du parsing.

<?php

/*

=== ADIF v3 Parser library v2.00 (2015-03-10) ===

   This Library his based on work by Jason Harris KJ4IWX but has been simplified and improved for better ADIF compatiblity by Yannick DEVOS XV4Y
   Version 2.00 introduces transparent support for ADX (XML) file format coming with ADIF v3 specifications
   
   Copyright 2011-2013 Jason Harris KJ4IWX (Apache v2.00 License)
   Copyright 2015 Yannick DEVOS XV4Y (GNU GPL v3.00 License)
   Any commercial use is subject to authorization from the respective authors
	
	This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

---

	This PHP library is designed to work with the Jelix 1.5 PHP Framework and PHP 5.4.
	However, it can simply be called by any PHP program.
	
	Usage for ADI and ADX files (with Jelix):
		$ADIFParser = jClasses::createInstance('yourmodule~ADIFParser');
		$ADIFParser->initialize($complete_path_to_the_file);

		echo ($ADIFParser->get_header('adif_ver'));			// Will display the ADIF version
		$all_headers_array = $ADIFParser->get_header(); 	// Will return the whole headers in an array
		while(($record = $ADIFParser->get_record())!=-1) {	// Check for end of file
			if ($record!=false) {							// If line is empty we skip
				if(count($record) == 0) {
					break;
				};
				echo ($record['call']);
				echo ($record['qso_date']);
				echo ($record['mode']);
				// Do whatever you like with the ADIF fields...
			};
		};

	Fore more infos find contacts at:
	http://xv4y.radioclub.asia/
	http://www.qscope.org/

*/

class ADIFParser
{
	var $is_adx = false;	// default to ADI file (traditionnal format)
	var $data; 				// the adif data
	var $i; 				// the iterator
	var $headers = array();
	
	public function initialize($file) //this function parses the header
	{
		// We read the first line and check if it is XML (ADX) or not
		$this->file_handler = fopen($file,'r');
		$firstline = fgets($this->file_handler);
		if (stripos($firstline, "<?xml")!==false) $this->is_adx = true;
		fclose($this->file_handler);

		if (!$this->is_adx) {	// This is an old fashion ADI file format
			$eoh=$eof=false;
			$this->data='';
		
			// And we start from the begining
			$this->file_handler = fopen($file,'r');
			while (!$eoh && !$eof) {	// We will try to locate the <EOH> tag
				$newdata = fgets($this->file_handler);	// $this->file_handler contains a handler in the case of ADI files
				if (!$newdata) {
					$eof = true;
				} else {
					if (stripos($newdata, "<eoh>")===false) {
						$this->data .= $newdata;
					} else {
						$eoh = true;
					}
				};
			}
			if($eoh == false) //did we find the end of headers?
			{
				// If this ADIF file has no header (like FT5ZM), we need to re-open the file because we already went to the end
				fclose($this->file_handler);
				$this->file_handler = fopen($file,'r');
				return 0;
			};
		
		
			//get headers
		
			$this->i = 0;
			$in_tag = false;
			$tag = "";
			$value_length = "";
			$value = "";
			$pos = strlen($this->data)-1;
				
			while($this->i < $pos)
			{
				//skip comments
				if($this->data[$this->i] == "#")
				{
					while($this->i < $pos)
					{
						if($this->data[$this->i] == "\n")
						{
							break;
						}
				
						$this->i++;
					}
				}else{
					//find the beginning of a tag
					if($this->data[$this->i] == "<")
					{
						$this->i++;
						//record the key
						while($this->data[$this->i] < $pos && $this->data[$this->i] != ':')
						{
							$tag = $tag.$this->data[$this->i];
							$this->i++;
						}
					
						$this->i++; //iterate past the :
					
						//find out how long the value is
					
						while($this->data[$this->i] < $pos && $this->data[$this->i] != '>')
						{
							$value_length = $value_length.$this->data[$this->i];
							$this->i++;
						}
					
						$this->i++; //iterate past the >
					
						$len = (int)$value_length;
						//copy the value into the buffer
						while($len > 0 && $this->i < $pos)
						{
							$value = $value.$this->data[$this->i];
							$len--;
							$this->i++;
						};

						$this->headers[strtolower(trim($tag))] = $value; //convert it to lowercase and trim it in case of \r
						//clear all of our variables
						$tag = "";
						$value_length = "";
						$value = "";
					
					}
				}
			
				$this->i++;
			};
			return 1;
		} else {	// This is the new ADX (XML) file format
			$this->is_adx = true;
			$this->xml_reader = new XMLReader;
			$this->xml_reader->open($file); // In this case, $file contains a file path
			if (!$this->xml_reader) return -1;	// Not able to open the file

			while ($this->xml_reader->read() && $this->xml_reader->name != 'HEADER');		// Looking for the header

			if ($this->xml_reader->name == 'HEADER') {
				// Load the current xml element into simplexml
				$xml = simplexml_load_string($this->xml_reader->readOuterXML());
				$this->headers = array_change_key_case(json_decode(json_encode($xml),TRUE),CASE_LOWER);

			} else {
				return 0;	// No header, this should mean invalid file for ADX
			}
			while ($this->xml_reader->read() && $this->xml_reader->name != 'RECORD');		// Now we go to the first RECORD
			if ($this->xml_reader->name != 'RECORD') {
				$this->xml_reader->close();
				return -2;	// This is an empty or invalid ADX file
			}
		return 1;	// All is normal
		}
	}
	
	//the following function does the processing of the array into its key and value pairs
	public function record_to_array(&$record)
	{
		$return = array();
		for($a = 0; $a < strlen($record); $a++)
		{
			if($record[$a] == '<') //find the start of the tag
			{
				$tag_name = "";
				$value = "";
				$len_str = "";
				$len = 0;
				$a++; //go past the <
				while($record[$a] != ':') //get the tag
				{
					$tag_name = $tag_name.$record[$a]; //append this char to the tag name
					$a++;
				};
				$a++; //iterate past the colon
				while($record[$a] != '>' && $record[$a] != ':')
				{
					$len_str = $len_str.$record[$a];
					$a++;
				};
				if($record[$a] == ':')
				{
					while($record[$a] != '>')
					{
						$a++;
					};
				};
				$len = (int)$len_str;
				while($len > 0)
				{
					$a++;
					$value = $value.$record[$a];
					$len--;
				};
				$return[strtolower($tag_name)] = $value;
			};
			//skip comments
			if($record[$a] == "#")
			{
				while($a < strlen($record))
				{
					if($record[$a] == "\n")
					{
						break;
					}
					$a++;
				}
			}
		};
		return $return;
	}
	
	
	//finds the next record in the file
	public function get_record()
	{
		if (!$this->is_adx) {
			$record ='';
			$out_condition = false;
			while (!$out_condition) {
				$newdata = fgets($this->file_handler);
				if ($newdata===false) {
					$out_condition = true;
					fclose($this->file_handler);
					return -1;	// This is the end of file
				} else {
					$end = stripos($newdata , "<eor>");
					if($end !== false) { //we got a full line
						$record .= substr($newdata, 0, $end);
						$out_condition = true;
						return $this->record_to_array($record); //process and return output
					} else {
					$record .= $newdata;	// The line is not complete and we need to fetch a new string
					}
				}
			}
		} else {
			// Loop through the Records
			if ($this->xml_reader->name == 'RECORD') {
				// Load the current xml element into simplexml and we’re off and running!
				$xml = simplexml_load_string($this->xml_reader->readOuterXML());
				$this->xml_reader->next('RECORD');
				$array = array_change_key_case(json_decode(json_encode($xml),TRUE),CASE_LOWER);

				return $array;
			} else {
				$this->xml_reader->close();
				return -1;
			}
		}
 	}
	
	public function get_header($key='')	// Changed to return the whole array if no key provided
	{
		if(array_key_exists(strtolower($key), $this->headers))
		{
			return $this->headers[strtolower($key)];
		} else if (empty($key)) {
			return $this->headers;
		} else {
			return NULL;
		}
	}
	
}
?>

Nouveautés dans QScope 2.8.1

Ces derniers jours j’ai pris un peu de temps pour améliorer QScope, mon service d’analyse de logs en ligne. En effet, plusieurs “responsables d’états” dans le les opérations portables W1AW du centenaire de l’ARRL (http://www NULL.arrl NULL.org/centennial-qso-party) utilisent QScope pour faire leurs statistiques. Comme ils doivent regrouper des fichiers logs d’origines diverses, ils sont en demande de facilités supplémentaires pour gérer leurs logs.

En fait, c’est d’ailleurs une tendance générale. Alors qu’à l’origine j’avais pensé QScope pour être purement un outil de statistiques, les besoins exprimés par les utilisateurs m’y ont fait ajouter beaucoup de fonctionnalités périphériques. Aujourd’hui les gens sont en attente d’applications “en ligne” et de données “dans le cloud“, QScope correspond bien à cette vision. Alors que maintenant la base de statistiques de QScope est bien ferme, les nouveautés que je compte y intégrer dans les mois qui viennent sont plus orientées autour de la gestion des logs en général.

Toujours dans cette tendance, j’ai aussi ajouté de la souplesse au service de logs publics. Après avoir téléchargé votre cahier de trafic sur QScope, vous pouvez choisir de le rendre visible publiquement. Mieux encore, vous pouvez aussi choisir d’afficher avec chaque recherche un ou plusieurs groupes de statistiques pour ce log. Par exemple, sur ma page de résultats de recherche j’affiche la carte des DXCCs que j’ai contacté, la répartition de mes QSOs par continents, bandes et mode

QScope sur GoogleEarth XV4Y (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2014/03/QScope_GoogleEarth NULL.png)Une dernière amélioration dont je suis plutôt fier, c’est celle de Log Replay. Elle m’a été demandé par K9CT qui prépare des présentations pour les grands salons qui sont à venir au printemps aux USA. Maintenant, en plus des fichiers KML pour les DXCCs et Locators, un fichier contenant l’ensemble des QSOs d’un log avec leur date et heure de contact est généré. Une fois chargé dans Google Earth, vous pouvez rejouer ce fichier dans le temps et vous voyez les QSOs s’afficher un par un sur la carte. C’est très utile pour étudier la propagation par exemple. La notion de date et heure a aussi été ajoutée aux fichiers KML DXCCs et Locators ce qui permet de voir votre progression durant un concours radio.

Punaises Virtuelles sur une carte des DXCC…

Carte générée par QScope avec le log XV4Y (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/11/Google_Maps_XV4Y_QScope NULL.png)Je viens d’ajouter une fonctionnalité intéressante à mon application de statistiques en ligne QScope (http://www NULL.qscope NULL.org/). Elle permet de générer un fichier KMZ (KML) qui peut être importé dans Google Maps ou Google Earth. Ce fichier contient la liste de toutes les entités DXCC que vous avez contacté et le nombre de QSOs.

Cela permet de créer des cartes intéressantes, montrant de manière très visuelle les entités DXCC qui manquent à votre cahier de trafic. Pour ma part, j’étais surpris de voir que peu de pays Africains n’ont pas encore garni mon log. L’Afrique de l’Est est plutôt facile à contacter pour moi car j’ai des ouvertures lorsque l’Europe n’en a pas encore. C’est plus dur pour la partie méditerranéenne qui tourne ses antennes vers l’Europe durant les contests par exemple. Les plus gros manques sont dans les Antilles et l’Amérique du Sud. Les Antilles principalement parce que les ouvertures sont plus rares et que souvent les stations sont sous l’assaut simultané des stations nord-américaines et européennes. L’Amérique du Sud principalement par manque de stations dans certains pays et méconnaissance de leurs parts des possibilités d’ouverture vers ma zone.

Concernant cette nouvelle fonctionnalité de QScope, elle est encore au stade expérimental, mais je compte lui adjoindre la possibilité de générer une carte à partir des QRA Locator (pour les concours VHF). Quand j’aurai eu assez de retours sur sa fiabilité, je lui retirerai le statut d’expérimental tout comme pour la fonctionnalité de modifications des champs DXCC / Continent / Zones à partir des fichiers Country-Files (http://www NULL.country-files NULL.com/) de AD1C qui fait un excellent travail. La plus grosse difficulté n’est pas dans la génération des statistiques par elle-mêmes, mais dans l’interprétation des fichiers. Il y a de nombreuses incohérences entres noms de pays (Heard Islands ou Heard I. ou Heard Isl.) et préfixes (VK0/M ou VK0M) dans les différents fichiers, parfois même d’une même source! Il en résulte tout un tas de cas particuliers à faire sans pouvoir exclure les erreurs de recoupement…

Bibliothèque PHP de traitement de fichiers logs ADIF v2

Une nouvelle version de la bibliothèque, compatible ADIF v3, est disponible (http://xv4y NULL.radioclub NULL.asia/2015/03/10/bibliotheque-php-de-traitement-de-fichiers-logs-adif-v3/).

Comme promis, je mets à disposition de la communauté ma version de la librairie de traitement (parser) de fichiers logs ADIF v2 (http://adif NULL.org/). Elle accepte les fichiers issus des logiciels les plus courants actuellement (N1MM, DXKeeper, HAM Radio Deluxe, WinLog…) et en extrait les différents champs.

Aucune vérification n’est faite sur la cohérence des informations et celle-ci dépend donc du logiciel de cahier de trafic qui a généré ces fichiers. Contrairement au parser ADIF de KJ4IWX depuis lequel j’étais parti au début, cette librairie n’a aucune limite de taille et elle peut tout à fait traiter des logs de plusieurs centaines de milliers de lignes. Tout dépend du traitement que vous faites ensuite, mais sur une machine aux performances actuelles, on peut facilement espérer insérer 2000 QSOs par minute dans une base de données. Sur QScope (http://www NULL.qscope NULL.org/) où la qualité d’origine des logs importés est très aléatoire de nombreuses vérifications sont nécessaires et la vitesse chute à environ 500 QSOs par minute.

<?php

/*

=== ADIF v2 Parser library v1.00 (2013-10-11) ===

   This Library his based on work by Jason Harris KJ4IWX but has been simplified and improvedfor better ADIF compatiblity by Yannick DEVOS XV4Y
   Copyright 2011-2013 Jason Harris KJ4IWX (Apache v2.00 License)
   Copyright 2013 Yannick DEVOS XV4Y (GNU GPL v3.00 License)
   Any commercial use is subject to authorization from the respective authors
	
	This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

---

	This PHP library is designed to work with the Jelix 1.5 PHP Framework and PHP 5.4.
	However, it can simply be called by any PHP program.
	
	Usage (with Jelix):
		$ADIFParser = jClasses::createInstance('yourmodule~ADIFParser');
		$file_handler = fopen($complete_path_to_the_file,'r');
		$ADIFParser->initialize($file_handler);

		while(($record = $ADIFParser->get_record($file_handler))!=-1) {	// Check for end of file
			if ($record!=false) {	// If line is empty we skip
				if(count($record) == 0) {
					break;
				};
				echo ($record['call']);
				echo ($record['qso_date']);
				echo ($record['mode']);
				// Do whatever you like with the ADIF fields...
			};
		};
		fclose($file_handler);

	Fore more infos find contacts at:
	http://xv4y.radioclub.asia/
	http://www.qscope.org/

*/

class ADIFParser
{

	var $data; //the adif data
	var $i; //the iterator
	var $headers = array();
	
	public function initialize($fhandler) //this function locates the <EOH>
	{
		$eoh=$eof=false;
		$this->data='';
		while (!$eoh && !$eof) {
			$newdata = fgets($fhandler);
			if (!$newdata) {
				$eof = true;
			} else {
				if (stripos($newdata, "<eoh>")===false) {
					$this->data .= $newdata;
				} else {
				$eoh = true;
				}
			};
		}
		if($eoh == false) //did we find the end of headers?
		{
			return 0;
		};
		
		
		//get headers
		
		$this->i = 0;
		$in_tag = false;
		$tag = "";
		$value_length = "";
		$value = "";
		$pos = strlen($this->data)-1;
				
		while($this->i < $pos)
		{
			//skip comments
			if($this->data[$this->i] == "#")
			{
				while($this->i < $pos)
				{
					if($this->data[$this->i] == "\n")
					{
						break;
					}
				
					$this->i++;
				}
			}else{
				//find the beginning of a tag
				if($this->data[$this->i] == "<")
				{
					$this->i++;
					//record the key
					while($this->data[$this->i] < $pos && $this->data[$this->i] != ':')
					{
						$tag = $tag.$this->data[$this->i];
						$this->i++;
					}
					
					$this->i++; //iterate past the :
					
					//find out how long the value is
					
					while($this->data[$this->i] < $pos && $this->data[$this->i] != '>')
					{
						$value_length = $value_length.$this->data[$this->i];
						$this->i++;
					}
					
					$this->i++; //iterate past the >
					
					$len = (int)$value_length;
					//copy the value into the buffer
					while($len > 0 && $this->i < $pos)
					{
						$value = $value.$this->data[$this->i];
						$len--;
						$this->i++;
					};

					$this->headers[strtolower(trim($tag))] = $value; //convert it to lowercase and trim it in case of \r
					//clear all of our variables
					$tag = "";
					$value_length = "";
					$value = "";
					
				}
			}
			
			$this->i++;
			
		};
		return 1;
	}
	
	//the following function does the processing of the array into its key and value pairs
	public function record_to_array(&$record)
	{
		$return = array();
		for($a = 0; $a < strlen($record); $a++)
		{
			if($record[$a] == '<') //find the start of the tag
			{
				$tag_name = "";
				$value = "";
				$len_str = "";
				$len = 0;
				$a++; //go past the <
				while($record[$a] != ':') //get the tag
				{
					$tag_name = $tag_name.$record[$a]; //append this char to the tag name
					$a++;
				};
				$a++; //iterate past the colon
				while($record[$a] != '>' && $record[$a] != ':')
				{
					$len_str = $len_str.$record[$a];
					$a++;
				};
				if($record[$a] == ':')
				{
					while($record[$a] != '>')
					{
						$a++;
					};
				};
				$len = (int)$len_str;
				while($len > 0)
				{
					$a++;
					$value = $value.$record[$a];
					$len--;
				};
				$return[strtolower($tag_name)] = $value;
			};
			//skip comments
			if($record[$a] == "#")
			{
				while($a < strlen($record))
				{
					if($record[$a] == "\n")
					{
						break;
					}
					$a++;
				}
			}
		};
		return $return;
	}
	
	
	//finds the next record in the file
	public function get_record($fhandle)
	{
		$record ='';
		$out_condition = false;
		while (!$out_condition) {
			$newdata = fgets($fhandle);
			if ($newdata===false) {
				$out_condition = true;
				return -1;	// This is the end of file
			} else {
				$end = stripos($newdata , "<eor>");
				if($end !== false) { //we got a full line
					$record .= substr($newdata, 0, $end);
					$out_condition = true;
					return $this->record_to_array($record); //process and return output
				} else {
				$record .= $newdata;	// The line is not complete and we need to fetch a new string
				}
			}
		}
 	}
	
	public function get_header($key)
	{
		if(array_key_exists(strtolower($key), $this->headers))
		{
			return $this->headers[strtolower($key)];
		}else{
			return NULL;
		}
	}
	
}
?>

Plug-in PHP requêtes asynchrones PostgreSQL pour Jelix/jDb

Je mets à disposition au téléchargement ce plug-in jDb (Jelix) écrit en PHP qui est un driver PostgreSQL permettant les requêtes asynchrones avec la base de données (http://xv4y NULL.radioclub NULL.asia/ftp/JELIX_JDB_PGSQL_ASY NULL.zip).

Logo QScope.org (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/08/logo_qscope_b NULL.png)Dans le cadre de mon projet de statistiques en ligne QScope.org (http://www NULL.qscope NULL.org/), je me retrouve à faire des calculs assez intensifs sur la base de données PostgreSQL, en particulier pour calculer le temps d’opération et les nombres réels de QSO/heure en pointe. Sur les gros logs d’expéditions DX ou pour les logs étalés sur plusieurs mois, les requêtes sur la base de données peuvent se chiffrer en milliards de lignes… et jusque 10 minutes de temps d’exécution.

Logo Postgresql (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/09/Postgresqlelephant NULL.png)Si les performances de PostgreSQL sont très bonnes et qu’il gère admirablement bien les connexions multiples, il ne permet pas l’exécution parallèle des requêtes. Cette fonctionnalité est encore une exclusivité des bases de données commerciales comme Oracle, Sybase ou MS SQL Server. Cela veut dire qu’une grosse requête ne pourra être répartie sur plusieurs micro-processeurs (ou coeurs) d’un serveur. La solution pour gagner du temps c’est alors de lancer plusieurs requêtes en parallèle pour réduire le temps d’exécution et profiter au mieux des ressource du serveur. PHP étant nativement un langage interprété mono-thread, ceci n’est pas intuitif à mettre en place. Il dispose toutefois dans son pilote PostgreSQL de commandes permettant de lancer les requêtes de manière asynchrone et de récupérer les résultats ensuite. En ouvrant plusieurs connexions simultanées cela ouvre la voie à une forme de traitement parallèle.

Logo Jelix (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/09/logo_jelix NULL.png)Si vous utilisez le framework PHP Jelix (http://jelix NULL.org/), ce plugin est simple d’utilisation puisqu’il reprend le pilote jDb PostgreSQL d’origine en lui ajoutant 3 fonctions pour gérer les requêtes asynchrones. Dans l’archive ZIP (http://xv4y NULL.radioclub NULL.asia/ftp/JELIX_JDB_PGSQL_ASY NULL.zip), se trouve un Readme en français et en anglais qui vous donne l’API et les exemples pour utiliser ce plugin.

QScope avec K9W et TO2TT

Logo QScope.org (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/08/logo_qscope_b NULL.png)En ce moment j’ai peu de temps pour le blog. Mes diverses activités professionnelles prennent la première place, avec entre autres un projet d’ouverture de restaurant qui redevient d’actualité. Ensuite j’ai été pas mal sollicité sur QScope (http://www NULL.qscope NULL.org/), d’abord par des nouvelles statistiques suggérées par les utilisateurs, ensuite par les bogues à corriger puis finalement par des revues et bulletins DX qui souhaitent avoir plus de détails afin de publier un article complet sur le projet.

La bonne nouvelle c’est que les organisateurs de deux grosses Expéditions à Wake Island (K9W) (http://www NULL.wake2013 NULL.org/) et à Mayotte (TO2TT) (http://www NULL.i2ysb NULL.com/idt/) ont annoncé leur volonté d’utiliser QScope en interne pour l’organisation de leurs opérations. Les statistiques produites leur permettront d’avoir jour par jour toutes les informations pour mieux répartir les tours d’opérateurs et la répartition des activités par bandes et modes dans le temps. Le but étant de maximiser le nombre de QSOs afin de mieux “rentabiliser” le coût de ces organisations. Le nombre de QSOs pour de telles DXpeditions en fin d’opération s’élève vite à 100.000, et ces utilisateurs utiliseront les statistiques les plus coûteuses en terme de temps machine!

J’ai donc du passer du temps afin de “préparer l’avenir”. En effet, si maintenant l’application compte 600 utilisateurs enregistrés et plus de 3 millions de lignes de log téléchargées, l’effervescence des premiers jours s’est calmé en moyenne il y a 10 utilisateurs “actifs” par jour qui produisent des statistiques et 1,8 millions de lignes de log dans la base de données (les utilisateurs effacent souvent leur logs après avoir produit les statistiques). La base de données principale fait 250 Mo, et les index occupent autant. Quand on tient compte de la RAM du serveur occupée par le système, le SGBDR lui-même et les différents shared buffers, on se rend compte qu’on se rapproche vite de la capacité maximale disponible de 1 Go actuelle sur le serveur. Rapidement, au lieu de lire les données dans la mémoire cache rapide, c’est sur le disque dur très lent que le SGBDR ira chercher ses informations à traiter. Les temps de calcul seront multiplié par 10 au moins…

Deux solutions à cela : augmenter la RAM du serveur ou mieux répartir les données. La première solution est la plus rapide à mettre en oeuvre, le problème c’est que chez le fournisseur que j’ai choisi c’est aussi la plus coûteuse. Les prix sont très attractif pour l’entrée de gamme, mais ensuite on grimpe très vite. L’autre solution est donc beaucoup plus intéressante sur le long terme. La réponse que je souhaitais mettre en place s’appliquait en trois phases :

  1. Mettre en place le mécanisme de Paritionnement de Tables de PostgreSQL. Simple à mettre en place en théorie, même si entièrement manuel contrairement aux SGBDR commerciaux comme Oracle ou Sybase, il permet de diminuer la taille de chaque table à charger en RAM et la taille des index. Je pensais donc répartir mes utilisateurs sur 27 tables (les 26 lettres de l’alphabet plus un “fourre-tout”) en fonction de la première lettre de leur indicatif. Après quelques tâtonnements (une maquette avec 2 utilisateurs est bien plus facile qu’une base de production avec 600 comptes) tout fonctionnait parfaitement. Les tables les plus grosses faisaient 40 Mo et les index avaient bien diminué en taille. Le “hic” c’est que les performances étaient pires qu’avant!!! Le problème venait de la fonction choisie pour répartir les utilisateurs sur les différentes tables. Celle-ci était coûteuse en temps machine et la pénalité se paye à chaque requête. J’ai donc du faire marche arrière… Le partitionnement reste intéressant, mais pour une répartition plus légère à calculer (simple comparaison) comme celle sur une date.
  2. Donner la possibilité d’attribuer à chaque utilisateur une base de données différente. Là, le travail se résumait à pas mal de réécriture du code existant pour permettre de se connecter sur différentes base de données “à la volée”. Rien d’insurmontable surtout que j’avais envisagé la chose assez tôt. En pratique ça me permet d’isoler facilement les “gros” utilisateurs comme les DXpeditions. Ils feront leurs calculs gourmands sur une base de taille plus petite que la base principale, diminuant les temps de traitement. La compartimentation au niveau du serveur aidera aussi les utilisateurs de la base principale à être moins impactés puisque ce seront deux emplacements mémoire et deux emplacements disques différent qui seront consultés, limitant les phénomènes de “lock“.
  3. Permettre d’utiliser plusieurs petits serveurs plutôt qu’un gros. Techniquement la solution est la même que la précédente, sauf que cette fois-ci au lieu d’interroger une autre base de données du même serveur je vais la chercher sur une autre machine. Cela est très intéressant car en répartissant les utilisateurs sur plusieurs bases on évite que celles-ci grossissent de trop et deviennent trop lourdes à gérer. Du point de vue financier, c’est aussi le plus intéressant car en doublant le coût, on double réellement la capacité (CPU, RAM et disque) alors que dans la grille tarifaire du fournisseur, doubler le prix payé pour le serveur ne faisait que doubler la RAM. Le contrecoup c’est que ça fait deux systèmes à gérer et donc plus de temps à passer. Toutefois, si les deux systèmes sont identiques et disposent de bonnes procédures automatisées, cette maintenance reste limitée.

Comme vous le voyez, je suis assez satisfait du temps passé sur QScope car cela me permettra de répondre plus facilement à l’augmentation de la demande qui va venir avec la saison des concours et des DXpeditions. L’échec de la mise en place du paritionnement n’est que partiel, et dans la réalité le serveur se comporte mieux que je ne l’avais craint. En effet, en plus de la RAM physique de 1 Go qui est allouée, 4 Go de disque SSD très rapide sont vus par le système comme de la RAM et permettent de diminuer les temps de réponse du disque tant qu’on reste dans des limites acceptables.

Dernières bétas pour QScope.org


QScope : temps de trafic par bande (http://www NULL.qscope NULL.org/)
Ces derniers jours, j’ai pas mal travaillé sur mon site de statistiques et graphiques pour les logs radioamateurs (http://www NULL.qscope NULL.org/). Je suis plutôt satisfait des résultats et il ne me reste plus que deux petites modifications à apporter pour atteindre les objectifs fixés pour la version 1.00. Alors qu’au départ je ciblais uniquement les amateurs de concours radio, aujourd’hui je me rends compte que beaucoup des statistiques que le site calcule sont intéressantes aussi pour les DXers. Par exemple j’aime assez ce graphe de répartition de mon temps de trafic en fonction des bandes. Comparé au nombre de QSOs et aux nombres d’entités DXCC par bande, cela permet de voir quelles sont les bandes les plus productives.

QScope : Répartition temps de trafic (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2013/08/XV4Y_OpTimeBandPieChart NULL.png)Je suis aussi très content des performances et aujourd’hui l’application tourne très bien avec un serveur mono-processeur et seulement 512Mo de RAM! Ce sera une autre histoire avec 1000 utilisateurs bien sûr mais toutes les optimisations que j’ai apporté et les choix que j’ai fait dès le départ seront payant pour garantir que la qualité de service suivra les améliorations du matériel qui pourraient être nécessaires. En particulier, j’ai soigné mes procédures stockées ce qui sur certaines opérations a permis de diviser les temps de traitement par 10 et surtout de mieux répartir la charge entre le serveur web et la base de données. J’ai encore quelques petites pistes pour réduire le temps de traitement pour dessiner les graphes, mais c’est un peu plus expérimental et moins urgent.

Les fonctionnalités offertes par QScope.org (http://www NULL.qscope NULL.org/) sont les suivantes :

  • Nombre de QSOs, Temps de trafic
  • Nombre d’indicatifs uniques contactés, d’entités DXCC uniques, de zones CQ et ITU
  • Taux de QSO/heure moyen et en pointe pour des périodes de 10, 30 et 60 minutes
  • Synthèse de l’Activité montrant quelle bande, mode ou table de trafic étaient les plus productives durant un concours
  • Graphes avec le nombres de QSOs par indicatif, bande, mode, ou opérateur
  • Graphes avec le nombres de Points par indicatif, bande, mode, ou opérateur
  • Graphes avec le nombres de QSOs par zones CQ ou ITU
  • Graphes heure par heure montrant le Nombre de QSOs et Points, le Nombre de nouvelles zones CQ et ITU, le Nombre de nouveaux pays DXCC et Préfixes
  • Graphes avec le Temps de trafic par bande, mode et opérateur avec temps de pause paramétrable
  • Graphes minute par minute montrant l’évolution taux de QSOs/heure en pointe pour votre log entier ou par opérateur ou par bande, avec la durée d’échantillon du calcul des pointes paramétrable entre 5 et 60 minute
  • Graphes avec les taux de QSO/heure en pointe par bande, mode ou opérateur avec la durée d’échantillon du calcul des pointes paramétrable entre 5 et 60 minutes

Bien entendu les informations que vous pouvez afficher dépendent de celles disponibles dans vos logs. Le format ADIF contenant plus de détails que le format Cabrillo. Pour l’instant les logs que j’interprète le mieux proviennent des logiciels de N1MM, puis DXKeeper, puis Ham Radio Deluxe (HRD).

Logo QScope.org (http://www NULL.qscope NULL.org/)