[Windows 10] Novedades y consejos sobre rendimiento

Branch-EngineeringIntroducción

Además de cuidar detalles como la funcionalidad o la apariencia
visual de nuestra aplicación, nuestra aplicación debe funcionar
correctamente bajo todas las condiciones en todos los dispositivos para
la que sea lanzada.

Factores como el consumo de memoria, CPU o la gestión de tiempos, a
veces cuestan y no se tienen en cuenta hasta que salta el problema, es
decir, tarde.

¿Es importante el rendimiento?

Las críticas de la tienda así como la posibilidad de comunicación
directa con los usuarios es una vía inmejorable para realizar una mejora
continua de la misma. Las críticas habituales suelen venir en un alto
porcentaje de errores (bugs) que no permiten realizar acciones contempladas en la aplicación y también muchas de ellas, por rendimiento.

 

Motivos de críticas negativas

Motivos de críticas negativas

Que la aplicación se “congele”, respuestas lentas, consumos
muy elevados de batería, que provoquen un sobrecalentamiento del
dispositivo son algunas causas habituales de opiniones como las
siguientes:

Ejemplos de críticas negativas

Ejemplos de críticas negativas

Por lo tanto, debemos tener en cuenta en el propio desarrollo
factores de rendimiento de igual forma que prestamos atención a la
correcta visualización de la interfaz en distintas condiciones por
ejemplo.

Mejoras de rendimiento en UWP

De entrada, buenas noticias. Se han incluido en Windows 10 mejoras como:

  • Rendimiento aumentado en Listview.
  • Mejoras en redimiento en ScrollViewer.
  • Interoperatibilidad XAML/DX.
  • Casting de elementos visuales.
  • Gestión Bitmap Source.
  • Etc.

Gracias a todas estas mejoras conseguimos sin cambios en nuestra
forma de trabajar o en el código, mejoras en el consumo de CPU y de
memoria.

Mejoras rendimiento UWP

Mejoras rendimiento UWP

Novedades en XAML

Con Windows 10 nos llegan un conjunto de novedades relacionados con
la gestión de enlace a datos, nuevas etiquetas de marcado y otros
detalles que afectan al rendimiento.

x:Bind

Data binding es un mecanismo mediante el cual
podemos enlazar los elementos de la interfaz de usuario con los objetos
que contienen la información a mostrar. Cuando realizamos data binding,
creamos una dependencia entre el valor de una propiedad llamada target con el valor de otra propiedad llamada source. Donde normalmente, la propiedad target recibirá el valor de la propiedad source.

Es el mecanismo base que nos permite utilizar el patrón MVVM en nuestras Apps móviles logrando:

  • Nos permite dividir el trabajo de manera muy sencilla (diseñadores – desarrolladores)
  • El mantenimiento es más sencillo.
  • Permite realizar Test a nuestro código.
  • Permite una más fácil reutilización de código.

Sin embargo, además de toda la potencia mencionada teníamos ciertas
limitaciones. Los errores de Binding no se producían en tiempo de
compilación de la App además de tener diferentes mejoras relacionadas
con el rendimiento. Limitaciones existentes hasta ahora…

x:Bind es una nueva síntaxis en XAML que cubre un
objetivo similar a Binding. Permite crear un enlace a datos pero con
significativas diferencias. Mientras que con Binding se utiliza
reflexión en tiempo de ejecución para resolver el enlace a datos, con
x:Bind se realiza una validación en tiempo de ejecución ya que son
fuertemente tipados y compilados. Además, ofrece potentes mejoras en el rendimiento.

Veamos unas simples comparativas entre enlace a datos clásico y precompilado y su impacto en el rendimiento.

Uso de CPU utilizando enlace a datos clásico:

Uso de CPU en Bindings clásicos

Uso de CPU en Bindings clásicos

Uso de CPU utilizando bindings compilados:

Uso de CPU en binding compilado

Uso de CPU en binding compilado

También se reduce el consumo de memoria en comparación con Bindings clásicos:

Comparativa de consumo de memoria entre Bindings

Comparativa de consumo de memoria entre Bindings

