ThinkGeo.com    |     Documentation    |     Premium Support

Caching shapefiles into memory

Hi there,


I've been prototyping with MapSuite 5.5 WPF as a possible replacement for our in-house developed PPI.


I have ported an example of our code to use the MapSuite map. My structure is as follows:


The WpfMap has 3 Overlays:

- One for the background world map using the countries02.shp file supplied in your examples (for now, ideally it will use ENC charts) 

- A second (SimpleMarkerOverlay) for vessel/feature markers

- Final layer for line shapes such as vessel trails (InMemoryFeatureLayer)


Whilst it is performing fairly well, I notice that when zooming / panning around the CPU usage will jump to ~40-50% (2 of my 4 cores) and the File IO climbs up to 20 MB/s. Even whan I am not zooming and simply refreshing layers (on a 2s timer if required) it continously spikes at 5MB/s with an associated CPU load spike).



Using Process Monitor (procmon.exe) I can see that is is the application making repeated open/read/close calls to the cuontries02.shp file. As in over 1000 per second.


Now this feels unneccesary. I searched for a way to cache shape files in memory (seeing as this file is less than 4MB in size) and came across this: gis.thinkgeo.com/Support/DiscussionForums/tabid/143/aff/22/aft/4844/afv/topic/Default.aspx#techtip


However, It is from 2007 and I can't seem to find anyway of implementing something similar in the current (5.5) WPF release.



Any tips?




Cheers

Rodney



Rodney,



  Let me help you with the two main questions in this post.  First the open / close calls were a bug in Map Suite.  There was a ticket a few months back where there was a scenario where the user wanted us to maintain the Layer's open status, i.e. if the layer was closed when drawing started then we should close it when we are done drawing.  Somehow that made it into the main codebase and it was a big mistake.  It causes exactly what you are seeing and is a big performance issue.  Our solution is to revert the functionality back to opening layers and leaving them open and then adding a new property to allow the user to override this, but the default is to open and leave it open.  If you grab the latest daily build version from our website this issue should be resolved.  



The second item is related to loading things from steam and allowing you to pin stuff in memory.  We have a new method for doing this and a sample of this ships with the How Do I Samples.  If you look in the Data Providers tree node the item is "Load a map from streams".  This technique allows you to pass in a fake shapefile name and when we internally need to access the stream we raise an event and you can pass in whatever you want.  In this way you can load the entire file into a memory stream and pass that into us.  It is also handy in environment where you have access to only isolated storage or read files from SQL server etc.  I went ahead and included the code from the sample as it's short and easy to read.  It might save you from digging it up.




using System;   
using System.IO;   
using System.Windows.Forms;   
using ThinkGeo.MapSuite.Core;   
using ThinkGeo.MapSuite.DesktopEdition;   
  
  
namespace CSharpWinformsSamples   
{   
    public class LoadAMapFromStreams : UserControl   
    {   
        public LoadAMapFromStreams()   
        {   
            InitializeComponent();   
        }   
  
        private void LoadAMapFromStreams_Load(object sender, EventArgs e)   
        {   
            winformsMap1.MapUnit = GeographyUnit.DecimalDegree;   
            winformsMap1.BackgroundOverlay.BackgroundBrush = new GeoSolidBrush(GeoColor.GeographicColors.ShallowOcean);   
  
            WorldMapKitWmsDesktopOverlay worldMapKitDesktopOverlay = new WorldMapKitWmsDesktopOverlay();   
            winformsMap1.Overlays.Add(worldMapKitDesktopOverlay);   
  
            ShapeFileFeatureLayer shapeFileLayer = new ShapeFileFeatureLayer(@"C:\DoesNotExistDirectory\Countries02.shp");   
            ((ShapeFileFeatureSource)shapeFileLayer.FeatureSource).StreamLoading += new EventHandler<STREAMLOADINGEVENTARGS>(LoadAMapFromStreams_StreamLoading);   
            shapeFileLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle = AreaStyles.CreateSimpleAreaStyle(GeoColor.SimpleColors.Transparent, GeoColor.FromArgb(100, GeoColor.SimpleColors.Green));   
            shapeFileLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;   
  
            LayerOverlay staticOverlay = new LayerOverlay();   
            staticOverlay.Layers.Add("WorldLayer", shapeFileLayer);   
            winformsMap1.Overlays.Add(staticOverlay);   
  
            winformsMap1.CurrentExtent = new RectangleShape(-139.2, 92.4, 120.9, -93.2);   
            winformsMap1.Refresh();   
        }   
  
        private void LoadAMapFromStreams_StreamLoading(object sender, StreamLoadingEventArgs e)   
        {   
            string fileName = Path.GetFileName(e.AlternateStreamName);   
            e.AlternateStream = new FileStream(@"..\..\SampleData\Data\" + fileName, e.FileMode, e.FileAccess);   
        }   
  
        private WinformsMap winformsMap1;   
    }   
}  
 




David

 



Rodney, 
  
   One thing I forgot to mention is that if you don’t want to grab the latest developer build another way to ‘fix’ this issue is to call the Layer.Open method on all of your layers.  If you open them before hand then our default logic should be to keep them open.  Let me know the results. 
  
