Análisis de Rendimiento

Otra forma de aumentar el rendimiento es con ánalisis de rendimiento. Los análisis de rendimientos buscan las ejecución del programa apuntar donde podrían estar los cuellos de botella y otros problemas de rendimiento como los picos de memoria. Una vez que sables donde están los puntos de problemas potenciales podemos cambiar nuestro código para eliminar o reducir su impacto.

Perfiles

Las Máquinas Vituales JavaTM (JVMs) han tenido la habilidad de proporcionar sencillos informes de perfiles desde Java Development Kit (JDKTM) 1.0.2. Sin embargo, la información que ellos proporcionaban estaban limitadas a una lista de los métodos que un programa había llamado.

La plataforma Java® 2 proporciona muchas más capacidades de perfilado que las anteriormente disponibles y el análisis de estos datos generado se ha hecho más fácil por la emergencia de un "Heap Analysis Tool" (HAT). Esta herramienta, como implica su nombre, nos permite analizar los informes de perfiles del heap. El heap es un bloque de memoria que la JVM usa cuando se está ejecutando. La herramienta de análisis de heap nos permite generar informes de objetos que fueron usado al ejecutar nuestra aplicación. No sólo podemos obtener un listado de los métodos llamados más frecuentemente y la memoria usada en llamar a esos métodos, pero también podemos seguir los picos de memeoria. Los picos de memoria pueden tener un significante impacto en el rendimiento.

Analizar un Programa

Para analizar el programa TableExample3 incluido en el directorio demo/jfc/Table de la plataforma Java 2, necesitamos generar un informe de perfil. El informa más sencillo de generar es un perfil de texto. Para generarlo, ejecutamos la aplicación el parámetro -Xhprof. En la versión final de la plataforma Java 2, esta opción fue renombrada como -Xrunhprof. Para ver una lista de opciones actualmente disponibles ejecutamos el comando:
java -Xrunhprof:help
Hprof usage: -Xrunhprof[:help]|[<option>=<value>, ...]
Nombre de Opción y Valor Descripción Por Defecto
-------------------------- --------------- ---------------
heap=dump|sites|all heap profiling all
cpu=samples|times|old CPU usage off
monitor=y|n monitor contention n
format=a|b ascii or binary output a
file=<file> write data to file java.hprof(.txt for ascii)
net=<host>:<port> send data over a socket write to file
depth=<size> stack trace depth 4
cutoff=<value> output cutoff point 0.0001
lineno=y|n line number in traces y
thread=y|n thread in traces? n
doe=y|n dump on exit? y
Example: java -Xrunhprof:cpu=samples,file=log.txt,
                               depth=3 FooClass

La siguiente invocación crea una fichero de texto que podemos ver sin la herramienta de análisis de heap llamado java.hprof.txt cuando el programa genera un seguimiento de pila o sale. Se utiliza una invocación diferente para crear un fichero binario para usarlo con la herramienta de análisis de heap:
  java -Xrunhprof TableExample3

  d:\jdk12\demo\jfc\Table> java -Xrunhprof TableExample3
  Dumping Java heap ... allocation sites ... done.
La opción de perfil literalmente hace un diario de cada objeto creado en el heap, por incluso cuando arrancamos y paramos el pequeño progeama TableExample3 resulta un ficheo de informe de cuatro megabytes. Aunque la herramienta de análisis de heap usa una versión binaria de este fichero y proporciona un sumario, hay algunas cosas rápidas y fáciles que podemos aprender desde el fichero de texto sin usar la herramienta de análisis de heap.

Nota: Para listar todas las opciones disponibles, usamos
java -Xrunhprof:help

Ver el Fichero de Texto

Elegimos un fichero que pueda manejar grandes ficheros y vamos hasta el final del fichero. Podría haber cientos o miles de líneas, por eso un atajo es buscar las palabras SITES BEGIN. Veríamos una lista de línea que empezarían un tango creciente de números seguido por dos números de porcentaje. La primera entrada en la lista sería similar a este ejemplo:
SITES BEGIN (ordered by live bytes) 
                  Sun Dec 20 16:33:28 1998
