How can I optimize Shell32 method calls?

4.1k views Asked by At

I'm designing a media player, and I've designed a Movie class. The Movie class has a MovieInfo member, which inherits from MediaInfo. MediaInfo has several properties representing the metadata (is that what you call it?) of the Movie file, such as file length, file size, file path, etc. In order to extract this information, I use Shell32.

The problem is that the methods provided in Shell32 are very, very slow. With 1 movie in my database, it's not a problem, but with 10 movies, it starts becoming noticeable, and with 100 movies, it takes around 5 minutes for the program to load, and there are a few cases where I have to reinitialize the Movies during runtime, which again halts the flow of the program.

The MediaInfo constructor calls the Initialize method:

    /// <summary>
    /// Initializes all the information variables of the class.
    /// </summary>
    private void Initialize()
    {
        Folder mediaFolder = Shell32Processing.GetShellFolder(this.path);

        FolderItem media = Shell32Processing.GetShellFolderItem(this.path);

        //initialize bit rate value
        this.bitrate = mediaFolder.GetDetailsOf(media, 28);

        //initialize date accessed value
        this.dateAccessed = mediaFolder.GetDetailsOf(media, 5);

        //initialize date created value
        this.dateCreated = mediaFolder.GetDetailsOf(media, 4);

        //initialize date modified value
        this.dateModified = mediaFolder.GetDetailsOf(media, 3);

        //initialize data rate value
        this.dataRate = mediaFolder.GetDetailsOf(media, 279);

        //initialize file name value
        this.fileName = mediaFolder.GetDetailsOf(media, 155);

        //initialize file type value
        this.fileType = mediaFolder.GetDetailsOf(media, 181);

        //initialize folder value
        this.folder = mediaFolder.GetDetailsOf(media, 177);

        //initialize folder name value
        this.folderName = mediaFolder.GetDetailsOf(media, 175);

        //initialize folder path value
        this.folderPath = mediaFolder.GetDetailsOf(media, 176);

        //initialize frame height value
        this.frameHeight = mediaFolder.GetDetailsOf(media, 280);

        //initialize frame rate value
        this.frameRate = mediaFolder.GetDetailsOf(media, 281);

        //initialize frame width value
        this.frameWidth = mediaFolder.GetDetailsOf(media, 282);

        //initialize length value
        this.length = mediaFolder.GetDetailsOf(media, 27);

        //initialize title value
        this.title = FileProcessing.GetFileTitle(this.path);

        //initialize total bitrate value
        this.totalBitrate = mediaFolder.GetDetailsOf(media, 283);

        //initialize size value
        this.size = mediaFolder.GetDetailsOf(media, 1);
    }

Here is the Shell32Processing.GetShellFolder method:

    /// <summary>
    /// Gets a Shell32 Folder, initialized to the directory of the file path.
    /// </summary>
    /// <param name="path">A string representing the file/folder path</param>
    /// <returns>The Shell32 Folder.</returns>
    public static Folder GetShellFolder(string path)
    {
        //extract the folder subpath from the file path
        //i.e extract "C:\Example" from "C:\Example\example.avi"

        string directoryPath = new FileInfo(path).Directory.ToString();

        return new Shell().NameSpace(directoryPath);
    }

And the Shell32Processing.GetShellFolderItem method:

    /// <summary>
    /// Gets a Shell32 FolderItem, initialized to the item specified in the file path.
    /// </summary>
    /// <param name="path">A string representing the file path.</param>
    /// <returns>The Shell32 FolderItem.</returns>
    /// <exception cref="System.ArgumentException">Thrown when the path parameter does not lead to a file.</exception>
    public static FolderItem GetShellFolderItem(string path)
    {
        if (!FileProcessing.IsFile(path))
        {
            throw new ArgumentException("Path did not lead to a file", path);
        }

        int index = -1; 

        //get the index of the path item
        FileInfo info = new FileInfo(path);
        DirectoryInfo directoryInfo = info.Directory;
        for (int i = 0; i < directoryInfo.GetFileSystemInfos().Count(); i++)
        {
            if (directoryInfo.GetFileSystemInfos().ElementAt(i).Name == info.Name) //we've found the item in the folder
            {
                index = i;
                break;
            }
        }

        return GetShellFolder(path).Items().Item(index);
    }

Each call to GetDetailsOf (which is code provided in Shell32) takes an incredible amount of time to process - I used the ANTS profiler to find this, because at first I wasn't able to identify what was slowing down my program so much.

So the question is: How can I optimize the Shell32 methods, and if I can't, is there an alternative?

2

There are 2 answers

3
Tergiver On BEST ANSWER

There are a number of things you are doing wrong in your code. You state that the problem lies outside the code you provided, but perhaps we can get somewhere by fixing what is broken.

public static Folder GetShellFolder(string path)
{
    //extract the folder subpath from the file path
    //i.e extract "C:\Example" from "C:\Example\example.avi"

    string directoryPath = new FileInfo(path).Directory.ToString();

    return new Shell().NameSpace(directoryPath);
}

You are going out and accessing the file system just to get the directory portion of a file path (new FileInfo(path).Directory). You can do this without hitting the disk drive with System.IO.Path.GetDirectoryName(path).

You are creating a new shell object every time you start processing a new item. I would create one, process all the items, then release it. So let's change GetShellFolder like this:

public static Folder GetShellFolder(Shell shell, string path)
{
    //extract the folder subpath from the file path
    //i.e extract "C:\Example" from "C:\Example\example.avi"
    string directoryPath = System.IO.Path.GetDirectoryName(path);

    return shell.NameSpace(directoryPath);
}

And pass a Shell object to your Initialize method. Next up GetShellFolderItem. Here is your code again:

public static FolderItem GetShellFolderItem(string path)
{
    if (!FileProcessing.IsFile(path))
    {
        throw new ArgumentException("Path did not lead to a file", path);
    }

    int index = -1; 

    //get the index of the path item
    FileInfo info = new FileInfo(path);
    DirectoryInfo directoryInfo = info.Directory;
    for (int i = 0; i < directoryInfo.GetFileSystemInfos().Count(); i++)
    {
        if (directoryInfo.GetFileSystemInfos().ElementAt(i).Name == info.Name) //we've found the item in the folder
        {
            index = i;
            break;
        }
    }

    return GetShellFolder(path).Items().Item(index);
}

The first mistake is using "does file exist" prior to accessing that file. Don't do this. Just access the file, if it doesn't exist, a FileNotFoundException will occur. All you're doing is adding extra work that will already get done. Whether you do it or not, it's still possible that it passes the "file exist test", but fails to be accessed.

Next you are parsing the directory to get the index of the file in the folder. This is a serious race condition. It's entirely possible for you to get the wrong index value here. It's also not necessary since Folder exposes a method to get the FolderItem by name: ParseName.

Finally, you are creating yet another Folder (by calling GetShellFolder) which also creates yet another Shell item. You already have a Folder, use it.

So we can alter GetShellFolderItem by removing it entirely:

FolderItem media = mediaFolder.ParseName(System.IO.Path.GetFileName(path));

And we can get rid of GetShellFolder just as neatly:

private void Initialize(Shell shell)
{
    Folder mediaFolder = null;
    FolderItem media = null;
    try
    {
        mediaFolder = shell.NameSpace(Path.GetDirectoryName(path));
        media = mediaFolder.ParseName(Path.GetFileName(path));

        ...
    }
    finally
    {
        if (media != null)
            Marshal.ReleaseComObject(media);
        if (mediaFolder != null)
            Marshal.ReleaseComObject(mediaFolder);
    }
}

Let's see how much of a difference all of that makes.

You are also calling GetDetailsOf for things you already know or can get from the media object:

    //initialize folder name value
    this.folderName = mediaFolder.GetDetailsOf(media, 175);

    //initialize folder path value
    this.folderPath = mediaFolder.GetDetailsOf(media, 176);

    //initialize size value
    this.size = mediaFolder.GetDetailsOf(media, 1);

Change these to:

    //initialize folder path value
    this.folderPath = Path.GetDirectoryName(path);

    //initialize folder name value
    this.folderName = Path.GetFileName(folderPath);

    //initialize size value
    this.size = media.Size;
0
RajeshKdev On

Check this Link. You will get more Clearance about GetDetailsOf() and its File Properties based on the Win-OS version wise.

List<string> arrHeaders = new List<string>();

 Shell shell = new ShellClass();
 Folder rFolder = shell.NameSpace(_rootPath);
 FolderItem rFiles = rFolder.ParseName(filename);

 for (int i = 0; i < short.MaxValue; i++)
 {
      string value = rFolder.GetDetailsOf(rFiles, i).Trim();
      arrHeaders.Add(value);
 }

Hope this may helpful to some one..