Vista las visibles mejoras a nivel de rendimiento, ¿cómo se usan?.

En esta ocasión, crearemos un listado de casas donde utilizaremos
x:Bind en la plantilla que representará cada elemento de la lista.

Nuestra interfaz sera muy simple en esta ocasión contando con un sencillo ListView:

<ListView
     ItemsSource="{Binding Houses}" />

Cargaremos el listado de casas con un método creando datos falsos en local de manera aleatoria:

private void LoadHouses()
{
     _houses = new ObservableCollection<House>();
     Random random = new Random();
     for (int i = 0; i < 100; i++)
     {
          _houses.Add(new House
          {
               Place = Places[random.Next(0, Places.Count)],
               Photo = string.Format("ms-appx:///Assets/{0}.png", random.Next(1, 4)),
               Price = string.Format("${0}", random.Next(10000, 100000).ToString())
          });
     }
}

La definición del template de cada casa:

<DataTemplate x:Key="HouseTemplate" x:DataType="model:House">
     <Grid Width="200"
           Height="80">
          <Grid.ColumnDefinitions>
               <ColumnDefinition Width="75" />
               <ColumnDefinition Width="*" />
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
               <RowDefinition Height="Auto" />
               <RowDefinition Height="Auto" />
           </Grid.RowDefinitions>
           <Image Grid.RowSpan="2"
              Source="{x:Bind Photo}"
              MaxWidth="70"
              MaxHeight="70" />
           <TextBlock Text="{x:Bind Place}"    
                  Grid.Column="1"
                  FontSize="18"/>
           <TextBlock Text="{x:Bind Price}"  
                  Grid.Column="1"  
                  Grid.Row="1"
                  FontSize="12" />
     </Grid>
</DataTemplate>

Utilizamos x:Bind para enlazar cada elemento visual de la plantilla a
la propiedad deseada. Importante resaltar además de compilados, son
fuertemente tipados. Es obligatorio para no tener errores de compilación
indicar el tipo de los datos a los que accedemos por enlace a datos.
Esto lo realizamos utilizando x:DataType.

Nuestro listado quedara:

<ListView
     ItemsSource="{Binding Houses}"
     ItemTemplate="{StaticResource HouseTemplate}" />

DataTemplate utilizando x:Bind

DataTemplate utilizando x:Bind

 

Lo visto hasta ahora nos indica que:
  • Tenemos la posibilidad de tener bindings compilados obteniendo errores en tiempo de compilación.
  • Son fuertemente tipados por lo que debemos indicar el tipo de la información.
  • Obtenemos mejoras en el rendimiento tanto en consumo de CPU como de memoria.

Por lo tanto, ¿lo usamos siempre?

La respuesta corta es no. Entrando en profundidad:

  • Los bindings compilados, en ocasiones,  tienen un comportamiento
    diferente al de los bindings clásicos existiendo situaciones no válidas
    para los primeros.
  • Los bindings compilados como indicamos al nombrarlos se compilan
    permitiéndonos obtener errores en tiempo de compilación pero tambien nos
    aporta limitaciones. No podemos crear bindings compilados dinámicamente
    (añadir o quitar bindings en runtime).
  • Con los bindings clásicos podemos crear un mismo template para
    entidades diferentes siempre y cuando el nombre de las propiedades
    coincida. Con los bindings compilados como hemos visto, estan
    fuertemente tipados y no podemos realizar lo mismo.

Podéis descargar el ejemplo utilizando x:Bind a continuación:

También podéis acceder al código fuente directamente en GitHub:

Ver GitHub

x:Phase

Otra etiqueta nueva incluida en Windows 10, en este caso destinada a
permitir realizar renderizado por fases. Con Windows 8.1 se introdujo en
listados el evento ContainerContentChanging que nos
permitía el renderizado progresivo de elementos. Requería código para
actualizar la plantilla que dificultaba el uso de enlace a datos.

Ahora tenemos una nueva etiqueta llamada x:Phase que nos permite realizar con suma facilidad renderizado progresivo. Estableceremos valores numéricos.

NOTA: Por defecto el valor implícito es x:Phase=”0″.

