Optimizaciones de JavaScript utilizadas en el proyecto de Prometheus

Introducción

Para los que no lo sepáis, próximamente se estrena la película Prometheus en Estados Unidos y desde Plain Concepts hemos desarrollado el training center del sitio web. Se puede acceder desde este enlace: http://www.projectprometheus.com/trainingcenter/. El proyecto ha estado financiado por Microsoft, más concretamente por el equipo de Internet Explorer, así que como página web que es, se ha desarrollado utilizando las últimas tecnologías web: HTML5 + CSS3.

Training center

El centro de entrenamiento es un sitio web donde los candidatos al proyecto Prometheus, de la empresa Weyland, puede probar su valía. El entrenamiento cuenta con 5 pruebas (juegos) que el recluta tiene que completar en un tiempo determinado. Una vez superadas las cinco pruebas el recluta puede formar parte de Weyland Industries. Los cinco juegos han sido desarrollados por gente de Plain Concepts:

Cada uno de los cuales ha desarrollado uno de los juegos del centro de entrenamiento. En mi caso he desarrollado el cubo de rubick en 2 y 3 dimensiones.

clip_image001

Aquí se puede ver una captura del juego en Internet Explorer llamado Prefrontal Cortex.

HTML5 / Javascript

Todos los juegos han sido desarrollados en HTML5 utilizando JavaScript para la parte de programación, en mi caso he utilizado Canvas para dibujar los cubos. Eso significa que todos los juegos funcionan perfectamente en todos los navegadores modernos, incluyendo Google Chrome, Firefox, Safari, Opera y Internet Explorer 9 y 10.

Para el desarrollo de los juegos se creo un motor en JavaScript que nos permitiera dibujar en un Canvas la geometría de los modelos de los cubos. Este motor no utiliza WebGL para renderizar los cubos, porque Internet Explorer no tiene soporte (además de en el resto es experimental), por lo que se opto por hacer un motor grafico completo desde cero. Es decir, todo el pipeline de grafica se tiene que hacer en JavaScript, esto significa, entre otras cosas, que tenemos que emular por software como funciona una tarjeta gráfica y eso normalmente es más lento que el propio hardware. Así que el desafío de implementar un pipeline gráfico por software es mayor ya que tiene que tener un rendimiento aceptable.

JavaScript 101

JavaScript es un lenguaje en el que existen varios tipos de datos básicos con los que podemos trabajar.

  • Object
  • Array
  • Number
  • String
  • Boolean

Los objetos son la forma más común a la hora de trabajar en JavaScript y se pueden utilizar de muchas maneras.

La forma más sencilla de crear un objeto es:

   1: var myObject = {}

A partir de ahí se pueden ir agregando propiedades al objeto sin ningún tipo de restricción. No son propiedades como las que se puede estar acostumbrado en C#, sino que el tipo object se comporta como una especie de diccionario de pares nombre valor.

Se pueden crear propiedades de la siguiente manera a un objeto previamente definido.

   1: myObject.name = ‘Luis’;

   2: myObject.number = 42;

Así el objeto pasará a tener dos propiedades, una llamada ‘name’ con el valor ‘Luis’ y otra llamada ‘number’ con el valor ‘42’.

También se puede acceder a esas propiedades como si de un diccionario se tratase. Las dos formas son igual de válidas y correctas.

   1: var name = myObject[‘name’];

Después de la ejecución de esta línea de código lo que se establece en la variable name es el valor de ‘Luis’, previamente establecido.

Con esta nueva forma de acceder a las propiedades no solo se pueden leer valores almacenados en un objeto sino que también se pueden guardar.

myObject[‘currentDate’] = new Date();

Vector3

Como ejemplo de objeto se va a definir Vector3; un vector de 3 dimensiones.

var vector3 = {x: 1, y: 1, z: 1}

¿Cuál es el problema con este Vector3?

El rendimiento. Como se ha dicho antes todos los objetos en JavaScript se comportan como un diccionario de pares nombre / valor, así que en cada una de las operaciones en las que se tenga que leer o escribir el valor de x, y o z, el runtime de JavaScript tiene que comprobar que la propiedad existe o no y luego leerla o almacenarla. Todo esto lleva tiempo. Es como si se programase todo el acceso a propiedades y campos en .NET utilizando únicamente la API de Reflexion (System.Reflection).

En el caso del motor de 3D en JavaScript Vector2, Vector3, Vector4, Color y Matrix son tipos que se están usando constantemente para dibujar la geometría de los cubos, así que esos tipos fueron los primero en ser optimizados.

