2025-11-17 Showing size of Org mode subtrees

I’ve been using Org mode for way over 10 years now, and some of my Org files grew to a considerable size. My largest Org files are over 3 megabytes, which means that working with them tends to be a bit slow. I’d like to be able to check which parts of the Org file are the largest and which are smaller. With files on the disk, the classical tool to achieve this is du, along with its interactive counterparts like ncdu or the insanely fast gdu.

I hoped for the existence of some kind of org-du which would do a similar thing with Org files. In fact, a tool with that name (written by the famous Karl Voit!) does exist, but it has three disadvantages which pretty much disqualify it for me: it counts lines instead of bytes, it is not working inside Emacs, and it is written in Python.

Let’s write an org-du command which would work a bit like org-clock-display, that is, it would show the size of each subtree next to it using overlays.

Now, there are two problems to solve here. One is to analyze the code of org-clock-display to learn how it performs creating overlays. The other is to perform the actual computations.

The first one seemed a bit tricky at first, but it boiled down to looking at org-clock-put-overlay, copying its code and changing it a bit. I decided to reuse the org-clock-overlays variable in spite of its name not really matching what my code will do. The reason is that org-ctrl-c-ctrl-c at one point checks if org-clock-overlays is bound and non-nil, and if yes, it runs org-clock-remove-overlays. Probably a slightly cleaner way would be to introduce another variable to collect my “size overlays”, define a suitable function to remove them and add it to org-ctrl-c-ctrl-c-hook. This, however, would result in having to write more code.

The second one was actually a bit tricky. I could do it in a naive way – start at the beginning of a headline, remember the position, go to the next headline on the same level, calculate the difference between point and the remembered position, show it in the overlay, and move to the next headline (possibly on a lower level). I decided to be a bit more fancy and move one heading at a time (irrespecitve of their levels), but use recursion to calculate the sizes of the subtrees. The idea was to write a function (I called it org-du--calculate-subtree-size-and-move) which would move to the next headline on the same level and put an overlay with the computed size on the one it started on, but instead of using org-forward-heading-same-level, it uses itself in a loop which ends when the level of the subheading is the same as the one we started at. This means that while moving, it calculates (and shows using overlays) the sizes of each subtrees as it passes over them.

And that’s pretty much it. The only remaining part is to copy the trick from org-clock-display to make sure that the overlays will be removed when the user modifes the buffer (for example, starts typing) – this means adding org-clock-remove-overlays to the before-change-functions hook. Of course, C-c C-c also removes them.

The code that does all this is actually quite short – just a bit over 50 lines, and about a third is the org-du--put-overlay function which is almost identical to org-clock-put-overlay. Here it is, in all its recursive glory;-):

(defun org-du ()
  "Compute (recursively) the size of the subtree at point.
Assume point is at the very beginning of the subtree.  Move point to the
next subtree at the same level.  Recursively create overlays with the
computed size."
  (interactive)
  (save-excursion
    (when org-remove-highlights-with-change
      (add-hook 'before-change-functions 'org-clock-remove-overlays
                nil 'local))
    (goto-char (point-min))
    (unless (org-at-heading-p)
      (outline-next-heading))
    (while (not (eobp))
      (org-du--calculate-subtree-size-and-move))))

(defun org-du--calculate-subtree-size-and-move ()
  "A helper function for `org-du'.
Perform the actual computations for this subtree and each of its
subtrees."
  (let ((level (org-current-level))
        (beg (point)))
    (outline-next-heading)
    (while (progn
             (when (> (org-current-level) level)
               (org-du--calculate-subtree-size-and-move))))
    (org-du--put-overlay beg (- (point) beg))))

;; Copied from `org-clock-put-overlay' and slightly changed
(defun org-du--put-overlay (pos size)
  "Put an overlay on the headline at point, displaying SIZE.
Create a new overlay and store it in `org-clock-overlays', so
that it will be easy to remove.  This function assumes point is
on a headline."
  (save-excursion
    (goto-char pos)
    (org-match-line org-complex-heading-regexp)
    ;; the following `or's are a workaround for a bug when the actual
    ;; headline is empty
    (goto-char (or (match-beginning 4) (match-beginning 2)))
    (let* ((headline (or (match-string 4) ""))
           (text (concat headline
                         (org-add-props
                             (make-string
                              (max (- (- 60 (current-column))
                                      (org-string-width headline)
                                      (length (org-get-at-bol 'line-prefix)))
                                   0)
                              ?\·)
                             '(face shadow))
                         (org-add-props
                             (format " %9s " (org-du--format-size size))
                             '(face org-clock-overlay))))
           (o (make-overlay (point) (line-end-position))))
      (org-overlay-display o text)
      (push o org-clock-overlays))))

(defun org-du--format-size (size)
  "Format SIZE in a human-friendly way."
  (cond ((> size 1048576)
         (format "%.1fM" (/ size 1048576.0)))
        ((> size 1024)
         (format "%.1fk" (/ size 1024.0)))
        (t size)))

And this is it for today. I hope you’ll find this useful!

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryOrgMode