Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you access an image's metadata without causing a leak in native memory?

The below use of BitmapDecoder, BitmapPropertySet, and BitmapTypedValue causes a leak in native memory. I'm hoping I'm just using them incorrectly... What is the correct way to use them? They don't implement IDisposable, so I'm not sure how to tell them to release the native memory they own.

There's a chance this leak cannot be avoided. If so, how would I shield my UWP app from its affects?

A Valloc trace from Windows Performance Recorder/Analyzer shows many allocations with no Decommit Time. Commit stacks for these objects indicate they are related to fetching metadata or are pieces of metadata.

WPR-WPA Valloc trace of running the below test code

using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Search;

namespace Tests
{
    [TestClass]
    public class TestBitmapDecoderLeak
    {
        // Path to a folder containing jpeg's that have metadata.
        private static readonly string ImageFolderPath = @"path to folder of jpegs";

        // The subset of the metadata you want to get
        private static readonly string [] MetadataPolicies = 
        {
            "System.Copyright",
            "System.SimpleRating",
            "System.Photo.ShutterSpeed",
            "System.Photo.Aperture",
            "System.Photo.CameraModel",
            "System.Photo.CameraManufacturer",
            "System.Photo.DateTaken",
            "System.Photo.ExposureTime",
            "System.Photo.Flash",
            "System.Photo.FlashEnergy",
            "System.Photo.ISOSpeed",
            "System.GPS.Longitude",
            "System.GPS.Latitude"
        };

        [TestMethod]
        public async Task RunTest()
        {
            // Start your profiler
            Debugger.Break();

            var imageFiles = await GetJpegFiles();

            // Get some metadata for each image, but do nothing with it.
            foreach (var imageFile in imageFiles)
            {
                using (var fileStream = await imageFile.OpenReadAsync())
                {
                    var decoder = await BitmapDecoder.CreateAsync(fileStream);

                    var DpiX            = decoder.DpiX;
                    var DpiY            = decoder.DpiY;
                    var PixelSizeWidth  = decoder.OrientedPixelWidth;
                    var PixelSizeHeight = decoder.OrientedPixelHeight;
                    var DecoderId       = decoder.DecoderInformation.CodecId;

                    var properties = await decoder.BitmapProperties.GetPropertiesAsync(MetadataPolicies);

                    var ShutterSpeed    = NullOrValType<double>(properties, "System.Photo.ShutterSpeed");
                    var Copyright       = NullOrRefType<string>(properties, "System.Copyright");
                    var SimpleRating    = NullOrValType<UInt16>(properties, "System.SimpleRating");
                    var Aperture        = NullOrValType<double>(properties, "System.Photo.Aperture");
                    var CameraModel     = NullOrRefType<string>(properties, "System.Photo.CameraModel");
                    var DateTaken       = NullOrValType<DateTimeOffset>(properties, "System.Photo.DateTaken");
                    var ExposureTime    = NullOrValType<double>(properties, "System.Photo.ExposureTime");
                    var Flash           = NullOrValType<UInt16>(properties, "System.Photo.Flash");
                    var FlashEnergy     = NullOrValType<double>(properties, "System.Photo.FlashEnergy");
                    var IsoSpeed        = NullOrValType<UInt16>(properties, "System.Photo.ISOSpeed");
                    var Longitude       = NullOrRefType<double[]>(properties, "System.GPS.Longitude");
                    var Latitude        = NullOrRefType<double[]>(properties, "System.GPS.Latitude");
                }
            }

            // Remove these so they don't add noise to the trace
            imageFiles = null;

            // Ensure everything is cleaned up to the best of the GC's abilities
            GC.Collect();
            GC.WaitForPendingFinalizers();

            // Stop your profiler
            Debugger.Break();
        }

        private static async Task<IEnumerable<StorageFile>> GetJpegFiles()
        {
            var folder = await StorageFolder.GetFolderFromPathAsync(ImageFolderPath);

            var queryOptions = new QueryOptions
            {
                FolderDepth = FolderDepth.Deep,
                IndexerOption = IndexerOption.DoNotUseIndexer,
            };
            queryOptions.FileTypeFilter.Add(".jpg");
            queryOptions.FileTypeFilter.Add(".jpeg");
            queryOptions.FileTypeFilter.Add(".jpe");

            var folderQuery = folder.CreateFileQueryWithOptions(queryOptions);
            return await folderQuery.GetFilesAsync();
        }

        private static T NullOrRefType<T>(IDictionary<string, BitmapTypedValue> properties, string policy)
            where T : class
        {
            if (properties == null || !properties.ContainsKey(policy))
                return null;

            return (T)properties[policy].Value;
        }

        private static T? NullOrValType<T>(IDictionary<string, BitmapTypedValue> properties, string policy)
            where T : struct
        {
            if (properties == null || !properties.ContainsKey(policy))
                return null;

            return (T)properties[policy].Value;
        }
    }
}
like image 674
Tristan Avatar asked Jan 17 '16 03:01

Tristan


1 Answers

I'm not 100% sure if this will help with the memory issues but you can access the file's properties from the StorageFile itself without having to load into the decoder.

You'd do that like this:

var systemProperties = new List<string>{ "System.Photo.ShutterSpeed" };
var props = await file.Properties.RetrievePropertiesAsync(systemProperties);

This will return you a Dictionary containing the system properties that you pass through. You can also pass null into the RetrievePropertiesAsync method and it will return all properties so you can filter from there too.

Then you would extract from the collection possibly like this depending on how you want to use the value:

if (props.ContainsKey("System.Photo.ShutterSpeed"))
{
    var cameraShutterSpeedObj = props["System.Photo.ShutterSpeed"];
    if (cameraShutterSpeedObj != null)
    {
        var cameraShutterSpeed = cameraShutterSpeedObj.ToString();
    }
}

Give that a try and see if you see any improvement.

like image 97
James Croft Avatar answered Nov 15 '22 00:11

James Croft