Usar un origen http en un BitmapImage ralentiza tu aplicación WPF

Quiero contar una experiencia que me ha ocurrido recientemente. Como ya comente en otra ocasión estoy terminando el Infotouch, pues os quiero contar un problema que he tenido de rendimiento.

Esta aplicación hace un uso intensivo de imágenes, las que la mayoría están online en un servidor y suelen cambiar cada día.

En WPF hay un control Image (System.Windows.Controls.Image) que utiliza como origen una clase BitmapImage (System.Windows.Media.Imaging.BitmapImage), pues bien en esta última clase en el constructor puedes pasarle por parametro una Uri, con la url del recurso a establecer. Pues bien es aquí donde empieza el problema, en algunos casos suelo utilizar aproximadamente 300 instancias diferentes de BitmapImage y todas las Uri son con http, cuando ejecuto la aplicación empiezo a ver que el rendimiento baja cuando tiene que cargar todas esas imagenes, y no estoy hablando de que tarde en descargarlas ni en dibujarlas, eso lo espero, sino que trabajando en local incluro con url de localhost la aplicación tarda mucho en cargarse y se lazan pues unas 800 excepciones en mi codigo.

Empiezo a investigar, depuro con el Visual Studio y en la ventana de salida encuentro esto:

   1: A first chance exception of type 'System.Deployment.Application.InvalidDeploymentException' occurred in System.Deployment.dll
   2: A first chance exception of type 'System.Deployment.Application.DeploymentException' occurred in System.Deployment.dll
   3: A first chance exception of type 'System.Deployment.Application.InvalidDeploymentException' occurred in System.Deployment.dll
   4: A first chance exception of type 'System.Deployment.Application.DeploymentException' occurred in System.Deployment.dll
   5: A first chance exception of type 'System.Deployment.Application.InvalidDeploymentException' occurred in System.Deployment.dll
   6: A first chance exception of type 'System.Deployment.Application.DeploymentException' occurred in System.Deployment.dll
   7: A first chance exception of type 'System.Deployment.Application.InvalidDeploymentException' occurred in System.Deployment.dll
   8: A first chance exception of type 'System.Deployment.Application.DeploymentException' occurred in System.Deployment.dll
   9: A first chance exception of type 'System.Deployment.Application.InvalidDeploymentException' occurred in System.Deployment.dll
  10: A first chance exception of type 'System.Deployment.Application.DeploymentException' occurred in System.Deployment.dll
  11: A first chance exception of type 'System.Deployment.Application.InvalidDeploymentException' occurred in System.Deployment.dll

Esas excepciones que se lanzan son causadas porque se intenta acceder a la propiedad System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed o a System.Deployment.Application.ApplicationDeployment.CurrentDeployment y en la implementación de esas dos propiedades se intenta acceder a la información de despliegue con ClicOnce.

Pues bien sabiendo esta información uno puede pensar, estas programando con clicOnce y está haciendo algo más, pues resulta que clicOnce no se utiliza en el proyecto, incluso el ensamblado System.Deployment no está referenciado con lo cual eso tiene que estar en código de Microsoft.

Vamos a investigar.