La solución por la que se opto fue eliminar la definición de los tipos, es decir, que por ejemplo Vector2, Vector3, Vector4 y Color pasaron a ser un array de 2, 3, 4 y 4 posiciones respectivamente. Así que por convención lo que se hizo fue que la posición dentro del array representaba una coordenada de las dimensiones del vector.

  • X: array[0]
  • Y: array[1]
  • Z: array[2]
  • W: arrat[3]

En el caso de Matrix que se tenía M11, M12…M21,M22..M31..M44 pasaron a ser también las posiciones de un array.

Veamos como se ha cambiado la multiplicación de matrices, uno de los cuellos de botella, en cuanto a rendimiento se refiere.

Antes

   1: function Multiply(matrix1, matrix2) {

   2:     var matrix = new Matrix();

   3:     matrix.M11 = (((matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21)) + (matrix1.M13 * matrix2.M31)) + (matrix1.M14 * matrix2.M41);

   4:     matrix.M12 = (((matrix1.M11 * matrix2.M12) + (matrix1.M12 * matrix2.M22)) + (matrix1.M13 * matrix2.M32)) + (matrix1.M14 * matrix2.M42);

   5:     matrix.M13 = (((matrix1.M11 * matrix2.M13) + (matrix1.M12 * matrix2.M23)) + (matrix1.M13 * matrix2.M33)) + (matrix1.M14 * matrix2.M43);

   6:     matrix.M14 = (((matrix1.M11 * matrix2.M14) + (matrix1.M12 * matrix2.M24)) + (matrix1.M13 * matrix2.M34)) + (matrix1.M14 * matrix2.M44);

   7:     matrix.M21 = (((matrix1.M21 * matrix2.M11) + (matrix1.M22 * matrix2.M21)) + (matrix1.M23 * matrix2.M31)) + (matrix1.M24 * matrix2.M41);

   8:     matrix.M22 = (((matrix1.M21 * matrix2.M12) + (matrix1.M22 * matrix2.M22)) + (matrix1.M23 * matrix2.M32)) + (matrix1.M24 * matrix2.M42);

   9:     matrix.M23 = (((matrix1.M21 * matrix2.M13) + (matrix1.M22 * matrix2.M23)) + (matrix1.M23 * matrix2.M33)) + (matrix1.M24 * matrix2.M43);

  10:     matrix.M24 = (((matrix1.M21 * matrix2.M14) + (matrix1.M22 * matrix2.M24)) + (matrix1.M23 * matrix2.M34)) + (matrix1.M24 * matrix2.M44);

  11:     matrix.M31 = (((matrix1.M31 * matrix2.M11) + (matrix1.M32 * matrix2.M21)) + (matrix1.M33 * matrix2.M31)) + (matrix1.M34 * matrix2.M41);

  12:     matrix.M32 = (((matrix1.M31 * matrix2.M12) + (matrix1.M32 * matrix2.M22)) + (matrix1.M33 * matrix2.M32)) + (matrix1.M34 * matrix2.M42);

  13:     matrix.M33 = (((matrix1.M31 * matrix2.M13) + (matrix1.M32 * matrix2.M23)) + (matrix1.M33 * matrix2.M33)) + (matrix1.M34 * matrix2.M43);

  14:     matrix.M34 = (((matrix1.M31 * matrix2.M14) + (matrix1.M32 * matrix2.M24)) + (matrix1.M33 * matrix2.M34)) + (matrix1.M34 * matrix2.M44);

  15:     matrix.M41 = (((matrix1.M41 * matrix2.M11) + (matrix1.M42 * matrix2.M21)) + (matrix1.M43 * matrix2.M31)) + (matrix1.M44 * matrix2.M41);

  16:     matrix.M42 = (((matrix1.M41 * matrix2.M12) + (matrix1.M42 * matrix2.M22)) + (matrix1.M43 * matrix2.M32)) + (matrix1.M44 * matrix2.M42);

  17:     matrix.M43 = (((matrix1.M41 * matrix2.M13) + (matrix1.M42 * matrix2.M23)) + (matrix1.M43 * matrix2.M33)) + (matrix1.M44 * matrix2.M43);

  18:     matrix.M44 = (((matrix1.M41 * matrix2.M14) + (matrix1.M42 * matrix2.M24)) + (matrix1.M43 * matrix2.M34)) + (matrix1.M44 * matrix2.M44);

  19:     return matrix;

  20: }