 David

Hi David, thanks for you speedy response.



Thanks for the tip on calling Open() on my ShapeFileFeatureLayer. That fixed the problem with the excessive File IO.


I'm now working on the second part of my query, that is caching a shapefile in memory to avoid repeated disk access.


I'm having some difficulty in getting your example working with a MemoryStream as opposed to a FileStream. 


To setup my layer:



public bool LoadWorldBackground()
        {
            string backgroundShape = ShapeFileDirectory + @"\Countries02.shp";
            
            if (Directory.Exists( ShapeFileDirectory ))
            {
                AreaStyle areaStyle = AreaStyles.CreateSimpleAreaStyle( new GeoColor( 255, 0xe2, 0xdb, 0xa4 ));

                ShapeFileFeatureLayer layer = new ShapeFileFeatureLayer( backgroundShape);
                ((ShapeFileFeatureSource)layer.FeatureSource).StreamLoading += new EventHandler<StreamLoadingEventArgs>(LoadAMapFromStreams_StreamLoading);

                layer.Name                                         = Constants.DefaultBackgroundLayer;
                layer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle    = areaStyle;
                layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
                layer.IsVisible                                    = true;
                layer.DrawingQuality                               = DrawingQuality.HighSpeed;

                BackgroundLayers.Add( Constants.DefaultBackgroundLayer, layer );

                return true;
            }
            else
            {
                Debug.Print( "Failed to find background shape file directory ({0}) or shapefile ({0}) - using default", 
                             ShapeFileDirectory, backgroundShape );
                BackgroundLayers.Add( Constants.DefaultBackgroundLayer,
                            new BackgroundLayer( new GeoSolidBrush( GeoColor.StandardColors.LightSteelBlue ) ) );
            }
            return false;
        }
 


 Callback method:



private void LoadAMapFromStreams_StreamLoading(object sender, StreamLoadingEventArgs e)   
        {
            Console.WriteLine("In Callback");
            if (m_WorldMapStream == null)
            { 
                m_Buffer         = File.ReadAllBytes(e.AlternateStreamName);
                m_WorldMapStream = new MemoryStream(m_Buffer, false);
            }
           
            e.AlternateStream = m_WorldMapStream;
        }
 


However when I run this I receive a divide by zero exception:



Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at ThinkGeo.MapSuite.WpfDesktopEdition.Tile.DrawException(GeoCanvas geoCanvas, Exception exception)
   at ThinkGeo.MapSuite.WpfDesktopEdition.Tile.Draw(GeoCanvas geoCanvas)
   at ThinkGeo.MapSuite.WpfDesktopEdition.LayerOverlay.DrawTileCore(Tile tile, RectangleShape targetExtent)
   at ThinkGeo.MapSuite.WpfDesktopEdition.TileOverlay.DrawTile(Tile tile, RectangleShape targetExtent)
   at ThinkGeo.MapSuite.WpfDesktopEdition.TileOverlay.vhQ=(RectangleShape vxQ=)
   at ThinkGeo.MapSuite.WpfDesktopEdition.TileOverlay.DrawCore(RectangleShape targetExtent, OverlayRefreshType overlayRefreshType)
   at ThinkGeo.MapSuite.WpfDesktopEdition.LayerOverlay.DrawCore(RectangleShape targetExtent, OverlayRefreshType refreshType)
   at ThinkGeo.MapSuite.WpfDesktopEdition.Overlay.Draw(RectangleShape targetExtent, OverlayRefreshType refreshType)

However, if I replace the e.AlternateStream assignment line in the callback with:


e.AlternateStream = new FileStream(e.AlternateStreamName, e.FileMode, e.FileAccess);  

However this obviously defeats the purpose of having a MemoryStream to cache the file. I did also try setting AlternateStream to a newly constructed MemoryStream (from my stored byte buffer cache) however that produced the same exception.


It is strange, both MemoryStream and FileStream are 'Stream's so I would expect the substitution to be invisible.


Any ideas?



Rodney,



  I have it working, so that's good news.  There are a few things that you maybe didn't realize lurking right below the surface of this problem.  The first is that the event is called multiple times, once for each type of file requested.  This mean that for a shapefile the method will be called once for the .shp, another time for the .shx file etc I think it's a total of five times.  In your sample you are caching this all to the same buffer which is causing confusion internally. :-)  The system thing the .shp is the .shx etc..  The alternate stream name will change and will always append the type of file we need to the end.  Put in a break point and watch the alternate stream name and you will see it change.  You can also look at the e.StreamType to see the type of file being requested.  Secondly you don't have to cache the memory stream or buffer to a private field, I saw you used m_Buffer etc.  You can just create the memory stream and pass it in.  The layer itself will hold on to the reference of the memory stream and keep it from getting collected.  We must also note that this works until the layer is closed so make sure you open the layer first to make sure the Desktop bug doesn't open and close the layer over and over.  You should be able to put some debug calls to verify it isn't getting called.  The code below is what I modified from the How Do I sample but with a small amount of tweaking it shoudl work for you.

 




