NeoID
NeoID

Reputation: 901

Memory leaks in WPF application using pages and BitmapImage

I'm still quite new to C# and programming in general, but I've made a few application until now. Normal applications that only do one task and then quit are simple enough, but having a system for example take 500 pictures of users a day gave me a harder challenge.

My issue is about memory consumption in WPF. I have the following page, and when loaded it eats up more and more memory. I've tried to use memory analyzer tool and created some snapshots trying to resolve this issue. However, I have a hard time understanding when/how to dispose object and be sure that GC takes care of the rest. One of the pages I'm having trouble with in particular is this:

Page 2:

using EDSDKLib;
using PhotoBooth.Functions;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace PhotoBooth.Pages
{
    /// <summary>
    /// Interaction logic for Picture.xaml
    /// </summary>
    public partial class Picture : Page
    {
        int secondsToWait = 4;
        DispatcherTimer dispatcherTimer;
        Action<BitmapImage> SetImageAction;
        ImageBrush bgbrush = new ImageBrush();

        public Picture()
        {
            InitializeComponent();

            // Define steps
            Global.CreateSteps(Global.SelectedMenuOrder, this, ((MasterPage)System.Windows.Application.Current.MainWindow).StepsWindow);
           
            // Create TempLocation
            Directory.CreateDirectory(Settings.TempLocation);
            
            // Handle the Canon EOS camera
            Global.CameraHandler.ImageSaveDirectory = Settings.TempLocation;
            SetImageAction = (BitmapImage img) => { bgbrush.ImageSource = img; };

            // Configure the camera timer
            dispatcherTimer = new DispatcherTimer();
            dispatcherTimer.Tick += DispatcherTimer_Tick;
            dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 800);
        }

        private void Page_Loaded(object sender, RoutedEventArgs e)
        {
            // Subscribe to camera events
            if (Global.CameraHandler != null)
            {
                Global.CameraHandler.LiveViewUpdated += CameraHandler_LiveViewUpdated;
                Global.CameraHandler.ImageSaved += CameraHandler_ImageSaved;
                Global.CameraHandler.CameraSDKError += CameraHandler_CameraSDKError;
            }

            // Start LiveView
            try
            {
                Console.WriteLine(Global.CameraHandler.IsLiveViewOn);
                if (!Global.CameraHandler.IsLiveViewOn)
                {
                    CameraLiveView.Background = bgbrush;
                    Global.CameraHandler.StartLiveView();
                }
            }
            catch (Exception)
            {
                // We cannot recover from that kind of errror. Reboot the application
                CameraCrashHandler();
            }
        }

        private void CameraTrigger_Click(object sender, RoutedEventArgs e)
        {
            // The user has clicked the trigger, change the layout
            CameraTrigger.Visibility = System.Windows.Visibility.Collapsed;
            CameraCountDown.Visibility = System.Windows.Visibility.Visible;
            CameraTrigger.IsEnabled = false;

            // Start the countdown
            secondsToWait = 4;
            dispatcherTimer.Start();
            Global.WriteToLog("INFO", "Camera shutter pressed... waiting for camera to take picture!");
        }

        private void DispatcherTimer_Tick(object sender, EventArgs e)
        {
            // Handles the countdown
            switch (secondsToWait)
            {
                case 4:
                    CameraTimer3.Foreground = new SolidColorBrush(Colors.White);
                    Global.PlaySound("pack://application:,,,/Resources/Audio/camera_beep.wav");
                    break;
                case 3:
                    CameraTimer2.Foreground = new SolidColorBrush(Colors.White);
                    Global.PlaySound("pack://application:,,,/Resources/Audio/camera_beep.wav");
                    break;
                case 2:
                    CameraTimer1.Foreground = new SolidColorBrush(Colors.White);
                    Global.PlaySound("pack://application:,,,/Resources/Audio/camera_beep.wav");
                    break;
                case 1:
                    CameraTimer0.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/icon_cameraWhite.png"));
                    Global.CameraFlashEffect(((MasterPage)System.Windows.Application.Current.MainWindow).CameraFlash);
                    Global.CameraHandler.TakePhoto();
                    break;
                case 0:
                    CameraTimer0.Source = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/icon_cameraRed.png"));
                    CameraTimer1.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#e8234a"));
                    CameraTimer2.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#e8234a"));
                    CameraTimer3.Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#e8234a"));

                    dispatcherTimer.Stop();
                    break;
                default:
                    break;
            }

            secondsToWait--;
        }

        private void Page_Unloaded(object sender, RoutedEventArgs e)
        {
            // Stop LiveView
            if (Global.CameraHandler.IsLiveViewOn)
            {
                CameraLiveView.Background = null;
                Global.CameraHandler.StopLiveView();
            }

            // Unsubscribe from events
            Global.CameraHandler.LiveViewUpdated -= CameraHandler_LiveViewUpdated;
            Global.CameraHandler.ImageSaved -= CameraHandler_ImageSaved;
            Global.CameraHandler.CameraSDKError -= CameraHandler_CameraSDKError;
        }

        #region CameraHandler

        void CameraHandler_CameraSDKError(string error)
        {
            // Handle known errors
            Global.WriteToLog("ERROR", "CameraSDKError: " + error);
            switch (error)
            {
                case "0x00008D01":
                    // Reset cameraTrigger for taking another photo
                    this.Dispatcher.Invoke((Action)(() =>
                    {
                        CameraTrigger.Visibility = System.Windows.Visibility.Visible;
                        CameraCountDown.Visibility = System.Windows.Visibility.Collapsed;
                        CameraTrigger.IsEnabled = true;
                    }));
                    break;
                default:
                    CameraCrashHandler();
                    break;
            }
        }

        void CameraHandler_ImageSaved(string img)
        {
            // Assign image to user
            Global.PersonObject.Image = img;

            // We have a image, let's navigate to the next page
            this.Dispatcher.Invoke((Action)(() =>
            {
                NavigationService.Navigate(Global.FindPageByString(Global.NavigateManager(this, Functions.Enums.Navigation.Forward)));
            }));
        }

        void CameraHandler_LiveViewUpdated(Stream img)
        {
            try
            {
                if (Global.CameraHandler.IsLiveViewOn)
                {
                    using (WrappingStream s = new WrappingStream(img))
                    {
                        img.Position = 0;
                        BitmapImage EvfImage = new BitmapImage();
                        EvfImage.BeginInit();
                        EvfImage.StreamSource = s;
                        EvfImage.CacheOption = BitmapCacheOption.OnLoad;
                        EvfImage.EndInit();
                        EvfImage.Freeze();
                        Application.Current.Dispatcher.Invoke(SetImageAction, EvfImage);
                    }
                }
            }
            catch (Exception ex)
            {
                Global.WriteToLog("ERROR", "LiveViewUpdated failed: " + ex.Message);
            }
        }

        static void CameraCrashHandler()
        {
            // Camera cannot start
            Global.WriteToLog("ERROR", "Unkown CameraSDKError. Automatic reboot needed");
            MessageWindow mw = new MessageWindow("CameraErrorTitle", "CameraErrorMessage");
            mw.ShowDialog();

            // We cannot recover from that kind of errror. Reboot the application
            System.Windows.Forms.Application.Restart();
            System.Windows.Application.Current.Shutdown();
        }
  
        #endregion
    }
}

