WPF: Render overhead on tile loading

Jul 14, 2010 at 1:59 PM

The problem is pretty simple: in GMap.Net tiles are loaded in separate threads, and in every thread the following code is executed:

                     if (!IsDragging)
                     {
                         if (OnNeedInvalidation != null)
                         {
                             OnNeedInvalidation();
                         }
                     }

(see void ProcessLoadTask(object obj) method in Core.cs for details)

 

Because of that, on every load of tiles (on application start, zoom change and so on) there are many calls of OnNeedInvalidation(), and that indirectly calls OnRender() as many times as the number of tiles we have.

You can easily reproduce the problem with the following Map class (replace existing one in WPF Demo).

 

    /// <summary>
    /// the custom map f GMapControl 
    /// </summary>
    public class Map : GMapControl
    {
        private int counter;

        public Map()
        {
            // ...
        }

        /// <summary>
        /// any custom drawing here
        /// </summary>
        /// <param name="drawingContext"></param>
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);

            counter++;
            FormattedText text = new FormattedText(counter.ToString(), CultureInfo.CurrentUICulture,
                new FlowDirection(), new Typeface("GenericSansSerif"), 36, Brushes.Orange);
            drawingContext.DrawText(text, new Point(5, 5));

            Random r = new Random();
            for (int i = 0; i < 50; i++)
            {
                drawingContext.DrawLine(SelectionPen, new Point(r.Next(1600), r.Next(900)), new Point(r.Next(1600), r.Next(900)));
            }
        }
    }

My initial solution of this problem was to change logic in Core.cs is such a manner that only the last tile will start invalidation. For that I just moved the code shown in the first code snippet two lines higher (inside this condition):

                  // last buddy cleans stuff ;}
                  if(last)
                  {
...
                  }
This works for 99% of times, but sometimes some tiles are not shown. I guess this is because of asyncronous calls. Am I correct? Is it possible to solve this problem in a better way? Thanks in advance!

Jul 14, 2010 at 2:08 PM
Edited Jul 14, 2010 at 2:26 PM
well, if you invalidate only after the last tile, then view will be empty most of the time. If you have code thats solve these problems, please share ;} p.s. i have new wpf system on the way, using canvas, so invalidation overhead would be solved automaticaly
Jul 14, 2010 at 2:48 PM
radioman wrote:
well, if you invalidate only after the last tile, then view will be empty most of the time. If you have code thats solve these problems, please share ;} p.s. i have new wpf system on the way, using canvas, so invalidation overhead would be solved automaticaly

Radioman, thank you for your quick reply!

1. The view is not empty for 99% of time in my current solution. But it doesn't work for 100%, unfortunately.

2. What is your roadmap for this new WPF version? Will it be available within a week, month or year? :) Is it partially done/committed? It would be interesting to take a look.

 

Jul 14, 2010 at 3:55 PM
check Testing\WPF-GMapControlNew, it is available now, but useful just for testing. p.s. now it should invalidate 100% ;}
Jul 15, 2010 at 7:25 AM
Edited Jul 15, 2010 at 8:05 AM
radioman wrote:
check Testing\WPF-GMapControlNew, it is available now, but useful just for testing. p.s. now it should invalidate 100% ;}

1. Will definitely take a look at this new control, thanks!

2. It doesn't invalidate in 100% of cases, unfortunately:

http://www.picfront.org/d/7IP7

(I reproduced that by zoom/unzoom).

 

I think the logic for invalidation should be as follows:

1. Wait for ALL threads to finish.

2. Execute some method that will perform invalidation.

 

I'm not so familiar with thread programming, but I guess something like EventWaitHandle should be used for enabling thread syncronization.

Jul 15, 2010 at 7:33 AM

did you test it on 6e4b902bd621 changeset?

Jul 15, 2010 at 8:08 AM

Yes, I've tested on that one.

I tried the following change, but it doesn't work in STA apartment:

 

            // One event is used for each tile load task.
            ManualResetEvent[] doneEvents = new ManualResetEvent[tileDrawingList.Count];
            int eventNumber = 0;

            foreach(Point p in tileDrawingList)
            {
               LoadTask task = new LoadTask(p, Zoom);
               {
                  lock(tileLoadQueue)
                  {
                     if(!tileLoadQueue.Contains(task))
                     {
                        doneEvents[eventNumber] = new ManualResetEvent(false);
                        tileLoadQueue.Enqueue(task);
                        ThreadPool.QueueUserWorkItem(ProcessLoadTaskCallback, doneEvents[eventNumber]);
                     }
                     else
                     {
                         // Loading of this tile has finished.
                         doneEvents[eventNumber] = new ManualResetEvent(true);
                     }
                  }
               }
               eventNumber++;
            }

            // Wait for all threads in pool.
            WaitHandle.WaitAll(doneEvents);