        private void LoadAMapFromStreams_StreamLoading(object sender, StreamLoadingEventArgs e)
        {
            string fileName = Path.GetFileName(e.AlternateStreamName);
            MemoryStream stream = null;                       
            {
                byte[] buffer = File.ReadAllBytes(@"..\..\SampleData\Data\" + fileName);
                stream = new MemoryStream(buffer, false);
            }           

            e.AlternateStream = stream;            
        }
 


David



Hi David, 



Thanks for the tip about the differing file types. I implemented the changes you suggested which stopped the exceptions. However I noticed my file IO going through the roof (Up to 100MB/s). It seems that despite calling Open() on the layer immediately after construction (and I tried Open() before and after callback assignment) the StreamLoading event callback was being entered several times on each overlay refresh. 



So I formed a hybrid solution whereby I cache the contents of each file in Dictionary<string, byte[]> and construct a new MemoryStream each time. I initially tried to cache the MemoryStream for each file but upon re-entering the event callback the stream had been closed, despite having called Open() on the layer initially. 


Edit - By the way I have realised I am using WPF Desktop version 5.0.0.0.


Edit edit - This WYSIWYG / Source editor interaction is pretty horrible!


Cheers, Rod




public bool LoadWorldBackground()
        {
            string backgroundShape = ShapeFileDirectory + @"\Countries02.shp";
            string backgroundIndex = ShapeFileDirectory + @"\Countries02.idx";
            
            if (Directory.Exists( ShapeFileDirectory ) && File.Exists( backgroundShape ))
            {
                AreaStyle areaStyle = AreaStyles.CreateSimpleAreaStyle( new GeoColor( 255, 0xe2, 0xdb, 0xa4 ));

                ShapeFileFeatureLayer.BuildIndexFile( backgroundShape, backgroundIndex, BuildIndexMode.DoNotRebuild);
                ShapeFileFeatureLayer layer = new ShapeFileFeatureLayer(backgroundShape, backgroundIndex, ShapeFileReadWriteMode.ReadOnly);
                ((ShapeFileFeatureSource)layer.FeatureSource).StreamLoading += new EventHandler<StreamLoadingEventArgs>(LoadAMapFromStreams_StreamLoading);
                
                layer.Name                                         = Constants.DefaultBackgroundLayer;
                layer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle    = areaStyle;
                layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
                layer.IsVisible                                    = true;
                layer.DrawingQuality                               = DrawingQuality.HighSpeed;
                
                // Open the world map layer to avoid excessive file IO
                layer.Open();

                BackgroundLayers.Add( Constants.DefaultBackgroundLayer, layer );

                return true;
            }
            else
            {
                Debug.Print( "Failed to find background shape file directory ({0}) or shapefile ({0}) - using default", 
                             ShapeFileDirectory, backgroundShape );
                BackgroundLayers.Add( Constants.DefaultBackgroundLayer,
                            new BackgroundLayer( new GeoSolidBrush( GeoColor.StandardColors.LightSteelBlue ) ) );
            }
            return false;
        }

        private void LoadAMapFromStreams_StreamLoading(object sender, StreamLoadingEventArgs e)   
        {
            Console.WriteLine("In Callback - {0}", e.AlternateStreamName);

            if (!m_BackgroundCache.ContainsKey(e.AlternateStreamName))
            {
                byte[] buffer = File.ReadAllBytes(e.AlternateStreamName);
                m_BackgroundCache.Add(e.AlternateStreamName, buffer);
            }
            
            e.AlternateStream = new MemoryStream(m_BackgroundCache[e.AlternateStreamName], false); 
        }



Rodney, 
  
   I will look into the the IO issue however I bet I know what is going on there.  The WPF Edition, in order to take advantage of multiple cores, clones the layers so it can draw on multiple tiles at the same time.  I bet each clone is getting its open called.  If you do not call the Overlay.Refresh then it should keep the clones cached.  You should only call the Map.Refresh(), the one without parameters, when some massive things have changed.  If not I suggest you call the Map.Refresh(Overlay) and only pass in the overlay that really changed, like you changed the styles.  Otherwise, once the styles are set,  there is not much need to call the refresh.  Anyway we will look into this and see if there is a way we can clone the layer without calling an open on it. :-)  Thanks for all of the great information.  If you need anything else please let me know. 
  
 David

Hi David, 
  
 I have been playing it safe on my calls to refresh (I have been calling Map.Refresh(overlay) but also including the background overlay as a ‘just in case’). I’m now stripping back my refresh calls to see what I can get away with. 
  
 Thanks for your help with this issue. The tweaks we’ve made are making it perform a lot better and will flow back to my colleagues that are using Map Suite in a proper application (I’m just prototyping to see if it will be suitable to replace the chart I currently use in the project I am attached to). 
  
 Cheers 
  
 Rod

As an aside, is the StreamLoading technique possible with MultipleShapeFileFeatureLayers?

Rodney, 
  
   It should work on the individual layers but I am not sure about the midx.  I am not that familiar with that layer and need to do a bit more research.  What I do know is that we can make it work. :-) 
  
 David