Wednesday, August 4, 2010

Large File Upload in Silverlight Part II

This is the second part of my Large File Upload in Silverlight code using chunking.

I made some changes that I think will give some better performance.  I changed Backgroundworker Thread to report back the number of bytes read as opposed to a percentage.  My brother suggested that having that bytes processed total available at the main UI thread level would be of more use and tend to agree.  While Microsoft might be trying to be helpful in naming the ProgressChangedEvent property ProgressPercentage it's actually kind of dumb.  They should have just called it Progress and let the developers decide how to use it.

I also added some code to allow the Backgroundworker to keep two WebClients running all the time and that increases the potential throughput by about 50%.  While this was fine for IE and Firefox, Chrome and Safari don't like it at all and the latter two tend to bog down while waiting for the underlying browser requests  complete their work. I need to test this on a real web server and not under the ASP.Net developer server.

The second set of change had to do with how I pass the data to the back end.  I added an offset and number of pass to back end call and implemented a shared file write approach that back in the 80's we called Random Access Files and I guess that still hold water.  I didn't bother adding FileStream range locking as my code is running very sequential and I'm not doing retries or pausing yet.

Well the XAML is the same so you can that that from the previous post. Note: Blogger editor does not handle the XML injected into the article well once it posted and when you edit it the you have to re-insert it from scratch.

First the new .ASHX code that allows for simultaneous byte range writes.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Text.RegularExpressions;

namespace LargeFileUpload.Web
{
    /// 
    /// Summary description for StoreFile
    /// 
    public class StoreFile : IHttpHandler
    {

        private const int READ_BUFFER_LEN = 57344;

        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "text/plain";
            context.Response.Write("Hello World");
            string check = context.Request["filestream"];
            int offset = Convert.ToInt32(context.Request["offset"]);

            check = Regex.Replace(check, " ", "+");
            byte[] bytes = Convert.FromBase64String(check);
            string fileName = context.Server.MapPath(context.Request["filename"]);

            FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write);
            fs.Seek(READ_BUFFER_LEN * offset, SeekOrigin.Begin);
            fs.Write(bytes, 0, bytes.Length);
            fs.Close();
            fs.Dispose();
            //System.Threading.Thread.Sleep(500);
        }

        private void fileit()
        {
            }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

Next is the main pages code and it's changes

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Threading;
using System.ComponentModel;

namespace LargeFileUpload
{
    public partial class MainPage : UserControl
    {
        public static System.IO.Stream fs;
        private BackgroundWorker bw = new BackgroundWorker();
        private long bytesReadTotal = 0;
        private const int READ_BUFFER_LEN = 57344;

        public MainPage()
        {
            InitializeComponent();

            bw.WorkerReportsProgress = true;
            bw.WorkerSupportsCancellation = true;
            bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
            bw.ProgressChanged += (s, e) => {
                // e.ProgressPercentage is actually the number of bytes last read
                bytesReadTotal += e.ProgressPercentage;
                int percentComplete = (int)((float)bytesReadTotal / (float)fs.Length * 100);
                textBox1.Text = String.Format("Transferred {0} out of {1}"
                    , fileSizeString(bytesReadTotal), fileSizeString(fs.Length));
                progressBar1.Value = percentComplete;
            };
            bw.DoWork += (s, e) => {uploadFileToWeb((string)e.Argument, (BackgroundWorker)s, e);  }; 
        }

        string fileSizeString(long bytes)
        {
            string f;
            if (bytes > 1073741824)
                f = String.Format("{0:0.00} GB", (float)bytes / (float)1073741824);
            else if (bytes > 1048576)
                f = String.Format("{0:0.00} MB", (float)bytes / (float)1048576);
            else if (bytes > 1024)
                f = String.Format("{0:0.00} KB", (float)bytes / (float)1024);
            else
                f = String.Format("{0:0.00} Bytes", bytes);
            return f;
        }

        void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if ((e.Cancelled == true))
            {
                progressBar1.Value = 0;
                textBox1.Text = "Upload Canceled!";
            }

            else if (!(e.Error == null))
            {
                textBox1.Text = ("Error: " + e.Error.Message);
            }

            else
            {
                progressBar1.Value = 100;
                cmdCancelUpload.IsEnabled = false;
                textBox1.Text = "File upload completed!";
            }
            bytesReadTotal = 0; 
            cmdStartUpload.IsEnabled = true;
        }

        private void cmdCancelUpload_Click(object sender, RoutedEventArgs e)
        {
            bw.CancelAsync();
            cmdCancelUpload.IsEnabled = false;
        }


        private void cmdStartUpload_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog of = new OpenFileDialog();
            
            bool? userClickedOK = of.ShowDialog();
            if (userClickedOK == true)
            {
                progressBar1.Value = 0;
                progressBar1.Maximum = 100;

                textBox1.Text = of.File.Name;

                fs = of.File.OpenRead();
                string fileName = of.File.Name;

                cmdCancelUpload.IsEnabled = true;
                cmdStartUpload.IsEnabled = false;
                bw.RunWorkerAsync(fileName); // calls DoWork()
            }

            
        }

        void uploadFileToWeb(string fileName, BackgroundWorker worker, DoWorkEventArgs e)
        {
            byte[] b = new byte[READ_BUFFER_LEN];
            int bytesRead = fs.Read(b, 0, READ_BUFFER_LEN);
            int offset = 0;
            int allow_at_most_two = 1;
            while (bytesRead > 0)
            {
                // has request to stop early been made?
                if (worker.CancellationPending)  
                {
                    e.Cancel = true;
                    break;
                }
                
                AutoResetEvent a = new  AutoResetEvent(false);

                WebClient wc = new WebClient();
                wc.Headers["Content-Type"] = "application/x-www-form-urlencoded";
                string errormessage = "";
                wc.UploadStringCompleted += (s, e1) =>
                {
                    string result = "";
                    if (e1.Error == null)
                        result = e1.Result;
                    else
                    {
                        errormessage = e1.Error.Message.ToString();
                        e.Cancel = true;  // set worker.CancellationPending
                    }
                    Interlocked.Decrement(ref allow_at_most_two);
                    a.Set();
                };

                wc.UploadStringAsync(new Uri("/StoreFile.ashx", UriKind.Relative), "POST"
                    , "filename=+" + fileName + "&offset=" + offset.ToString() + "&filestream=" 
                    + Convert.ToBase64String(b, 0, bytesRead));

                if (allow_at_most_two == 2)
                    a.WaitOne();

                Interlocked.Increment(ref allow_at_most_two);

                Interlocked.Increment(ref offset);

                worker.ReportProgress(bytesRead);
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    break;  // request is stopping
                }

                bytesRead = fs.Read(b, 0, READ_BUFFER_LEN);
            }
        }

    }
}

Hope you like the new stuff. Let me know what you think or what improvements can be made.

1 comment:

Khalid said...

thank you so much for sharing this code with everyone. xoxo
it helped me a lot, I had some memory issues and your code gave me some hint how to tweak mine.

Blog Archive