I guess I will try to solve this problem in an hour or so, and will post the results here.

Jul 15, 2010 at 8:23 AM

So here is the solution (sorry, I don't know how to send you a patch without commit):

 

diff --git a/GMap.NET.Core/GMap.NET.Internals/Core.cs b/GMap.NET.Core/GMap.NET.Internals/Core.cs
--- a/GMap.NET.Core/GMap.NET.Internals/Core.cs
+++ b/GMap.NET.Core/GMap.NET.Internals/Core.cs
@@ -47,6 +47,7 @@
 

       public readonly Queue<LoadTask> tileLoadQueue = new Queue<LoadTask>();

       readonly WaitCallback ProcessLoadTaskCallback;

+      readonly WaitCallback ProcessInvalidationCallback;

 

       public static readonly string googleCopyright = string.Format("©{0} Google - Map data ©{0} Tele Atlas, Imagery ©{0} TerraMetrics", DateTime.Today.Year);

       public static readonly string openStreetMapCopyright = string.Format("© OpenStreetMap - Map data ©{0} OpenStreetMap", DateTime.Today.Year);

@@ -410,6 +411,7 @@
       public Core()

       {

          ProcessLoadTaskCallback = new WaitCallback(ProcessLoadTask);

+         ProcessInvalidationCallback = new WaitCallback(ProcessInvalidation);

 

 #if PocketPC

          loaderLimit = new Semaphore(2, 2);

@@ -937,6 +939,8 @@
 

       void ProcessLoadTask(object obj)

       {

+         ManualResetEvent doneEvent = (ManualResetEvent) obj;

+

          bool last = false;

 

          LoadTask? task = null;

@@ -1088,42 +1092,40 @@
                      {

                         OnTileLoadComplete();

                      }

+                  }

 

-                     if(OnNeedInvalidation != null)

-                     {

-                        Thread.Sleep(333);

-                        OnNeedInvalidation();

-                     }

-                     lock(this)

-                     {

-                        LastInvalidation = DateTime.Now;

-                     }

-                  }

-                  else

-                  {

-                     lock(this)

-                     {

-                        if((DateTime.Now - LastInvalidation).TotalMilliseconds > 111)

-                        {

-                           if(OnNeedInvalidation != null)

-                           {

-                              OnNeedInvalidation();

-                           }

-                           LastInvalidation = DateTime.Now;

-                        }

-                        else

-                        {

-                           Debug.WriteLine("SkipInvalidation, Delta: " + (DateTime.Now - LastInvalidation).TotalMilliseconds + "ms");

-                        }

-                     }

-                  }

+                  //if(!IsDragging)

+                  //{

+                  //   if(OnNeedInvalidation != null)

+                  //   {

+                  //      OnNeedInvalidation();

+                  //   }

+                  //}

                }

             }

             loaderLimit.Release();

          }

+

+         // Notify that this thread has finished.

+         doneEvent.Set();

       }

 

-      DateTime LastInvalidation = DateTime.Now;

+      void ProcessInvalidation(object obj)

+      {

+          ManualResetEvent[] doneEvents = (ManualResetEvent[]) obj;

+

+          // Wait for all threads in pool.

+          WaitHandle.WaitAll(doneEvents);

+

+          if (!IsDragging)

+          {

+              if (OnNeedInvalidation != null)

+              {

+                  OnNeedInvalidation();

+              }

+          }

+      }

+

 

       /// <summary>

       /// updates map bounds

@@ -1150,6 +1152,10 @@
             }

 #endif

 

+            // One ManualResetEvent is used for each tile load task.

+            ManualResetEvent[] doneEvents = new ManualResetEvent[tileDrawingList.Count];

+