<DataTemplate x:Key="XPhaseHouseTemplate" x:DataType="model:House">
     <Grid Width="200"
           Height="80">
             <Grid.ColumnDefinitions>
                 <ColumnDefinition Width="75" />
                 <ColumnDefinition Width="*" />
             </Grid.ColumnDefinitions>
             <Grid.RowDefinitions>
                 <RowDefinition Height="Auto" />
                 <RowDefinition Height="Auto" />
             </Grid.RowDefinitions>
             <Image
                 Grid.RowSpan="2"
                 Source="{x:Bind Photo}"
                 x:Phase="2"
                 MaxWidth="70"
                 MaxHeight="70" />
             <TextBlock
                 Text="{x:Bind Place}"  
                 Grid.Column="1"
                 FontSize="18"/>
             <TextBlock
                 Text="{x:Bind Price}"  
                 x:Phase="1"
                 Grid.Column="1"  
                 Grid.Row="1"
                 FontSize="12" />
     </Grid>
</DataTemplate>

El uso de x:Phase esta asociado al uso de bindings compilados.

Ejemplo de x:Phase:

También tenéis el código fuente disponible e GitHub:

Ver GitHub

x:DeferLoadStrategy

x:DeferLoadStrategy nos permite retrasar la creación
de un elemento y sus elementos hijos lo que reduce los tiempos
necesarios para la creación de la UI y por lo tanto de carga. Sin
embargo, nada en la vida es gratis. A cambio, incrementamos levemente el
consumo de memoria.

NOTA: Cada elemento que retrasamos en su inicialización con x:DeferloadStrategy añade 600 Bytes en el consumo de memoria.

Podemos deducir que a mayor cantidad de elementos que nos ahorremos
del árbol visual, en menor tiempo se realizara la inicialización de la
vista pero aumentando el consumo de memoria. Por lo tanto, el uso de la
etiqueta es recomendado aunque requiere un análisis mínimo.

Creamos un ejemplo básico donde vamos a retrasar la creación de un Grid que contiene una imagen para crearlo bajo nuestro propio interés al pulsar el botón.

<Grid x:Name="DeferredPanel"  
      x:DeferLoadStrategy="Lazy">
      <Image
           Stretch="UniformToFill"
           Source="ms-appx:///Assets/NinjaCat.jpg" />
</Grid>

Utilizamos la etiqueta x:DeferLoadStrategy=”Lazy” en nuestro Grid. De esta forma indicamos que retrasamos la creación del panel y todo su contenido. Para utilizar la etiqueta debemos:

  • Definir un nombre con x:Name. Para iniciar posteriormente la incialización utilizaremos el nombre.
  • Podemos utilizarlo con cualquier elemento visual derivado de UIElement. No podemos utilizarlo con Page o UserControl.
  • No podremos utilizar con XAML XamlReader.Load.

Nos centramos ahora en el código que se ejecutará pulsando el botón:

page.FindName("DeferredPanel");

Utilizamos el método FindName pasándole el nombre del elemento.

Al pulsar el botón, el panel que retrasamos se crea. En este momento:

  • Se lanza el evento Loaded del panel.
  • Se evalúan los Bindings establecidos en el elemento.

Utilizando x:DeferLoadStrategy

Ejemplo de x:DeferLoadStrategy:

También tenéis el código fuente disponible e GitHub:

Ver GitHub

Otras consideraciones

Virtualización

Tanto el control ListView como el control GridView soportan nativamente virtualización. Sin embargo, hay varios modos para perder la virtualización, debemos tener en cuenta:

  • Si envolvemos un control ListView o GridView sobre un ScrollViewer,
    su tamaño tiende a infinito, sin establecer límites perderíamos la
    virtualización.
  • Podemos organizar los elementos utilizando diferentes paneles. Paneles como por ejemplo WrapGrid no soporta virtualización.

Optimiza imágenes

Las imágenes de tamaños muy elevados, sobretodo si las vamos a
mostrar en tamaños mucho mas reducidos, no compensan directamente debido
a la enorme cantidad de memoria que consumen.

