Monthly Archives: July 2013

No more “unknown” icons

UnknownIconsIn recent versions of Dolphin, the view sometimes looked like this just after entering a directory.

Some of the files and sub-directories have “unknown” icons, which are replaced by the correct icons later.

This will not happen any more in Dolphin 4.11.

Why did we show you “unknown” icons at all in the first place?

Dolphin can show you a lot of information about your files. Some of this information can be obtained quite easily, like, e.g., the name, the file size, and the modification time.

However, some other properties of a file or directory might be more expensive to determine. Examples are:

  • The type of a file: if it cannot be determined unambiguously from the file name, we have no choice but to read the contents of the file.
  • The icon that has to be shown for a file depends on the type. A directory icon, on the other hand, can be chosen by the user in the “Properties” dialog. The chosen custom icon is then stored in a file named “.directory” inside the directory, so determining the icon for a directory always requires a disk access (because we don’t know in advance if there is a .directory file or not).
  • If previews are enabled, the preview for each item has to be retrieved from the .thumbnails directory in your home folder. If no cached preview can be found there, it gets worse, and the (possibly large) file must be read from disk to generate a new preview image.
  • Counting the number of files inside a directory also requires a disk access. It is shown, e.g., in the “Size” column in Details View.
  • Meta data provided by Nepomuk.

To make things more complicated, some of these properties can have “preliminary” and “final” values. For example, the preliminary icon for any directory is the generic “folder” icon, but the final icon is only known after checking the contents of the corresponding .directory file.

We obviously cannot determine the final values of all these properties synchronously, i.e., blocking the application’s main thread, for all files and directories. In large directories, it might take very long until this process is finished. Therefore, we have to find some compromise between “show the items in the view as quickly as possible” and “only show any kind of information if we know what its final state is”.

What happened in earlier Dolphin versions

We have a class called KFileItemModelRolesUpdater (.h, .cpp) which is responsible for determining the data that are possibly expensive to retrieve.

When you enter a directory, it tries to determine the final icons for the visible items for up to 200 milliseconds. It then then returns to the event loop, such that the view can be painted, mouse events can be processed, etc. The remaining icons and other data are determined asynchronously – first for the visible items in ascending order, then for all invisible ones in random order. We still do it in the main thread, but only for one item at a time, such that it never blocks for too long until the next events can be processed. Moreover, the asynchronous processing of the remaining items is suspended if the view is scrolled. This ensures that scrolling feels smooth.

This approach has two downsides:

  1. Some “unknown” icons can remain if 200 milliseconds are not sufficient to determine the final icons for all visible items. This can happen easily if there are many directories in the view, whose final icons can only be determined by accessing the contents of the directory.
  2. It wastes resources when viewing large directories. Loading previews for thousands of images (I know from user feedback at bugs.kde.org and forum.kde.org that it is not uncommon to have hundreds of thousands of images in one directory) can easily make the hard drive and the CPU busy for a few minutes, even though it is very unlikely that the user will ever scroll through all these images in the view. Moreover, the previews need a lot of memory.

Improvements in Dolphin 4.11

KFileItemModelRolesUpdater still only tries to load the final icons for 200 milliseconds. However, just before an item is shown on the screen, Dolphin will check if its final icon has been determined already. If that is not the case, it will use the “preliminary” icon, which can be determined very fast. In many cases, the preliminary icons are actually equal to the final ones. Notable exceptions are directories with custom icons, and most of the files in /usr/bin.

Some changes in kdelibs were required to make this possible, see this discussion on the mailing list for details about this, and how “Poor Man’s Profiling” helped to solve the problem. Many thanks to David Faure for his help!

Thanks to this modification, Dolphin will never show you “unknown” icons again for items which do have a valid icon.

Moreover, Dolphin will not try to load icons/previews/etc. for all items in the directory asynchronously any more. It will just do that for the items which are currently visible, and those which can be reached quickly by scrolling up or down a few pages or to the top or bottom of the view (up to 500 at a time).

This greatly reduces the amount of memory and CPU cycles that we use to load and show a directory.

Some more information about the code changes that were required for this can be found in the corresponding review requests: 1, 2, 3, 4, 5, 6.

