3

I have a Windows Forms application that copies and re-sizes images references in a GEDCOM genealogy file. The user selects the file and output directory as well as the re-sizing options from the main form which then opens another form as a dialog which contains a label, progress bar, and button. I’m updating the application to use the new asynchronous features in .NET 4.5 and also modifying it to make use of parallel processing. Everything works fine, except I’m noticing that the UI responsiveness is a bit choppy (stutters); If I don’t update the message label with the percent, then it’s much smoother. Also, when I cancel the task, the UI will hang for anywhere from 1 to 15 seconds. The application is just for my personal use, so it’s not that big of a deal, but I’m curious what might be causing the issue and what the recommended way to deal with it is. Is the parallel processing just overloading the CPU with having too many threads to process? I tried adding a Thread.Sleep(100) to each loop iteration and it seemed to help a little bit.

Here is a minimal version of the application that still causes the issues. To reproduce:

  1. Create a new windows forms application with the below form.
  2. Create a directory with a bunch of jpeg images (50+ images)
  3. Replace the _SourceDirectoryPath and _DestinationDirectoryPath variables with your directories.
  4. Run application

Designer.cs:

partial class Form1
{
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.lblMessage = new System.Windows.Forms.Label();
        this.pgProgressBar = new System.Windows.Forms.ProgressBar();
        this.btnStart = new System.Windows.Forms.Button();
        this.btnCancel = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // lblMessage
        // 
        this.lblMessage.AutoSize = true;
        this.lblMessage.Location = new System.Drawing.Point(32, 25);
        this.lblMessage.Name = "lblMessage";
        this.lblMessage.Size = new System.Drawing.Size(0, 13);
        this.lblMessage.TabIndex = 0;
        // 
        // pgProgressBar
        // 
        this.pgProgressBar.Location = new System.Drawing.Point(35, 51);
        this.pgProgressBar.Name = "pgProgressBar";
        this.pgProgressBar.Size = new System.Drawing.Size(253, 23);
        this.pgProgressBar.TabIndex = 1;
        // 
        // btnStart
        // 
        this.btnStart.Location = new System.Drawing.Point(132, 97);
        this.btnStart.Name = "btnStart";
        this.btnStart.Size = new System.Drawing.Size(75, 23);
        this.btnStart.TabIndex = 2;
        this.btnStart.Text = "Start";
        this.btnStart.UseVisualStyleBackColor = true;
        this.btnStart.Click += new System.EventHandler(this.btnStart_Click);
        // 
        // btnCancel
        // 
        this.btnCancel.Location = new System.Drawing.Point(213, 97);
        this.btnCancel.Name = "btnCancel";
        this.btnCancel.Size = new System.Drawing.Size(75, 23);
        this.btnCancel.TabIndex = 3;
        this.btnCancel.Text = "Cancel";
        this.btnCancel.UseVisualStyleBackColor = true;
        this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
        // 
        // Form1
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(315, 149);
        this.Controls.Add(this.btnCancel);
        this.Controls.Add(this.btnStart);
        this.Controls.Add(this.pgProgressBar);
        this.Controls.Add(this.lblMessage);
        this.Name = "Form1";
        this.Text = "Form1";
        this.ResumeLayout(false);
        this.PerformLayout();

    }

    #endregion

    private System.Windows.Forms.Label lblMessage;
    private System.Windows.Forms.ProgressBar pgProgressBar;
    private System.Windows.Forms.Button btnStart;
    private System.Windows.Forms.Button btnCancel;
}