Page 3:

using PhotoBooth.Functions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace PhotoBooth.Pages
{
    /// <summary>
    /// Interaction logic for PreviewID.xaml
    /// </summary>
    public partial class PreviewID : Page
    {
        public PreviewID()
        {
            InitializeComponent();

            // Define steps
            Global.CreateSteps(Global.SelectedMenuOrder, this, ((MasterPage)System.Windows.Application.Current.MainWindow).StepsWindow);

            // Load image and data
            PreviewIDText = GetIDText(Global.PersonObject, PreviewIDText);
            PreviewIDCard.Source = GetIDPhoto(Global.PersonObject);
            PreviewIDPhoto.Background = new ImageBrush(new BitmapImage(new Uri(Global.PersonObject.Image)));
        }

        private void PreviewIDRetry_Click(object sender, RoutedEventArgs e)
        {
            Global.WriteToLog("INFO", "User did not approve the image, retry!");
            NavigationService.Navigate(Global.FindPageByString(Global.NavigateManager(this, Functions.Enums.Navigation.Backward)));
        }

        private void PreviewIDAccept_Click(object sender, RoutedEventArgs e)
        {
            Global.WriteToLog("INFO", "User approved the image");
            NavigationService.Navigate(Global.FindPageByString(Global.NavigateManager(this, Functions.Enums.Navigation.Forward)));
        }

        public TextBlock GetIDText(Functions.Enums.Person p, TextBlock tb)
        {
            tb.Text = "";
            tb.FontSize = 24;

            if (p.Affiliation == Functions.Enums.Affiliation.Employee)
            {
                // Ansatt
                tb.Inlines.Add(new Run(p.FirstName + " " + p.LastName + Environment.NewLine) { FontWeight = FontWeights.Bold, FontSize = 30 });
                tb.Inlines.Add(p.Title + Environment.NewLine);
                tb.Inlines.Add(p.Department + Environment.NewLine);
                tb.Inlines.Add("Ansatt nr: " + p.EmployeeNumber + Environment.NewLine);
            }
            else
            {
                // Student
                tb.Inlines.Add("Last name:  ");
                tb.Inlines.Add(new Run(p.LastName + Environment.NewLine) { FontWeight = FontWeights.Bold });
                tb.Inlines.Add("First name:  ");
                tb.Inlines.Add(new Run(p.FirstName + Environment.NewLine) { FontWeight = FontWeights.Bold });
                tb.Inlines.Add("Date of birth:  ");
                tb.Inlines.Add(new Run("dd.mm.yyyy" + Environment.NewLine) { FontWeight = FontWeights.Bold });
                tb.Inlines.Add("Studentnr:  ");
                tb.Inlines.Add(new Run("xxxxxx" + Environment.NewLine) { FontWeight = FontWeights.Bold });
            }

            return tb;
        }

        public BitmapImage GetIDPhoto(Functions.Enums.Person p)
        {
            BitmapImage result;
            switch (p.Affiliation)
            {
                case PhotoBooth.Functions.Enums.Affiliation.Student:
                    result = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/idcard_student.png"));
                    break;
                case PhotoBooth.Functions.Enums.Affiliation.Employee:
                    result = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/idcard_employee.png"));
                    break;
                default:
                    result = new BitmapImage(new Uri("pack://application:,,,/Resources/Images/idcard_student.png"));
                    break;
            }

            return result;
        }

        private void Page_Unloaded(object sender, RoutedEventArgs e)
        {
            PreviewIDPhoto.Background = null;
        }
    }
}

