jueves, 4 de junio de 2015

El Demo del Día: Performace entre Serializadores .NET

Performace entre Serializadores .NET

El Problema de la Performance

Uno de los problemas principales de los Sistemas Informáticos Modernos es la Performance, sea Windows o Web la Aplicación, con el crecimiento del uso (aumento de usuarios) dentro (Intranet) y fuera de la empresa (Internet) las aplicaciones cada vez son mas lentas.

Esta lentitud es tratada de resolver aumentando Hardware como mas Servidores, mas Memorias, mas CPUs, mas Ancho de Banda, etc. Todo menos hacer una reingeniería del desarrollo que es la principal causa, es decir el problema es la forma cómo se programa (Software) donde lo común es que se realizan muchas conexiones al Servidor de Base de Datos, se usan estructuras de datos pesadas (por ejemplo en .NET los DataSets), se hacen bucles expensivos, etc.

Solución al Problema de la Performance

Aprender las Buenas Prácticas de Programación que permitan consumir la menor cantidad de memoria con el menor tiempo de procesamiento, para lo cual hay diferentes las cuales compartiremos a partir de hoy, empezando por una Comparación de Serializadores para elegir el mejor.

Definición de Serialización

Según Wikipedia, la Serialización (o Marshalling en inglés) es el proceso de codificación de un objeto en un medio de almacenamiento (como puede ser un archivo, o un buffer de memoria) con el fin de transmitirlo a través de una conexión en red como una serie de bytes o en un formato humanamente más legible como XML o JSON, entre otros.

La serie de bytes o el formato pueden ser usados para crear un nuevo objeto que es idéntico en todo al original, incluido su estado interno (por tanto, el nuevo objeto es un clon del original).

La Serialización es un mecanismo ampliamente usado para transportar objetos a través de una red, para hacer persistente un objeto en un archivo o base de datos, o para distribuir objetos idénticos a varias aplicaciones o localizaciones.

Importancia de elegir un Serializador

Es necesario saber elegir adecuadamente un Serializador, ya que cada vez que enviamos datos entre dos procesos se usa la serialización. 

A continuación listamos los Serializadores .NET junto con los tipos de aplicaciones o tecnologías distribuidas que las usan por defecto:

- Serializador Binario: Usado en Sockets y .NET Remoting (dll)
- Serializador SOAP: Usado por Servicios Web XML (asmx)
- Serializador XML: Usado por los Servicios WCF (svc y dll)
- Serializador JSON: Usado en Web API
- Serializador Personalizado: Hay que crearlo uno mismo.

La importancia de seleccionar bien el Serializador es que este es el factor principal de performance al momento de usar un Servicio o una Aplicación Distibuida, es decir, depende de este el tiempo de serialización deserealización y sobre todo el formato y el tamaño de los datos que se pasan por la red.

En este post, comprobaremos cual es el mejor, sin embargo la mayoría siempre usa el peor y después quiere que sus Servicios sean rápidos, es decir, quieren una cosa (Performance) pero hacen otra (Estándar).

Crear una Aplicación Windows Forms en C#

Abrir Visual Studio y crear una aplicación Windows Forms en C# llamada "Comparacion_Serializacion", cambiarle de nombre al formulario por "frmListaProductos" y realizar el diseño similar a la figura mostrada:


Nota: En el diseño hay un DataGridView llamado "dgvProducto", un menú contextual llamado "mnuContextual" con 6 opciones: mnuCargarDatos, mnuBinario, mnuSOAP, mnuXML, mnuJSON y mnuPersonalizado.

Crear el Procedimiento Almacenado en SQL Server

Crear el Procedimiento llamado "uspProductsListar" en SQL Server:

Create Procedure uspProductsListar
As
Select ProductID, ProductName, SupplierID, 
CategoryID,UnitPrice, UnitsInStock
From Products

Crear la Clase Entidad del Negocio

Agregar una clase al proyecto llamada: "beProducto" y escribir el siguiente código:

using System;
using System.Runtime.Serialization;

