1

I have a timer that calls a method every 15 seconds and that method takes some time to finish. I have converted it to async as much as possible for me but it still freezes the UI for almost 1 sec when it runs and since it runs every 15 seconds, it becomes annoying.

Any idea how to make this async method run completely off the grid? This is the timer method:

public static DispatcherTimer UpdateList = new DispatcherTimer();
//GlobalVars.MainList = new List<string>();  //saved list from previous declaration

public MainFunction()
{
    this.InitializeComponent();
    UpdateList.Tick += UpdateList_Tick; ;
    UpdateList.Interval = new TimeSpan(0, 0, 0, 0, 15000);
    UpdateList.Start();
    //...
}
private async void UpdateList_Tick(object sender, object e)
    {   
    using (var client = new ImapClient())
            {
                using (var cancel = new CancellationTokenSource())
                {
                    await client.ConnectAsync("imap.gmail.com", 993, true, cancel.Token);
                    client.AuthenticationMechanisms.Remove("XOAUTH");

                    await client.AuthenticateAsync("email.com", "mail12345", cancel.Token);

                    var inbox = client.Inbox;
                    await inbox.OpenAsync(FolderAccess.ReadOnly, cancel.Token);

                    // let's try searching for some messages...
                    DateTime date = DateTime.Now;
                    DateTime mondayOfLastWeek = date.AddDays(-(int)date.DayOfWeek - 6);
                    var query = SearchQuery.DeliveredAfter(mondayOfLastWeek)
                        .And(SearchQuery.SubjectContains("search"))
                        .And(SearchQuery.All);
                    List<string> newList = new List<string>();
                    foreach (var uid in inbox.Search(query, cancel.Token))
                    {
                        var message = inbox.GetMessage(uid, cancel.Token);
                        string trimmedMSGEmtyLines = Regex.Replace(message.TextBody, @"^\s+$[\r\n]*", "", RegexOptions.Multiline);
                        newList.Add(message.Date.LocalDateTime + Environment.NewLine + trimmedMSGEmtyLines);
                    }
                    await client.DisconnectAsync(true, cancel.Token);
                    if (!GlobalVars.MainList.SequenceEqual(newList))
                    {
                        GlobalVars.MainList = newList;
                    }
                }
            }
}

Update: MailKit, Portable.Text.Encoding, MimeKit