Code:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private CancellationTokenSource _CancelSource;
    private string _SourceDirectoryPath = @"Your\Source\Directory";
    private string _DestinationDirectoryPath = @"Your\Destination\Directory";

    public Form1()
    {
        InitializeComponent();
        lblMessage.Text = "Click Start to begin extracting images";
        btnCancel.Enabled = false;
        _CancelSource = new CancellationTokenSource();
    }

    private async void btnStart_Click(object sender, EventArgs e)
    {
        btnStart.Enabled = false;
        btnCancel.Enabled = true;

        List<string> files = await Task.Run(() => Directory.GetFiles(_SourceDirectoryPath, "*.jpg").ToList());

        // scan/extract files
        Progress<int> progress = new Progress<int>(UpdateProgress);
        int result = await Task.Run(() => ExtractFiles(files, progress, _CancelSource.Token));

        if (_CancelSource.IsCancellationRequested)
        {
            lblMessage.Text = "Extraction cancelled by user.";
        }
        else
        {
            lblMessage.Text = string.Format("Extraction Complete: {0} files extracted.", result);
        }
        btnStart.Enabled = true;
        btnCancel.Enabled = false;
    }

    private void btnCancel_Click(object sender, EventArgs e)
    {
        lblMessage.Text = "Cancelling...";
        btnCancel.Enabled = false;
        _CancelSource.Cancel();
    }

    private void UpdateProgress(int value)
    {
        lblMessage.Text = string.Format("Extracting files: {0}%", value);
        pgProgressBar.Value = value;
    }

    public int ExtractFiles(List<string> fileReferences, IProgress<int> progress, CancellationToken cancelToken)
    {
        double totalFiles = fileReferences.Count;
        int processedCount = 0;
        int extractedCount = 0;
        int previousPercent = 0;
        Directory.CreateDirectory(_DestinationDirectoryPath);

        Parallel.ForEach(fileReferences, (reference, state) =>
        {
            if (cancelToken.IsCancellationRequested)
            {
                state.Break();
            }

            string fileName = Path.GetFileName(reference);
            string filePath = Path.Combine(_DestinationDirectoryPath, fileName);

            using (Image image = Image.FromFile(reference))
            {
                using (Image newImage = ResizeImage(image, 1000, 1000))
                {
                    newImage.Save(filePath);
                    Interlocked.Increment(ref extractedCount);
                }
            }

            Interlocked.Increment(ref processedCount);
            int percent = (int)(processedCount / totalFiles * 100);
            if (percent > previousPercent)
            {
                progress.Report(percent);
                Interlocked.Exchange(ref previousPercent, percent);
            }
        });

        return extractedCount;
    }

    public Image ResizeImage(Image image, int maxWidth, int maxHeight)
    {
        Image newImage = null;

        if (image.Width > maxWidth || image.Height > maxHeight)
        {
            double widthRatio = (double)maxWidth / (double)image.Width;
            double heightRatio = (double)maxHeight / (double)image.Height;
            double ratio = Math.Min(widthRatio, heightRatio);
            int newWidth = (int)(image.Width * ratio);
            int newHeight = (int)(image.Height * ratio);

            newImage = new Bitmap(newWidth, newHeight);
            using (Graphics graphic = Graphics.FromImage(newImage))
            {
                graphic.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
                graphic.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                graphic.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
                graphic.DrawImage(image, 0, 0, newWidth, newHeight);
            }
        }

        return newImage;
    }
}
Matt P.
  • 57
  • 5
  • Side note: dont throw inside parallel foreach to break. There is already break option for that. Put if statement CancelationTokenRequested and then state.Break http://stackoverflow.com/questions/12571048/break-parallel-foreach – M.kazem Akhgary Nov 23 '15 at 05:39
  • Try switching off the progress reporting; the tasks might all be stuck waiting for the UI thread. Also if all these threads use disk io they might be stuck waiting for disk access as well. – Emond Nov 23 '15 at 05:51
  • 2
    Without [a good, _minimal_, _complete_ code example](http://stackoverflow.com/help/mcve) that reliably reproduces the problem, it will be impossible to say what might be going on. The short answer is that, no...concurrent processing doesn't inherently lead to stuttering in the UI. But there are definitely ways that could happen in some cases. If you want useful help with your specific scenario, you need to provide a good code example for people to work with. – Peter Duniho Nov 23 '15 at 07:14
  • Is it better to break instead of throwing exception? There seems to be mixed recommendations on which to use. I originally was doing a state.break(); – Matt P. Nov 24 '15 at 01:41
  • I switched off the reporting and there is no stuttering when dragging the form, but when hovering over the button, it takes a couple seconds for the onhover highlighting of the button. It also still takes a while to cancel. I'll see what I can do for a minimal complete code example. – Matt P. Nov 24 '15 at 01:44
  • Ok... I replaced the code with a minimal complete code example. Thank you for your help :) – Matt P. Nov 24 '15 at 02:52
  • You shouldn't call `Directory.GetFiles` or `Directory.CreateDirectory` on a UI thread, both are slow file-system operations. – Ian Mercer Nov 24 '15 at 03:02
  • Not relevant to answer but using `Image.FromStream` is preferable to `Image.FromFile`. The latter has 'issues'. See elsewhere on SO. – Ian Mercer Nov 24 '15 at 03:04
  • Sorry... just did that for the example... the actual application creates the directories within the Parallel.Foreach loop since the files are in different sub-directories. Also the file paths are being read from a separate file which is done on a separate thread using Task.Run. – Matt P. Nov 24 '15 at 04:48

2 Answers2

1

I believe I found the issue. The GDI+ is being blocked in the UI thread when calling Graphics.DrawImage() in the background thread. See Why do Graphics operations in background thread block Graphics operations in main UI thread?

An apparent solution would be to use multiple processes (see: Parallelizing GDI+ Image Resizing .net)

Community
  • 1
  • 1
Matt P.
  • 57
  • 5
0

I can see two potential issues here:

  1. You are checking for cancellation at the beginning of the loop body which does not allow interruption of each loop iteration while the operation is being done. The stutter after cancellation is probably due to the image resize still executing. It would probably be better to abort the thread (which is not recommended, but in this scenario it might work faster).
  2. The _CancelSource.Cancel() is blocking the UI thread. You could do cancellation as an async task. Check the related post: Why does cancellation block for so long when cancelling a lot of HTTP requests?.

As for the CPU overload, that is also possible. You could use a profiler to check the CPU usage. Visual Studio has an integrated profiler that works very well with C#.

Community
  • 1
  • 1
Igor Ševo
  • 5,459
  • 3
  • 35
  • 80
  • I tried calling cancel on a separate thread, but it didn't make a difference, so i'm thinking the pause is caused by the stutter that occurs before canceling. The CPU usage would peak around 85-95%. – Matt P. Nov 26 '15 at 05:00