namespace Comparacion_Serializacion
{
    //[Serializable]
    //[DataContract]
    public class beProducto
    {
        //[DataMember]
        public int IdProducto { get; set; }
        //[DataMember]        
        public string Nombre { get; set; }
        //[DataMember]
        public int IdProveedor { get; set; }
        //[DataMember]
        public int IdCategoria { get; set; }
        //[DataMember]
        public decimal PrecioUnitario { get; set; }
        //[DataMember]
        public short Stock { get; set; }
    }
}

Nota: Los atributos de serialización (Serializable, DataContract y DataMember) han sido comentados inicialmente para ver que sucede cuando se intenta serializar con cada técnica.

Crear la Clase de Acceso a Datos

Agregar una clase al proyecto llamada: "daProducto" y escribir el siguiente código:

using System;
using System.Collections.Generic; //List
using System.Data; //CommandType, CommandBehavior
using System.Data.SqlClient; //SqlConnection, SqlCommand, SqlDataReader

namespace Comparacion_Serializacion
{
    public class daProducto
    {
        public List<beProducto> listar(SqlConnection con)
        {
            List<beProducto> lbeProducto = null;

            SqlCommand cmd = new SqlCommand("uspProductsListar", con);
            cmd.CommandType = CommandType.StoredProcedure;
            SqlDataReader drd = cmd.ExecuteReader(CommandBehavior.SingleResult);
            if (drd != null)
            {
                lbeProducto = new List<beProducto>();
                int posIdProducto = drd.GetOrdinal("ProductID");
                int posNombre = drd.GetOrdinal("ProductName");
                int posIdProveedor = drd.GetOrdinal("SupplierID");
                int posIdCategoria = drd.GetOrdinal("CategoryID");
                int posPrecioUnitario = drd.GetOrdinal("UnitPrice");
                int posStock = drd.GetOrdinal("UnitsInStock");
                beProducto obeProducto;
                while (drd.Read())
                {
                    obeProducto = new beProducto();
                    obeProducto.IdProducto = drd.GetInt32(posIdProducto);
                    obeProducto.Nombre = drd.GetString(posNombre);
                    obeProducto.IdProveedor = drd.GetInt32(posIdProveedor);
                    obeProducto.IdCategoria = drd.GetInt32(posIdCategoria);
                    obeProducto.PrecioUnitario = drd.GetDecimal(posPrecioUnitario);
                    obeProducto.Stock = drd.GetInt16(posStock);
                    lbeProducto.Add(obeProducto);
                }
                drd.Close();
            }

            return (lbeProducto);
        }
    }
}

Crear la Clase de Reglas del Negocio

Agregar una clase al proyecto llamada: "brProducto" y escribir el siguiente código:

using System;
using System.Configuration; //ConfigurationManager
using System.Data.SqlClient; //SqlConnection
using System.Collections.Generic; //List

namespace Comparacion_Serializacion
{
    public class brProducto
    {
        public string CadenaConexion { get; set; }
        private string archivoLog;

        public brProducto()
        {
            CadenaConexion = 
                ConfigurationManager.ConnectionStrings["conNW"].ConnectionString;
        }

        public List<beProducto> listar()
        {
            List<beProducto> lbeProducto = null;
            using (SqlConnection con = new SqlConnection(CadenaConexion))
            {
                try
                {
                    con.Open();
                    daProducto odaProducto = new daProducto();
                    lbeProducto = odaProducto.listar(con);
                }
                catch (SqlException ex)
                {
                    //grabarLog(ex);
                }
                catch (Exception ex)
                {
                    //grabarLog(ex);
                }
            } //con.Close(); con.Dispose();
            return (lbeProducto);
        }
    }
}

Nota: Para usar la clase "ConfigurationManager", primero hay que hacer referencia a la librería "System.Configuration.dll"

Crear una Clase para la Serialización Personalizada de Strings

Agregar una clase al proyecto llamada: "CustomSerializer" y escribir el siguiente código:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.IO;

