lunes, 21 de diciembre de 2009

Análisis Gráfico de Excepciones Java en Bitácoras (Logs)

En ocasiones nos va tocar analizar qué está sucendiendo con nuestra aplicación en vivo porque aparentemente estamos teniendo "muchas" excepciones. Entrecomillo "muchas" porque como desarrolladores o encargados del monitoreo de la aplicación, alguien puede reportarnos que está viendo muchas excepciones en las bitácoras (logs) pero en realidad no sabemos si son muchas hasta que las contemos y veamos cuál es el promedio normal de apariciones de cierta excepción; en otras palabras, ver cuál es la tendencia de cierto tipo de excepciones. Si tenemos que analizar bitácoras enormes donde el comando buscar del editor de texto ("CTRL + F") no es suficiente para tener una perspectiva a gran escala, es entonces donde tenemos que atacar el problema de una forma más automatizada.

Quiero compartir en este artículo la fórmula que he utilizado en el pasado para analizar grandes cantidades de excepciones en grandes extenciones de bitácoras. Para generar el gráfico que vamos analizar uso un script de Perl para formatear los datos de una manera resumida y adecuada para luego procesarlos en una hoja de cálculo. En mi caso uso Calc de Open Office pero una hoja de Excel podría ser usada de igual manera.

El siguiente script de Perl extrae el nombre de la excepción y la fecha con la hora en que fue lanzada, y salva estos dos datos en dos columnas separadas por el carácter "~":


#!/usr/local/bin/perl

my $dir = $ARGV[0];

print "\n\n\t\tReading directory: " . $dir . " ...\n\n";
opendir(DIR, $dir);

my @files = readdir(DIR);
my $output = $dir."_results.txt";
closedir(DIR);

open (RESULTS, '>'.$output);
print RESULTS "date~exception\n";
$exception_counter = 0;

foreach $file( @files){
unless ( ($file eq ".") || ($file eq "..") ) {
my $total_size_read = 0;
my $file_path = $dir . '\\' . $file;
my $filesize;
{
use strict;
use warnings;
$filesize = -s $file_path ;
$filesize = $filesize / 1000; #Kbytes
}

print "\nReading file " .$file_path . "(" . $filesize . " kb)\n";
open (LOG, $file_path) || die "couldn't open the file!";

$line_number = 1;

while ($record = ) {

if ($record =~ m/([a-zA-Z1-9\.]*Exception)+/) {
$exception_description = $1;

if ($record =~ m/(((\d{4})-(\d{2})-(\d{2})\s(\d{2}))(:\d{2}:\d{2}))/) {
$year= $3;
$month=$4;
$day=$5;
$hour=$6;
print RESULTS $year . "-". $month . "-" . $day . " " . $hour . ":00~" . $exception_description ."\n" ;
$exception_counter++;
}
}

$line_number++;
if ($line_number % 1000 eq 0) {
my $progress = sprintf("%.2f",(($total_size_read /1000) / $filesize) * 100);
print "\rTotal lines read: " . $line_number . " | Exceptions found: " . $exception_counter . " | Progress:" . $progress . "%";

}
{
use bytes;
my $byte_size = length($record);
$total_size_read = $total_size_read + $byte_size;
}
}

close(LOG);
}
}

close (RESULTS);


No voy a detallar mucho la lógica del script la cual de todas maneras es bastante simple. Recibe como argumento el nombre de un directorio y procesa todos los archivos que hayan dentro de él para extraer las aparaciones de excepciones de cada archivo.



El resultado final del script es un archivo con el siguiente formato:

date~exception
2009-08-16 21:00~Exception
2009-08-16 21:00~Exception
2009-08-16 21:00~java.lang.NullPointerException
2009-08-16 21:00~Exception
2009-08-16 21:00~Exception
2009-08-16 21:00~Exception
2009-08-16 21:00~Exception
2009-08-16 21:00~Exception
2009-08-16 21:00~Exception
2009-08-16 21:00~Exception
2009-08-16 22:00~Exception
2009-08-16 22:00~Exception
2009-08-16 22:00~Exception
2009-08-16 22:00~Exception
2009-08-16 22:00~java.lang.NullPointerException