Para poder depurar este código lo único que sabemos es que se lanza una excepción pero no se propaga sino que está dentro de un catch, ¿Como podemos hacer que Visual Studio se pare en ese trozo de código?, la pregunta es dificil, pero se puede hacer. Se puede hacer algo parecido, ya que no disponemos del código fuente del .net framewokr (si la tenemos, pero supongamos que no) tenemos que usar el WinDbg. Lo abrimos y cargamos la dll de depuración de código administrador en Windbg (sos.dll) a través del comando .loadby sos mscorwks una vez que tenmos eso tenemos que decirle al Windbg que se pare cuando una excepción del tipo System.Deployment.Application.InvalidDeploymentException ocurra, pues bien eso se puede hacer con el comando, !soe -create System.Deployment.Application.InvalidDeploymentException 1 (soe de StopOnException), así que una vez que el Windbg se para analizamos la excepción con !analyze -v y nos encontramos con esto:

   1:  
   2: FAULTING_IP: 
   3: KERNEL32!RaiseException+58
   4: 766742eb c9              leave
   5:  
   6: EXCEPTION_RECORD:  ffffffff -- (.exr 0xffffffffffffffff)
   7: ExceptionAddress: 766742eb (KERNEL32!RaiseException+0x00000058)
   8:    ExceptionCode: e0434f4d (CLR exception)
   9:   ExceptionFlags: 00000001
  10: NumberParameters: 1
  11:    Parameter[0]: 80131501
  12:  
  13: FAULTING_THREAD:  00000f78
  14:  
  15: DEFAULT_BUCKET_ID:  CLR_EXCEPTION
  16:  
  17: PROCESS_NAME:  infotouch2.exe
  18:  
  19: ERROR_CODE: (NTSTATUS) 0xe0434f4d - <Unable to get error code text>
  20:  
  21: NTGLOBALFLAG:  70
  22:  
  23: APPLICATION_VERIFIER_FLAGS:  0
  24:  
  25: MANAGED_STACK: 
  26: (TransitionMU)
  27: 011EDDF4 6A325FC8 System_Deployment_ni!System.Deployment.Application.ApplicationDeployment.get_CurrentDeployment()+0xf4
  28: 011EDE28 6A326056 System_Deployment_ni!System.Deployment.Application.ApplicationDeployment.get_IsNetworkDeployed()+0x1a
  29: 011EDE54 53943BD2 PresentationCore_ni!MS.Internal.AppModel.SiteOfOriginContainer.get_SiteOfOriginForBrowserApplications()+0x62
  30: 011EDE60 539E289D PresentationCore_ni!MS.Internal.PresentationCore.SecurityHelper.ExtractUriForClickOnceDeployedApp()+0x15
  31: 011EDE64 539E28DC PresentationCore_ni!MS.Internal.PresentationCore.SecurityHelper.BlockCrossDomainForHttpsApps(System.Uri)+0x34
  32: 011EDE7C 53B6908A PresentationCore_ni!System.Windows.Media.Imaging.BitmapDownload.BeginDownload(System.Windows.Media.Imaging.BitmapDecoder, System.Uri, System.Net.Cache.RequestCachePolicy, System.IO.Stream)+0x502
  33: 011EDF20 53B77A3F PresentationCore_ni!System.Windows.Media.Imaging.LateBoundBitmapDecoder..ctor(System.Uri, System.Uri, System.IO.Stream, System.Windows.Media.Imaging.BitmapCreateOptions, System.Windows.Media.Imaging.BitmapCacheOption, System.Net.Cache.RequestCachePolicy)+0xf3
  34: 011EDF44 53D015FD PresentationCore_ni!System.Windows.Media.Imaging.BitmapDecoder.CreateFromUriOrStream(System.Uri, System.Uri
  35: EXCEPTION_OBJECT: !pe 12d8faa8
  36: Exception object: 12d8faa8
  37: Exception type: System.Deployment.Application.InvalidDeploymentException
  38: Message: Application identity is not set.
  39: InnerException: <none>
  40: StackTrace (generated):
  41: <none>
  42: StackTraceString: <none>
  43: HResult: 80131501
  44:  
  45: MANAGED_OBJECT: !dumpobj 33ec694
  46: Name: System.String
  47: MethodTable: 790fd8c4
  48: EEClass: 790fd824
  49: Size: 82(0x52) bytes
  50:  (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
  51: String: Application identity is not set.
  52: Fields:
  53:       MT    Field   Offset                 Type VT     Attr    Value Name
  54: 79102290  4000096        4         System.Int32  1 instance       33 m_arrayLength
  55: 79102290  4000097        8         System.Int32  1 instance       32 m_stringLength
  56: 790ff328  4000098        c          System.Char  1 instance       41 m_firstChar
  57: 790fd8c4  4000099       10        System.String  0   shared   static Empty
  58:     >> Domain:Value  01300268:790d884c <<
  59: 7912dd40  400009a       14        System.Char[]  0   shared   static WhitespaceChars
  60:     >> Domain:Value  01300268:02ca1388 <<
  61:  
  62: EXCEPTION_MESSAGE:  Application identity is not set.
  63:  
  64: LAST_CONTROL_TRANSFER:  from 79f071ac to 766742eb
  65:  
  66: PRIMARY_PROBLEM_CLASS:  CLR_EXCEPTION
  67:  
  68: BUGCHECK_STR:  APPLICATION_FAULT_CLR_EXCEPTION
  69:  
  70: STACK_TEXT:  
  71: 6a325fc8 System_Deployment_ni!System.Deployment.Application.ApplicationDeployment.get_CurrentDeployment
  72: 6a326056 System_Deployment_ni!System.Deployment.Application.ApplicationDeployment.get_IsNetworkDeployed
  73: 53943bd2 PresentationCore_ni!MS.Internal.AppModel.SiteOfOriginContainer.get_SiteOfOriginForBrowserApplications
  74: 539e289d PresentationCore_ni!MS.Internal.PresentationCore.SecurityHelper.ExtractUriForClickOnceDeployedApp
  75: 539e28dc PresentationCore_ni!MS.Internal.PresentationCore.SecurityHelper.BlockCrossDomainForHttpsApps
  76: 53b6908a PresentationCore_ni!System.Windows.Media.Imaging.BitmapDownload.BeginDownload
  77: 53b77a3f PresentationCore_ni!System.Windows.Media.Imaging.LateBoundBitmapDecoder..ctor
  78:  
  79:  
  80: FOLLOWUP_IP: 
  81: System_Deployment_ni+5fc8
  82: 6a325fc8 8b4dd4          mov     ecx,dword ptr [ebp-2Ch]
  83:  
  84: SYMBOL_STACK_INDEX:  0
  85:  
  86: SYMBOL_NAME:  System_Deployment_ni!System.Deployment.Application.ApplicationDeployment.get_CurrentDeployment+5fc8
  87:  
  88: FOLLOWUP_NAME:  MachineOwner
  89:  
  90: MODULE_NAME: System_Deployment_ni
  91:  
  92: IMAGE_NAME:  System.Deployment.ni.dll
  93:  
  94: DEBUG_FLR_IMAGE_TIMESTAMP:  47577e63
  95:  
  96: STACK_COMMAND:  dds 11eddf4 ; kb
  97:  
  98: FAILURE_BUCKET_ID:  CLR_EXCEPTION_e0434f4d_System.Deployment.ni.dll!System.Deployment.Application.ApplicationDeployment.get_CurrentDeployment
  99:  
 100: BUCKET_ID:  APPLICATION_FAULT_CLR_EXCEPTION_System_Deployment_ni!System.Deployment.Application.ApplicationDeployment.get_CurrentDeployment+5fc8
 101:  
 102: Followup: MachineOwner

La pila nos encontramos con MS.Internal.PresentationCore.SecurityHelper.BlockCrossDomainForHttpsApps(System.Uri) que llama a MS.Internal.PresentationCore.SecurityHelper.ExtractUriForClickOnceDeployedApp(), ahí está todo el problema, resulta que para descargarse una imagen el framewokr necesita comprobar que la aplicación esta desplegada con ClicOnce, pero ¿Porqué?. Si es una aplicación de escritorio normal porque tiene que hacer eso si simplemente lo que quiero es descargar la imagen. Si vemos la implementación de ese método nos encontramos con esto:

   1: internal static void BlockCrossDomainForHttpsApps(Uri uri)
   2: {
   3:     Uri uri2 = ExtractUriForClickOnceDeployedApp();
   4:     if ((uri2 != null) && (uri2.Scheme == Uri.UriSchemeHttps))
   5:     {
   6:         if (uri.IsUnc || uri.IsFile)
   7:         {
   8:             new FileIOPermission(FileIOPermissionAccess.Read, uri.LocalPath).Demand();
   9:         }
  10:         else
  11:         {
  12:             new WebPermission(NetworkAccess.Connect, BindUriHelper.UriToString(uri)).Demand();
  13:         }
  14:     }
  15: }
  16:  
  17:  
  18:  
  19:  
  20:  
  21:  
  22:  
  23: internal static Uri SiteOfOriginForBrowserApplications
  24: {
  25:     [FriendAccessAllowed]
  26:     get
  27:     {
  28:         Uri deploymentUri = null;
  29:         if (_debugSecurityZoneURL.Value != null)
  30:         {
  31:             return _debugSecurityZoneURL.Value;
  32:         }
  33:         if (_browserSource.Value != null)
  34:         {
  35:             return _browserSource.Value;
  36:         }
  37:         if (ApplicationDeployment.IsNetworkDeployed)
  38:         {
  39:             deploymentUri = GetDeploymentUri();
  40:         }
  41:         return deploymentUri;
  42:     }
  43: }

En el que al final del todo se llama a ApplicationDeployment.IsNetworkDeployed causando que la aplicación lanza excepciones.

¿ Cómo se puede puede solucionar esto ?

Pues de una manera muy sencilla, puesto que el problema es que el propio descargador de Bitmaps tiene que comprobar la seguridad si o sí pues descargemos nosotros esa imagen en memoria y dejemos al framework que la cargue desde un MemoryStream, así:

   1: WebClient wc = new WebClient();
   2: byte[] data = wc.DownloadData(new Uri("http://localhost/img.jpg"));
   3:  
   4: MemoryStream ms = new MemoryStream(data);
   5:  
   6: BitmapImage bi = new BitmapImage();
   7: bi.BeginInit();
   8:  
   9: bi.StreamSource = ms;
  10:  
  11: bi.EndInit();

Luis.

Ya esta aqu&iacute; el .net framework 3.5 SP1

Hoy ha salido la versión beta del .net Framewokr 3.5 SP1, que contiene un monton de mejoras en el framework, y muchos añadidos que por ahora te podias descargar por separado, las cosas que más me parecen interesantes:

  • Mejora del rendimiento del editor de Html de Visual Studio 2008. Francamente el editor de html de VS2008 es muy bueno, la edición de css se ha mejorado mucho, pero el problema es que cuando tienes una pagina asp.net con controles web, cuando haces clic en un control de servidor, por ejemplo un Label, desde el momento en el que pulsas el Label, hasta que en la ventana de propiedades aparecen las propiedades, pueden pasar perfectamente 5 segundos, algo que era un coñazo total. 
  • Posibilidad de redistribuir una versión personalizada del framework. Cuando se desarrollan aplicaciones en .net una de las cosas que siempre hay que tener en cuenta es que el cliente tenga instalado el .net framework en la máquina de destino, porque sino el software no funciona. En este sentido, Microsoft, tiene muy buenas herramientas para redistribuir su framework tanto en un instalador de msi como desde ClicOnce. Pues ahora con esta mejora puedes hacer una versión reducida del .net framework en el que solo se incluyan lo elementos que tú aplciación necesita, haciendo que los tiempos de descarga e instalación sean mejores. Dentro de estas mejoras se encuentra un Bootstrapper, que se encarga de comprobar que tienes la versión correcta del framework, y también mejoras dentro de la publicación con ClicOnce.

Dentro de todas las mejoras, las que más me interesan de WPF (Windows Presentation Foundation) entre las que encontramos.

  • Animaciones más suvaes
  • Aceleración por hardware de los BitmapsEffects como Blur y DropShadow
  • Mejoras de rendimiento y velocidad en el renderizado de fuentes
  • Mejoras en gráficos 2D
  • Una nueva clase WriteableBitmap que permite actualización de bitmaps al vuelo sin necesidad de generar uno nuevo, al más puro estilo  DirectX.
  • Mejoras en el rendimiento del calculo del Layout

 

Y la mejor de todas, que ahora los BitmapsEffects soportan PixelShader. Esto es algo importante porque cuando se programa con WPF, lo efectos de bitmap, están prohibidos, ¿Porqué? Pues por que cuando añades un Bitmap Effect a cualquer elemento, se deshabilita la aceleración por hardware de la aplicación mientras el Bitmap Effect esté dentro del arbol visible, esto ni que decir tiene que es algo muy cutre.

Dentro de la web de ScottGu’s Blog está el post describiendo todas las características.

Infotouch 2.0

Además de mi trabajo en ilita, también hago otro tipo de proyectos aparte de la empresa, uno en el que llevaba bastante tiempo pero que no hay terminado era el infotouch. Pero ahora puedo mostrar la versión 2.0 de este software para buscar ofertas de viajes para agencias del grupo Almeida.

Os dejo unas capturas de pantalla para que veáis como ha quedado el software.

clip_image002[6]

clip_image002

clip_image002[8]

Depuraci&oacute;n 3 &ndash; Breakpoints

Una vez que tenemos completamente configurado el entorno de depuración podemos hacer una aplicación de ejemplo para probar que realmente lo tenemos todo configurado.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:   
   6:  namespace ConsoleApplication1
   7:  {
   8:      class Program
   9:      {
  10:          static void Main(string[] args)
  11:          {
  12:              string[] values = new string[] { "uno", "dos", 
"tres", "cuatro", "cinco" };
  13:              for (int x = 0; x < values.Length; x++)
  14:              {
  15:                  Console.WriteLine(values[x]);
  16:              }
  17:          }
  18:      }
  19:  }

Con esta aplicación podemos comprobar si tenemos bien configurado el servidor de símbolos de Microsoft para ello tenemos que establecer un punto de interrupción en algún punto del código fuente y pulsar F5, entonces Visual Studio aparecerá así.

Visual Studio

Si nos fijamos bien tenemos el depurador parado en la línea 15 en la sentencia Console.WriteLine(values[x]); podemos ver abajo las lista con los hilos (Threads) de la aplicación y a la derecha la pila de llamadas (Call Stack), si nos fijamos en la pila de llamadas vemos como algunos de los marcos están en gris y otros están en negro. Las funciones que estan en gris significa que no tenemos el código fuente disponible y no podemos ver el código fuente origina, para los que están en negro podemos ver el fichero de código fuente original para depurarlo como si fuera de nuestro proyecto.

Esta aplicación de ejemplo es una aplicación de Consola, si nos fijamos en la pila de llamadas vemos que la primera de las funciones que se invoca está en mscorlib.dll (es el ensamblado principal de .net framework) y llama a la función System.Threading.ThreadHelper.ThreadStart() además VS nos dice que esa función esta en la linea 82 y tiene un desplazamiento de 0×20 bytes. Si hacemos doble clic encima de esa línea de la pila de llamadas podemos acceder a código fuente de la clase ThreadHelper. Lo podemos ver aquí.

Visual Studio

Si nos fijamos de nuevo en la pila de llamadas del proceso podemos ver que ahora el marco que está seleccionado es justamente el de mscorlib.dllSystem.Threading.ThreadHelper.ThreadStart() Line 82 + 0×20 bytes, y además en la vista de código podemos ver el fichero del código fuente original, en C#, de la clase ThreadHelper.

Para poder llegar a ver la información que tenemos disponible se ha tenido que establecer un breakpoint para que la aplicación se pare en ese momento y podamos examinar los datos. Los puntos de interrupción son unos de los métodos por el cual una aplicación puede parar su ejecución, además de este hay otros. Los breakpoints en Visual Studio además de establecerlos nos permiten una serie de funcionalidad añadida, podemos eliminarlos o deshabilitarlos, y además podemos establecer información condicional para personalizar nuestro breakpoint. Que podemos hacer con un breakpoint, si pulsamos con el botón contrario encima del círculo rojo de la derecha del código, nos aparece un menú contextual con una serie de opciones.

  • Location:
    • Especifica donde está establecido el breakpoint, el fichero, la línea y el carácter.
  • Condition:
    • Esta es una de las opciones más interesantes. Esta opción permite establecer una condición en la cual el breakpoint se tiene que parar. Eso que significa que podemos poner una expresión boleana que Visual Studio evaluará para determinar si el breakpoint se parará. En el ejemplo que nosotros tenemos podemos poner una condición para que se pare cuando el valor actual es: tres.
    •  image
  • Hit Count:
    • Determina cuantas veces se se tiene que pasar por el breakpoint antes de que se pare, es algo útil si queremos para en un bucle en un valor determinado.
  • Filter:
    • Permite establecer un filtro a partir de una serie de valores de entorno predeterminados, es como la condición pero con valores que Visual Studio nos proporciona.
  • When Hit:
    • Esta opción se utiliza si queremos que Visual Studio, imprima un mensaje en la salida con un determinado valor, o queremos que ejecute una macro.

Con esto damos por concluido la configuración del entorno de depuración de .net dentro de Visual Studio.