+            int eventNumber = 0;

             foreach(Point p in tileDrawingList)

             {

                LoadTask task = new LoadTask(p, Zoom);

@@ -1158,12 +1164,22 @@
                   {

                      if(!tileLoadQueue.Contains(task))

                      {

+                        doneEvents[eventNumber] = new ManualResetEvent(false);

                         tileLoadQueue.Enqueue(task);

-                        ThreadPool.QueueUserWorkItem(ProcessLoadTaskCallback);

+                        ThreadPool.QueueUserWorkItem(ProcessLoadTaskCallback, doneEvents[eventNumber]);

+                     }

+                     else

+                     {

+                         // Loading of this tile has finished.

+                         doneEvents[eventNumber] = new ManualResetEvent(true);

                      }

                   }

                }

+               eventNumber++;

             }

+

+            // Start invalidation thread that will wait finishing of all tile load tasks and then will perform invalidation.

+            ThreadPool.QueueUserWorkItem(ProcessInvalidationCallback, doneEvents);

          }

          finally

          {


The only (evident) side effect is that all tiles are now shown simultaneusly, after their loading :)

Jul 15, 2010 at 8:48 AM

Radioman, I finally combined yours and mine solutions. Here it is:

 

diff --git a/GMap.NET.Core/GMap.NET.Internals/Core.cs b/GMap.NET.Core/GMap.NET.Internals/Core.cs
--- a/GMap.NET.Core/GMap.NET.Internals/Core.cs
+++ b/GMap.NET.Core/GMap.NET.Internals/Core.cs
@@ -47,6 +47,7 @@
 

       public readonly Queue<LoadTask> tileLoadQueue = new Queue<LoadTask>();

       readonly WaitCallback ProcessLoadTaskCallback;

+      readonly WaitCallback ProcessInvalidationCallback;

 

       public static readonly string googleCopyright = string.Format("©{0} Google - Map data ©{0} Tele Atlas, Imagery ©{0} TerraMetrics", DateTime.Today.Year);

       public static readonly string openStreetMapCopyright = string.Format("© OpenStreetMap - Map data ©{0} OpenStreetMap", DateTime.Today.Year);

@@ -410,6 +411,7 @@
       public Core()

       {

          ProcessLoadTaskCallback = new WaitCallback(ProcessLoadTask);

+         ProcessInvalidationCallback = new WaitCallback(ProcessInvalidation);

 

 #if PocketPC

          loaderLimit = new Semaphore(2, 2);

@@ -937,6 +939,8 @@
 

       void ProcessLoadTask(object obj)

       {

+         ManualResetEvent doneEvent = (ManualResetEvent) obj;

+

          bool last = false;

 

          LoadTask? task = null;

@@ -1088,42 +1092,48 @@
                      {

                         OnTileLoadComplete();

                      }

+                  }

 

-                     if(OnNeedInvalidation != null)

-                     {

-                        Thread.Sleep(333);

-                        OnNeedInvalidation();

-                     }

-                     lock(this)

-                     {

-                        LastInvalidation = DateTime.Now;

-                     }

+                  // Invalidate 10 times in a second.

+                  lock (this)

+                  {

+                      if ((DateTime.Now - LastInvalidation).TotalMilliseconds > 100)

+                      {

+                          if (OnNeedInvalidation != null)

+                          {

+                              OnNeedInvalidation();

+                          }

+                          LastInvalidation = DateTime.Now;

+                      }

                   }

-                  else

-                  {

-                     lock(this)

-                     {

-                        if((DateTime.Now - LastInvalidation).TotalMilliseconds > 111)

-                        {

-                           if(OnNeedInvalidation != null)

-                           {

-                              OnNeedInvalidation();

-                           }

-                           LastInvalidation = DateTime.Now;

-                        }

-                        else

-                        {

-                           Debug.WriteLine("SkipInvalidation, Delta: " + (DateTime.Now - LastInvalidation).TotalMilliseconds + "ms");

-                        }

-                     }

-                  }

+

                }

             }

             loaderLimit.Release();

          }

+

+         // Notify that this thread has finished.

+         doneEvent.Set();

       }

 

-      DateTime LastInvalidation = DateTime.Now;

+      private DateTime LastInvalidation = DateTime.Now;

+

+      void ProcessInvalidation(object obj)

+      {

+          ManualResetEvent[] doneEvents = (ManualResetEvent[]) obj;

+

+          // Wait for all threads in pool.

+          WaitHandle.WaitAll(doneEvents);

+

+          if (!IsDragging)

+          {

+              if (OnNeedInvalidation != null)

+              {

+                  OnNeedInvalidation();

+              }

+          }

+      }