La multiplicación simplemente accedía a cada uno de los índices de la matriz, los multiplicaba y luego los asignada de vuelta a la matriz de resultado. Como hemos dicho antes, esto implica leer una gran cantidad de propiedades durante el dibujado de un frame de la escena.

Ahora

   1: function Multiply(matrix1, matrix2) {

   2:     var matrix = new Matrix();

   3:     var position = matrix.position;

   4:     var position1 = matrix1.position;

   5:     var position2 = matrix2.position;

   6:     position[0] = (((position1[0] * position2[0]) + (position1[1] * position2[4])) + (position1[2] * position2[8])) + (position1[3] * position2[12]);

   7:     position[1] = (((position1[0] * position2[1]) + (position1[1] * position2[5])) + (position1[2] * position2[9])) + (position1[3] * position2[13]);

   8:     position[2] = (((position1[0] * position2[2]) + (position1[1] * position2[6])) + (position1[2] * position2[10])) + (position1[3] * position2[14]);

   9:     position[3] = (((position1[0] * position2[3]) + (position1[1] * position2[7])) + (position1[2] * position2[11])) + (position1[3] * position2[15]);

  10:     position[4] = (((position1[4] * position2[0]) + (position1[5] * position2[4])) + (position1[6] * position2[8])) + (position1[7] * position2[12]);

  11:     position[5] = (((position1[4] * position2[1]) + (position1[5] * position2[5])) + (position1[6] * position2[9])) + (position1[7] * position2[13]);

  12:     position[6] = (((position1[4] * position2[2]) + (position1[5] * position2[6])) + (position1[6] * position2[10])) + (position1[7] * position2[14]);

  13:     position[7] = (((position1[4] * position2[3]) + (position1[5] * position2[7])) + (position1[6] * position2[11])) + (position1[7] * position2[15]);

  14:     position[8] = (((position1[8] * position2[0]) + (position1[9] * position2[4])) + (position1[10] * position2[8])) + (position1[11] * position2[12]);

  15:     position[9] = (((position1[8] * position2[1]) + (position1[9] * position2[5])) + (position1[10] * position2[9])) + (position1[11] * position2[13]);

  16:     position[10] = (((position1[8] * position2[2]) + (position1[9] * position2[6])) + (position1[10] * position2[10])) + (position1[11] * position2[14]);

  17:     position[11] = (((position1[8] * position2[3]) + (position1[9] * position2[7])) + (position1[10] * position2[11])) + (position1[11] * position2[15]);

  18:     position[12] = (((position1[12] * position2[0]) + (position1[13] * position2[4])) + (position1[14] * position2[8])) + (position1[15] * position2[12]);

  19:     position[13] = (((position1[12] * position2[1]) + (position1[13] * position2[5])) + (position1[14] * position2[9])) + (position1[15] * position2[13]);

  20:     position[14] = (((position1[12] * position2[2]) + (position1[13] * position2[6])) + (position1[14] * position2[10])) + (position1[15] * position2[14]);

  21:     position[15] = (((position1[12] * position2[3]) + (position1[13] * position2[7])) + (position1[14] * position2[11])) + (position1[15] * position2[15]);

  22:     return matrix;

  23: }

Lo primero que se observa es que el código pasa a ser más críptico que el anterior, es decir, que ahora únicamente se tiene son los diferentes índices de position dentro de tres arrays, que representan las tres matrices con las que se esta trabajando en este momento.

Así que se ha pasado de,

matrix.M11 = (((matrix1.M11 * matrix2.M11) + (matrix1.M12 * matrix2.M21)) + 

(matrix1.M13 * matrix2.M31)) + (matrix1.M14 * matrix2.M41);

a esto:

position[0] = (((position1[0] * position2[0]) + (position1[1] * position2[4])) + 

(position1[2] * position2[8])) + (position1[3] * position2[12]);

Ya que ahora todas las posiciones de la matriz están almacenadas en un array de 16 posiciones lo que se tiene que hacer si se quiere acceder al valor M11 es acceder a la posición 0 de array, en el caso del valor M31 a la posición 8 de array y así sucesivamente.

Otras optimizaciones

Tamaño de los arrays