While most of the Global. functions are used on all other pages, they seem to be fine as it's this page creating most of my troubles as far as I can tell.

Here's a screenshot of my memory performance test.

  1. User navigates from page 1 to page 2 (or other pages), no issues as far as I can see. Memory usage seems stable.

  2. User triggers a Canon camera with LiveView (page 2). Memory consumption goes up and down, but stable.

  3. User takes image and get's a preview (page 3), retries (goes back to page 2), takes another, retries and so on...

    Every time the code behind is loaded:

enter image description here

Are the BitmapImages causing this issue? If there's nothing obvious in that code, how should I proceed to test memory leaks?

Upvotes: 3

Views: 1990

Answers (2)

NeoID
NeoID

Reputation: 901

According to the memory profiler I have found and fixed this issue. I still don't quite understand why, but it works. :)

The issue was the following line of code:

PreviewIDPhoto.Background = new ImageBrush(new BitmapImage(new Uri(Global.PersonObject.Image)));

Once I had that commented out I didn't have any memory issues anymore. However, I had a hard time rewriting this to work. Seems like user1690200 was right about freezing the image, but not quite the code he posted.

This worked for me:

            BitmapImage image = new BitmapImage();
            image.BeginInit();
            image.CacheOption = BitmapCacheOption.OnLoad;
            image.UriSource = new Uri(Global.PersonObject.Image);
            image.DecodePixelWidth = 275; // Important as we do not want to load the whole image
            image.EndInit();

            image.Freeze(); // Call this after EndInit and before using the image.
            PreviewIDPhoto.Source = image;

Upvotes: 0

P3N9U1N
P3N9U1N

Reputation: 28

I think it is because of the DispatcherTimer. Probably just stopping the timer isnt sufficient when unloading the page, because the WPF will still store a reference to your class.Try unregistering the event when unloading the page.

Upvotes: 1

Related Questions