Se pueden utilizar las propiedades DecodePixelHeight y DecodePixelWidth de un BitmapImage para establecer el alto y ancho en el que se decodifica la imagen.

BitmapImage bi = new BitmapImage(new Uri(baseUri, path));
bi.DecodePixelHeight = 120;
bi.DecodePixelWidth = 180;

Ejemplo:

También tenéis el código fuente disponible e GitHub:

Ver GitHub

Optimiza textos

El renderizado de texto es en ocasiones hasta un 50% más rápido en Windows 10. Podemos aumentar el rendimiento usando:

  • CharacterSpacing
  • Typography
  • LineStackingStregy=BaselineToBaseline/MaxHeight
  • IsTextSelectionEnabled = true

Herramientas

Cada vez que nos llega a los desarrolladores un nuevo SDK, es un
momento especial con una mezcla de altísima curiosidad y ganas de probar
novedades. Entre las novedades principales siempre hay nuevas APIs,
controles y otros elementos para poder realizar Apps que antes no eran
posibles. Sin embargo, entre el conjunto de novedades siempre suelen
venir nuevas herramientas que facilitan ciertas tareas: obtener más analíticas, mejores medidores de rendimiento, más opciones en emuladores, etc.

Visual Tree Inspector

Desde versiones anteriores de Visual Studio, una de las herramientas más demandadas son herramientas de depuración de UI XAML.

El árbol visual dinámico es la primera de dos piezas fundamentales para depurar UI XAML.

Visual Tree Inspector

Esta herramienta nos permite ver el árbol de controles de la App en
ejecución indicando el número de elementos hijos de cada elemento ideal
para entender la estructura visual de una vista compleja y entender
problemas de rendimiento.

La segunda pieza relacionada con las herramientas de depuración de UI XAML es el explorador de propiedades dinámico.

Live Property Explorer

Esta herramienta nos permite ver todas las propiedades del elemento
seleccionado, incluso aquellas sobreescritas. Podemos ver si las
propiedades estan establecidas con valores directos, accediendo a
recursos, etc. Además, y la parte más interesante, permite cambiar los
valores de la App en ejecución directamente viendo los cambios de manera
inmediata.

PerfTips

Generalmente y a pesar de contar con herramientas de diagnóstico, no
se suelen utilizar hasta que surgen problemas, es decir, tarde. En estos
casos, una vez detectados problemas de rendimiento, además de utilizar
las herramientas de diagnóstico se suelen poner puntos de ruptura entre
diferentes bloques para tener una idea de donde se pierde tiempo.

Estas prácticas no suelen ser muy buena idea. Por un lado se “caza”
al problema cuando ya es un problema grande, y por otro lado, con puntos
de ruptura o añadiendo líneas para obtener tiempos entre dos puntos del
código, no suele ser muy exacto.

PerfTips llega para ayudar a a entender que ocurre
en su aplicación a nivel de rendimiento mientras depura. En los puntos
de ruptura aparecerán popups con información relacionada con el
rendimiento.

PerfTips

PerfTips

PerfTips indica tiempos aproximados excluyendo los tiempos de pausa
en un punto de ruptura así como la carga de símbolos y tiempo
correspondiente al debugger.

Herramientas de diagnóstico

Las herramientas de diagnóstico son un conjunto de
ventanas destinadas a ofrecer información relacionada con el rendimiento
de la aplicación. Tenemos opciones para ver problemas de renderizado y parsing en la aplicación, monitorear consumo de Memoria y CPU o detectar problemas en el consumo de red entre otras opciones.

Consumo de CPU

Muestra el uso de CPU en todos los cores disponibles:

Consumo de CPU

Consumo de CPU

Consumo de memoria

Monitorea el consumo de memoria de la aplicación mientras estamos depurando.

Consumo de Memoria

Consumo de Memoria

Ahora disponible siempre tras cada sesión de depuración.

Renderizado y parsing

Identifica problemas de rendimiento relacionados con:

  • Parsing & Layout
  • Código de la App que provoca consume alto de CPU
Línea de tiempo

Línea de tiempo

Más información