[FORENSIC] StorageFile vs IsolatedStorage

Hola!

Durante el pasado Codemotion 2014 celebrado en Madrid, Sergio Navarro me comentó que tenía un grave problema de rendimiento en una aplicación Windows Store. Sergio usa en sus aplicaciones una estupenda base de datos NoSQL multi plataforma llamada SiaqoDb. Es una base de datos estupenda, que facilita el almacenamiento de datos en aplicaciones destinadas a múltiples plataformas.

El problema

El problema en cuestión es que SiaqoDb tenía un rendimiento muy pobre en las lecturas en las aplicaciones para Windows 8.1, usando WinRT, en comparación con las aplicaciones Windows Phone usando Silverlight. En concreto estimaron que el cambio de rendimiento era de cerca de 40 veces más lento en WinRT.

La gente de SiaqoDb incluso le enviaron un ejemplo en C#, implementando la interfaz de archivos que usan internamente, para demostrar que el problema venía del lado del sistema. Si tenéis curiosidad por reproducir el problema, aquí tenéis el código original. La mayor peculiaridad que tiene, es que no leen el archivo completo de una sola vez, lo hacen en pequeñas partes. En el ejemplo, 5000 lecturas.

La verdad es que me picó mucho la curiosidad, pero en un primer momento pensé que la diferencia de rendimiento se podría deber a un tema puramente de hardware. Los smartphones llevan memoria solida y quizás si comparas su rendimiento con un disco duro normal, la lectura se puede resentir. Pero, ¿40 veces? Es una gran diferencia.

Por otro lado, Juan Manuel Montero también estaba dandole un vistazo al código y parecio que resolvio el problema, cambiando la forma en la que se leía el archivo. Pasó de leerlo por trozos a leerlo de una sola vez. Esto resolvía el problema de velocidad, pero no era aplicable en el contexto de SiaqoDb, que necesita realizar la lectura por partes.

Así que más intrigado todavía, me puse a darle un vistazo al código. Empecé por ejecutar el código original, para descartar el hardware, ejecutándolo en un PC de escritorio con disco duro magnético y en una Surface Pro 3 con disco SSD, los resultados en WinRT fueron igual de malos: unos 6.4 segundos en leer un archivo de 5Mb. A continuación probé la ejecución en un smartphone, en concreto en mi Lumia 1520. Resultado: 0.3 segundos en leer el mismo archivo de 5Mb.

Silverlight

Estaba muy claro que algo pasaba con las lecturas en WinRT, veamos el código original del método de lectura para Windows Phone (Silverlight):

Read – Silverlight
public virtual int Read(long pos, byte[] buf)
{
    file.Seek(pos, SeekOrigin.Begin);
    return file.Read(buf, 0, buf.Length);
}

Simple y efectivo 🙂 el objeto file es de tipo IsolatedStorageFileStream. Simplemente nos movemos a la posición deseada dentro del archivo y leemos la información dentro de un array de bytes.

WinRT

Veamos ahora el método ReadAsync de WinRT:

ReadAsync – WinRT
public virtual async Task<int> ReadAsync(long pos, byte[] buf)
{
    fileStream.Seek((ulong)pos);
    if (buf.Length > 0)
    {
        var buffer = Windows.Security.Cryptography.CryptographicBuffer.CreateFromByteArray(buf);
        IBuffer rd = await fileStream.ReadAsync(buffer, (uint)buf.Length, InputStreamOptions.None);
        rd.CopyTo(buf);
    }
    return (int)buf.Length;
}

Mmm, sencillo, pero podemos empezar a ver la causa del problema, ¿Lo puedes ver? En este caso, el código es asíncrono como todas las APIs de acceso a disco de WinRT. Necesitamos crear una instancia de IBuffer, tipo que usa el método ReadAsync de la clase FileRandomAccessStream. A continuación, ser hace una copia del IBuffer resultante del método ReadAsync al array de bytes para devolver su tamaño.