namespace Comparacion_Serializacion
{
    public class CustomSerializer
    {
        public static void Serializar<T>(List<T> lista, string archivo, char separadorCampo, 
        char separadorRegistro)
        {
            StringBuilder sb = new StringBuilder();
            PropertyInfo[] propiedades = lista[0].GetType().GetProperties();
            for (int i = 0; i < propiedades.Length; i++)
            {
                sb.Append(propiedades[i].Name);
                if (i < propiedades.Length - 1) sb.Append(separadorCampo);
            }
            sb.Append(separadorRegistro);
            for (int j = 0; j < lista.Count; j++)
            {
                propiedades = lista[j].GetType().GetProperties();
                for (int i = 0; i < propiedades.Length; i++)
                {
                    if (propiedades[i].GetValue(lista[j], null) != null) 
                    sb.Append(propiedades[i].GetValue(lista[j], null).ToString());
                    else sb.Append("");
                    if (i < propiedades.Length - 1) sb.Append(separadorCampo);
                }
                if (j < lista.Count - 1) sb.Append(separadorRegistro);
            }
            File.WriteAllText(archivo,sb.ToString());
        }

        public static List<T> Deserializar<T>(string archivo, char separadorCampo, 
        char separadorRegistro)
        {
            List<T> lista = new List<T>();
            if (File.Exists(archivo))
            {
                string contenido = File.ReadAllText(archivo);
                string[] registros = contenido.Split(separadorRegistro);
                string[] campos;
                string[] cabecera = registros[0].Split(separadorCampo);
                string registro;
                Type tipoObj;
                T obj;
                dynamic valor;
                Type tipoCampo;
                for (int i = 1; i < registros.Length; i++)
                {
                    registro = registros[i];
                    tipoObj = typeof(T);
                    obj = (T)Activator.CreateInstance(tipoObj);
                    campos = registro.Split(separadorCampo);
                    for (int j = 0; j < campos.Length; j++)
                    {
                        tipoCampo = obj.GetType().GetProperty(cabecera[j]).PropertyType;
                        valor = Convert.ChangeType(campos[j],tipoCampo);
                        obj.GetType().GetProperty(cabecera[j]).SetValue(obj, valor);
                    }
                    lista.Add(obj);
                }
            }
            return (lista);
        }
    }
}

Nota: Para crear mi propio Serializador uso Reflection y Listas Genéricas. 
Una de las claves para hacer la Deserealización es crear una instancia del objeto dinámicamente con Activator.CreateInstance y luego escribir el valor desde la cadena al objeto para lo cual uso Convert.ChangeType.

Escribir el Código del Formulario frmListaProducto

Antes de escribir el código de los Serializadores hay que hacer las siguientes Referencias:
- System.Runtime.Serialization.Formatters.Soap.dll para la Serialización SOAP
- System.Runtime.Serialization para la Serialización JSON

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO; //FileStream
using System.Linq; //ToList
using System.Diagnostics; //Stopwatch
using System.Runtime.Serialization.Formatters.Binary; //BinaryFormatter
using System.Runtime.Serialization.Formatters.Soap; //SoapFormatter
using System.Xml.Serialization; //XmlSerializer
using System.Runtime.Serialization.Json; //DataContractJsonSerializer

namespace Comparacion_Serializacion
{
    public partial class frmListaProducto : Form
    {
        private List<beProducto> lbeProducto;

        public frmListaProducto()
        {
            InitializeComponent();
        }

        private void cargarDatos(object sender, EventArgs e)
        {
            brProducto obrProducto = new brProducto();
            lbeProducto = obrProducto.listar();
            dgvProducto.DataSource = lbeProducto;
        }

        private void medirTiempo(Action metodo)
        {
            Stopwatch cronometro = new Stopwatch();
            cronometro.Start();
            metodo();
            MessageBox.Show(String.Format("Tiempo de Duración: {0:n0} msg", 
            cronometro.Elapsed.TotalMilliseconds));
            cronometro.Stop();
        }