+

 

       /// <summary>

       /// updates map bounds

@@ -1150,6 +1160,10 @@
             }

 #endif

 

+            // One ManualResetEvent is used for each tile load task.

+            ManualResetEvent[] doneEvents = new ManualResetEvent[tileDrawingList.Count];

+

+            int eventNumber = 0;

             foreach(Point p in tileDrawingList)

             {

                LoadTask task = new LoadTask(p, Zoom);

@@ -1158,12 +1172,22 @@
                   {

                      if(!tileLoadQueue.Contains(task))

                      {

+                        doneEvents[eventNumber] = new ManualResetEvent(false);

                         tileLoadQueue.Enqueue(task);

-                        ThreadPool.QueueUserWorkItem(ProcessLoadTaskCallback);

+                        ThreadPool.QueueUserWorkItem(ProcessLoadTaskCallback, doneEvents[eventNumber]);

+                     }

+                     else

+                     {

+                         // Loading of this tile has finished.

+                         doneEvents[eventNumber] = new ManualResetEvent(true);

                      }

                   }

                }

+               eventNumber++;

             }

+

+            // Start invalidation thread that will wait finishing of all tile load tasks and then will perform invalidation.

+            ThreadPool.QueueUserWorkItem(ProcessInvalidationCallback, doneEvents);

          }

          finally

          {


It works as previously (from visual perspective), but performance is much better on load and zoom/unzoom.

Jul 15, 2010 at 8:56 AM
Edited Jul 15, 2010 at 9:44 AM

I've uploaded a patch.

But... it looks like I've missed the right project... :)

http://offlinemaps.codeplex.com/SourceControl/PatchList.aspx

Radioman, please download it and take a look.

And, by the way, why it is not allowed to upload patched to GreatMaps? :( Can you change this?

Jul 15, 2010 at 11:17 AM
Edited Jul 15, 2010 at 11:29 AM

g ;] can you create a fork?

p.s. btw, does it change anything? because i do invalidation using last == true, and is it building for mobile?

Jul 15, 2010 at 11:29 AM

I can, but i don't want to do that :)

The reason is simple - my changes are not fork, it is just a patch.

Please download my patch from the link above and just apply it to your sources.

And it would be very convenient to have "Upload patch" option right here...

Jul 15, 2010 at 11:33 AM

mercurial use forks. effing patches are history ;}

Jul 15, 2010 at 11:33 AM
radioman wrote:

p.s. btw, does it change anything? because i do invalidation using last == true, and is it building for mobile?

It does. In guarantees that there will be final invalidation (100%).

You can even comment out invalidation inside tile loading thread, and there will be only one invalidation per load/zoom/unzoom. But the tiles will not be shown as they are loading. For some people this kind of behavior can be also interesting.

 

Jul 15, 2010 at 11:41 AM

i need invalidation each 100ms + one after all tiles loaded to guarantee full view, so whats the difference than using 'bool last' ?

Jul 15, 2010 at 11:59 AM
Edited Jul 16, 2010 at 7:50 AM

1. I've created fork with name "Iljas" and committed my changes.

2. I don't know whether this change is building for mobile platforms as I don't have mobile framework and this project is not building on my machine at all ;)

3. I've tested your changes in the morning and did it once again few minutes ago. There are rare cases when one or several tiles are shown blank (even after disappearing of progress bar)

You can try to reproduce this by doing multiple zoom-unzoom operations. Here is another prooflink (done few minutes ago): http://www.picfront.org/d/7IQU

And the possible reason for such behavior, I think, is simultaneous end of work in several threads.

My solution obtains 100% guarantee for invalidation :)

 

Jul 19, 2010 at 10:49 AM

Hello Radioman!

 

I've tested your last commit, it works perfectly!

The only question I have: what was the reason of removing normal thread pool? As far as I know, creation and killing of threads takes some time, and it is possible to avoid overhead by using thread pool (you've used it previously...).

Jul 19, 2010 at 11:26 AM

threads are created only once, 5 for desktop, 2 for mobile. The desktop version threads has each 5min idle timeout after that thread dies and recreated only when needed, but if you keep 'browsing', they stay as long as needed. I don't know if 5min is really best choice ;}

Jul 19, 2010 at 11:52 AM

Got your idea, thanks!