En este punto abrimos Calc de Open Office, seleccionamos los resultados del script, los copiamos y pegamos en la hoja de cálculo. Automáticamente se abrirá una caja de dialogo como la siguiente en la cual marcamos la caja de otros ("others") y escribimos el carácter de "raba de chancha" para separar los datos en dos columnas.

Una vez hecho esto es recomendable ordenar los datos de manera ascendente acorde con la columna de fecha para tener una secuencia cronológica de las excepciones. Seleccionamos las dos columnas excepto por la primera fila y usamos la opción de ordenamiento (Data->Sort...).



Seguidamente usamos, en mi opinión, una de las herramientas más poderosas de una hoja de cálculo: el DataPilot o Tabla Pivot en lenguaje Excel. El DataPilot nos permite agrupar datos de distintas columnas y contar o sumar el número de apariciones de acuerdo a las categorias que definimos con las columnas que seleccionamos. Seleccionamos las dos columnas, Data->DataPilot->Start...


En el diálogo que se abre arrastramos la caja "date" en los campos de columna, y la caja "exception" en los campos de fila. Tomamos nuevamente la caja "exception" y la arrastramos en el medio y damos clic en opciones. La opción por defecto es "Sum"; la cambiamos por "Count" para contar el número de apariciones por fecha y excepción. Más abajo hay otras opciones útiles que debemos usar:
  1. Resultados en una nueva hoja: "Results to - new sheet -".
  2. Ignorar filas vacias: "Ignore empty rows".

A partir de este momento podemos comenzar a analizar la línea cronológica de excepciones, o verificar un punto discreto de nuestra línea de tiempo si sabemos de antemano que algo sucedió en determinado día u hora. Para completar nuestro análisis vamos a generar un gráfico con la tabla que recien generamos.

Seleccionamos el rango de nuestra tabla para adelantarnos en uno de los pasos en la creación de nuestro gráfico. Buscamos el icono de gráfico ("chart") y lo presionamos.


1. Lo primero que tenemos que hacer es seleccionar el tipo de gráfico. En este caso uso el de puntos y líneas:


2.
Dado que ya había seleccionado el rango de datos, la primera caja ya contiene nuestra selección previa. Debemos seleccionar la opción de series de datos en filas ("data series in rows") y chequear las dos opciones de primera fila y columna como etiquetas.


3. En las series de datos no hay mucho que modificar. Podemos quitar la serie de "Total Result " si nos causa algún ruido en nuestro análisis. Siempre podemos regresar a modificar esta sección después de generado el gráfico.


4. En elementos del gráfico no cambiamos nada relevante. En mi caso me gusta cambiar la posición de las leyendas del eje Y, o sea cada uno de los nombre de excepción, para situarlas arriba del gráfico, de otra manera quedan posicionados a la derecha y resulta a veces que son demasiadas leyendas con nombres extensos que acortan el área del gráfico que nos interesa analizar.


Generamos finalmente nuestro gráfico el cual como podrán notar, sale un poco desordenado por la gran cantidad de elementos en el eje X. Para solucionar esto podemos editar el gráfico en las propiedades del eje X.

Clic derecho al gráfico -> Edit -> Clic derecho al eje X -> Object Properties
Vamos a la sección de etiqueta ("label") y rotamos el texto desde su posición de 0° hasta algún nivel de inclinación que nos permita apreciar mejor nuestro gráfico. En mi caso uso 45°. 90° sería también útil para no escalar tanto nuestro gráfico.


Finalmente quedamos con un producto final del cual podemos sacar algunas conclusiones interesantes. Por ejemplo podemos ver una tendencia hasta cierto punto normal de número de excepciones desde el periodo del 14 Agosto pero después súbitamente el número de excepciones de NullPointerException se disparan el 17 del mismo mes. ¿Habrá sucedido algún evento especial en esa fecha?