Some thoughts on algorithmic complexity, part 2: Be careful if you insert into or remove from a std::vector, QVector, or QList in a loop

This is the second part of a series of blog posts. Like in part 1, most or all of what I write here will be obvious to many readers. However, I have seen problems which are equivalent to those that I show here more than once in widely-used code, so I think it makes sense to make people aware of these issues and possible solutions.

If you are familiar with containers like std::vector already, feel free to skip the introduction.

Introduction

Different kinds of data structures, also called containers, are a core part of most applications. Depending on the programming language, various containers are provided by the language itself, by libraries which are shipped with the compiler (like the STL in the case of C++), or by libraries like Qt. Containers like vectors, linked lists, sets, hashes, etc., have quite different properties. Each container has advantages in some situations and disadvantages in others. If you want to know more about these, you might want to have a look at the relevant page in the Qt docs or the “Containers” page at cppreference.com for a general overview, or a Qt quarterly article and an excellent article by Marc Mutz for some very interesting details.

QVector, QList, and std::vector

In this post, I will focus on containers which store their elements in one contiguous block of memory. These have the following useful properties:

  • The items have a well-defined order (this is not the case, e.g., in a QSet).
  • Each item has an integer index: the item stored at the beginning of the block has the index 0, the next one has the index 1, and so on. Moreover, accessing an item with a certain index is very fast, no matter how many items there are. It takes O(1), or constant, time, in contrast to linked lists, which also have a well-defined order, but require O(N) time to access a particular element.
  • Appending items at the end also needs constant time. The containers reserve some free space at the end to make sure that appending many items one by one will not require an expensive memory reallocation every time. Removing the last item also needs constant time.

However, inserting or removing items at a random position takes O(N) time because all items after the inserted/removed item (on average, that is N/2 items) have to be moved.

Special case: QList

One important difference between QList and the other two containers is that it does not store the items themselves in the contiguous block of memory if an item requires more size than a pointer. It rather stores pointers to the actual items in the contiguous block then. Moreover, it never uses less than the size of a pointer (4 or 8 bytes on a 32-bit or 64-bit platform, respectively) for an item. These properties can be beneficial in some situations (for example, it enables QLists with different data types to share much of their code in the binary, and it makes moving large items inside the list faster), but storing many bools in a QList might waste a lot of memory.

Another difference is that QList can even add items at the beginning or remove the first item in O(1) time because it also reserves some memory at the beginning. When inserting/removing at a random position, it checks if there are less items to the left or to the right and moves the smaller part of the memory block. Therefore, only N/4 items have to be moved on average, but that is still O(N).

Removing many items can be very slow if implemented incorrectly

Let’s assume that we have stored some items of a type T in a QVector, and we have to remove all items for which a function filter(T) returns true.

Simple example: remove all odd numbers from a QVector with many numbers.

remove

The bad solution

A possible solution would be to go through all numbers and remove any odd number that we find:

template<class Container, class Filter>
void remove1(Container& c, Filter filter)
{
    auto it = c.begin();

    while (it != c.end()) {
        if (filter(*it)) {
            it = c.erase(it);
        } else {
            ++it;
        }
    }
}

To test the efficiency of the function, I wrote a small benchmark that creates a sorted QVector containing the integers between 1 and N, and then removes either the first half or the second half of these numbers, or all odd numbers. The results are shown in this plot (the y-axis shows the time in milliseconds):

plot1

It turns out that the function is quite inefficient and has O(N^2) complexity in all test cases. The reason should be clear: every time an item is removed, all items after that one are moved one position to the front. In other words, O(N) items are moved O(N) times, so we get O(N^2) behaviour. What actually happens is shown here:

removeBad

Note that we could fix the “remove many items from the end” case by going through the items in reverse order (remember that removing the last item is fast). However, this would not help much in the other cases.

Good solution 1

Rather than moving all following items one position to the front when an item is removed, we could just replace it with the last item. This guarantees that removing a single item will always cause at most one other item to be moved:

template<class Container, class Filter>
void remove2(Container& c, Filter filter)
{
    auto it = c.begin();
    auto end = c.end();

    while (it != end) {
        if (filter(*it)) {
            --end;
            *it = std::move(*end);
            c.erase(end);
        } else {
            ++it;
        }
    }
}