Desde luego realizamos muchas más operaciones, creamos objetos, hacemos copias… ¿Cuanto tiempo nos penalizan estas acciones? Midiéndolas, de una forma muy burda, podemos ver lo siguiente:

  • El método CreateFromByteArray de la clase CryptographicBuffer, consume en total 0.6 segundos. Esto por sí solo ya es el doble que la lectura completa del archivo en Silverlight.
  • El método CopyTo de IBuffer, consume otros 0.3 segundos.

En total, solo en convertir los datos que necesitamos, hemos invertido casi 1 segundo, tres veces el tiempo necesario para toda la operación en Silverlight. Hasta 6.4 segundos, 5.4 segundos los consume el método ReadAsync de FileRandomAccessStream.

La solución

Aquí entramos en un terreno interesante. Está claro que la forma de trabajar con async/await es mucho más costosa que en un código secuencial y síncrono como tenemos en Silverlight. El método ReadAsync internamente, además de realizar la operación de lectura propiamente dicha, devuelve el progreso de la operación. Todo esto conlleva una sobrecarga en el tiempo de ejecución.

¿Y porqué si leemos el archivo de una sola vez, el rendimiento mejora?

Para verlo claro, veamos cuantos milisegundos tarda en ejecutar la lectura Silverlight. Silverlight tarda 75 milisegundos en realizar los 5000 lecturas. El problema que tenemos, en mi opinión, es que la operación de lectura individual es tan rápida, que la sobrecarga de cada lectura introducida por la infrastructura de async/await es notoria. Cuando leemos el archivo de una sola vez, el rendimiento mejora porque esa sobre carga solo se produce una vez.

Cuando el equipo de ingeniería detrás de WinRT planeo las APIs asíncronas, tuvo en mente un objetivo claro: Cualquier operación que tardase, o pudiese tardar más de 30 milisegundos, sería asíncrona. Pero este no es nuestro caso. ¿Y si nos deshacemos de la asíncronía en WinRT, que pasa? Pero, ¿Como lo hacemos si las APIs de disco de WinRT son todas asíncronas?

El objeto FileRandomAccessStream tiene un método extensor llamado AsStream, que nos devuelve la instancia de Stream que usa internamente para trabajar. y este objeto Stream contiene los métodos Read y Write síncronos que usamos tradicionalmente en Silverlight. En el constructor de nuestra clase de archivo, vamos a guardarnos la referencia a esa Stream y a continuación, podemos reescribir el método ReadAsync, de forma síncrona, así:

Read – WinRT
public virtual int Read(long pos, byte[] buf)
{
    syncfileStream.Seek(pos, SeekOrigin.Begin);
    if (buf.Length > 0)
    {
        return syncfileStream.Read(buf, 0, buf.Length);
    }
    return 0;
}

Usando la instancia de Stream que nos guardamos en el constructor, simplemente realizamos un Seek para posicionarnos en el lugar de la Stream que queremos leer y a continuación leemos el archivo. Tiempo total de lectura usando este método síncrono: 0.25 segundos. ¡Eso ya está mejor! Ya nos movemos en los mismos tiempos que con Silverlight, básicamente porque estamos usando el mismo código que en Windows Phone 8.0, por lo que el resultado no puede  ser muy diferente.

Conclusión

Y hasta aquí este pequeño análisis, después de hablar con Sergio y con la gente de SiaqoDb, han probado la solución y la van a implementar en su código base, han realizado la certificación de una aplicación usando esta solución y ha pasado a la store sin problemas.

Además de darme la oportunidad de escribir este pequeño artículo, esta situación enseña lo importante de la comunidad. Entre todos, hemos llegado a resolver un problema latente, que podría impedir que algunas aplicaciones tuviesen el rendimiento deseado. Así que es una victoria de todos los que creemos en compartir un poco de nuestro tiempo y conocimientos para ayudar a los demas.

Un saludo y HappyCoding!