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í 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.
Depuración 3 – 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í.
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 0x20 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í.
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 + 0x20 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.
-
- 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.