Si se tiene un array que tiene una propiedad length por la que se quiere iterar para realizar una acción por cada uno de los elementos del array, es recomendable no poner directamente el valor de myArray.length para comprobar si se ha llegado al final de array, sino guardar el tamaño del array en una variable y usar esta variable.

   1: var myArray = new Array();

   2:  

   3: for (var i = 0; i < myArray.length; i++) {

   4:     myArray[i] = i;

   5: }

   6:  

   7: var length = myArray.length;

   8: for (var i = 0; i < length; i++) {

   9:     myArray[i] = i;

  10: }

Cachear variables

Si durante la ejecución de un método se tiene variables que vamos a usar y estas variables son propiedades de un objeto, es mejor definirlas como variables en el ámbito del método que no referenciarlas desde el objeto original.

   1: var myObject =

   2:     {

   3:     name: 'luis',

   4:     company: {

   5:         name: 'PlainConcepts',

   6:         location: 'address'

   7:         }

   8:     };

   9:  

  10: var companyAddress = myObject.company.location;

  11: var company = myObject.company;

  12: companyAddress = company.location;

Espero que estas notas sobre optimización de JavaScript os sean útiles.

Luis Guerrero.

Autorización de usuarios para una API web en WCF o como hacer una gestión light de sesión en WCF

En uno de los proyectos en los que estoy trabajando ahora mismo tenemos que hacer una API para que se consuma desde Javascript puro, es un proyecto en HTML5, así que tenemos que maximizar la productividad para este tipo de cliente.

Nuestra API tiene un login de usuarios, un registro y partir de ahí los servicios debería de ser securizados, es decir, solamente para el usuario actual. Así que me surge la necesidad de autenticar estas peticiones para asegurarme de que es un usuario válido para acceder al servicio.

Viendo un poco como los demás servicios, Twitter, Facebook y compañía lo hacen, decidí que cuando el usuario hacer login se le devuelva un token de autorización temporal (que tienen este aspecto VvTnZEpvrYBDZfF1hCIR8kZR0yW7jKrA) obligar a que cada petición se añada una cabera más de Autrorization para que yo desde el servidor puede leerla y comprobar que es un usuario válido.

Ahora bien yo estoy desarrollando mi solución con WCF utilizando JSON como formato de cliente, para que así sea más cómodo consumirlo con el cliente, así que tenía dos maneras de solucionar esta manera de autorización, habilitar la compatibilidad de ASP.NET en WCF y hacerlo a través del objeto de HttpContext.Request o directamente utilizar la infraestructura de WCF.

Decidí usar únicamente WCF.

Autorizar al usuario

Lo primero de todo es que tengo que comprobar las credenciales del usuario en el login, podéis elegir el mejor mecanismo para eso. Una vez que sabemos que el usuario es un usuario válido tenemos que devolver el token de autorización para que pueda usarlo en sucesivas peticiones al servicio. ¿Cómo generamos esa autorización?

Yo he preferido hacerlo de la manera más sencilla y mantenerlo lo más sencillo posible. Yo genero un string formado por la id del usuario logeado y la fecha del login en ticks, así que me queda algo como esto: 1345-634475405148831292.

Evidentemente enviar ese string directamente al cliente es un grave problema de seguridad así que lo que tenemos que hacer es encriptar y añadir un hash a esa cadena.

private string CreateAuthorizationString(User user)
{
    string result = null;

    if (user != null)
    {
        string key = "{0}-{1}";
        key = string.Format(key, user.UserId, DateTime.Now.Ticks);

        ICryptoTransform transform = new TripleDESCryptoServiceProvider().CreateEncryptor(this.key, this.iv);
        byte[] input = Encoding.Default.GetBytes(key);
        byte[] buff = new byte[input.Length];
        buff = transform.TransformFinalBlock(input, 0, input.Length);
        
        result = Convert.ToBase64String(buff);
    }

    return result;
}

Yo para ese caso utilizo TripleDES como algoritmo simétrico y luego el string generado lo convierto a Base64 para tenerlo en un cómo string.

Comprobar la autorización en WCF

Una vez que tenemos generado el token de autorización tenemos que implementar un mecanismo para poder comprobar esa autorización en el servicio, teniendo un caso especial, uno cuando el usuario se quiere autorizar (hay que permitir la petición) y cualquier otra petición.

Yo en la definición de mi servicio tengo un webHttpBinding y tengo aplicado un endPointConfiguration y un serviceBehaviorConfiguration.