John P.
  • 1,199
  • 2
  • 10
  • 33
  • 2
    Is there a reason that using a [BackgroundWorker](https://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396) won't work for you? – khargoosh Mar 08 '17 at 23:11
  • @khargoosh: `BackgroundWorker` is just like `async`/`await`, only harder to use. The OP _should_ be using the modern idiom. – Peter Duniho Mar 08 '17 at 23:15
  • 5
    You have a lot of synchronous code in that `async` method. Calls to `inbox.Search()` and `Regex.Replace()` are both potentially costly, and a `foreach` loop doesn't help either. You might try changing to `foreach (var uid in await Task.Run(() => inbox.Search(query, cancel.Token)))` as a first try to improve latency. Beyond that, without a good [mcve] that reliably reproduces the problem, it's not possible to say for sure what you need to fix. – Peter Duniho Mar 08 '17 at 23:17
  • @PeterDuniho for simple use cases I've found it easier to use, but this is probably a matter of my experience. – khargoosh Mar 08 '17 at 23:17
  • 3
    @khargoosh: or perhaps, lack of experience with `async`/`await`. I've been using `BackgroundWorker` for a decade and a half, but once we got `async`/`await`, I _never_ use BW anymore. The code is _so_ much easier to read and write than when dealing with BW (which was great for its time, but is now looking pretty rickety). – Peter Duniho Mar 08 '17 at 23:18
  • 1
    Also, since you are already using `async`/`await` and you want the code executed on 15 second intervals, you might consider just putting the thing in a `while` loop and using `await Task.Delay(TimeSpan.FromSeconds(15));` as your timer, rather than creating a timer explicitly. – Peter Duniho Mar 08 '17 at 23:20
  • @PeterDuniho you really believe giving a shot to a `while` loop on a gui would help here instead of using a timer? I am not meaning to offend you, just asking in case it help, so I can give it a shot. The other suggestions are pretty good I guess but I am not meaning to improve the latency simply. It is a gui, even a `millisecond` latency is an issue. I am looking for a way to really remove this delay completely. – John P. Mar 08 '17 at 23:30
  • I was thinking of having a second console application which would do that separately and save the results in a file. Then have my gui check the file for any changes instead of checking the mail client. But I don't know if it is really worth the trouble. Any the 1 million dollars question; is there a way to remove the latency completely of the grid before I get to use a second console application? – John P. Mar 08 '17 at 23:31
  • _"you really believe giving a shot to a while loop on a gui would help here instead of using a timer?"_ -- that suggestion has nothing to do with the question of the delay, just that it's more in keeping with the code that already uses `async`/`await`. Note that `await Task.Delay(...)` keeps the thread unblocked just like all the other places you use `await`. _"even a millisecond latency is an issue"_ -- not true at all. Latencies of tens of millisecond, even up to a couple hundred ms in some cases, are completely unnoticeable by human beings. – Peter Duniho Mar 08 '17 at 23:37
  • In any case, you cannot literally _"remove this delay completely"_, unless you actually remove the code itself completely. The point is to keep as little as possible on the UI thread, so that the user doesn't notice the interruption in UI responsiveness. And no, moving the code to an external process isn't going to help...it should and will be sufficient to move the longer-running operations to a worker thread, such as by using `Task.Run()` as I suggested. – Peter Duniho Mar 08 '17 at 23:37
  • @PeterDuniho The code above is really reproducible but I wouldn't recommend you test it since it requires packages installing. However we are not talking about tens of ms here but 800-1000 ms delay on each call. I have already done the modifications you suggested, slightly improved the latency but still, 800 ms is a issue. My project includes writing so even 100 ms would be an issue I believe. – John P. Mar 08 '17 at 23:50
  • 1
    _"The code above is really reproducible but I wouldn't recommend you test it since it requires packages installing"_ -- please read [mcve], so that you understand what that phrase means. See also [ask], and especially the articles linked at the bottom. _"requires packages installing"_ is just another way to say that the code is _not_ **a good [mcve]**. – Peter Duniho Mar 08 '17 at 23:53
  • @PeterDuniho I have updated the question to make it reproducible I hope. All relevant packages have been included as well. – John P. Mar 09 '17 at 00:10
  • @khargoosh: [BGW is an outdated approach; `Task.Run` is superior in every way](http://blog.stephencleary.com/2013/05/taskrun-vs-backgroundworker-intro.html). – Stephen Cleary Mar 09 '17 at 12:41
  • For starters, don't use `DispatcherTimer`. That timer explicitly runs *on* the grid, not off it. (I.e., it runs on the dispatcher ;i.e., on the UI thread.) You want a timer that runs on a background thread, like a `ThreadPoolTimer`. – Raymond Chen Mar 13 '17 at 04:51

1 Answers1

1

async / await is useful when executing long running operations, which are decoupled from the UI, while running on a UI thread. This is exactly what you are doing - but it seems you have no business being on the UI thread in the first place. Really, you could await the entire contents of your event handler as it's wholly decoupled from the UI. So it's not the right tool for the job.

BackgroundWorker was suggested, which would be a great option. I chose to provide a solution using System.Threading.Timer because there is not much code to change vs. your existing code. If you add any code which updates the UI, you will need to invoke it back to the dispatcher thread.

And all those async methods could probably run synchronously. I would remove async / await then choose synchronous methods, for example client.ConnectAsync() >> client.Connect()

public System.Threading.Timer UpdateTimer;

public MainFunction()
{
    this.InitializeComponent();
    UpdateTimer = new System.Threading.Timer(UpdateList_Tick);
    UpdateTimer.Change(0, 15000);
    //...
}

private async void UpdateList_Tick(object state)
{   
    using (var client = new ImapClient())
    {
        using (var cancel = new CancellationTokenSource())
        {
            await client.ConnectAsync("imap.gmail.com", 993, true, cancel.Token);
            client.AuthenticationMechanisms.Remove("XOAUTH");

            await client.AuthenticateAsync("email.com", "mail12345", cancel.Token);

            var inbox = client.Inbox;
            await inbox.OpenAsync(FolderAccess.ReadOnly, cancel.Token);

            // let's try searching for some messages...
            DateTime date = DateTime.Now;
            DateTime mondayOfLastWeek = date.AddDays(-(int)date.DayOfWeek - 6);
            var query = SearchQuery.DeliveredAfter(mondayOfLastWeek)
                .And(SearchQuery.SubjectContains("search"))
                .And(SearchQuery.All);
            List<string> newList = new List<string>();
            foreach (var uid in inbox.Search(query, cancel.Token))
            {
                var message = inbox.GetMessage(uid, cancel.Token);
                string trimmedMSGEmtyLines = Regex.Replace(message.TextBody, @"^\s+$[\r\n]*", "", RegexOptions.Multiline);
                newList.Add(message.Date.LocalDateTime + Environment.NewLine + trimmedMSGEmtyLines);
            }
            await client.DisconnectAsync(true, cancel.Token);
            if (!GlobalVars.MainList.SequenceEqual(newList))
            {
                GlobalVars.MainList = newList;
            }
        }
    }
}
djv
  • 15,168
  • 7
  • 48
  • 72
  • I have a question about invoking the UI. I am on UWP. I want to call a form. `SettingsFlyout fly = new SettingsFlyout(); fly.Show();`. However this doesn't call it since it is on another thread. How would I call it? `.Invoke and .BeginInvoke` are not available here. – John P. Mar 09 '17 at 14:04
  • 1
    Admittedly, I don't know anything about UWP, but I found [this question](http://stackoverflow.com/questions/10579027/run-code-on-ui-thread-in-winrt) which has some info which may help you. – djv Mar 09 '17 at 15:24