        private void serializarBinario()
        {
            BinaryFormatter bf = new BinaryFormatter();
            using (FileStream fs = new FileStream("Binario.txt",
                FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
            {
                bf.Serialize(fs, lbeProducto);
            }
        }

        private void deserializarBinario()
        {
            BinaryFormatter bf = new BinaryFormatter();
            using (FileStream fs = new FileStream("Binario.txt",
                FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                lbeProducto=(List<beProducto>)bf.Deserialize(fs);
            }
        }

        private void serializarSOAP()
        {
            SoapFormatter sf = new SoapFormatter();
            using (FileStream fs = new FileStream("SOAP.txt",
                FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
            {
                sf.Serialize(fs, lbeProducto.ToArray());
            }
        }

        private void deserializarSOAP()
        {
            SoapFormatter sf = new SoapFormatter();
            using (FileStream fs = new FileStream("SOAP.txt",
                FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                lbeProducto=((beProducto[])sf.Deserialize(fs)).ToList();
            }
        }

        private void serializarXML()
        {
            XmlSerializer xs = new XmlSerializer(typeof(List<beProducto>));
            using (FileStream fs = new FileStream("XML.txt",
                FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
            {
                xs.Serialize(fs, lbeProducto);
            }
        }

        private void deserializarXML()
        {
            XmlSerializer xs = new XmlSerializer(typeof(List<beProducto>));
            using (FileStream fs = new FileStream("XML.txt",
                FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                lbeProducto = (List<beProducto>)xs.Deserialize(fs);
            }
        }

        private void serializarJSON()
        {
            DataContractJsonSerializer js = new DataContractJsonSerializer(typeof(List<beProducto>));
            using (FileStream fs = new FileStream("JSON.txt",
                FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
            {
                js.WriteObject(fs, lbeProducto);
            }
        }

        private void deserializarJSON()
        {
            DataContractJsonSerializer js = new DataContractJsonSerializer(typeof(List<beProducto>));
            using (FileStream fs = new FileStream("JSON.txt",
                FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                lbeProducto=(List<beProducto>)js.ReadObject(fs);
            }
        }

        private void serializarString()
        {
            CustomSerializer.Serializar(lbeProducto, "Strings.txt", ',', '|');
        }

        private void deserializarString()
        {
            lbeProducto=CustomSerializer.Deserializar<beProducto>("Strings.txt", ',', '|');
        }

        private void mnuSerializarBinario_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarBinario);
        }

        private void mnuDeserializarBinario_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarBinario);
            dgvProducto.DataSource = lbeProducto;
        }

        private void mnuSerializarSOAP_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarSOAP);
        }

        private void mnuDeserializarSOAP_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarSOAP);
            dgvProducto.DataSource = lbeProducto;
        }

        private void mnuSerializarXML_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarXML);
        }

        private void mnuDeserializarXML_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarXML);
            dgvProducto.DataSource = lbeProducto;
        }

        private void mnuSerealizarJSON_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarJSON);
        }

        private void mnuDeserealizarJSON_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarJSON);
            dgvProducto.DataSource = lbeProducto;
        }

        private void mnuSerializarPersonalizado_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarString);
        }

        private void mnuDeserializarPersonalizado_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarString);
            dgvProducto.DataSource = lbeProducto;
        }
    }
}

Nota: Todos las funciones manejadoras de eventos llaman a la función "medirTiempo" para ejecutar la Serialización y Deserealización.

Modificar el Archivo de Configuración

Abrir el archivo app.Config y aumentar la cadena de conexión:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="conNW" providerName="SQLServer"
      connectionString="uid=UsuarioNW;pwd=123456;
      server=DSOFT\Sqlexpress;database=Northwind"/>
  </connectionStrings>
  <startup> 
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>

Ejecutar y Probar la Aplicación Windows Forms

Grabar la aplicación y pulsar F5 para ejecutarla. Aparecerá una ventana similar a la siguiente figura:


Clic derecho y se mostrará el menú contextual, del cual seleccionar la primera opción: "Cargar Datos" y se listarán los productos.


Serialización Binaria

Clic derecho y del menú contextual seleccionar la opción "Binario" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 11 msg y se creará un archivo llamado "Binario.txt" con el siguiente contenido:


Notas
- Si se intenta Serializar el archivo a Binario se caerá ya que la Entidad (clase beProducto) no es serializable, proceder a descomentar el atributo [Serializable].
- Observar que el tamaño del archivo Binario.txt es de 5756 bytes (en mi caso).

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 8 msg y se cargarán los datos desde el archivo "Binario.txt" en la lista de objetos y se mostrará en la grilla.