<behaviors>
  <endpointBehaviors>
    <behavior name="JsonEndpointBehavior">
      <webHttp defaultBodyStyle="Bare" defaultOutgoingResponseFormat="Json"
        automaticFormatSelectionEnabled="true" faultExceptionEnabled="true" />
    </behavior>
  </endpointBehaviors>
  <serviceBehaviors>
    <behavior name="DefaultServiceBehavior">
      <serviceMetadata httpGetEnabled="true" />
      <serviceDebug includeExceptionDetailInFaults="true" />
      <serviceAuthorization principalPermissionMode="Custom" serviceAuthorizationManagerType="Microsoft.Magazine.Foundation.MagazineServiceAuthorizationManager, Microsoft.Magazine.Foundation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </behavior>
  </serviceBehaviors>
</behaviors>

En el serviceBehavior tengo aplicado un ServiceAuthorization en el que especifico que el manager de objetos principal (identidades) será personalizado y especifico el tipo que se encargará de gestionar la autorización de las identidades.

Así que lo que tenemos que hacer es implementar los dos casos, cuando el usuario está intentado hacer login, tenemos que permitir la autorización y cuando el usuario hacer cualquier otra petición tenemos que asegurarnos de que es un usuario válido.

protected override bool CheckAccessCore(OperationContext operationContext)
{
    bool result = false;

    Message message = operationContext.RequestContext.RequestMessage;
    object value;
    if (message.Properties.TryGetValue("HttpOperationName", out value))
    {
        if ((string)value == "LoginUser")
        {
            result = true;
        }
    }

    if (!result)
    {
        HttpRequestMessageProperty httpRequestMessage;
        object httpRequestMessageObject;
        if (message.Properties.TryGetValue(HttpRequestMessageProperty.Name, out httpRequestMessageObject))
        {
            httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
            if (!string.IsNullOrEmpty(httpRequestMessage.Headers["Authorization"]))
            {
                string authorization = httpRequestMessage.Headers["Authorization"];
                result = new Login().IsValidAuthorization(authorization);
            }
        }
    }

    if (result)
    {
        operationContext.ServiceSecurityContext.AuthorizationContext.Properties["Principal"] = Thread.CurrentPrincipal;
    }

    return result;
}

La manera que tengo de comprobar que el usuario quiere hacer login es comprobando la operación de http que está invocando, que es, justamente la operación del servicio que invoca. Así que si está invocando LoginUser significa que está haciendo login así que result lo establezco en true.

En caso de que result no sea true, tengo que asegurarme de que la petición tiene la cabecera authorization, para ello tenemos que extraer de las propiedades del mensaje el objeto del tipo HttpRequestMessageProperty que contiene las propiedades de la petición http asociada a este mensaje. Acordaros que nosotros usábamos webHttpBinding con WebGet.

Dentro de ese objeto tenemos acceso a las cabeceras de http normales, buscamos Authorization y entonces intentamos validar ese token.

Validar el token de autorización

Una vez que ya tenemos el string que representa el token de autorización tenemos que desencriptar el contenido y parsear el formato para verificar la id del usuario y la fecha del login.

public bool IsValidAuthorization(string value)
{
    bool result = false;

    value.EnsureIsNotNullOrEmpty();

    ICryptoTransform transform = new TripleDESCryptoServiceProvider().CreateDecryptor(this.key, this.iv);
    byte[] buff = Convert.FromBase64String(value);

    buff = transform.TransformFinalBlock(buff, 0, buff.Length);
    string ticket = Encoding.Default.GetString(buff);

    string[] values = ticket.Split('-');
    if (values != null && values.Length == 2)
    {
        int userId;
        long ticks;
        if (int.TryParse(values[0], out userId) && long.TryParse(values[1], out ticks))
        {
            if (IsValidUser(userId) && Math.Abs((new DateTime(ticks) - DateTime.Now).Hours) < 1)
            {
                result = true;
            }
        }
    }

    return result;
}

Así que dentro de mi infraestructura validar el usuario es comprobar que es un usuario válido (está en la base de datos) y que el tiempo de la última vez que el usuario hizo login fue una hora.

Conclusiones

Con estos pasos tengo un sistema centralizado de autorización, utilizo en todo momento la infraestructura de WCF, sin habilitar la compatibilidad con ASP.NET, que penaliza el rendimiento, y no necesito en cada petición obtener la referencia al usuario actual sino que seré capaz de obtenerlo a través del objeto principal del thread que procesa la petición.

Es importante resaltar la importancia de encriptar el token de autorización para evitar problemas de robo de sesiones y generar sesiones automáticamente, ya que la clave y el vector de inicialización del algoritmo TricpleDES está seguro en la parte de servidor.

Luis Guerrero.