The new function has linear complexity for all types of test cases:

plot2

The obvious disadvantage is that the relative order of the items is not preserved. However, if the order does not matter, then this solution might be worth considering, in particular if only a few items have to be removed (note that removing a single item is always O(1) with this solution).

Good solution 2

We can achieve linear complexity and still preserve the sort order. The solution is to make sure that every item is moved at most once: we move an item to the front only when its final position is known.

removeGood

Implementing this idea is quite straightforward, but it’s not even necessary, because this is just what std::remove_if does.

template<class Container, class Filter>
void remove3(Container& c, Filter filter)
{
    const auto oldEnd = c.end();
    const auto newEnd = std::remove_if(c.begin(), oldEnd, filter);
    c.erase(newEnd, oldEnd);
}

In our test cases, this is faster than the “replace by the last item” solution by a factor of 2:

plot3

Final remarks

  • If you insert many items into an existing std::vector, QVector, or QList, similar considerations apply.
  • Such problems really do exist in the software you use, even though they might not cause any serious trouble as long as you don’t let applications handle a large data set that triggers the worst case behaviour. I’ve fixed a few such problems in Dolphin recently, and I will tell you about more problems of this kind in the next post.
  • Thanks for all the interesting feedback that I got after my last post! In particular, I would like to mention Emilio Coppa from the University of Rome here. He works on aprof, a Valgrind-based tool that can help to find inefficient functions like those that I presented here. Just run your program inside this tool and make sure that all interesting functions are fed with various amounts of input data. I ran my benchmark in aprof (I just made it consider a larger set of different vector sizes to make the output look nicer and decreased the overall size of the vectors to reduce the runtime on Valgrind’s simulated CPU). Here is the visualization of the output for the function remove1:
    aprof-remove1
    And the same for remove2:
    aprof-remove2
    Note that you don’t have to write a benchmark at all to get such results with aprof. You can just run your application normally, make sure that it handles inputs with different sizes, and then you can spot the inefficient functions quite quickly (provided that you trigger the inefficient behavior of these functions). If you want to know how the tool works, have a look at this paper. It found a bug that was basically just the strlen issue that I discussed in my last post in the word frequency counter “wf”.
  • Just to prevent misunderstandings: I’m not saying that algorithms with O(N^2) worst-case complexity are always evil. If it is guaranteed that N will never get really large or that the input data will never trigger a bad worst-case behaviour, then there may be reasons why such an algorithm might be preferable.

Dolphin bugs fixed in June 2013

The last bug fix update for Dolphin 2.2 has been shipped recently. Here is a summary of the changes that you can enjoy if you upgrade:

  • Bug 320823: Rename the right item when items are added to the view while renaming. See git commit 4de9a233, review request 110909.
  • Bug 319912: Do not rename files unexpectedly when changing the URL while renaming. See git commit 5948dc0f, review request 110908.
  • Bug 310705, bug 312014: Ensure that switching to “Sort by Type” takes effect without reloading the view. See git commit 735c046d, review request 111004.
  • Bug 320897: Prevent that the rubber band selection rectangle gets a width or height of zero pixels. See git commit 22d93e8f, review request 111144.
  • Bug 321234: Allow renaming multiple files without providing one or more ‘#’ as a placeholder for a number if all extensions are different. This was an “accidental” feature that we were not aware of at all until a user reported that it had been broken by a recent bug fix. See git commit 6f2e1403, review request 111079.
  • Bug 316209: Prevent possible infinite recursion in the ViewProperties constructor, which caused crashes on MacOS. See git commit 04229f8f, review request 111182.
  • Bug 307336: When dragging a file from a search result to KMail, use the correct URL. See git commit c847aacf, review request 111209.
  • Bug 321198: Make searching recent files work correctly. See git commit 25a2255e.

Moreover, Dawit Alemayehu, who has fixed an amazing number of kdelibs bugs that affect file management, made two more commits last month which:

These improvements were contributed and/or tested by Dawit Alemayehu, Emmanuel Pescosta, Kurt Hindenburg, and Vishesh Handa. Thanks for your help! If I forgot anyone, please let me know.

Unfortunately, I’m unable to attend Akademy, but I hope that everyone who is in Bilbao has a good time there!