percent live alloc'ed stack class
rank self accum bytes objs bytes objs trace name
1 55.86% 55.86% 826516 5 826516 5 3981 [S
La notación [S al final de la última línea indica que es la primera entrada de un array de short, un tipo primitivo. Esto es lo esperado con aplicaciones (AWT). Los primeros cinco contadores bajo la cabecera objs significa que actualmente hay cinco de esos arrays, y sólo ha habido cinco durante el tiempo de vida de esta aplicación, y han ocupado 826516 bytes. La referencia clase de este objeto es el valor listado bajp stack trace. Para encontrar donde se creo esté objeto en este ejmplo, buscamos TRACE 3981. Veremos esto:
TRACE 3981:
java/awt/image/DataBufferUShort.<init>(
                           DataBufferUShort.java:50)
java/awt/image/Raster.createPackedRaster(
                                    Raster.java:400)
java/awt/image/DirectColorModel.
      createCompatibleWritableRaster(
		             DirectColorModel.java:641)
sun/awt/windows/WComponentPeer.createImage(
                             WComponentPeer.java:186)
El código TableExample3 selecciona un scrollpane de 700 por 300. Cuando miramos el fuente de Raster.java, qu está en el fichero src.jar, encontraremos estas sentencias en la línea 400:
  case DataBuffer.TYPE_USHORT:
         d = new DataBufferUShort(w*h);
         break;
Los valores w y h son la anchura y altura de la llamada a createImage que arranca en TRACE 3981. El constructor DataBufferUShort crea un array de shorts:
  data = new short[size];
donde size es w*h. Por eso, en teoría debería hacer una entrada en el array para 210000 elementos. Buscamos una enteada por cada ejemplarización de esta clase buscando por trace=3981. Una de las cinco entradas se parecerá a esto:
  OBJ 5ca1fc0 (sz=28, trace=3979, 
	class=java/awt/image/DataBufferUShort@9a2570)
   data 5ca1670
   bankdata 5ca1f90
   offsets 5ca1340
   ARR 5ca1340 (sz=4, trace=3980, nelems=1, 
                                elem type=int)
   ARR 5ca1670 (sz=420004, trace=3981, nelems=210000, 
                                elem type=short)
   ARR 5ca1f90 (sz=12, trace=3982, nelems=1, 
                                elem type=[S@9a2d90)
   [0] 5ca1670
Podemos ver que los valores de los datos de estas referencias de imagen en un array 5ca1670 que devuelve un alista de 210000 elementos short de tamaño 2. Esto significa qu este array usa 420004 bytes de memoria.

De este dato podemos concluir que el programa TableExample3 usa cerca de 0.5Mb para mapear cada tabal. Si la aplicación de ejemplo se ejecuta en una máquina con poca memoria, debemos asegurarnos de que no mantenemos referencias a objetos geandes o a imágenes que fueron construidas con el método createImage.

La Herramienta de Análisis de Heap

Esta herramienta puede analizar los mismos datos que nosotros, pero requere un fichero de informe binario como entrada. Podemos generar un fichero de informa binario de esta forma:
  java -Xrunhprof:file=TableExample3.hprof,format=b 
                                       TableExample3
Para generar el informe binario, cerramos la ventana TableExample3. El fichero de informe binario TableExample3.hprof se crea al salir del programa. La Herramienta de Análisis de Heap arranca un servidor HTTP que analiza el fichero de perfil binario y muestra el resultado en un HTML que podemos ver en un navegador.

Podemos obtener una copia de la Herramienta de Análisis de Heap de la site java.sun.com. Una vez instalado, ejecutamos los scripts shell y batch en el directorio bin instalado para poder ejecutar el servidor de la Herramienta de Análisis de Heap de esta forma:

  >hat TableExample3.hprof
  Started HCODEP server on port 7000
  Reading from /tmp/TableExample3.hprof...
  Dump file created Tue Jan 05 13:28:59 PST 1999
  Snapshot read, resolving...
  Resolving 17854 objects...
  Chasing references, 
            expect 35 dots.......................
  Eliminating duplicate 
                references.........................
  Snapshot resolved.
  Server is ready.
La salida de arriba nos dice que nuestro servidor HTTP se ha arrancado en el puerto 7000. Para ver este informe introducimos la URL http://localhost:7000 o http://your_machine_name:7000 en nuestro navegador Web. Si tenemos problema en arrancar el servidor usando el script, podemos alternativamente ejecutar la aplicación incluyendo el fichero de clases hat.zip en nuestro CLASSPATH y usar el siguiente comando:
  java hat.Main TableExample3.hprof
La vista del informe por defecto contiene una lista de todas las clases. En la parte de abajo de está página inicial están las dos opciones básicas de informes:
  Show all members of the rootset
  Show instance counts for all classes
Si seleccionamos el enlace Show all members of the rootset, veremos un alista de las siguientes referencias porque estas referencias apuntan a picos potenciales de memoria.
 Java Static References
 Busy Monitor References
 JNI Global References
 JNI Local References
 System Class References
Lo que vemos aquí son ejemplares en la aplicación que tienen referencias a objetos que tienen un riesgo de no se recolectados para la basura. Esto puede ocurrir algunas veces en el caso del JNI su se asigna memoria para un objeto, la memoria se deja para que la libere el recolector de basura, y el recolector de basura no teine la información que necesita para hacerlo. En esta lista de referencias, estamos principalmente interesados en un gran número de referencias a objetos o a objetos de gran tamaño.

El otro informe clave es el Show instance counts for all classes. Este lista los números de llamadas a un método particular. Los objetos array String y Character, [S y [C, están siempre en la parte superior de esta lista, pero algunos objetos son un poco más intrigantes. ¿Por qué hay 323 ejemplares de java.util.SimpleTimeZone, por ejemplo?

  5109 instances of class java.lang.String
  5095 instances of class [C
  2210 instances of class java.util.Hashtable$Entry
  968 instances of class java.lang.Class
  407 instances of class [Ljava.lang.String;
  323 instances of class java.util.SimpleTimeZone
  305 instances of class 
        sun.java2d.loops.GraphicsPrimitiveProxy
  304 instances of class java.util.HashMap$Entry
  269 instances of class [I
  182 instances of class [Ljava.util.Hashtable$Entry;
  170 instances of class java.util.Hashtable
  138 instances of class java.util.jar.Attributes$Name
  131 instances of class java.util.HashMap
  131 instances of class [Ljava.util.HashMap$Entry;
  130 instances of class [Ljava.lang.Object;
  105 instances of class java.util.jar.Attributes
Para obtener más información sobre los ejemplares SimpleTimeZone, pulsamos sobre el enlace (la línea que empieza por 323). Esto listará las 323 referencias y calculará cuánta memoria ha sido utilizada. en este ejemplo, se han utilizado 21964 bytes.
  Instances of java.util.SimpleTimeZone

  class java.util.SimpleTimeZone

  java.util.SimpleTimeZone@0x004f48c0 (68 bytes)
  java.util.SimpleTimeZone@0x003d5ad8 (68 bytes)
  java.util.SimpleTimeZone@0x004fae88 (68 bytes)
  .....
  Total of 323 instances occupying 21964 bytes.
Si pulsamos sobre uno de estos ejemplares SimpleTimeZone, veremos donde fue asignado este objeto.
  Object allocated from:

  java.util.TimeZoneData.<clinit>(()V) : 
        TimeZone.java line 1222
  java.util.TimeZone.getTimeZone((Ljava/lang/String;)
	Ljava/util/TimeZone;) : 
	TimeZone.java line (compiled method)
  java.util.TimeZone.getDefault(
        ()Ljava/util/TimeZone;) : 
	TimeZone.java line (compiled method)
  java.text.SimpleDateFormat.initialize(
        (Ljava/util/Locale;)V) : 
	SimpleDateFormat.java line (compiled method)
En este ejemplo el objeto fue asignado desde TimeZone.java. El fichero fuente de este fichero están el fichero estándard src.jar, y examinando este fichero, podemos ver que de hehco hay cerca de 300 de estos objetos en memoria.
  static SimpleTimeZone zones[] = {
   // The following data is current as of 1998.
   // Total Unix zones: 343
   // Total Java zones: 289
   // Not all Unix zones become Java zones due to 
   // duplication and overlap.
   //-------------------------------------------
   new SimpleTimeZone(-11*ONE_HOUR, 
                  "Pacific/Niue" /*NUT*/),
Desafortunadamente, no tenemos control sobre la memoria usada en este ejemplo, porque es asignada cuando el programa hizo la primera solicitud al timezone por defecto. Sin embargo, esta misma técnica puede aplicarse para analizar nuestra propia aplicación donde probablemente podríamos hacer algunas mejoras.

¿Dónde Gasta el Tiempo la Aplicació?

De nuevo, podemos usar el parámetro -Xrunhprof para obtener información sobre el tiempo que gasta la aplicación procesando un método particular.

Podemos usar una o dos opciones de perfil de CPU para conseguir esto. La primera opción es cpu=samples. Esta opción devuelve el resultado de un muestreo de ejecución de threads de la Máquina Virtual Java con un conteo estadístico de la frecuencia de ocurrencia con que se usa un método particular para encontrar secciones ocupadas de la aplicación. La segunda opción es cpu=times, que mide el tiempo que tardan los métodos individuales y genera un ranking del porcentaje total del tiempo de CPU ocupado por la aplicación.

Usando la opción cpu=times, deberíamos ver algo como esto al final del fichero de salida:

CPU TIME (ms) BEGIN (total = 11080) 
                       Fri Jan  8 16:40:59 1999
rank   self   accum   count  trace   method
 1   13.81%  13.81%       1   437   sun/
    awt/X11GraphicsEnvironment.initDisplay
 2    2.35%  16.16%       4   456   java/
    lang/ClassLoader$NativeLibrary.load
 3    0.99%  17.15%      46   401   java/
    lang/ClassLoader.findBootstrapClass
Si constrastamos esto con la salida de cpu=samples, veremos la diferencia entre la frecuencia de ejecuciónde un método durante la ejecución de la aplicación comparada con el tiempo que tarda ese método.
CPU SAMPLES BEGIN (total = 14520) 
                   Sat Jan 09 17:14:47 1999
rank  self   accum   count  trace   method
 1    2.93%  2.93%   425    2532    sun/
    awt/windows/WGraphics.W32LockViewResources
 2    1.63%  4.56%   237     763    sun/
    awt/windows/WToolkit.eventLoop
 3    1.35%  5.91%   196    1347    java/
    text/DecimalFormat.<init>
El método W32LockView, que llama a una rutina de bloqueo de ventana nativa, se llama 425 veces. Por eso cuando aparecen en los threads activos porque también toman tiempo para completarse. En contraste, el método initDisplay sólo se le llama una vez, pero es el método que tarda más tiempo en completarse en tiempo real.

Herramientas de Rendimiento de Sistema Operativo

Algunas veces los cuellos de botella del rendimiento ocurren al nivel del sistema operativo. Esto es porque la JVM depende en muchas operacioens de las librerías del sistema operativo para funcionalidades como el acceso a disco o el trabajo en red. Sin embargo, lo que ocurre después de que la JVM haya llamado a estas librerías va más alla de las herramientas de perfilado de la plataforma Java.

Aquí hay una lista de herramietnas que podemos usar para analizar problemas de rendimiento en algunos sistemas operativos más comunies.

Plataforma Solaris

System Accounting Reports, sar, informa de la actividad del sistema en términos de I/O de disco, actividad del programa de usuario, y actividad a nivel del sistema. Si nuestra aplicación usa una cantidad de memoria excesiva, podría requerir espacio de intercambio en disco, por lo que veriamos grandes porcentajes en la columna WIO. Los programas de usuario que se quedan en un bucle ocupado muestran un alto porcentaje en la columna user:
developer$ sar 1 10

SunOS developer 5.6 Generic_105181-09 sun4u    
                                      02/05/99

11:20:29    %usr    %sys    %wio   %idle
11:20:30      30       6       9      55
11:20:31      27       0       3      70
11:20:32      25       1       1      73
11:20:33      25       1       0      74
11:20:34      27       0       1      72
El comando truss sigue y guarda los detalles de cada llamada al sistema por la JVM al kernel Solaris. Un forma común de usar truss es:
   truss -f -o /tmp/output -p <process id>
El parámetro -f sigue cualquier proceso hijo que haya creado, el parámetro -o escribe la salida en el fichero nombrado, y el parámetro -p sigue un programa en ejecución desde sis ID de proceso. De forma alternativa podemos reemplazar -p <process id> con la JVM, por ejemplo:
   truss -f -o /tmp/output java MyDaemon
El /tmp/output es usado para almacenar la salida de truss, lo que se debería parecer a esto:
15573:  execve("/usr/local/java/jdk1.2/solaris/
                bin/java", 0xEFFFF2DC,
	        0xEFFFF2E8)  argc                   = 4
15573:  open("/dev/zero", O_RDONLY)                 = 3
15573:  mmap(0x00000000, 8192, 
             PROT_READ|PROT_WRITE|PROT_EXEC,
	     MAP_PRIVATE, 3, 0) = 0xEF7C0000
15573:  open("/home/calvin/java/native4/libsocket.so.1", 
              O_RDONLY) Err#2 ENOENT
15573:  open("/usr/lib/libsocket.so.1", 
              O_RDONLY)                             = 4
15573:  fstat(4, 0xEFFFEF6C)                        = 0
15573:  mmap(0x00000000, 8192, PROT_READ|PROT_EXEC, 
              MAP_SHARED, 4, 0) = 0xEF7B00 00
15573:  mmap(0x00000000, 122880, PROT_READ|PROT_EXEC, 
              MAP_PRIVATE, 4, 0) = 0xEF7 80000
15573:  munmap(0xEF78E000, 57344)                   = 0
15573:  mmap(0xEF79C000, 5393, 
              PROT_READ|PROT_WRITE|PROT_EXEC,
              MAP_PRIVATE|MAP_FIXED, 4, 49152) 
              = 0xEF79C000
15573:  close(4)                                    = 0
En la salida de truss, buscamos los ficheros que fallaran al abrirlos debido a problemas de acceso, como un error ENOPERM, o un error de fichero desaparecido ENOENT. También podemos seguir los datos leidos o escrito con los parámetros de truss: -rall para seguir todos los datos leídos, o -wall para seguir todos los datos escritos por el programa. Con estos parámetros, es posible analizar datos enviados a través de la red o a un disco local.

Plataforma Linux

Linux tiene un comando trace llamado strace. Sigue las llamadas del sistema al kernel Linux. Este ejemplo sigue el ejemplo SpreadSheet del directorio demo del JDK:
$ strace -f -o /tmp/output 
                      java sun.applet.AppletViewer 
                      example1.html
$ cat /tmp/output

639   execve("/root/java/jdk117_v1at/java/
               jdk117_v1a/bin/java", ["java",
               "sun.applet.AppletViewer ", 
               "example1.html"], [/* 21 vars */]) = 0
639   brk(0)                              = 0x809355c
639   open("/etc/ld.so.preload", O_RDONLY)       = -1 
        ENOENT (No such file or directory)
639   open("/etc/ld.so.cache", O_RDONLY)          = 4
639   fstat(4, {st_mode=0, st_size=0, ...})       = 0
639   mmap(0, 14773, PROT_READ, MAP_PRIVATE, 
             4, 0) = 0x4000b000
639   close(4)                                    = 0
639   open("/lib/libtermcap.so.2", O_RDONLY)      = 4
639   mmap(0, 4096, PROT_READ, MAP_PRIVATE, 
             4, 0) = 0x4000f000
Para obtener información del sistema similar al comando sar de Solaris, lee los contenidos del fichero /proc/stat. El formato de este fichero se describe en las páginas del manual proc. Miramos la línea cpu para obtener la hora del sistema de usuario:
   cpu  4827 4 1636 168329
En el ejemplo anterior, la salida cpu indica 48.27 segundos de espacio de usuario, 0.04 de prioridad máxima, 16.36 segundos procesando llamadas al sistema, y 168 segundos libre. Esta es una ejecución total, las entradas para cada proceso están disponibles en /proc/<process_id>/stat.

Plataforma Windows95/98/NT

No hay herramientas de análisis de rendimiento estándard incluidas en estas plataformas, pero si hay herramientas de seguimiento disponibles mediante recursos freeware o shareware como http://www.download.com .

Análisis de memoria: Memory meter

Análisis de Red: Traceplus


Ozito