Archive

Posts Tagged ‘.NET’

Aplicaciones internacionales con ASP.NET

Me sigue chocando seguir viendo soluciones de lo más artesanal para internacionalizar páginas web hospedadas con IIS, cuando la plataforma nos las da de serie y con un mínimo esfuerzo por parte del programador.

He visto usar parámetros en las URLs, pasarlos de módulo a módulo, y construir complejas funcionalidades para arrastrar el parámetro de página en página… Realmente no hay que hacer nada de eso, simplemente dejar que el navegador nos ayude y .NET (e IIS) lo aprovechen.

Todo empieza en el navegador

Es posible que no lo sepáis, pero los navegadores incluyen el idioma deseado en cada petición que realizan al servidor. Las peticiones HTTP (y HTTPS, por supuesto) incluyen una cabecera donde se incluye información útil para que el servidor pueda realizar su trabajo: desde la página que se está solicitando, un método (GET, POST) si soporta compresión o no, y, lo que nos interesa, el idioma esperado para la respuesta. La cabecera en cuestión se llama “Accept-Language” y da la lista de idiomas configurados por el usuario.

Cread (o usad una página web ya existente) en vuestro servidor – en este ejemplo usaremos una página WebForms clásica “TestPage.aspx”, y abridla con vuestro navegador favorito. Usando las herramientas de desarrollo de Chrome, por ejemplo, en la parte de visualización de red, encontraréis algo como esto, y la entrada en la que estamos particularmente interesados es “Accept-Language”:

AcceptLanguageHeader

En este ejemplo, Chrome está solicitando la página en inglés, y ofrece dos opciones: la “específica” (“en-US”, inglés de Estados Unidos) y la genérica (“en”, Inglés).

Obviamente, esa información está al alcance de una línea de código en el servidor. En nuestra página TestPage.aspx podemos incluir lo siguiente para comprobarlo:

protected void Page_Load(object sender, EventArgs e)
{
  var requestedCulture = Request.Headers["Accept-Language"];
  System.Diagnostics.Debug.WriteLine("Culture:" + requestedCulture);
}

En la ventana de Depuración veréis lo siguiente:

AcceptLanguageHeader_Server

Pero qué ocurre cuando configuramos Chrome para que solicite páginas en francés como idioma preferido e inglés como secundario? Podemos configurarlo navegando la url “chrome://settings” y abriendo “Ajustes avanzados”. Configuraremos los idiomas así:

ChromeLangSettings

y refrescamos la página: en el servidor veremos lo siguiente, con la nueva opción (“fr”) como primera preferencia:

AcceptLanguageHeader_Server_2

Y ahora, antes de que os pongáis a leer la cabecera “a pelo” en todas vuestras páginas, seguid leyendo… porque no tenéis que hacerlo!!!

Una página para experimentar