Como ven el gráfico a escala nos permite iniciar nuestro análisis de manera más focalizada sin comenzar a dar tumbos a ciegas. De aquí en adelante nos tocará tomar otros pasos para continuar la investigación, pero te aseguro que si presentas en una reunión a tu jefe y/0 cliente un gráfico de este tipo, vas a lucir mucho más profesional que simplemente decir:

-"Tenemos muchas excepciones de NullPointer"

miércoles, 16 de diciembre de 2009

Provincias, Cantones y Distritos

Un típico problema a resolver para los programadores de Costa Rica en aplicaciones locales ,es manejar los catálogos de provincias, cantones y distritos (PCD's). La relación de estos tres catálogos refleja una relación jerárquica donde una provincia agrupa cantones y un cantón agrupa distritos.

Así lucen los scripts para crear estas tablas en una base de datos MySQL:

Provincia:
DROP TABLE IF EXISTS `PROVINCIA_CR`;
CREATE TABLE `PROVINCIA_CR` (
  `codigo_provincia` smallint(5) unsigned NOT NULL auto_increment,
  `nombre_provincia` varchar(45) NOT NULL,
  PRIMARY KEY  USING BTREE (`codigo_provincia`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=latin1;

Cantón:
DROP TABLE IF EXISTS `CANTON_CR`;
CREATE TABLE `CANTON_CR` (
  `codigo_canton` smallint(5) unsigned NOT NULL,
  `codigo_provincia` smallint(5) unsigned NOT NULL,
  `nombre_canton` varchar(45) NOT NULL,
  PRIMARY KEY  USING BTREE (`codigo_canton`),
  KEY `FK_CANTON_PROVINCIA` (`codigo_provincia`),  
  CONSTRAINT `FK_CANTON_PROVINCIA` FOREIGN KEY (`codigo_provincia`) REFERENCES `PROVINCIA_CR` (`codigo_provincia`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Distrito:


DROP TABLE IF EXISTS `DISTRITO_CR`;
CREATE TABLE `DISTRITO_CR` (
  `codigo_distrito` int(10) unsigned NOT NULL,
  `codigo_canton` smallint(5) unsigned NOT NULL,
  `nombre_distrito` varchar(45) NOT NULL,  
  PRIMARY KEY  (`codigo_distrito`),
  KEY `FK_DISTRITO_CANTON` (`codigo_canton`),
  CONSTRAINT `FK_DISTRITO_CANTON` FOREIGN KEY (`codigo_canton`) REFERENCES `CANTON_CR` (`codigo_canton`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;


Hace poco necesitaba crear estos catálogos y tuve que darme a la tediosa tarea de manipular una página html con el contenido de las PCD's para poder generar las inserciones a la base de datos. Así que dejo a disposición dos documentos para cualquiera que les sea útil:
  1. Documento hoja de cálculo con toda la información necesaria. Se puede editar para generar las inserciones para otro administrador de BD que no sea MySQL.
  2. Documento con los scripts para crear las tablas y las inserciones.
En este otro artículo muesto como crear la serie de selectores para una interfaz web.

viernes, 11 de diciembre de 2009

Iterando sobre estructuras complejas de mapas

¿Te has encontrado alguna vez con estructura de datos anidadas y para colmo con un colocho de genéricos ("Generics")? Aún peor ¿has sido el autor intelectual de estas sentencias confusas? Dejame mostrar un ejemplo de lo que me refiero y que he encontrado al interactuar con las prácticas "inusuales" de algunos compañeros de proyecto.

En nuestra aplicación de ejemplo necesitamos guardar y mostrar la información de matrícula para un cuatrimestre de universidad. La información está estructurada de una manera jerárquica donde el primer nodo es el año en curso, luego le siguen los cuatrimestres, seguidamente los cursos y en el nivel más bajo los estudiantes enlistados en esos cursos.

Año (Year)
++ Cuatrimestre (Quatrimester)
+++ Curso (Course)
++++ Estudiante (Student)

Pareciera que podríamos sentarnos con una buena tacita de café, un hoja de papel y lapiz para sentarnos gustosamente a diseñar nuestras clases; bueno voy a mostrar lo que puede pasar
cuando alguien quiere brincarse este paso y "ahorrar" un poco de tiempo de diseño:


Map<Integer, Map<Integer, Map<String, List<Student>>>> studentsByQuarter = new HashMap<Integer, Map<Integer, Map<String, List<Student>>>>();


¿Para qué complicar más nuestra aplicación agregando más archivos para representar nuestras clases cuando todo se puede resumir en una sola linea? Ese es el acercamiento implicito al problema cuando alguien decide crear este nivel de complejidad que para él o ella puede ser comprendible pero pobre de la persona que tenga que mantener ese código y comenzar por comprender ese colocho.

Voy a desarrollar más esta manera de codificar para mostrar la complejidad intrínseca que se obtiene al no aislar los distintos componentes del problema que queremos resolver.

La clase "Student" as bastante simple con solamente un atributo "fullName":


public class Student {
private String fullName;

public Student(String fullName) {
this.fullName = fullName;
}

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}
}


Ahora agregamos un clase "EnrollmentProcessVr1" con la ya introducidad variable "monstruo" y dos métodos adicionales:

+ public static void startEnrollment(int year, int quarter){...} // ¨Para guardar los datos de matrícula.
+ public static void showEnrollment() {...} // Muestra los datos contenidos en la estructura de datos.

Veamos lo complicado que se vuelve tanto agregar como mostrar datos desde esta estructura:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class EnrollmentProcessVr1 {
private static Map<Integer, Map<Integer, Map<String, List<Student>>>> studentsByQuarter =
new HashMap<Integer, Map<Integer, Map<String, List<Student>>>>();

public static void startEnrollment(int year, int quarter) {
// Create a Year.
Integer currentYear = new Integer(year);

// Create a quatrimester.
Integer quatrimester = new Integer(quarter);

// Create courses.
String course1 = "Discrete Maths";
String course2 = "Programming I";

// Create List of Students.
List<Student> students = new ArrayList<Student>();
students.add(new Student("Cristobal Colón"));
students.add(new Student("Juan SantaMaría"));

// Create a Map of Courses/Students.
Map <String, List<Student>> studentsByCourses = new HashMap<String, List<Student>>();

// Add Students to courses.
studentsByCourses.put(course1, students);
studentsByCourses.put(course2, students);

// Create a Map of Quarter/Courses.
Map<Integer, Map <String, List<Student>>> coursesQuarterMap =
new HashMap<Integer, Map<String,List<Student>>>();

coursesQuarterMap.put(quatrimester, studentsByCourses);

// Add quarter to year.
studentsByQuarter.put(currentYear, coursesQuarterMap);
}

public static void showEnrollment() {
Iterator studentsByQuarterIterator = studentsByQuarter.entrySet().iterator();
while (studentsByQuarterIterator.hasNext()) {
Map.Entry studentsByQuarterPairs = (Map.Entry)studentsByQuarterIterator.next();
System.out.println("Year: " + String.valueOf(studentsByQuarterPairs.getKey()));

Map<Integer, Map<String, List<Student>>> quarterCoursesMap =
(Map<Integer, Map<String, List<Student>>>) studentsByQuarterPairs.getValue();

Iterator quarterCoursesIterator = quarterCoursesMap.entrySet().iterator();

while (quarterCoursesIterator.hasNext()) {
Map.Entry coursesByQuarterPairs = (Map.Entry)quarterCoursesIterator.next();
System.out.println("\tQuarter: " + String.valueOf(coursesByQuarterPairs.getKey()));

Map<String, List<Student>> coursesStudentsMap =
(Map<String, List<Student>>) coursesByQuarterPairs.getValue();

Iterator coursesStudentsIterator = coursesStudentsMap.entrySet().iterator();

while (coursesStudentsIterator.hasNext()) {
Map.Entry coursesStudentsPairs = (Map.Entry)coursesStudentsIterator.next();
System.out.println("\t\tCourse: " + String.valueOf(coursesStudentsPairs.getKey()));

List<Student> studentsByCourse = (List<Student>) coursesStudentsPairs.getValue();

for(Student student : studentsByCourse) {
System.out.println("\t\t\tStudent: " + student.getFullName());
}
}
}
}
}

public static void main(String[] args) {
EnrollmentProcessVr1.startEnrollment(2009, 3);
EnrollmentProcessVr1.showEnrollment();
}
}


Salida del método main:



Mi foco en este post será mostrar una manera de resolver la complejidad intrisica de manejar estructuras de mapa, no necesariamente como hacer "refactoring" de un mal diseño de estructura de datos, pero el desarrollo de este ejemplo ayuda a probar un punto de que incluso con un buen diseño de clases podemos tener mucho código para hacer un recorrido de las estructuras internas de mapa de nuestras clases.

Volvamos entonces al modo diseño de clases:




EnrollmentTrackVr1:


import java.util.LinkedHashMap;
import java.util.Map;

public class EnrollmentTrackVr1 {
private Map<Integer, QuatrimestersVr1> quatrimesters;

public EnrollmentTrackVr1() {
quatrimesters = new LinkedHashMap<Integer, QuatrimestersVr1>();
}

public Map<Integer, QuatrimestersVr1> getQuatrimesters() {
return quatrimesters;
}


public void addStudent(Student student, int year, int quatrimesterNumber, String courseName) {
if (!quatrimesters.containsKey(year)) {
quatrimesters.put(year, new QuatrimestersVr1());
}
quatrimesters.get(year).addCourse(quatrimesterNumber, courseName, student);
}
}



QuatrimestersVr1:


import java.util.HashMap;
import java.util.Map;

public class QuatrimestersVr1 {

private Map<Integer, CoursesVr1> courses;

public QuatrimestersVr1() {
courses = new HashMap<Integer, CoursesVr1>();
}

public void addCourse(int quatrimesterNumber, String courseName, Student student) {
if (!courses.containsKey(quatrimesterNumber)) {
courses.put(quatrimesterNumber, new CoursesVr1());
}
courses.get(quatrimesterNumber).addStudent(courseName, student);
}

public Map<Integer, CoursesVr1> getCourses() {
return courses;
}
}


CoursesVr1:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class CoursesVr1 {
private Map<String, List<Student>> students;

public CoursesVr1() {
students = new HashMap<String, List<Student>>();
}

public void addStudent(String courseName, Student student) {
if (!students.containsKey(courseName)) {
students.put(courseName, new ArrayList<Student>());
}
students.get(courseName).add(student);
}

public Map<String, List<Student>> getStudents() {
return students;
}

}


En la nueva versión de la clase EnrollmentProcess vemos como el código para agregar nueva información se simplifica significativamente pero la iteración de la estructura
sigue siendo un código a mi gusto complejo porque se necesita accesar los iteradores de cada una de las estructuras internas de cada clase de nuestro diseño:


import java.util.Iterator;
import java.util.List;
import java.util.Map;


public class EnrollmentProcessVr2 {
private static EnrollmentTrackVr1 enrollmentTrackVr1;

public static void startEnrollment(int year, int quarter) {

String course1 = "Discrete Maths";
String course2 = "Programming I";
Student student1 = new Student("Cristobal Colón");
Student student2 = new Student("Juan Santamaría");

enrollmentTrackVr1 = new EnrollmentTrackVr1();
enrollmentTrackVr1.addStudent(student1, year, quarter, course1);
enrollmentTrackVr1.addStudent(student2, year, quarter, course1);
enrollmentTrackVr1.addStudent(student1, year, quarter, course2);
enrollmentTrackVr1.addStudent(student2, year, quarter, course2);

}

public static void showEnrollment() {
Iterator enrollmentIterator =
enrollmentTrackVr1.getQuatrimesters().entrySet().iterator();

while (enrollmentIterator.hasNext()) {
Map.Entry enrollmentPairs = (Map.Entry)enrollmentIterator.next();
System.out.println("Year: " + String.valueOf(enrollmentPairs.getKey()));

QuatrimestersVr1 quatrimesters =
(QuatrimestersVr1) enrollmentPairs.getValue();

Iterator quatrimestersIterator =
quatrimesters.getCourses().entrySet().iterator();

while (quatrimestersIterator.hasNext()) {
Map.Entry quatrimestersPairs = (Map.Entry)quatrimestersIterator.next();
System.out.println("\tQuarter: " + String.valueOf(quatrimestersPairs.getKey()));

CoursesVr1 courses = (CoursesVr1) quatrimestersPairs.getValue();

Iterator coursesIterator =
courses.getStudents().entrySet().iterator();

while (coursesIterator.hasNext()) {
Map.Entry coursesPairs = (Map.Entry)coursesIterator.next();
System.out.println("\t\tCourse: " + String.valueOf(coursesPairs.getKey()));

List<Student> studentsByCourse = (List<Student>) coursesPairs.getValue();

for(Student student : studentsByCourse) {
System.out.println("\t\t\tStudent: " + student.getFullName());
}
}
}
}
}

public static void main(String[] args) {
EnrollmentProcessVr2.startEnrollment(2009, 3);
EnrollmentProcessVr2.showEnrollment();
}
}



¿No sería más pura vida que nuestras clases pudieran ser iteradas en la misma manera que hacemos con una lista de Java Collections?


List<String> fooList = new ArrayList<String>();
...
for (String fooObject : fooList) {
System.out.println(fooObject);
}


Es posible hacer esto si aprendemos a usar las interfaces "Iterable" y "Iterator". En el diseño que voy a mostrar a continuación uso mi propia interface la cual extiende de estas dos y además agregamos un nuevo método que nos será útil para retornar la llave del objeto en curso que estamos iterando (explicaré en detalle más adelante).

Así es como luce el nuevo diseño:



La nueva interface recibe un nuevo elemento genérico "K" que será el tipo/clase de la llave del mapa.


import java.util.Iterator;

public abstract interface MapIterable<K, V> extends Iterable<V>, Iterator<V>{
public K currentKey();
}


Explicamos a continuación en detalle cuáles métodos necesitamos implementar para que nuestras clases sean iterables:

+ public boolean hasNext() : este método indica si la clase puede avanzar en la estructura que está iterando. En mi implementación agrego una variable privada (currentKeyYearIndex)
para mantener el estado del índice actual del arreglo. Esta variable es incrementada con el método next().

+ public V next(): retorna el siguiente objeto de la iteración. En esta implementación obtengo el keyset del mapa interno de la clase y lo paso a un arreglo para obtener el elemento
de la locación indicada por el índice actual.

+ public void remove(): remueve el objeto actual. No necesitaba esta funcionalidad así que solamente arrojo una excepción de tipo UnsupportedOperationException;

+ public Iterator iterator(): Obtiene el iterador. Simplemente retorna la clase.

+ public K currentKey(): el nuevo método que agregué extra para retornar la llave actual de la iteración.

EnrollmentTrackVr2:


import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class EnrollmentTrackVr2 implements MapIterable<Integer, QuatrimestersVr2> {

private Map<Integer, QuatrimestersVr2> quatrimesters;
private int currentKeyYearIndex;

public EnrollmentTrackVr2() {
quatrimesters = new LinkedHashMap<Integer, QuatrimestersVr2>();
currentKeyYearIndex = 0;
}

public Map<Integer, QuatrimestersVr2> getQuatrimesters() {
return quatrimesters;
}


public void addStudent(Student student, int year, int quatrimesterNumber, String courseName) {
if (!quatrimesters.containsKey(year)) {
quatrimesters.put(year, new QuatrimestersVr2());
}
quatrimesters.get(year).addCourse(quatrimesterNumber, courseName, student);
}

public boolean hasNext() {
if (quatrimesters.keySet().toArray().length > currentKeyYearIndex) {
return true;
}
return false;
}

public QuatrimestersVr2 next() {
int currentKeyYear = (Integer)
quatrimesters.keySet().toArray()[currentKeyYearIndex++];
return quatrimesters.get(currentKeyYear);
}

public void remove() {
throw new UnsupportedOperationException();
}

public Iterator<QuatrimestersVr2> iterator() {
return this;
}
public Integer currentKey() {
return (Integer)
quatrimesters.keySet().toArray()[currentKeyYearIndex-1];
}
}


QuatrimestersVr2:


import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class QuatrimestersVr2 implements MapIterable<Integer, CoursesVr2> {

private Map<Integer, CoursesVr2> courses;
private int currentQuatriIndex;
private int year;

public QuatrimestersVr2() {
courses = new HashMap<Integer, CoursesVr2>();
currentQuatriIndex = 0;
}

public QuatrimestersVr2(int year) {
this();
this.year = year;
}

public void addCourse(int quatrimesterNumber, String courseName, Student student) {
if (!courses.containsKey(quatrimesterNumber)) {
courses.put(quatrimesterNumber, new CoursesVr2());
}
courses.get(quatrimesterNumber).addStudent(courseName, student);
}

public Map<Integer, CoursesVr2> getCourses() {
return courses;
}

public boolean hasNext() {
if (courses.keySet().toArray().length > currentQuatriIndex) {
return true;
}
return false;
}

public CoursesVr2 next() {
int currentQuatri = (Integer)
courses.keySet().toArray()[currentQuatriIndex++];
return courses.get(currentQuatri);
}

public void remove() {
throw new UnsupportedOperationException();
}

public Iterator<CoursesVr2> iterator() {
return this;
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public Integer currentKey() {
return (Integer)
courses.keySet().toArray()[currentQuatriIndex-1];
}
}



CoursesVr2:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class CoursesVr2 implements MapIterable<String, List<Student>> {

private Map<String, List<Student>> students;
private int currentCourseIndex;
private int quatrimester;

public CoursesVr2() {
students = new HashMap<String, List<Student>>();
currentCourseIndex = 0;
}

public CoursesVr2(int quatrimester) {
this();
this.quatrimester = quatrimester;
}

public void addStudent(String courseName, Student student) {
if (!students.containsKey(courseName)) {
students.put(courseName, new ArrayList<Student>());
}
students.get(courseName).add(student);
}

public Map<String, List<Student>> getStudents() {
return students;
}

public boolean hasNext() {
if (students.keySet().toArray().length > currentCourseIndex) {
return true;
}
return false;
}

public List<Student> next() {
String currentCourse =
students.keySet().toArray()[currentCourseIndex++].toString();
return students.get(currentCourse);
}

public void remove() {
throw new UnsupportedOperationException();
}

public Iterator<List<Student>> iterator() {
return this;
}

public int getQuatrimester() {
return quatrimester;
}

public void setQuatrimester(int quatrimester) {
this.quatrimester = quatrimester;
}

public String currentKey() {
return students.
keySet().toArray()[currentCourseIndex-1].toString();
}
}



Finalmente veamos como queda nuestro proceso de iteración:

EnrollmentProcessVr3:


import java.util.List;

public class EnrollmentProcessVr3 {
private static EnrollmentTrackVr2 enrollmentTrackVr2;

public static void startEnrollment(int year, int quarter) {

String course1 = "Discrete Maths";
String course2 = "Programming I";
Student student1 = new Student("Cristobal Colón");
Student student2 = new Student("Juan Santamaría");

enrollmentTrackVr2 = new EnrollmentTrackVr2();

enrollmentTrackVr2.addStudent(student1, year, quarter, course1);
enrollmentTrackVr2.addStudent(student2, year, quarter, course1);

enrollmentTrackVr2.addStudent(student1, year, quarter, course2);
enrollmentTrackVr2.addStudent(student2, year, quarter, course2);

}

public static void showEnrollment() {
for(QuatrimestersVr2 quatrimesters : enrollmentTrackVr2) {
System.out.println("Year: " + enrollmentTrackVr2.currentKey());
for (CoursesVr2 courses : quatrimesters) {
System.out.println("\tQuatrimester: " + quatrimesters.currentKey());
for (List<Student> students : courses) {
System.out.println("\t\tCourses: " + courses.currentKey());
for (Student student : students) {
System.out.println("\t\t\tStudent: " + student.getFullName());
}
}
}
}
}

public static void main(String[] args) {
EnrollmentProcessVr3.startEnrollment(2009, 3);
EnrollmentProcessVr3.showEnrollment();
}
}



El código puede que tenga algunas pulgas, creo que olvidé reinicializar la llave actual en algu punto, pero la idea no era tener un código a prueba de balas sino demostrar el poder de usar la interface "Iterable". Espero haya sido de interés este tema y ojalá puedan poner en práctica el uso de Iterable.

jueves, 3 de diciembre de 2009

Análizando el desempeño de nuestra aplicación web con Eclipse TPTP

Usar una herramienta de profiler (perdón por el anglisismo pero no encuentro una término adecuado en español, se puede decir "perfilador" supongo) es una buena manera de encontrar esos cuellos de botella de nuestra aplicación web.

Hoy quisiera recomendar una herramienta que he utilizado en el pasado. Puede que hayan otras más adecuadas dependiendo del tipo de análisis que se quiera efectuar, pero por ahora describiré rápidamente la que conozco: TPTP (Test and Perfomance Tools Platform) o "Prueba y Plataforma de Herramientas de Desempeño".

En mis propias palabras, un profiler es una herramienta que nos permite tener datos estadísticos para probar aquello que ya sabíamos antes (talvez de manera intituiva) por mucho tiempo, pero necesitabamos alguna herramienta sofisticadilla para culpar el mal desempeño de un código, esperando que no sea el que creamos nosotros :). Poniéndonos serios, el uso de estas herramientas para recopilar datos y resumirlos por nosotros realmente nos quita una carga cuando necesitamos apuntar de manera certera, qué parte de nuestra aplicación está degradando el desempeño.

Primero descargamos e instalamos TPTP en Eclipse. Puede ser descargado en un paquete "todo-en-uno" con la versión Galileo.




Abrimos el proyecto al que deseamos crear el perfil ("profile") y usamos la opción "profile" para comenzar el proyecto:

Clic derecho en el proyecto -> “Profile As“ -> “Profile Configurations...”



Seleccionar la opción de “Tomcat 5.x”:



Seleccionar el tipo de análisis. En este caso estamos seleccionando el Análisis de Tiempo de Ejecución (“Execution Time Analysis”).



Seleccionamos Editar Opciones ("Edit Options") para seleccionar el nivel de detalle del análisis:



Hay algunas otras opciones que pueden ser configuradas pero las dejamos a exploración del lector. Finalmente damos clic on "Profile". Eclipse iniciará el servidor Tomcat y la perspectiva "Profiling and Logging" se abrirá.



Doble clic en "Execution Time Analysis" para abrir la tabla de resumen:



Una vez que el profiler está corriendo, necesitamos navegar localmente en el sitio para que la mayoría, o al menos las partes de interés, de nuestro código sea examinado y así recolectar todos los datos de las diferentes clases de la aplicación.

Este análisis mide la cantidad de tiempo (en segundos) consumido por cada método de cada clase. En la tabla de resumen solamente los 10 primeros paquetes más altos en tiempo base ("base time") son mostrados. No obstante se puede ver el detalle de todos lo paquetes ejecutados si se desea.

Tiempo base (Base Time): La cantidad de tiempo (en segundos) que un método especifico acumuló en todas las llamadas de la apliación. Esto no incluye el tiempo de ejecución de otros métodos anidados (sub llamados a otros métodos).

Tiempo promedio base: (Average base time): Es el tiempo base divido entre el número total de llamados hechos por la aplicación.

Tiempo base acumulado (Cumulative base time): La cantidad de tiempo (en segundos) que al método le toma ejecutarse incluyendo todos los sub llamados hechos por el mismo.
Siempre: Tiempo base acumulado >= Tiempo base (Base Time)

Cabe concluir diciendo que esta operación de recolección de datos por parte del profiler es una operación sumamente pesada que consume muchos recursos del equipo dependiendo de lo complejo de la aplicación web. Es recomendable no hacer otras cosas en el equipo porque sino, por experiencia propia, el Eclipse tiende a congelarse y no responder más.