Serialización SOAP

Detener la ejecución y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "SOAP" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 30 msg y se creará un archivo llamado "SOAP.txt" con el siguiente contenido:


Nota: Observar que el tamaño del archivo SOAP.txt es de 73586 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 51 msg y se cargarán los datos desde el archivo "SOAP.txt" en la lista de objetos y se mostrará en la grilla.

Serialización XML

Detener la ejecución y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "XML" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 192 msg y se creará un archivo llamado "XML.txt" con el siguiente contenido:


Nota: Observar que el tamaño del archivo XML.txt es de 22270 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 209 msg y se cargarán los datos desde el archivo "XML.txt" en la lista de objetos y se mostrará en la grilla.

Serialización JSON sin atributos

Detener la ejecución y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "JSON" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 474 msg y se creará un archivo llamado "JSON.txt" con el siguiente contenido:


Nota: Observar que el tamaño del archivo JSON.txt es de 19716 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 347 msg y se cargarán los datos desde el archivo "JSON.txt" en la lista de objetos y se mostrará en la grilla.

Serialización JSON con atributos (DataContract y DataMember)

Detener la ejecución, decomentar los atributos [DataContract] y [DataMamber] de la entidad beProducto, además renombrar el archivo JSON.txt por JSON2.txt y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "JSON" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 27 msg y se creará un archivo llamado "JSON.txt" con el siguiente contenido:



Nota: Observar que el tamaño del archivo JSON.txt es tan solo de 10332 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 92 msg y se cargarán los datos desde el archivo "JSON.txt" en la lista de objetos y se mostrará en la grilla.

Serialización Personalizada con Strings

Detener la ejecución y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "Personalizado" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 15 msg y se creará un archivo llamado "Strings.txt" con el siguiente contenido:



Nota: Observar que el tamaño del archivo Strings.txt es de 3125 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Personalizado" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 53 msg y se cargarán los datos desde el archivo "Strings.txt" en la lista de objetos y se mostrará en la grilla.

Resumen Comparativo

A continuación una figura con un cuadro resumen de tiempos y tamaños por serializador (formato):



Esta resaltado los que mejor rendimiento tienen, es decir el tiempo de Serializado el ganador es el Binario, pero en tamaño (ancho de banda) el ganador es el Strings (Personalizado), además para Servicios Web el Binario no es aplicable por tanto, el mejor Serializador en Performance sería el String.

Sin embrago el comportamiento de los desarrolladores ha sido:
- Web Services XML con SOAP (2002-2007)
- WCF Services con XML (2008-2011)
- Web API con JSON (2012-2015)

Comentario Final

Después de haber tratado detalladamente los Serializadores .NET y analizado su tiempo y tamaño, la recomendación final es que si desean consumir el mínimo ancho de banda, hay que crear su propio serializador (yo le estoy dejando el mio, que poco a poco lo voy a optimizar) ya que es el que mejor rendimiento tiene en velocidad, tamaño e inter-operabilidad.

Claro, muchos dirán que No es Estandar el formato creado, y otros dirán y como hago para consumirlos desde los clientes, es decir como muestro estos datos, así como hay Templates para JSON yo tambien tengo mis propias rutinas que muestran en JavaScript cualquier Strings separados por caracteres, los cuales serán motivos de otros post.

Descarga del Código


5 comentarios:

  1. Excelente explicación y aún mejor el Demo, en estos días estaba realizando una aplicación WCF y trataba de leerlo desde otra aplicación en Android sin buenos resultados, esta publicación me ha servido de mucho para aclarar muchísimas dudas. Gracias profesor por el aporte...

    ResponderBorrar
  2. Profesor ojala profundice más en este tema para los siguientes post, estaré a la expectativa...

    ResponderBorrar
  3. Excelente Demo!!!.... lo aplicaré en mi chamba... sigamos adelante

    ResponderBorrar
  4. Excelente Demo!!!.... lo aplicaré en mi chamba... sigamos adelante

    ResponderBorrar