Vamos a añadir una simple página (también WebForms, aunque es exactamente lo mismo en Razor, o WebAPI, o MVC) para experimentar un poco. El código de la página es el siguiente (por simplicidad he eliminado el código posterior (“code behind”):

<%@ Page Language="C#" Inherits="System.Web.UI.Page" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Welcome</title>
</head>
<body>
    <h1>Welcome to my web!!</h1>
    <p>Today is <% =DateTime.Today.ToLongDateString() %></p>
</body>
</html>

Y vamos a navegarla con Chrome configurado para pedir contenidos en francés. El resultado es, como podéis esperar, una página en… inglés?

Intl_Nav_1

El primer problema es obvio: el ordenador que hospeda IIS corre un sistema operativo en español, así que las fechas salen en el idioma de Cervantes. Vamos mal… pero aunque parezca increíble, todo el proceso empieza con una sola línea en el archivo web.config. Cambiad la sección <system.web> de esto:

  <system.web>
    <compilation debug="true" targetFramework="4.5.2"/>
    <httpRuntime targetFramework="4.5.2"/>
  </system.web>

a esto:

  <system.web>
    <compilation debug="true" targetFramework="4.5.2"/>
    <httpRuntime targetFramework="4.5.2"/>
    <globalization culture="auto" uiCulture="auto" enableClientBasedCulture="true"/>
  </system.web>

Con una simple directiva IIS aplicará la región solicitada por el navegador a todas (si, todas!) las peticiones que gestione. Simplemente arrancad de nuevo, navegad la página otra vez y veréis el cambio:

Intl_Nav_2

La fecha aparece perfectamente formateada en francés… sin escribir ni una sola línea de código.

Al hacer la cultura de interface de usuario (uiCulture) automática y permitir a IIS que aplique la cultura requerida por el usuario (enableClientBasedCulture) lo que ocurre es que IIS, en las primeras fases de la cola de procesos que implica responder a una solicitud web, lee la cabecera Accept-Language, la analiza y busca la mejor cultura (CultureInfo) disponible y la aplica de manera automática al resto del proceso. Cuando en la página escribimos DateTime.Today.ToLongDateString(), la cultura está correctamente configurada para generar la fecha en el idioma correcto. Y tenemos acceso a dicha cultura automática por medio de la propiedad CurrentUICulture de CultureInfo. Si cambiamos nuestra página de pruebas a lo siguiente:

protected void Page_Load(object sender, EventArgs e)
{
  var requestedCulture = CultureInfo.CurrentUICulture;
  System.Diagnostics.Debug.WriteLine("Culture:" + requestedCulture.NativeName);
}

al navegarla con Chrome el resultado es el siguiente:

AcceptLanguageHeader_Server_3

Ya, pero, y el resto de los textos?

Si, hasta ahora hemos traducido una parte extremadamente pequeña de nuestra página. Ahora viene lo realmente importante, nuestros textos.

Y .NET ha facilitado la operación de obtener textos traducidos de forma automática desde el primer día, empleando archivos de recursos. Los archivos de recursos son colecciones de elementos dependientes de idioma (habitualmente cadenas de texto, pero también imágenes) gestionados por .NET. Para empezar a traducir nuestra página web vamos a empezar trasladando lo textos actuales a un archivo de recursos.

Empezaremos añadiendo un archivo con los textos en inglés (ya que nuestra web está actualmente en inglés). En Visual Studio cread una carpeta “Resources” y dentro de ella cread un nuevo archivo de recursos. Llamadlo “WebStrings.resx”:

NewResource

(Nota: si usáis WebForms hay una forma más específica de añadir archivos .resx y de gestionarlos desde la página. El método que estoy documentando es genérico y válido tanto para páginas web como para aplicaciones nativas).

Una vez creado el archivo, abridlo en VisualStudio:

StringEditor_1

Importante: antes de empezar a añadir los textos, cambiad (en la parte superior) el modificador de acceso para los recursos de “Interno” a “Público”.

Ahora añadid los diversos textos necesarios en la página:

StringEditor_2

Las cadenas que estamos añadiendo se convierten en miembros de una clase que podemos emplear en cualquier parte de nuestro código. Y lo mejor, al ser un tipo generado automáticamente, disponemos de Intellisense a la hora de trabajar con él:

UsingStrings_Intellisense

En nuestro ejemplo, y dado que la aplicación se llama “IntlWeb”, el namespace para nuestras cadenas es IntlWeb.Resources.WebStrings. Una vez reemplazados todos los textos, la página queda así:

<%@ Page Language="C#" Inherits="System.Web.UI.Page" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title><% =IntlWeb.Resources.WebStrings.Page_Title %></title>
</head>
<body>
    <h1><% =IntlWeb.Resources.WebStrings.Body_WelcomeMessage %></h1>
    <p><% =IntlWeb.Resources.WebStrings.Body_TodayIs %> <% =DateTime.Today.ToLongDateString() %></p>
</body>
</html>

Obviamente, nuestra página sigue mostrando mensajes en inglés. Ahora tenemos que añadir los textos en francés. Para ello (y tendréis que disculparme, no hablo francés así que los textos pueden no estar del todo correctos) duplicad desde VisualStudio el archivo WebStrings.resx y ponedle como nombre WebStrings.fr.resx. El nombre del archivo es extremadamente importante: el código del idioma debe ir entre el nombre y la extensión, y puede ser un idioma general (“.es.”) o un código completo cuando necesitamos más “finura” a la hora de ofrecer textos (“.es-ES.”). Una vez copiado, abridlo y cambiad los textos:

StringEditor_3

Si navegamos la página desde Chrome, el resultado será el siguiente:

Intl_Nav_3

Como no podría ser de otro modo, añadamos también textos en español. Como podréis imaginar el nombre del archivo será WebStrings.es.resx:

StringEditor_4

Y desde Chrome volved a abrir la configuración y cambiad los idiomas colocando el Español el primero de la lista (no es necesario eliminar el idioma francés):

ChromeLangSettings_2

Aceptad los ajustes, volved a la página y simplemente refrescadla:

Intl_Nav_4

Cuando una aplicación pide una de las cadenas de recursos guardadas en un archivo .resx, .NET busca primero el archivo con la cultura que más se aproxime y luego los va evaluando por prioridad descendente. Si, por ejemplo, la cultura pedida por el usuario es “Alemán de Austria” (“de-AU”) buscará primero un archivo con la extensión “de-AU.resx”, Si no lo encuentra continuará con la versión “Alemán” (".de.resx") y finalmente leerá los contenidos de la versión genérica (“.resx”). Procurad por ello emplear en lo posible archivos de idiomas lo más generales posibles: si empleáis “.es-MX.resx”, un usuario desde España verá los textos en el idioma predeterminado al no encontrar los recursos ni específicos (“es-ES.resx”) ni generales para el idioma (“.es.resx”).

Ya, pero… y el JavaScript?

Pues es una estupenda pregunta. Si bien localizar textos en el servidor está perfectamente soportado por .NET (e IIS) de “fábrica”, las páginas emplean cada vez más librerías de cliente escritas en JavaScript. Pero aunque dicha funcionalidad no exista en el servidor, no significa que no podamos añadirla muy fácilmente.

Volviendo a nuestro ejemplo, imaginemos que nuestra página usa código JavaScript para realizar alguna operación en el cliente. Nuestro archivo se llama “App.js” y actualmente usa textos en inglés, junto con controles en la misma página. En este ejemplo vamos a traducir (también!) los controles de la página desde JavaScript en lugar de utilizar los recursos del servidor como hemos hecho hasta ahora – traducción totalmente en el lado del cliente.

El código de la página es el siguiente:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <script src="App.js"></script>
</head>
<body>
  <button id="cmd1">Command 1</button>
  <button id="cmd2">Command 2</button>
  <button id="cmd3">Command 3</button>
</body>
</html>

y el archivo JavaScript (“App.js”) contiene lo siguiente:

window.onload = function ()
{
  var cmd1 = document.getElementById("cmd1");
  var cmd2 = document.getElementById("cmd2");
  var cmd3 = document.getElementById("cmd3");

  cmd1.addEventListener("click", function ()
  {
    alert("You clicked on the first button");
  });
  cmd2.addEventListener("click", function ()
  {
    alert("You clicked on the second button");
  });
  cmd3.addEventListener("click", function ()
  {
    alert("You clicked on the third button");
  });
};

Como podréis imaginar, no es que haga mucho, pero la idea es simple: cuando el usuario pulsa sobre uno de los botones, se muestra un texto informando de la importantísima operación que se ha llevado a cabo:

JavaScript_1

El tema es: cómo traducimos las cadenas incluidas en el programa JavaScript? Si bien IIS no tiene soporte directo para traducir javascript, nosotros podemos añadirlo.

Lo primero que vamos a hacer es algo parecido a lo que hicimos con nuestra página: convertir las cadenas de texto en variables que guardaremos en sendos archivos javascript, empleando un nombre que nos permita identificarlos por idioma. El primero que escribiremos será el archivo con los textos genéricos, en nuestro caso, en inglés. Para identificar que el archivo contiene los textos de nuestra aplicación, lo llamaremos “App.strings.js”:

// App.strings.js
var strings =
  {
    cmd1Caption: "Button 1",
    cmd2Caption: "Button 2",
    cmd3Caption: "Button 3",
    cmd1Clicked: "You clicked on the first button",
    cmd2Clicked: "You clicked on the second button",
    cmd3Clicked: "You clicked on the third button",
  };

También modificaremos nuestro programa para usar las cadenas del archivo en lugar de las predeterminadas:

window.onload = function ()
{
  var cmd1 = document.getElementById("cmd1");
  var cmd2 = document.getElementById("cmd2");
  var cmd3 = document.getElementById("cmd3");

  cmd1.innerText = strings.cmd1Caption;
  cmd2.innerText = strings.cmd2Caption;
  cmd3.innerText = strings.cmd3Caption;

  cmd1.addEventListener("click", function ()
  {
    alert(strings.cmd1Clicked);
  });
  cmd2.addEventListener("click", function ()
  {
    alert(strings.cmd2Clicked);
  });
  cmd3.addEventListener("click", function ()
  {
    alert(strings.cmd3Clicked);
  });
};

Y finalmente, en la página añadiremos la referencia al archivo de cadenas (App.strings.js) además de nuestro código (App.js) en la cabecera de la página:

<head>
  <meta charset="utf-8" />
  <script src="App.strings.js"></script>
  <script src="App.js"></script>
</head>

Hasta ahora todo muy bien, pero nuestra página sigue funcionando únicamente en inglés y, de hecho, si la ejecutamos ahora veremos exactamente lo mismo que antes. Cómo conseguir ahora que los textos (o, mejor dicho, el archivo App.strings.js) se traduzca? Ahora es cuando IIS nuevamente vuelve a echarnos una mano. Vamos a añadir un “handler” al servidor que, aprovechando que IIS nos informará del idioma que desea el usuario, se encargue de devolvernos el archivo .strings.js correcto.

Cuando el handler reciba una petición para un archivo cuyo nombre termine en “.strings.js”, usará la propiedad CurrentUICulture para construir diversos nombres de archivo. Si por ejemplo el navegador solicita el archivo “app.strings.js” y el idioma es “de-AU”, primero buscará un archivo “app.de-AU.strings.js”, a continuación buscará “app.de.strings.js” y finalmente probará con “app.strings.js”.

using System.Globalization;
using System.IO;
using System.Web;

namespace IntlWeb
{
  public class JSLocalizer : IHttpHandler
  {
    private  const string Extension = ".strings.js";

    public bool IsReusable => true;

    public void ProcessRequest(HttpContext context)
    {
      var culture = CultureInfo.CurrentUICulture;
      var basePath = context.Request.AppRelativeCurrentExecutionFilePath;

      basePath = basePath.Remove(basePath.Length - Extension.Length);
      var physPath = context.Server.MapPath(basePath);

      // <filename>.ll-RR.strings.js
      var fileName = $"{physPath}.{culture.Name}{Extension}";
      if (File.Exists(fileName) == false)
      {
        // <filename>.ll.strings.js
        fileName = $"{physPath}.{culture.TwoLetterISOLanguageName}{Extension}";
        if (File.Exists(fileName) == false)
        {
          // <filename>.strings.js
          fileName = $"{physPath}{Extension}";
          if (File.Exists(fileName) == false)
          {
            context.Response.StatusCode = 404// File not found
            return;
          }
        }
      }

      var response = context.Response;
      response.ContentType = "text/javascript";
      response.Charset = System.Text.Encoding.UTF8.WebName;
      response.ContentEncoding = System.Text.Encoding.UTF8;
      response.Expires = 24 * 60;   // 1 day user caching

      response.WriteFile(fileName);
    }
  }
}

Solo queda registrarlo en el archivo web.config para que gestione las peticiones de cualquier archivo “*.strings.js”:

  <system.webServer>
    <handlers>
      <add name="Script String Localizer" verb="*" path="*.strings.js" type="IntlWeb.JSLocalizer" />
    </handlers>
  </system.webServer>

Finalmente, sólo nos queda probarlo. Vamos a añadir un archivo con los textos en español a nuestra aplicación JavaScript, copiando el archivo App.strings.js en uno que se llame App.es.strings.js. Cambiemos los textos y pidamos la página, a ver qué pasa:

// App.es.strings.js
var strings =
  {
    cmd1Caption: "Botón 1",
    cmd2Caption: "Botón 2",
    cmd3Caption: "Botón 3",
    cmd1Clicked: "Ha pulsado en el primer botón",
    cmd2Clicked: "Ha pulsado en el segundo botón",
    cmd3Clicked: "Ha pulsado en el tercer botón",
  };

Cuando el navegador solicita el archivo App.strings.js con los ajustes del navegador para solicitar contenido en castellano, la petición acabará en nuestro handler, que conoce el idioma esperado por el usuario (via la propiedad CultureInfo.CurrentUICulture). Primero busca un archivo App.es-ES.strings.js. Al no encontrarlo, busca la versión App.es.strings.js, que sí encuentra y devuelve al cliente. El resultado es:

Intl_Nav_5

Tanto el contenido de los botones como el mensaje mostrado por Javascript aparecen en castellano. Al igual que con los archivos .resx, ahora en el servidor tendremos archivos Javascript con versiones de los textos para cada idioma. El handler se encargará de devolver la mejor versión posible.

Este (largo!) artículo ha intentado describir de forma rápida las principales herramientas que permiten traducir de forma sencilla tanto aplicaciones como servicios web hospedados en IIS como aplicaciones de usuario escritas en JavaScript. Y recordad que todo esto empieza con una simple línea de configuración en web.config, que no está nada mal. Aprovechadla!

C#–Cómo escribir código realmente rápido

Vuelvo tras una larga ausencia con un tema con el que llevo luchando el último año: optimizar código para mejorar la respuesta a los usuarios tanto a nivel de servidor como de aplicación usando incluso menos recursos que antes. Y si piensas en distribuir tu aplicación en la nube, incluso puede que ahorres dinero.

Cualquiera que me conozca sabe que mi obsesión es cómo exprimir las máquinas al máximo. Y por “exprimir” no significa “tenerlas echando humo” haciendo cosas, sino más bien al contrario: que un solo servidor pueda ejecutar las mismas funcionalidades pero con un consumo mínimo de CPU y memoria. Y en C#, mínimos cambios en los programas pueden acelerarlos de forma más que notable.

No hay que pensar en segundos… toca hacerlo en milisegundos

Es lo que veo muchísimas veces hablando con otros programadores que no ven ningún problema en su código: prácticamente hagas lo que hagas hoy en día con un ordenador, si sólo lo haces una vez, es virtualmente instantáneo. Escriben código, ejecutan el programa de prueba y violà! el resultado en “menos de un segundo”. El problema es que cuando eso mismo hay que hacerlo decenas de miles de veces por minuto (o segundo!) la cosa se complica: lo que antes tardaba “menos de un segundo” ahora tarda horas. Y es donde las micro-optimizaciones o los “refactorings” complejos (que veremos como parte final de este artículo) muestran sus resultados: limando no segundos, sino milisegundos a cada operación se obtienen beneficios simplemente espectaculares en el rendimiento de nuestros programas.

Importante: hay cosas que no se pueden optimizar: hay que rehacerlas

Y no se nos deben caer los anillos por ello. Puede que algo simplemente sea lento. Muy lento. Excesivamente lento. Y es muy probable que no tenga por qué serlo y la “optimización” no sea la respuesta, sino más bien un rediseño. Y no pasa nada: es lo que he hecho durante el último año. Si estás leyendo esto pensando en que vas a encontrar una pauta de programación que apliques “en dos líneas” a tu código y que de repente funcione deprisa, deja de leer ya: si, algo ganarás, pero no mucho. Pero sí hay infinidad de cosas a saber para mejorar de forma más que notable la velocidad y el rendimiento de nuestros actuales programas sin tener que tirarlos enteros y empezarlos de cero.

El truco? Optimizar bloque por bloque, función por función, y no pensando en que “quede más bonito”, sino que quede “más rápido” (lamentablemente en muchos casos no es compatible lo uno con lo otro, pero recuerda: comenta tu código). Y siempre: usa el Profiler de Visual Studio!

Pero vamos a un ejemplo real…

new() y el recolector de basura

O “garbage collector” como muchos lo conocemos. Nos permite olvidarnos de la gestión de memoria, de acordarnos de liberar los objetos que ya no usamos, pero con un precio: reservar memoria es “caro”, pero es que la gestión de la recogida de basura es carísima. Y eso que cada generación de la máquina virtual .NET mejora particularmente en eso, en la reserva y la limpieza de la memoria.

Por supuesto no estoy diciendo que crear objetos con new() sea malo, en absoluto. El problema, particularmente con C# y otros lenguajes gestionados, es que hay montones de new()’s implícitos en infinidad de operaciones que podemos evitar. Menos reservas de memoria, más velocidad. Menos objetos que gestionar, mucha más velocidad todavía. El otro problema es asumir que el coste de hacer new() es cero: nada más lejos de la realidad (creo que sólo hay otra operación más cara que reservar memoria: lanzar una excepción, pero eso es otra historia).

Un ejemplo simple pero práctico: típico programa que recibe registros en texto separados por comas. Cuantas veces habéis visto (o escrito) algo como esto:

static int[] Decode(string data)
{
  string[] dataFragments = data.Split(new char[] { ',' });
  List dataValues = new List();
  foreach (string fragment in dataFragments)
  {
    dataValues.Add(Int32.Parse(fragment));
  }
  return (dataValues.ToArray());
}

Alguien ve algo “raro”? No?

Algo tan simple como ese new char[] { ‘,’ } tan inocente es responsable de una merma en la velocidad de la función.

Si, esta función se ejecuta en muy, muy poco tiempo – si sólo se llama una vez. El problema es si hay que llamarla miles de veces. Miles de veces crearemos un array en memoria, de un carácter de capacidad, pero toca buscar memoria, asignarla, inicializarla… Una vez no pasa nada… un par de millones de veces y la cosa se complica.

Esa es la típica situación que mejora simplemente con un mínimo cambio en el programa:

private static readonly char[] splitChars = new char[] { ',' };
static int[] Decode(string data)
{
  string[] dataFragments = data.Split(splitChars);
  List dataValues = new List();
  foreach (string fragment in dataFragments)
  {
    dataValues.Add(Int32.Parse(fragment));
  }
  return (dataValues.ToArray());
}

Tan sencillo como usar una variable estática, inicializada al principio de la ejecución del programa, y eliminamos miles y miles de reservas de memoria. Para comprobarlo, vamos a escribir un programa de pruebas que ejecute las dos funciones un millón de veces, para comprobar si hay alguna diferencia:

public static void Run()
{
  Console.WriteLine("Código inicial en ejecución.");

  string record = @"23,51,181,47,193,119,651,45,213,194,413,188";
  var start = DateTime.Now;

  int sum = 0;
  for (int dd = 0; dd < 1000000; dd++)
  {
    var dataElements = Decode(record);
    sum = 0;
    for (int zz = 0; zz < dataElements.Length; zz++)
      sum += dataElements[zz];
  }

  GC.Collect();
  var span = DateTime.Now - start;
  Console.WriteLine("Suma: " + sum + " - Tiempo total: " + span.ToString());
}

Este programa simplemente decodifica el mismo registro 1 millón de veces y, por hacer algo, simplemente suma los valores de los números (así verificamos que todas las variantes devuelven los mismos resultados). Ahora ejecutamos el programa en versión Release con las dos versiones de la función:

Base y primera optimización

Simplemente por evitar esa reserva de memoria nos ahorramos 140 milisegundos: un 5% del tiempo total (he calculado el ahorro sobre varias ejecuciones – nunca tarda lo mismo). No está mal para una sola línea modificada.

Pero vayamos un poco más allá. Hay otra reserva de memoria al final de la función, al hacer una conversión a array (return(dataValues.ToArray()). Si devolvemos directamente el objeto dataValues, sin conversión, qué pasa? Cambiemos

static int[] Decode(string data)
{
  ...
  return (dataValues.ToArray());
}

por

static List<int> Decode(string data)
{
  ...
  return (dataValues);
}

y ajustamos un poco la función de prueba para que use la propiedad Count en lugar de la propiedad Length de los arrays. El resultado:

StringOptimizationRun2

Otros 50 milisegundos que hemos limado al tiempo original. Y todavía no hemos trabajado mucho para un ahorro del 7%.

Poco más se puede rascar, verdad? Pues no… aún queda mucho por hacer.

Empieza el refactoring

Hasta ahora hemos simplemente cambiado cosas de sitio o hemos eliminado alguna reserva de memoria. Cuando ese proceso no da para más, toca pensar cómo cambiar la estructura del código para que sea más eficiente.

El siguiente responsable de reservas de memoria es el objeto List<int> que usamos para almacenar los valores mientras se leen del registro de entrada. Y si en lugar de eso usamos un array permanente para devolver los valores? Vamos con el refactoring, y aquí es donde tenemos que poner un límite a los datos.

Hasta ahora nuestro código podía soportar cantidades arbitrarias de elementos en un registro, pero es necesario realmente? O podemos poner un límite de, digamos 100 elementos? 200? 500 quizá?

Cambiemos la función para que quede como sigue:

private static readonly char[] splitChars = new char[] { ',' };
static List Decode(string data)
{
  string[] dataFragments = data.Split(splitChars);
  List dataValues = new List();

  foreach (string fragment in dataFragments)
  {
    dataValues.Add(Int32.Parse(fragment));
  }
  return (dataValues);
}

Si os fijáis la función espera ahora un array de enteros que rellenará con los valores que obtenga del registro, devolviendo como resultado el número de elementos que ha leído. Otro poco más de refactoring en la función de pruebas para que quede como sigue:

int[] values = new int[512];
int sum = 0;
for (int dd = 0; dd < 1000000; dd++)
{
  var elementCount = Decode(record,values);
  sum = 0;
  for (int zz = 0; zz < elementCount; zz++)
    sum += values[zz];
}

La función reserva primero un único array de enteros que usa para todas las llamadas a la función. Da igual que el array sea local a la función o miembro de la clase: sólo se crea uno y se reutiliza continuamente. Si ejecutamos el programa ahora con la nueva versión obtenemos lo siguiente:

StringOptimizationRun3

Ahora sí se ve una mejora realmente sólida del rendimiento, del orden nada menos que del 20% con respecto al código original.

Pero se puede ir mucho, muchísimo más lejos.

El siguiente responsable de la velocidad de la función es el Split() que hacemos de la cadena. Split tiene que fabricar internamente un List<String> para almacenar cada cadena que va truncando, reservar cada objeto String de cada elemento separado, inicializar los contenidos (sacando copias de fragmentos de la cadena original), agregarlo a la colección para finalmente invocar el método ToArray() y devolvernos el array de cadenas. Y una vez que terminamos la función, todo ese trabajo se destruye y pasa al recolector de basura. Y todo eso destruye el rendimiento, cada reserva de memoria y copias por la RAM machacan los contenidos del caché del procesador, volviéndolo prácticamente una inutilidad. Los procesadores modernos son felices leyendo memoria secuencial, y el mero concepto de crear miles de objetos desperdigados por la memoria del PC va en detrimento del rendimiento.

Pero qué se puede hacer en ese caso?

Optimización final

Pues en este caso, nada tan facil como no usar lo que parece obvio. Vamos a pensar en el registro que nos dan no como en una cadena que sólo podemos utilizar por medio de otras funciones, creando copias, fragmentándola, sino como un simple array de caracteres. Porque, curiosamente, lo es.

La versión final de la función recorre la cadena de caracteres uno por uno: si el caracter es un dígito lo acumula, si el caracter es una “coma” completa la acumulación y añade el valor al array de enteros del resultado, haciendo simplemente operaciones aritméticas simples, con las que el procesador es feliz (por cierto: si sabes C++, te sentirás como en casa):

static int Decode(string data,int[] values)
{
  int charCount = data.Length;
  int valueIndex = 0;
  int acc = 0;
  for (int dd=0;dd<charCount;dd++)
  {
    char c = data[dd];
    if ((c >= '0') && (c <= '9'))
    {
      acc *= 10;
      acc += (c & 0xF);
      continue;
    }
    if (c == ',')
    {
      values[valueIndex++] = acc;
      acc = 0;
      continue;
    }
    throw new Exception("Invalid character in data.");
  }
  // Añadir el último valor
  values[valueIndex++] = acc;

  return (valueIndex);
}

Tampoco ha sido para tanto… pero veamos cuanto hemos ganado:

StringOptimizationRun4

No, no es una broma: de un proceso que tardaba 2,5 segundos en ejecutarse se ha quedado al final en 100 milisegundos. 25 veces más rápido que el original.

Y el truco simplemente consiste en optimizar el código en bloques funcionales de tamaños como el que acabamos de ver, que permiten concentrarse en un problema concreto, determinar los requerimientos exactos y escribir un código que haga “eso” y “nada más que eso”.

Pensando en la nube

Y si realmente te estás planteando hospedar tu flamante proyecto en la nube, empezar pensando en las optimizaciones como las que acabas de ver te puede ahorrar un dineral, en éste caso concreto en el costo de máquinas virtuales, o en caso de máquinas físicas poder emplear PCs mucho más económicos con el mismo rendimiento. Pero hay que ser metódico, no puedes decirte a tí mismo “bueno, ya optimizaré al final”, porque quizá, cuando llegue el momento, descubras que el “refactoring” requiere de una reescritura completa. Y créeme, llegado ese momento ya es tarde…

Por cierto, otra pequeña optimización de la que no he hablado: cuantos componentes de .NET tenéis referidos en la sección References de vuestros proyectos? Seguro que los usáis todos? Quizá puedas eliminar alguno de ellos o más de los que crees. Y una DLL que no se carga (junto con sus propias dependencias) es otro ahorro de recursos.

El proyecto completo con todas las versiones de optimización podéis descargarlo aquí. Y podéis comprobar qué referencias usa. Sólo una: System.DLL.

Categorías:Desarrollo, Tutorial Etiquetas: , , ,

Tutorial: Leer Twitter en Windows Phone 7 con el mínimo esfuerzo

En este tutorial vamos a comprobar lo sencillo que es explotar servicios online en una aplicación Windows Phone 7 y de paso conocer un poco más las herramientas de desarrollo para esta plataforma.

Actualización: Lo siento mucho chicos, pero Twitter cambió hace unos meses su API para evitar, precisamente, aplicaciones como esta. Las técnicas de programación para Windows Phone siguen siendo válidas, pero mucho me temo que descargar los tweets, ya no.

Aunque parezca increíble, no tengo cuenta en Twitter. Sí, lo sé, podéis decirme lo que queráis, creo que simplemente no es para mi. Lo cual no quita para que sea un ávido lector de tweets de mucha gente, particularmente de fuentes técnicas y si (debilidad de uno) del mundillo de la Fórmula 1 y del de motociclismo. La ventaja es que no hace falta tener cuenta en Twitter para leer los mensajes de otros, y el portal pone a disposición de los programadores un amplísimo surtido de servicios web para acceder a la información.

Así que vamos a hacer, paso a paso, un lector de Tweets para Windows Phone 7. Para este tutorial he utilizado Visual Studio 2010 junto con el SDK de Windows Phone 7.5 Mango, pero vamos a seleccionar como “target” Windows Phone 7.0 “actual”.

Crear el proyecto

El primer paso es abrir VisualStudio y crear un nuevo proyecto para Windows Phone 7: seleccionamos el menú “File”, “New”, “Project” y podremos elegir el tipo de proyecto que deseemos. De las plantillas instaladas buscaremos “Visual C#”, y dentro de éstas “Silverlight for Windows Phone”. A la derecha podremos elegir “Windows Phone Application”.

En la parte inferior del diálogo de creación del nuevo proyecto escogeremos un directorio de trabajo y un nombre para nuestro nuevo y flamante programa. Yo he llamado al mío “tweetRead”:

image

En la parte superior del diálogo aparece un selector con la versión a utilizar de .Net Framework: dado que vamos a realizar una aplicación para WIndows Phone, la selección es indiferente, porque el target es Silverlight para WIndows Phone.

Una vez creado el proyecto, aparecerá abierta la página principal en Visual Studio, listo para empezar a realizar cambios:

image

Perfilemos el interface de usuario

Visual Studio crea un proyecto a partir de una plantilla con el formato predeterminado de las aplicaciones para WP7. En nuestro caso, vamos a reducir el interface para ir directamente a la implementación. Así que buscaremos en el fuente XAML el fragmento que define la cabecera de la aplicación (“TitlePanel”) y lo reemplazaremos por una Grid con los controles que nos permitirán seleccionar un usuario de Twitter. Vamos a reemplazar totalmente este fragmento:

<StackPanel x:Name=”TitlePanel” Grid.Row=”0″ Margin=”12,17,0,28″>
<TextBlock x:Name=”ApplicationTitle” Text=”MY APPLICATION” Style=”{StaticResource PhoneTextNormalStyle}”/>
<TextBlock x:Name=”PageTitle” Text=”page name” Margin=”9,-7,0,0″ Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>

Por este otro:

<!–ContentPanel – place additional content here–>
<Grid x:Name=”TitlePanel” Grid.Row=”0″ Margin=”12,17,0,28″>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row=”0″ Grid.ColumnSpan=”2″ Text=”Tweets recientes de”/>
<TextBox Grid.Row=”1″ Grid.Column=”0″ x:Name=”tweetName”/>
<Button Grid.Row=”1″ Grid.Column=”1″ Content=”Ir!” x:Name=”btnLoad” Click=”TweetLoadClicked” />
</Grid>

Hemos reemplazado la cabecera clásica por algo un poco más “funcional” en nuestro ejemplo: un campo de texto donde el usuario escribirá la fuente de tweets que desea leer y un botón que lanza la descarga. Hemos aprovechado incluso para definir qué función se encargará del evento “Click” del botón, la función “TweetLoadClicked”. En cuanto hayamos pegado este fragmento, la simulación del interface cambiará completamente a algo parecido a esto:

image

Un poco más abajo se declara una Grid con el contenido de la página del programa. En este caso vamos a añadir un control ListBox que será el que finalmente muestre los mensajes:

<!–ContentPanel – place additional content here–>
<Grid x:Name=”ContentPanel” Grid.Row=”1″ Margin=”12,0,12,0″>
<ListBox x:Name=”tweetFeed”>
</ListBox>
</Grid>

Al código!

Ahora es cuando vamos a empezar a añadir funcionalidad a la aplicación. Para ello, hacemos doble click sobre el archivo de código del formulario (MainPage.xaml.cs), para encontrarnos con el desolador espectáculo de una clase que no hace “casi” nada:

image

Si recordamos de antes, tenemos un TextBlock cuyo evento “Click” va a ejecutar la función TweetLoadClicked. La función iniciará la descarga de los tweets usando el nombre tecleado por el usuario:

private void TweetLoadClicked(object sender, RoutedEventArgs e)
{
WebClient tweetRequest = new WebClient();
tweetRequest.DownloadStringCompleted += new DownloadStringCompletedEventHandler(DownloadComplete);
tweetRequest.DownloadStringAsync(new Uri(@”
http://api.twitter.com/1/statuses/user_timeline.xml?screen_name=” + tweetName.Text));
}

El método DownloadStringAsync() de WebClient simplemente descarga el contenido de la url que le demos y una vez terminado invoca el evento que se haya definido en DownloadStringCompleted (en nuestro caso, será la función “DownloadComplete”). Así que para poder experimentar un poco, vamos a empezar la función y ver qué pasa:

void DownloadComplete(object sender, DownloadStringCompletedEventArgs e)
{
System.Diagnostics.Debug.WriteLine(e.Result);
}

Si ponemos un breakpoint en la línea con el WriteLine y ejecutamos el programa, podremos ver si funciona. Así que pulsamos F5, arrancará nuestro querido emulador y nuestro programa tomará el control. Vamos a pedir tweets de Engadget, a ver qué pasa:

image

Cuando pulsemos el botón “Ir!” la función TweetLoadClicked comenzará su trabajo y empezará a descargar el contenido usando los APIs de Twitter. Unos segundos más tarde, VisualStudio se detendrá en el breakpoint con los resultados. Basta con colocar el ratón sobre la palabra “Result” para que aparezcan los contenidos:

image

Si necesitamos analizar la información con más detalle, podemos emplear el visor que aparece al selecciona el menú con la lupa a la izquierda y seleccionar el visor XML:

image

Veremos directamente la información XML enviada por Twitter:

image

Mostrando los resultados

Bueno, esto está muy bien, pero ahora toca ver los resultados de nuestros denodados esfuerzos de programación. Lo primero, vamos a desensamblar los contenidos XML de la respuesta del servidor en algo un poco más manejable, y para ello usaremos nuestro querido XmlReader para ir desensamblando los fragmentos de información enviados por el servidor. Pero antes, vamos a crear una clase que contiene la información de un tweet. Para ello, en VisualStudio pulsaremos con el botón derecho sobre el proyecto tweetRead y seleccionaremos “Add Class” (añadir clase):

image

Como nombre de archivo emplearemos “Tweet.cs”, y usaremos el código siguiente:

using System;

namespace tweetRead
{
public class Tweet
{
public string message { get; set; }
public string userName { get; set; }
public String imageUrl { get; set; }
}
}

Volviendo a la función DownloadComplete, vamos a cambiarla por algo más “funcional” que lo que hasta ahora hace:

void DownloadComplete(object sender, DownloadStringCompletedEventArgs e)
{
List<Tweet> Tweets = new List<Tweet>();
using (System.Xml.XmlReader reader = System.Xml.XmlReader.Create(new System.IO.StringReader(e.Result)))
{
while (reader.ReadToFollowing(“status”))
{
reader.ReadToFollowing(“text”);
String msg = reader.ReadElementContentAsString();
reader.ReadToFollowing(“user”);
reader.ReadToFollowing(“name”);
String user = reader.ReadElementContentAsString();
reader.ReadToFollowing(“profile_image_url”);
String url = reader.ReadElementContentAsString();

      Tweets.Add(new Tweet()
{
message = msg,
userName = user,
imageUrl = url
});
}
}
tweetFeed.ItemsSource = Tweets;
}

Empleando un XmlReader, recorremos los elementos de respuesta del servidor, creando instancias de nuestra clase Tweet y almacenándolos en un array. Una vez leídos todos, asignamos el array como origen de datos (ItemSource) del ListView.

Sólo queda una cosa por hacer: definir el formato de cada Tweet en nuestro interface de usuario. Si recordáis, nuestro ListView estaba completamente vacío, pero le estamos pasando directamente una colección de objetos Tweet. Para definir el aspecto final de un Tweet en el interface de usuario, añadiendo simplemente la definición para “un ítem” definiendo el “ItemTemplate” del control, incluyendo propiedades que debe presentar. Así que vamos a ampliar la declaración del ListBox con el formato completo:

<!–ContentPanel – place additional content here–>
<Grid x:Name=”ContentPanel” Grid.Row=”1″ Margin=”12,0,12,0″>
<ListBox x:Name=”tweetFeed”>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin=”0,0,0,10″>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”Auto”/>
<ColumnDefinition Width=”*”/>
</Grid.ColumnDefinitions>
<Image Grid.Column=”0″ Source=”{Binding imageUrl}” VerticalAlignment=”Top”/>
<Grid Grid.Column=”1″ Margin=”5″>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row=”0″ Text=”{Binding userName}” VerticalAlignment=”Top” Style=”{StaticResource PhoneTextAccentStyle}”/>
<TextBlock Grid.Row=”1″ Text=”{Binding message}” TextWrapping=”Wrap” VerticalAlignment=”Top” Style=”{StaticResource PhoneTextNormalStyle}”/>
</Grid>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>

El “truco” es, simplemente, el valor para los campos que nos interesen de los controles (Text en nuestro caso). En lugar de establecer un valor concreto, empleamos la sintáxis “{Binding <campo>}”, haciendo que los controles “busquen” una propiedad con ese nombre del ItemSource (en el caso del ListBox) que le asignemos, que es la única fuente a la información que el código ha tenido que establecer. De igual manera, el control imagen tiene asignada su propiedad Source a otro de los campos.

Esta es la parte fantástica de Silverlight: en el código no hay absolutamente ninguna referencia al formato en pantalla de los datos, simplemente se preparan, se “empaquetan” y se le envían al interface de usuario. El interface “sabe” cómo debe presentar los datos, pero desconoce totalmente el origen o formato original de los mismos. De esta forma, manipular el formato en pantalla, adaptarlo a otras resoluciones o formatos, se convierte en una tarea totalmente desvinculada de la programación.

Como prueba final, vamos a ver cómo queda el programa con todos los cambios:

image

No es el cliente Twitter definitivo, pero para estar hecho en media hora no está mal. Lo importante es lo sencillo que es consumir servicios web desde Windows Phone y la potencia del databinding para separar el código de la presentación.

Categorías:Desarrollo, Tutorial, Windows Phone Etiquetas: , ,