Skip to content

Latest commit

 

History

History
3276 lines (2773 loc) · 177 KB

notes.org

File metadata and controls

3276 lines (2773 loc) · 177 KB

Tasks

[#A] Add :auto keyword to (planning) predicate

It should act like (or (deadline auto) (scheduled :to today)).

[#A] Document sorters

Note that the built-in sorting only works on Org elements, which is the default :action. So if a different action is used, sorting will not work. In that case, the action should be mapped across the Org element results from outside the org-ql form.

[#A] Org block to insert results of queries with links to entries

[2020-01-16 Thu 06:20] This idea just came to me when I was thinking about using the search-based paradigm vs. outline-based. This would allow both, e.g. some kind of #+BEGIN_QUERY block that would update when C-c C-c is pressed on it.

[#A] Outline path in buffers-files arg

e.g.

(org-ql (olp "~/org/inbox.org" "Emacs" "Ideas")
  (todo "NEXT"))

Also, should support an id one.

[#A] Tools for saving queries and accessing them [2/4]

  • Added example to examples.org.

Bookmarks

Org link types

This would be useful for having a menu of saved queries as Org links, or even bookmarking saved queries.

For all parameters

For saved queries

Access saved query from saved query list

Save query from ql-agenda buffer

UNDERWAY [#A] Outline path predicate

[2019-10-07 Mon 11:15] There are two potential types of matching on outline paths: matching on any part of the outline path, and matching a specific path. For example, with this file:

* Food

** Fruits

*** Blueberries

*** Grapes

** Vegetables

*** Carrots

*** Potatoes

Matching could work like this:

(outline "Food")
Would return all nodes.
(outline "Fruits")
Would return all fruits.

Matching at a specific path would be something like:

(outline-path "Food" "Fruits")
Would return all fruits. But if there were another Fruits heading somewhere in the file, under a different outline path, it would not return its nodes.

I’m not sure the second type of matching belongs in predicates, but rather in this.

To implement this with good performance probably needs an outline-path cache. I can probably repurpose the tags caching, but maybe it should be generalized.

[2019-10-07 Mon 13:09] This is basically done with be2bf6df316b96b3ed56851b8ffe0e227796b621 and be2bf6df316b96b3ed56851b8ffe0e227796b621, but not the specific-path matching. I left a MAYBE in the code about “anchored” path matching, which would accomplish that.

[#A] Helm command

In branch wip/helm-org-ql. Works really well, should add it and demonstrate it.

Add

Demonstrate

Parsing non-Lisp queries

[2019-09-12 Thu 12:56] Lisp is so much easier to deal with, but some people don’t like parentheses. So I’m trying to add a non-Lisp-style query syntax. It gets complicated. The peg library helps, but its documentation is sparse and incomplete. This seems to work fairly well for single-token queries, but I’m not sure if I can or should cram it all into one parser, or use separate ones for certain keywords.

(-let* ((input "todo:check|someday")
        (input "tags:universe+space")
        (input "heading:\"spaced phrase\"")
        (input "")
        (input "heading:\"spaced phrase\"+another")
        combinator
        (parsed (peg-parse-string ((predicate (substring keyword) ":" (opt args))
                                   (keyword (or "heading" "tags" "todo" "property"))
                                   (args (+ (and (or quoted-arg unquoted-arg) (opt separator))))
                                   (quoted-arg "\"" unquoted-arg "\"")
                                   (unquoted-arg (substring (+ (not (or separator "\"")) (any))))
                                   (separator (or (and "|" (action (setf combinator 'or)))
                                                  (and "+" (action (setf combinator 'and)))
                                                  (and ":" (action (setf combinator 'arg))))))
                                  input 'noerror))
        ((predicate . args) (nreverse parsed)))
  (when predicate
    (list :predicate predicate :args args :combinator combinator)))
;;=> (:predicate "heading" :args ("spaced phrase" "another" t) :combinator and)

I don’t know where the t is coming from.

The next step is to make it work with multi-token queries. It needs to handle all of the tokens in one parser so it can handle quoted phrases (if we split on spaces, it would split quoted phrases). But that makes getting the arguments out of it more difficult. Probably need to do something like this:

(-let* ((input "todo:check|someday")
        (input "tags:universe+space")
        (input "heading:\"spaced phrase\"")
        (input "")
        (input "heading:\"spaced phrase\"+another")
        combinator
        (parsed (peg-parse-string ((query (+ (or (and predicate `(pred args -- (list :predicate pred :args args)))
                                                 (and plain-string `(s -- (list :predicate 'regexp :args s))))
                                             (opt (syntax-class whitespace))))
                                   (plain-string (substring (+ (not (syntax-class whitespace)) (any))))
                                   (predicate (substring keyword) ":" (opt args))
                                   (keyword (or "heading" "tags" "todo" "property"))
                                   (args (+ (and (or quoted-arg unquoted-arg) (opt separator))))
                                   (quoted-arg "\"" unquoted-arg "\"")
                                   (unquoted-arg (substring (+ (not (or separator "\"")) (any))))
                                   (separator (or (and "|" (action (setf combinator 'or)))
                                                  (and "+" (action (setf combinator 'and)))
                                                  (and ":" (action (setf combinator 'arg))))))
                                  input 'noerror)))
  parsed)

In which lists are pushed onto the stack and returned, rather than strings. But I don’t understand yet exactly how to use the var forms to consume input from the “value stack”; I need to study the examples more. I’m also not sure if that will even work with a variable number of arguments.

This seems to work, but we’ll have to parse the args again in a separate step:

(-let* ((input "todo:check|someday")
        (input "tags:universe+space")
        (input "heading:\"spaced phrase\"")
        (input "")
        (input "heading:\"spaced phrase\"+another")
        (input "heading:\"spaced phrase\"+another todo:check")
        combinator
        (parsed (peg-parse-string ((query (+ (or (and predicate `(pred args -- (list :predicate pred :args args)))
                                                 (and plain-string `(s -- (list :predicate 'regexp :args s))))
                                             (opt (+ (syntax-class whitespace) (any)))))
                                   (plain-string (substring (+ (not (syntax-class whitespace)) (any))))
                                   (predicate (substring keyword) ":" (opt args))
                                   (keyword (or "heading" "tags" "todo" "property"))
                                   (args (substring (+ (and (or quoted-arg unquoted-arg) (opt separator)))))
                                   (quoted-arg "\"" (+ (not (or separator "\"")) (any)) "\"")
                                   (unquoted-arg (+ (not (or separator "\"" (syntax-class whitespace))) (any)))
                                   (separator (or (and "|" (action (setf combinator 'or)))
                                                  (and "+" (action (setf combinator 'and)))
                                                  (and ":" (action (setf combinator 'arg))))))
                                  input 'noerror)))
  parsed)
  ;;=> (t (:predicate "todo" :args "check") (:predicate "heading" :args "\"spaced phrase\"+another"))

Well, a bit of fiddling (lots of trial-and-error required) produced this:

(-let* ((input "todo:check|someday")
        (input "tags:universe+space")
        (input "heading:\"spaced phrase\"")
        (input "")
        (input "heading:\"spaced phrase\"+another")
        (input "heading:\"spaced phrase\"+another todo:check")
        combinator
        (parsed (peg-parse-string ((query (+ (or (and predicate `(pred args -- (list :predicate pred :args args)))
                                                 (and plain-string `(s -- (list :predicate 'regexp :args s))))
                                             (opt (+ (syntax-class whitespace) (any)))))
                                   (plain-string (substring (+ (not (syntax-class whitespace)) (any))))
                                   (predicate (substring keyword) ":" (opt args))
                                   (keyword (or "heading" "tags" "todo" "property"))
                                   (args (list (+ (and (substring (or quoted-arg unquoted-arg)) (opt separator)))))
                                   (quoted-arg "\"" (+ (not (or separator "\"")) (any)) "\"")
                                   (unquoted-arg (+ (not (or separator "\"" (syntax-class whitespace))) (any)))
                                   (separator (or (and "|" (action (setf combinator 'or)))
                                                  (and "+" (action (setf combinator 'and)))
                                                  (and ":" (action (setf combinator 'arg))))))
                                  input 'noerror)))
  parsed)
  ;;=> (t (:predicate "todo" :args ("check")) (:predicate "heading" :args ("\"spaced phrase\"" "another")))

That seems pretty usable!

[#B] Add more sorters?

  • [ ] category
[ ] Any date
e.g. it would search for timestamps (active/inactive?) anywhere in an entry

[#B] Change (deadline)’s auto argument to :auto and/or :auto t

For consistency, because plain auto looks like a variable, and even though it’s in a quoted form, it could be confusing.

[#B] Default sort

Would probably be useful to have a default sort option.

[#B] Normalize queries

[2019-07-16 Tue 11:49] This serves two purposes:

  1. Equivalent queries will return the same results from the cache.
  2. The selectors that can be converted to the fastest preamble regexps will be sorted first, so the fastest preamble will be used. Although this may not always be straightforward. For example, in a file with only a few TODO items, the (todo "TODO") selector would convert to a preamble that would quickly search through the file. But if there were a thousand TODO items, it wouldn’t be as much of a benefit, and a (regexp "something") selector’s preamble might be much faster, depending on how many times something appears in the file.

So the second purpose might actually be a drawback, because it would prevent users from optimizing their queries with knowledge of their data. Maybe there should be an option to not normalize queries, so advanced users can order their selectors manually.

[#B] Quickly change sorting/grouping in search views

With caching, the search doesn’t need to be repeated, so resorting/regrouping can be very fast.

[#B] Recursive queries

For lack of a better term. A way to query for certain headings, and then gather results of a different query at each result of the first query, displaying all results in a single view.

This works pretty well. It needs polishing, and some refactoring so items can be indented completely (rather than leaving the keyword unindented, as it is now).

(cl-defun org-ql-agenda-recursive (buffers-or-files queries &key action narrow sort)
  (cl-labels ((rec (queries element indent)
                   (org-with-point-at (org-element-property :org-marker element)
                     (when-let* ((results (progn
                                            (org-narrow-to-subtree)
                                            (org-ql-select (current-buffer)
                                              (car queries)
                                              :action 'element-with-markers
                                              :narrow t
                                              :sort sort))))
                       ;; Indent entry for each level
                       (setf results (--map
                                      (org-element-put-property it :raw-value
                                                                (concat (s-repeat (* 5 indent) " ")
                                                                        (org-element-property :raw-value it)))
                                      results))
                       (cons it (if (cdr queries)
                                    (--map (rec (cdr queries) it)
                                           results)
                                  results))))))
    (when-let* ((indent 0)
                (results (org-ql-select buffers-or-files
                           (car queries)
                           :action 'element-with-markers
                           :narrow narrow
                           :sort sort)))
      (->> (if (cdr queries)
               (--map (rec (cdr queries) it (1+ indent))
                      results)
             results)
           (-flatten-n (1- (length queries)))
           -non-nil
           (org-ql-agenda--agenda nil nil
             :entries)))))

(cl-defun org-ql-select-recursive (buffers-or-files queries &key action narrow sort)
  (cl-labels ((rec (queries element indent)
                   (org-with-point-at (org-element-property :org-marker element)
                     (when-let* ((results (progn
                                            (org-narrow-to-subtree)
                                            (org-ql-select (current-buffer)
                                              (car queries)
                                              :action 'element-with-markers
                                              :narrow t
                                              :sort sort))))
                       ;; Indent entry for each level
                       (setf results (--map
                                      (org-element-put-property it :raw-value
                                                                (concat (s-repeat (* 5 indent) " ")
                                                                        (org-element-property :raw-value it)))
                                      results))
                       (cons it (if (cdr queries)
                                    (--map (rec (cdr queries) it)
                                           results)
                                  results))))))
    (when-let* ((indent 0)
                (results (org-ql-select buffers-or-files
                           (car queries)
                           :action 'element-with-markers
                           :narrow narrow
                           :sort sort)))
      (->> (if (cdr queries)
               (--map (rec (cdr queries) it (1+ indent))
                      results)
             results)
           (-flatten-n (1- (length queries)))
           -non-nil))))

[#B] Timeline view

e.g. as mentioned by Samuel Wales at https://lists.gnu.org/archive/html/emacs-orgmode/2019-08/msg00330.html. Prototype code:

(cl-defun org-ql-timeline (buffers-files query)
  (let ((results
         (org-ql-select buffers-files
           query :action
           (lambda ()
             (let* ((heading-string
                     (->> (org-element-headline-parser
                           (line-end-position))
                          org-ql--add-markers
                          org-ql-agenda--format-element))
                    (timestamps
                     (cl-loop with limit = (org-entry-end-position)
                              while (re-search-forward org-ts-regexp-both
                                                       limit t)
                              collect (ts-parse-org (match-string 0))))
                    (timestamp-strings
                     (->> timestamps
                          (-sort #'ts<)
                          (--map (concat " " (ts-format it))))))
               (s-join "\n" (cons heading-string timestamp-strings))))
           :sort '(date))))
    (org-ql-agenda--agenda nil nil :strings results)))

(org-ql-timeline (org-agenda-files)
                 '(and "Emacs" (ts)))

;; More timeline-like version, organized by date rather than task.

(cl-defun org-ql-timeline* (buffers-files query &key filter-ts)
  (let* ((ts-ht (ht))
         (results (org-ql-select buffers-files
                    query
                    :action (lambda ()
                              (let* ((heading-string
                                      (->> (org-element-headline-parser
                                            (line-end-position))
                                           org-ql--add-markers
                                           org-ql-agenda--format-element))
                                     (date-timestamps
                                      ;; Each one set to 00:00:00.
                                      (cl-loop with limit = (org-entry-end-position)
                                               while (re-search-forward org-ts-regexp-both
                                                                        limit t)
                                               collect (->> (match-string 0)
                                                            ts-parse-org
                                                            (ts-apply :hour 0 :minute 0 :second 0)))))
                                (setf date-timestamps (delete-dups date-timestamps))
                                (when filter-ts
                                  (setf date-timestamps (cl-remove-if-not filter-ts date-timestamps)))
                                (--each date-timestamps
                                  (push heading-string (gethash it ts-ht)))))))
         (tss-sorted (-sort #'ts< (ht-keys ts-ht)))
         (strings (cl-loop for ts in tss-sorted
                           collect (concat "\n"
                                           (propertize (ts-format "%Y-%m-%d" ts)
                                                       'face 'org-agenda-structure))
                           append (ht-get ts-ht ts))))
    (org-ql-agenda--agenda nil nil :strings strings)))

Another, more up-to-date implementation:

;; NOTE: ts structs don't (sometimes? or always?) compare properly
;; with default hash tables, e.g. this code:

;; (let* ((ts-a #s(ts nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil 1572670800.0))
;; (ts-b #s(ts nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil 1572584400.0)))
;; (list :equal (equal ts-a ts-b)
;; :sxhash-equal (equal (sxhash ts-a) (sxhash ts-b)))) ;;=> (:equal nil :sxhash-equal t)

;; So we must use the "contents-hash" table as described in the Elisp manual.
(define-hash-table-test 'contents-hash 'equal 'sxhash-equal)

(cl-defun org-ql-view-timeline (buffers-files &key from to on)
  "FIXME: DOcstring"
  (cl-flet ((parse-ts-arg
             (arg type)
             ;; Parse ARG as a string or TS struct and adjust it to the beginning
             ;; or end of its day, depending on whether TYPE is `:begin' or `:end'.
             (-let (((hour minute second) (cl-ecase type
                                            (:begin '(0 0 0))
                                            (:end '(23 59 59)))))
               (->> (cl-typecase arg
                      (string (ts-parse arg))
                      (ts arg))
                    (ts-apply :hour hour :minute minute :second second)))))
    (let* ((ts-predicate `(lambda (ts)
                            ,(cond (on `(ts-in ,(parse-ts-arg on :begin)
                                               ts
                                               ,(parse-ts-arg on :end)))
                                   ((and from to) `(ts-in ,(parse-ts-arg from :begin)
                                                          ts
                                                          ,(parse-ts-arg to :end)))
                                   (from `(ts<= ,(parse-ts-arg from :begin) ts))
                                   (to `(ts<= ts ,(parse-ts-arg to :end)))
                                   (t (user-error "Huh?")))))
           (query (cond (on `(ts :from ,(parse-ts-arg on :begin)
                                 :to ,(parse-ts-arg on :end)))
                        (t (append (list 'ts)
                                   (when from
                                     `(:from ,(parse-ts-arg from :begin)))
                                   (when to
                                     `(:to ,(parse-ts-arg to :end)))))))
           (date-ts-table (make-hash-table :test 'contents-hash))
           (_results (org-ql-select buffers-files query
                       :action (lambda ()
                                 (let* ((string (->> (org-element-headline-parser
                                                      (line-end-position))
                                                     org-ql--add-markers
                                                     org-ql-view--format-element)))
                                   (cl-loop with limit = (org-entry-end-position)
                                            while (re-search-forward org-ts-regexp-both limit t)
                                            for ts = (->> (match-string 0) ts-parse-org)
                                            when (funcall ts-predicate ts)
                                            do (cl-pushnew (cons ts (concat (ts-format " %H:%M" ts)
                                                                            string))
                                                           (gethash (ts-apply :hour 0 :minute 0 :second 0 ts)
                                                                    date-ts-table)
                                                           :test #'equal))))))
           (date-tss-sorted (->> date-ts-table hash-table-keys (-sort #'ts<)))
           (string (cl-loop for date-ts in date-tss-sorted
                            for date-string = (propertize (ts-format "%Y-%m-%d" date-ts)
                                                          'face 'org-agenda-structure)
                            concat (concat "\n" date-string)
                            concat (cl-loop for (ts . entry) in (->> (gethash date-ts date-ts-table)
                                                                     (-sort (-on #'ts< #'car)))
                                            concat (concat "\n" entry)))))
      (org-ql-view--display :buffer "Timeline"
        :header "Timeline"
        :string string))))

;; Used like: 
;; (org-ql-view-timeline "~/org/main.org" :from "2019-11-01")

[#B] Timeline view

e.g. as mentioned by Samuel Wales at https://lists.gnu.org/archive/html/emacs-orgmode/2019-08/msg00330.html. Prototype code:

(cl-defun org-ql-timeline (buffers-files query)
  (let ((results
         (org-ql-select buffers-files
           query :action
           (lambda ()
             (let* ((heading-string
                     (->> (org-element-headline-parser
                           (line-end-position))
                          org-ql--add-markers
                          org-ql-agenda--format-element))
                    (timestamps
                     (cl-loop with limit = (org-entry-end-position)
                              while (re-search-forward org-ts-regexp-both
                                                       limit t)
                              collect (ts-parse-org (match-string 0))))
                    (timestamp-strings
                     (->> timestamps
                          (-sort #'ts<)
                          (--map (concat " " (ts-format it))))))
               (s-join "\n" (cons heading-string timestamp-strings))))
           :sort '(date))))
    (org-ql-agenda--agenda nil nil :strings results)))

(org-ql-timeline (org-agenda-files)
                 '(and "Emacs" (ts)))

;; More timeline-like version, organized by date rather than task.

(cl-defun org-ql-timeline* (buffers-files query &key filter-ts)
  (let* ((ts-ht (ht))
         (results (org-ql-select buffers-files
                    query
                    :action (lambda ()
                              (let* ((heading-string
                                      (->> (org-element-headline-parser
                                            (line-end-position))
                                           org-ql--add-markers
                                           org-ql-agenda--format-element))
                                     (date-timestamps
                                      ;; Each one set to 00:00:00.
                                      (cl-loop with limit = (org-entry-end-position)
                                               while (re-search-forward org-ts-regexp-both
                                                                        limit t)
                                               collect (->> (match-string 0)
                                                            ts-parse-org
                                                            (ts-apply :hour 0 :minute 0 :second 0)))))
                                (setf date-timestamps (delete-dups date-timestamps))
                                (when filter-ts
                                  (setf date-timestamps (cl-remove-if-not filter-ts date-timestamps)))
                                (--each date-timestamps
                                  (push heading-string (gethash it ts-ht)))))))
         (tss-sorted (-sort #'ts< (ht-keys ts-ht)))
         (strings (cl-loop for ts in tss-sorted
                           collect (concat "\n"
                                           (propertize (ts-format "%Y-%m-%d" ts)
                                                       'face 'org-agenda-structure))
                           append (ht-get ts-ht ts))))
    (org-ql-agenda--agenda nil nil :strings strings)))

(org-ql-timeline* (org-agenda-files)
                  '(ts :from -14)
                  :filter-ts `(lambda (ts)
                                (ts<= ,(ts-adjust 'day -14 (ts-now)) ts)))

[2019-09-26 Thu 21:28] Would probably make sense to implement this using the view-sections someday.

[#B] Update view screenshots

e.g. doesn’t currently show the View header.

Dynamic blocks

For example, this blog article shows a way that Org’s existing dynamic columnview blocks can be very useful. org-ql queries could be useful in them as well.

UNDERWAY [#B] “Node” caching

[2019-09-05 Thu 12:30] At each node checked by a predicate, make a struct that stores attributes we can query for, as well as parent node position. This would let us speed up ancestor-based queries, like (ancestor (todo "WAITING")). Ideally it would also serve as the tag hierarchy cache.

It would probably be an all-encompassing system, because predicates would need to refer to the cached node when available. So maybe the struct should be like ts-defstruct, with lazy, caching accessors, which would move some of the predicates’ code into the accessors.

Maybe a good improvement to make later, after the project is more developed.

[2019-10-07 Mon 13:08] This has basically been implemented in be2bf6df316b96b3ed56851b8ffe0e227796b621, but as functions and values rather than with structs. It remains to be seen how this works with ancestor queries, but I suspect it will help a lot.

Struct PoC code

This works okay (except the priority accessor needs to be fixed, because Org priorities are awkward to get). I’m guessing all the extra function calls would make it undesirable in cases of returning many results, but it’s a flexible concept that makes sorting easy.

(ts-defstruct org-ql-node
  file position marker
  (level
   nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
                        (org-outline-level)))
  (heading
   nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
                        ;; TODO: Org 9.2+ adds 2 more args to `org-get-heading'.
                        (org-get-heading t t)))
  (priority
   nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
                        (org-get-priority )))
  (tags
   nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
                        (->> (org-ql--tags-at (point))
                             -flatten
                             (delq 'org-ql-nil))))
  (todo
   nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
                        (org-get-todo-state)))
  (outline-path
   nil :accessor-init (org-with-point-at (org-ql-node-marker struct)
                        (org-split-string (org-format-outline-path (org-get-outline-path)
                                                                   nil nil "")
                                          ""))))

(defcustom helm-org-ql-sort
  '(org-ql-node-priority org-ql-node-todo)
  "FIXME"
  )

(cl-defun helm-org-ql (buffers-files &optional no-and)
  "Display results in BUFFERS-FILES for an `org-ql' query using Helm.
Interactively, search the current buffer.

NOTE: Atoms in the query are turned into strings where
appropriate, which makes it unnecessary to type quotation marks
around words that are intended to be searched for as indepenent
strings.

Also, unless NO-AND is non-nil (interactively, with prefix), all
query tokens are wrapped in an implied (and) form. This is
because a query must be a sexp, so when typing multiple clauses,
either (and) or (or) would be required around them, and (and) is
typically more useful, because it narrows down results.

For example, this raw input:

 Emacs git

Is transformed into this query:

 (and \"Emacs\" \"git\")

However, quoted strings remain quoted, so this input:

 \"something else\" (tags \"funny\")

Is transformed into this query:

 (and \"something else\" (tags \"funny\"))"
  (interactive (list (current-buffer) current-prefix-arg))
  (let ((helm-input-idle-delay helm-org-ql-input-idle-delay))
    (helm :sources
          (helm-build-sync-source "helm-org-ql-agenda-files"
            :candidates (lambda ()
                          (let* ((query (helm-org-ql--input-to-query helm-pattern no-and))
                                 (window-width (window-width (helm-window))))
                            (when query
                              (let ((results (org-ql-select buffers-files
                                               query
                                               :action '(make-org-ql-node :marker (point-marker)))))
                                (when helm-org-ql-sort
                                  (dolist (sorter (reverse helm-org-ql-sort))
                                    (setf results (sort results sorter))))
                                (cl-loop for it in-ref results
                                         do (setf it (concat (buffer-name (org-ql-node-file it)) ":"
                                                             (or (org-ql-node-todo it) "")
                                                             (or (org-ql-node-priority it) "")
                                                             (org-ql-node-heading it) "\\"
                                                             (org-ql-node-outline-path it))))
                                results))))
            :match #'identity
            :fuzzy-match nil
            :multimatch nil
            :volatile t
            :action #'helm-org-goto-marker))))

UNDERWAY [#B] Implement view with tabulated-list-mode or magit-section

[2019-09-02 Mon 05:20] Especially with some of the new packages that make tabulated-list-mode easier to use, like navigel. However, it would probably break grouping, or require some kind of adapter or extension to do grouping, so I don’t know if that would work. Something like magit-section would be more flexible, and could be recursively grouped, like in magit-todos.

[2019-09-08 Sun 10:06] Came up with a prototype yesterday, in branch wip/view-section. Seems to work pretty well.

One of the things in that branch is org-ql-item, which is a struct used to carry data for query results. It seems to work well.

Another idea for it is to simply store the element from org-element-headline-parser in one of its slots, and populate all of the other slots lazily, like ts. It already does that for a couple of slots, but I think it makes sense to do it for all of them, to reduce the overhead of making the struct for every query result.

Experiment with widget

The code that powers the customization UI. Has collapsible and customizable widgets. Might be perfect. Might even enable editing items in the list, with functions to make the changes in the source buffers.

Code idea

Inserting items into a view could look something like this:

(org-ql-view--insert-items
 :header (ts-format "%Y-%m-%d" (ts-now))
 :items (org-ql-query
          :select #'org-ql-current-item
          :from (org-agenda-files)
          :where '(or (deadline auto)
                      (scheduled :on today)
                      (ts-active :on today)))
 :group-by '(org-ql-item-priority
             org-ql-item-todo))

Items would be structs, and the group-by argument would be a list of accessors, like how magit-todos works. Arbitrary functions could also be passed to group-by, as whatever value the function returns is used to group them. org-ql-current-item would be a function that turns the result of org-element-headline-parser into the struct.

Not sure if it should automatically add the number of items to the header, or if that should be done manually.

Prior art

Appears to be another implementation of magit-section-like expandable sections. Not sure which came first. Its code seems like it may be helpful.

magit-section

MAYBE Fancier plain query syntax for tags and properties

e.g. something like Organice has now.

[2020-02-13 Thu 00:42] Something is needed to help search property values by partial matches. For example:

* [[https://github.com/fniessen/org-html-themes][org-html-themes: Framework including two themes, Bigblow and ReadTheOrg]]
  :PROPERTIES:
  :author:   Fabrice Niessen
  :END:

Searching that with a query like property:author=Fabrice returns nothing; the full value must be used, like property:author="Fabrice Niessen"~. It should be possible to do something like ~property:author=~Fabrice to search for partial matches.

MAYBE Overlay-based caching inspired by org-num-mode

[2019-12-30 Mon 22:42] Newer versions of Org have org-num-mode, which uses font-lock and after-change-functions to update overlays in the buffer with outline numbering. Maybe a similar approach could be used to cache arbitrary values for headings in a buffer without having to discard the whole buffer’s cache when the buffer changes.

MAYBE Use Bovine or Wisent to parse non-sexp syntax

They’re both built-in to Emacs, so we could drop the peg dependency.

Byte-compile lambdas

  • State “DONE” from [2018-05-09 Wed 17:30]

elfeed-search--update-list byte-compiles lambdas returned by elfeed-search-compile-filter. Maybe I could do something like this too.

If I can get this working, I should profile it to see what difference it makes.

Profiling

Going to try byte-compiling the predicate function:

(elp-profile 10 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
                 (and (or (date :date '= (org-today))
                          (date :deadline '<= (+ org-deadline-warning-days (org-today)))
                          (date :scheduled '<= (org-today)))
                      (not (apply #'todo org-done-keywords-for-agenda)))))
FunctionTimes calledTotal timeAverage time
org-agenda-ng–agenda100.83705810390.0837058104
org-agenda-finalize-entries100.6528866080.0652886608
org-super-agenda–filter-finalize-entries100.6417945010.0641794501
org-super-agenda–group-items100.6360570060.0636057006
org-super-agenda–group-dispatch1300.6319118490.0048608603
org-super-agenda–group-tag500.5928838690.0118576773
list27200.57927951690.0002129704
mapcar3310.23335919200.0007050126
org-agenda-ng–filter-buffer100.092476260.009247626
org-agenda-ng–format-element1500.06493204790.0004328803
org-entry-get8600.04082853494.747…e-05
org-agenda-ng–date-p9100.03856462494.237…e-05
org-element-headline-parser1500.03744174700.0002496116
org-is-habit-p2700.02901073890.0001074471
org–property-local-values2700.02686159799.948…e-05
org-get-property-block2700.02446133099.059…e-05
org-get-tags-at1500.0178758640.0001191724
org-super-agenda–group-habit100.0159106560.0015910655
mapc25400.01586162906.244…e-06
org-agenda-ng–add-faces1500.01433296709.555…e-05

Now the same thing without byte-compiling:

(elp-profile 10 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
                 (and (or (date :date '= (org-today))
                          (date :deadline '<= (+ org-deadline-warning-days (org-today)))
                          (date :scheduled '<= (org-today)))
                      (not (apply #'todo org-done-keywords-for-agenda)))))
FunctionTimes calledTotal timeAverage time
org-agenda-ng–agenda100.8466455370.0846645537
org-agenda-finalize-entries100.6628968050.0662896805
sort400.5911232560.0147780814
org-entries-lessp4000.59012016200.0014753004
mapcar2010.23182705990.0011533684
org-agenda-ng–filter-buffer100.0925197870.0092519787
org-super-agenda–filter-finalize-entries100.06642780400.0066427804
org-agenda-ng–format-element1500.0646589940.0004310599
org-super-agenda–group-items100.06025040890.0060250408
org-super-agenda–group-dispatch1300.05619044700.0004322342
org-entry-get8600.04374588895.086…e-05
org-agenda-ng–date-p9100.03826234094.204…e-05
org-element-headline-parser1500.03746629200.0002497752
org-is-habit-p2700.03208610790.0001188374
org–property-local-values2700.02986904300.0001106260
org-get-property-block2700.02747166490.0001017469
org-super-agenda–group-habit100.0191179010.0019117901
org-get-tags-at1500.01789589300.0001193059
mapc24700.01503611306.087…e-06
org-agenda-ng–add-faces1500.01430921699.539…e-05

Virtually indistinguishable. Going to try moving the byte-compile call from the org-agenda-ng macro to other places…

(elp-profile 10 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
                 (and (or (date :date '= (org-today))
                          (date :deadline '<= (+ org-deadline-warning-days (org-today)))
                          (date :scheduled '<= (org-today)))
                      (not (apply #'todo org-done-keywords-for-agenda)))))
FunctionTimes calledTotal timeAverage time
org-agenda-ng–agenda100.84763167790.0847631678
mapcar3310.81594522200.0024650913
org-agenda-ng–filter-buffer100.6742179120.0674217912
org-element-headline-parser1500.61711958890.0041141305
line-beginning-position6200.58025796800.0009358999
org-agenda-finalize-entries100.0820651570.0082065157
org-super-agenda–filter-finalize-entries100.07087722790.0070877227
org-super-agenda–group-items100.0655231030.0065523103
org-agenda-ng–format-element1500.06527837400.0004351891
org-super-agenda–group-dispatch1300.06142535890.0004725027
org-entry-get8600.04940230295.744…e-05
org-agenda-ng–date-p9100.03884355194.268…e-05
org-is-habit-p2700.03756875490.0001391435
org–property-local-values2700.03538929290.0001310714
org-get-property-block2700.03297004400.0001221112
org-super-agenda–group-habit100.0244686010.0024468601
re-search-backward15000.01863440891.242…e-05
org-get-tags-at1500.01800388090.0001200258
mapc25400.01565180996.162…e-06
org-agenda-ng–add-faces1500.01441410809.609…e-05

Doesn’t seem to make any difference.

Document/figure out tag inheritance

I think it should probably be enabled in most cases, to avoid missing results that users would expect to find, but it will reduce performance in some cases, so users should be able to turn it off when they don’t need it.

[2018-06-12 Tue 14:32] The docstring for org-map-entries says:

If your function needs to retrieve the tags including inherited tags at the current entry, you can use the value of the variable ‘org-scanner-tags’ which will be much faster than getting the value with ‘org-get-tags-at’. If your function gets properties with ‘org-entry-properties’ at the current entry, bind ‘org-trust-scanner-tags’ to t around the call to ‘org-entry-properties’ to get the same speedup. Note that if your function moves around to retrieve tags and properties at a different entry, you cannot use these techniques.

[2019-09-26 Thu 21:31] Handled with the tag caching recently implemented.

[#B] Dual matching with regexp and predicates

Note: This is underway in the =preamble-re= branch.

Searching and matching could be sped up by constructing a regexp that searches directly to the next possible match, and then matching with predicate functions.

For example, a search like:

(org-ql (org-agenda-files)
  (and (regexp "lisp")
       (scheduled < today)))

Only entries that contain the word lisp can be matches, and searching each entry for that word is wasteful. Instead, we could search the buffer for the next occurrence of lisp, then check the scheduled date for that entry.

This would require processing the predicate to pull out matchers that can be done as buffer-wide regexps, e.g. regexp, heading-regexp, todo, and possibly tags. Org has some regexp-building functions that might make this fairly easy, and then we could probably use rx to make an optimized version of the regexp. It would also require some refactoring to the searching that would go directly to regexp matches when possible, rather than checking every entry with the predicate.

[2019-07-16 Tue 11:14] Made new branch preamble-re-new based on current master. Seems to work well. Here’s some code for testing and comparing performance (bench-multi-lets is from here).

[2019-07-16 Tue 11:56] Going to merge to master as 0.2, so marking this as done, even though there’s a bit more that can be done from here.

Benchmark code

(cl-defmacro org-ql-preamble-bench (&key query (file "tests/data.org") (times 10))
  `(bench-multi-lets :times ,times :ensure-equal t
     :lets (("preamble" ((org-ql-use-preamble t)))
            ("no preamble" ((org-ql-use-preamble nil))))
     :forms ((,(prin1-to-string query) (org-ql-select,file
                                        ',query
                                        :action (lambda () (org-get-heading t t)))))))
(org-ql-preamble-bench :query (regexp "Emacs") :times 100)
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (regexp “Emacs”)1.220.14176700
no preamble: (regexp “Emacs”)slowest0.17239800
(org-ql-preamble-bench :file "~/org/inbox.org" :query (regexp "Emacs") :times 5)
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (regexp “Emacs”)1.592.01104300
no preamble: (regexp “Emacs”)slowest3.20637000
(org-ql-preamble-bench :file "~/org/inbox.org" :query (and (regexp "Emacs") (todo)) :times 5)
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (and (regexp “Emacs”) (todo))1.592.21150300
no preamble: (and (regexp “Emacs”) (todo))slowest3.51274100
(org-ql-preamble-bench :file "~/org/inbox.org" :query (and (regexp "Emacs") (todo) (scheduled)) :times 5)
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (and (regexp “Emacs”) (todo) (scheduled))1.692.04245600
no preamble: (and (regexp “Emacs”) (todo) (scheduled))slowest3.45375600
(org-ql-preamble-bench :file "~/org/inbox.org" :query (todo "WAITING") :times 2)
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (todo “WAITING”)15.600.07068400
no preamble: (todo “WAITING”)slowest1.10272200

Wow, that’s a huge improvement!

Operate on list of heading positions

  • State “DONE” from [2018-05-10 Thu 15:02]

[2017-12-31 Sun 17:54] I wonder if, instead of parsing the whole buffer with org-element-parse-buffer, we could simply work on a list of heading positions, e.g. a loop would search forward to the next heading position, then call whatever predicates it needed at the heading’s position, using save-excursion around each function call. The predicates would need to be updated to get their data from the buffer, instead of using org-element-property, but that wouldn’t be hard.

[2018-05-10 Thu 15:01] I already changed to using buffer-parsing predicates instead of org-element-parse-buffer.

Use macros for date

  • State “DONE” from [2018-05-10 Thu 14:59]

If I made the date selector a macro, I could avoid the need to quote the comparator.

Also, maybe instead of having a single date selector, I should have scheduled, deadline, etc.

[#C] org-agenda-skip-function

As discussed here, this is a cool feature that allows further integration into existing custom agenda commands. Example:

;;; lima-0ac22.el --- -*- lexical-binding: t; -*-

(defun org-ql-skip-function (query)
  "Return a function for `org-agenda-skip-function' for QUERY.
Compared to using QUERY in `org-ql', this effectively turns QUERY
into (not QUERY)."
  (let* ((predicate (org-ql--query-predicate '(regexp "ryo-modal"))))
    (lambda ()
      ;; This duplicates the functionality of `org-ql--select'.
      (let (orig-fns)
        (--each org-ql-predicates
          ;; Save original function mappings.
          (let ((name (plist-get it :name)))
            (push (list :name name :fn (symbol-function name)) orig-fns)))
        (unwind-protect
            (progn
              (--each org-ql-predicates
                ;; Set predicate functions.
                (fset (plist-get it :name) (plist-get it :fn)))
              ;; Run query.
              ;; FIXME: "If this function returns nil, the current match should not be skipped.
              ;; Otherwise, the function must return a position from where the search
              ;; should be continued."
              (funcall predicate))
          (--each orig-fns
            ;; Restore original function mappings.
            (fset (plist-get it :name) (plist-get it :fn))))))))

(let ((org-agenda-custom-commands
       '(("z" "Z"
          ((tags-todo "PRIORITY=\"A\"+Emacs/!SOMEDAY"))
          ((org-agenda-skip-function (org-ql-skip-function '(regexp "ryo-modal")))))
         ((org-agenda-files ("~/org/inbox.org"))))))
  (org-agenda nil "z"))

I should benchmark it to see how much difference it makes, because all those fset calls on each heading isn’t free. But if a macro were used to rewrite the built-in predicates to their full versions, all of that could be avoided…

[#C] Test caching

See notes on 1dce9467f25428b5289d3665cd840820969ed65a. It would be good to test the caching explicitly, at least for some queries, because if I were to completely break it again, in such a way that results were stored but retrieval always failed, the tests wouldn’t catch it.

[#C] Test caching

See notes on 1dce9467f25428b5289d3665cd840820969ed65a. It would be good to test the caching explicitly, at least for some queries, because if I were to completely break it again, in such a way that results were stored but retrieval always failed, the tests wouldn’t catch it.

[#C] Update commentary

MAYBE [#C] Fancier searching for inherited tags

When tag inheritance is enabled, and the given tags aren’t file-level tags, we could search directly to headings containing the matching tags, and then only do per-heading matching on the subtrees. Sometimes that would be much faster. However, that might make the logic special-cased and complicated. Might need a redesign of the whole matching/predicate system to do cleanly.

References

John Kitchin on rewriting the Org agenda code

[2019-10-28 Mon 08:28] Originally from this entry in my notes.

From: John Kitchin <jkitchin@andrew.cmu.edu> Subject: Re: [Orgmode] Slow speed of week and month views Newsgroups: gmane.emacs.orgmode To: Karl Voit <news1142@karl-voit.at> Cc: “emacs-orgmode@gnu.org” <emacs-orgmode@gnu.org> Date: Sat, 5 Aug 2017 18:17:09 -0400 (4 hours, 11 minutes, 38 seconds ago)

I can think of two possibilities for a future approach (besides a deep dive on profiling the current elisp to improve the speed there). They both involve some substantial coding though, and would probably add dependencies. I am curious what anyone things about these, or if there are other ideas.

One is to use the new dynamic module capability to write an org parser in C, or a dedicated agenda function, which would presumably be faster than in elisp. This seems hard, and for me would certainly be a multiyear project I am sure! The downside of this is the need to compile the module. I don’t know how easy it would be to make this work across platforms with the relatively easy install org-mode currently has. This could have a side benefit though of a c-lib that could be used by others to expand where org-mode is used.

The other way that might work is to rely more heavily on a cached version of the files, perhaps in a different format than elisp, that is faster to work with. The approach I have explored in this is to index org files into a sqlite database. The idea then would be to generate the agenda from a sql query. I use something like this already to “find stuff in orgmode anywhere”. One of the reasons I wrote this is the org-agenda list of files isn’t practical for me because my files are so scattered on my file system. I had a need to be able to find TODOs in research projects in a pretty wide range of locations.

The code I use is at https://github.com/jkitchin/scimax/blob/master/org-db.el, and from one database I can find headlines, contacts, locations, TODO headlines across my file system, all the files that contain a particular link, and my own recent org files. This approach relies on emacsql, and a set of hook functions to update the database whenever a file is changed. It is not robust, e.g. the file could be out of sync with the db if it is modified outside emacs, but this works well enough for me so far. Updated files get reindexed whenever emacs is idle. It was a compromise on walking the file system all the time or daily, or trying to use inotify and you can always run a command to prune/sync all the files any time you want.

sqlite is ok, but with emacsql you cannot put strings in it directly (at least when I wrote the org-db code), which has limited it for full-text search so far. Also with text, the db got up to about 0.5 GB in size, and started slowing down. So it doesn’t have text in it for now. It has all the other limitations of sqlite too, limited support for locking, single process….

I am moderately motivated to switch from sqlite to MongoDB, but the support for Mongo in emacs is pretty crummy (I tried writing a few traditional interfaces, but the performance was not that good, and limited since Mongo uses bson, and it is just not the same as json!). Why Mongo? Mostly because the Mongo query language is basically json and easy to generate in Emacs, unlike sql. Also, it is flexible and easy to adapt to new things, e.g. indexing src-blocks or tables or whatever org-element you want. (And I want to use Mongo for something else too ;). Obviously these all add dependencies, and might not be suitable for the core org-mode distribution. But I do think it is important to think about ways to scale org-mode while maintaining compatibility with the core.

The main point of the database was to get a query language, persistence and good performance. I have also used caches to speed up using bibtex files, and my org-contacts with reasonable performance. These have been all elisp, with no additional dependencies. Maybe one could do something similar to keep an agenda cache that is persistent and updated via hook functions.

Thoughts?

John

:archive.is: http://archive.is/33R9M

Nicolas Goaziou’s org-element cache implementation

[2020-01-04 Sat 08:31] https://code.orgmode.org/bzg/org-mode/src/master/lisp/org-element.el#L4817 I guess I haven’t been keeping up, because I’m not sure when exactly he made or committed this, or what his plans for it are. It’s currently disabled by default.

[2020-01-04 Sat 09:03]

Examples / testing

(org-agenda-ng org-agenda-files
  (and (or (date :deadline '<= (org-today))
           (date :scheduled '<= (org-today)))
       (not (apply #'todo org-done-keywords-for-agenda)))
  ((group (tags "bills"))
   (group (todo "SOMEDAY"))))

(org-agenda-ng org-agenda-files
  (and (or (date :deadline '<= (org-today))
           (date :scheduled '<= (org-today)))
       (not (apply #'todo org-done-keywords-for-agenda))))

(org-agenda-ng "~/org/main.org"
  (and (or (date :deadline '<= (org-today))
           (date :scheduled '<= (org-today)))
       (not (apply #'todo org-done-keywords-for-agenda))))

(org-ql org-agenda-files
  (and (todo "SOMEDAY")
       (tags "Emacs")))
(org-ql org-agenda-files
  (and (todo "SOMEDAY")
       (tags "Emacs")
       (priority >= "B")))
(org-ql "~/org/main.org"
  (and (or (tags "Emacs")
           (priority >= "B"))
       (not (done))))
(org-ql "~/org/main.org"
  (and (or (tags "Emacs")
           (priority >= "B"))
       (done)))

Sorting

(org-ql "~/src/emacs/org-super-agenda/test/test.org"
  (regexp "over")
  :sort (priority deadline scheduled))

(org-ql "~/src/emacs/org-super-agenda/test/test.org"
  (regexp "over")
  :sort (date))

(org-ql "~/src/emacs/org-super-agenda/test/test.org"
  (todo)
  :sort (todo))

Regexp matching

(org-ql "~/src/emacs/org-super-agenda/test/test.org"
  (regexp "over"))

(org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
  (regexp "over"))

Property matching

(org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
  (property "agenda-group"))

(org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
  (property "agenda-group" "plans"))

Screenshot code

(org-super-agenda--test-with-org-today-date "2017-07-08 00:00"
  (org-ql "~/src/emacs/org-super-agenda/test/test.org"
    (and (or (date = today)
             (deadline <=)
             (scheduled <= today))
         (not (done)))))

In the wild

;; Hydras
(defhydra gtd-agenda (:color blue
                             :body-pre
                             (if (aj/has-heading-p +INBOX)
                                 (org-ql-search `(,+INBOX) "*"
                                   :sort '(date))
                               (org-ql-search (org-agenda-files)
                                 '(todo "NEXT")
                                 :sort '(date priority todo)
                                 :groups '((:auto-category t))))
                             )
  "agenda"
  ("a" (org-ql-agenda (org-agenda-files)
         (and (or (ts-active :on today)
                  (deadline auto)
                  (scheduled :to today))
              (not (done)))) "agenda")

  ("t" (org-ql-search (org-agenda-files)
         '(todo "TODO")
         :sort '(date priority todo)
         :groups '((:auto-category t))) "todo")

  ("n" (org-ql-search (org-agenda-files)
         '(todo "NEXT")
         :sort '(date priority todo)
         :groups '((:auto-category t))) "next")

  ("h" (org-ql-search (org-agenda-files)
         '(todo "HOLD")
         :sort '(date priority todo)
         :groups '((:auto-category t))) "hold")

  ("s" (org-ql-search (org-agenda-files)
         '(tags "someday")
         :sort '(date priority todo)
         :groups '((:auto-category t))) "someday")

  ("r" (org-ql-search (org-agenda-files)
         '(ts :from -7 :to today)
         :sort '(date priority todo)
         :groups '((:auto-ts t))) "recent")

  ("i" (org-ql-search `(,+INBOX) "*"
         :sort '(date)) "inbox")
  )
("f" "fastdev?"
 ((org-ql-block '(tags "refile")
                ((org-agenda-overriding-header "Refile tasks")))
  (tags-todo ,(concat dev-tag "/!" (mapconcat #'identity my/active-projects-and-tasks "|"))
             ((org-agenda-overriding-header "Stuck Projects")
              (org-agenda-skip-function 'my/dev-show-stuck-projects)
              (org-tags-match-list-sublevels 'indented)
              (org-agenda-sorting-strategy
               '((agenda category-keep)))))
  (tags-todo ,(concat dev-tag "-short" "/!" (mapconcat #'identity my/active-projects-and-tasks "|"))
             ((org-agenda-overriding-header "Active Projects")
              (org-agenda-skip-function 'my/dev-show-active-projects)
              (org-tags-match-list-sublevels 'indented)
              (org-agenda-sorting-strategy
               '((agenda category-keep)))))
  (org-ql-block '(and (tags "dev")
                      (todo "WAIT"))
                ((org-agenda-overriding-header "Waiting tasks")))
  (org-ql-block '(and (tags "dev")
                      (todo "NEXT"))
                ((org-agenda-overriding-header "Things to do")))
  (agenda ""
          ((org-agenda-skip-function 'my/agenda-custom-skip)
           (org-agenda-span 'day)
           (org-agenda-tag-filter-preset (quote (,dev-tag)))
           (org-agenda-skip-deadline-if-done t)
           (org-agenda-skip-scheduled-if-done t)
           (org-super-agenda-groups '((:name "Overdue" :and (:deadline past :log nil))
                                      (:name "Upcoming" :deadline future)
                                      (:name "Should do" :and (:scheduled past :log nil))
                                      (:name "Today" :time-grid t
                                             :and (:not (:and (:not (:scheduled today)
                                                                    :not (:deadline today)))))))))))

Profiling

Preambles

Not sure if clearing the cache is necessary here, because it seemed to make nearly no difference in the results, but I don’t know why.

(cl-defmacro org-ql-preamble-bench (&key query (file "tests/data.org") (times 10))
  `(bench-multi-lets :times ,times :ensure-equal t
     :lets (("preamble" ((org-ql-use-preamble t)
                         (org-ql-cache (ht))))
            ("no preamble" ((org-ql-use-preamble nil)
                            (org-ql-cache (ht)))))
     :forms ((,(prin1-to-string query) (org-ql-select ,file
                                         ',query
                                         :action '(org-get-heading t t))))))

closed

(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (closed))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (closed)4.800.08655300
no preamble: (closed)slowest0.41516500
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (closed <= "2019-01-01"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (closed <= “2019-01-01”)4.210.10578200
no preamble: (closed <= “2019-01-01”)slowest0.44537400

deadline

(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (deadline))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (deadline)27.630.01465600
no preamble: (deadline)slowest0.40495200
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (deadline <= "2019-01-01"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (deadline <= “2019-01-01”)27.910.01460600
no preamble: (deadline <= “2019-01-01”)slowest0.40768200

habit

(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (habit))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (habit)70.090.01648900
no preamble: (habit)slowest1.15564900

level

(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (level 1))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (level 1)1.340.56295000
no preamble: (level 1)slowest0.75405000

property

(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (property "agenda-group"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (property “agenda-group”)70.440.01657100
no preamble: (property “agenda-group”)slowest1.16720300
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (property "ID"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (property “ID”)3.510.36983000
no preamble: (property “ID”)slowest1.29968400
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (property "agenda-group" "plans"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (property “agenda-group” “plans”)72.540.01686200
no preamble: (property “agenda-group” “plans”)slowest1.22319700

scheduled

(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (scheduled))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (scheduled)4.450.10096800
no preamble: (scheduled)slowest0.44932100
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (scheduled <= "2019-01-01"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (scheduled <= “2019-01-01”)4.130.11106700
no preamble: (scheduled <= “2019-01-01”)slowest0.45872600

tags

If tag inheritance is enabled, we have to check tags on every heading. When it’s disabled, we can search directly to headings with the given tags.

(let ((org-use-tag-inheritance t))
  (org-ql-preamble-bench :times 1
                         :file "~/org/inbox.org"
                         :query (tags "Emacs")))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
no preamble: (tags “Emacs”)1.011.89964700
preamble: (tags “Emacs”)slowest1.92179900
(let ((org-use-tag-inheritance nil))
  (org-ql-preamble-bench :times 1
                         :file "~/org/inbox.org"
                         :query (tags "Emacs")))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (tags “Emacs”)2.080.27455500
no preamble: (tags “Emacs”)slowest0.57011600

ts

(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (ts)1.130.47564600
no preamble: (ts)slowest0.53595000
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts :from "2019-01-01"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
no preamble: (ts :from “2019-01-01”)1.110.53744500
preamble: (ts :from “2019-01-01”)slowest0.59453400
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts :from "2017-01-01"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
no preamble: (ts :from “2017-01-01”)1.130.52689100
preamble: (ts :from “2017-01-01”)slowest0.59436000

Not sure why that one is slower with preamble.

(org-ql-preamble-bench :times 10
                       :query (ts :from "2017-01-01"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
no preamble: (ts :from “2017-01-01”)1.040.02568800
preamble: (ts :from “2017-01-01”)slowest0.02664200
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts :to "2010-01-01"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
no preamble: (ts :to “2010-01-01”)1.100.53860300
preamble: (ts :to “2010-01-01”)slowest0.59346600

ts-active

(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts-a))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (ts-a)4.770.07148900
no preamble: (ts-a)slowest0.34089600
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts-a :from "2017-07-06"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (ts-a :from “2017-07-06”)1.780.18836900
no preamble: (ts-a :from “2017-07-06”)slowest0.33597500
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts-a :to "2017-07-06"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (ts-a :to “2017-07-06”)4.640.07530700
no preamble: (ts-a :to “2017-07-06”)slowest0.34944500
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts-a :on "2017-07-06"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (ts-a :on “2017-07-06”)4.330.07607500
no preamble: (ts-a :on “2017-07-06”)slowest0.32910600

ts-inactive

(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts-i))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
preamble: (ts-i)1.210.45915200
no preamble: (ts-i)slowest0.55563200
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts-i :from "2019-07-06"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
no preamble: (ts-i :from “2019-07-06”)1.090.53197600
preamble: (ts-i :from “2019-07-06”)slowest0.57974500
(org-ql-preamble-bench :times 1
                       :file "~/org/inbox.org"
                       :query (ts-i :to "2019-07-06"))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
no preamble: (ts-i :to “2019-07-06”)1.340.55342800
preamble: (ts-i :to “2019-07-06”)slowest0.74388100

with/without ts.el

[2019-08-11 Sun 15:39] These results seem to show a minor performance improvement by using ts, and the code is simpler.

;; (require 'ts)

(org-ql--defpred ts-ts (&key from to _on)
  ;; The underscore before `on' prevents "unused lexical variable" warnings, because we
  ;; pre-process that argument in a macro before this function is called.
  "Return non-nil if current entry has a timestamp in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.

If FROM, return non-nil if entry has a timestamp on or after
FROM.

If TO, return non-nil if entry has a timestamp on or before TO.

If ON, return non-nil if entry has a timestamp on date ON.

FROM, TO, and ON should be strings parseable by
`parse-time-string' but may omit the time value."
  ;; TODO: DRY this with the clocked predicate.
  ;; NOTE: FROM and TO are actually expected to be Unix timestamps.  The docstring is written
  ;; for end users, for which the arguments are pre-processed by `org-ql-select'.
  ;; FIXME: This assumes every "clocked" entry is a range.  Unclosed clock entries are not handled.
  (cl-macrolet ((next-timestamp ()
                                `(when (re-search-forward org-element--timestamp-regexp end-pos t)
                                   (ts-parse-org (match-string 0))))
                (test-timestamps (pred-form)
                                 `(cl-loop for next-ts = (next-timestamp)
                                           while next-ts
                                           thereis ,pred-form)))
    (save-excursion
      (let ((end-pos (org-entry-end-position)))
        (cond ((not (or from to)) (re-search-forward org-element--timestamp-regexp end-pos t))
              ((and from to) (test-timestamps (and (ts<= from next-ts)
                                                   (ts<= next-ts to))))
              (from (test-timestamps (ts<= from next-ts)))
              (to (test-timestamps (ts<= next-ts to))))))))

Without timestamp argument

(bench-multi-lexical :times 1 :ensure-equal t
  :forms (("old ts" (org-ql "~/org/inbox.org"
                      (ts)))
          ("ts.el ts" (org-ql "~/org/inbox.org"
                        (ts-ts)))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
ts.el ts1.142.25180100
old tsslowest2.56028000
(bench-multi-lexical :times 20 :ensure-equal t
  :forms (("old ts" (org-ql "~/src/emacs/org-ql/tests/data.org"
                      (ts)))
          ("ts.el ts" (org-ql "~/src/emacs/org-ql/tests/data.org"
                        (ts-ts)))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
ts.el ts1.050.10371400
old tsslowest0.10866300

:from

(bench-multi-lexical :times 1 :ensure-equal t
  :forms (("old ts" (org-ql "~/org/inbox.org"
                      (ts :from "2017-01-01")))
          ("ts.el ts" (org-ql "~/org/inbox.org"
                        (ts-ts :from "2017-01-01")))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
ts.el ts1.321.29996600
old tsslowest1.71302700

:to

(bench-multi-lexical :times 1 :ensure-equal t
  :forms (("old ts" (org-ql "~/org/inbox.org"
                      (ts :to "2019-01-01")))
          ("ts.el ts" (org-ql "~/org/inbox.org"
                        (ts-ts :to "2019-01-01")))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
ts.el ts1.011.30008400
old tsslowest1.31220800

:on

(bench-multi-lexical :times 1 :ensure-equal t
  :forms (("old ts" (org-ql "~/org/inbox.org"
                      (ts :on "2019-05-14")))
          ("ts.el ts" (org-ql "~/org/inbox.org"
                        (ts-ts :on "2019-05-14")))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
ts.el ts1.170.55728100
old tsslowest0.65214900

Using org-element-parse-buffer

This basically works, as a very basic kind of agenda view, but we can already see that it’s much slower (at least, for single-day views) because org-element-parse-buffer is slow compared to the agenda code.

[2018-05-10 Thu 15:03] Note: This is the old, much slower code that used org-element-parse-buffer.

Macro

(defmacro elp-profile (times prefixes &rest body)
  (declare (indent defun))
  (let ((prefixes (append '(org- string- s- buffer- append delq map
                                 list car save- outline- delete-dups
                                 sort line- nth concat char-to-string
                                 rx- goto- when search- re-)
                          prefixes)))
    `(let (output)
       (dolist (prefix ',prefixes)
         (elp-instrument-package (symbol-name prefix)))
       (dotimes (x ,times)
         ,@body)
       (elp-results)
       (elp-restore-all)
       (point-min)
       (forward-line 20)
       (delete-region (point) (point-max))
       (setq output (buffer-substring-no-properties (point-min) (point-max)))
       (kill-buffer)
       (delete-window)
       (let ((rows (s-lines output)))
         (append (list (list "Function" "Times called" "Total time" "Average time")
                       'hline)
                 (cl-loop for row in rows
                          collect (s-split (rx (1+ space)) row 'omit-nulls)))))))

[2018-05-09 Wed 17:31] Note: I seem to have misplaced the org-agenda-ng--test-agenda-today function I used in these tests.

ng

(elp-profile 1 (org-agenda-ng--test-agenda-today))

orig

Make sure to kill any existing agenda buffers first.

(elp-profile 1 (org-agenda-list nil nil 'week))

Profile org-element-map

(elp-profile 1 (with-current-buffer (find-buffer-visiting "~/org/main.org")
                 (org-element-parse-buffer 'headline)))

Profiling position-based

Macro

(defmacro elp-profile (times &rest body)
  (declare (indent defun))
  `(let ((prefixes '("org-" "string-" "s-" "buffer-" "append" "delq" "map"
                     "list" "car" "save-" "outline-" "delete-dups"
                     "sort" "line-" "nth" "concat" "char-to-string"
                     "rx-" "goto-" "when" "search-" "re-"))
         output)
     (dolist (prefix prefixes)
       (elp-instrument-package prefix))
     (dotimes (x ,times)
       ,@body)
     (elp-results)
     (elp-restore-all)
     (point-min)
     (forward-line 20)
     (delete-region (point) (point-max))
     (setq output (buffer-substring-no-properties (point-min) (point-max)))
     (kill-buffer)
     (delete-window)
     output))

orig

Make sure to kill any existing agenda buffers first.

(elp-profile 1 (org-agenda-list nil nil 'week))

ng-funcall

(elp-profile 5 (org-agenda-ng--test-agenda-today))

ng-flet

(elp-profile 5 (org-agenda-ng--test-agenda-today))

Profiling org-trust-scanner-tags

[2018-05-10 Thu 12:59] Turned on org-trust-scanner-tags, going to try profiling again:

;; (elp-profile 1 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
;;                      (tags "world")))

(elp-profile 10 nil (org-agenda-ng org-agenda-files
                     (tags "Emacs")))
FunctionTimes calledTotal timeAverage time
org-agenda-ng–agenda1044.0925982824.4092598282
mapcar28240.2345167070.1426755911
org-agenda-ng–filter-buffer8026.8954924710.3361936558
org-element-headline-parser398010.3876143620.0026099533
org-agenda-finalize-entries109.1944582520.9194458252
org-agenda-ng–tags-p702508.18973798490.0001165799
org-agenda-ng–format-element39806.59443256790.0016568926
outline-next-heading703206.11901804908.701…e-05
re-search-forward970505.87064678296.049…e-05
org-get-tags-at742305.40781580597.285…e-05
org-super-agenda–filter-finalize-entries105.23201234000.5232012340
org-super-agenda–group-items105.12609592100.5126095921
org-super-agenda–group-dispatch1305.1193336240.0393794894
sort203.82043685690.1910218428
org-element–parse-objects61803.53865789290.0005725983
org-is-habit-p59703.24977559200.0005443510
org-entry-get59703.23479640490.0005418419
org–property-local-values59703.17963573190.0005326023
org-get-property-block59703.07679196800.0005153755
org-entries-lessp200202.65639600790.0001326871

Now trying again without it:

;; (elp-profile 1 nil (org-agenda-ng "~/src/emacs/org-super-agenda/test/test.org"
;;                      (tags "world")))

(elp-profile 10 nil (org-agenda-ng org-agenda-files
                     (tags "Emacs")))
FunctionTimes calledTotal timeAverage time
mapcar179157.0963045380.0318795670
org-agenda-ng–agenda1054.2321335065.4232133505
org-agenda-ng–filter-buffer8030.0651670400.3758145880
org-get-tags-at7423013.8402024950.0001864502
org-agenda-ng–format-element398013.4292977970.0033741954
org-element-headline-parser398012.7717766520.0032089891
org-agenda-finalize-entries109.14394339900.9143943399
org-agenda-ng–tags-p702509.02496537300.0001284692
org-super-agenda–filter-finalize-entries107.3005158590.7300515859
outline-next-heading703207.23844356490.0001029357
org-super-agenda–group-items104.9185858550.4918585855
org-super-agenda–group-dispatch1304.91258935090.0377891488
re-search-forward1010204.62948238504.582…e-05
org-up-heading-safe73704.46298856200.0006055615
org-is-habit-p59604.27723519100.0007176569
org-entry-get59604.25953508000.0007146870
org-super-agenda–group-tag503.89420449290.0778840898
re-search-backward261503.36600834900.0001287192
org–property-local-values59603.17934763290.0005334475
org-get-property-block59603.06624259790.0005144702

Wow, using org-trust-scanner-tags saves a lot of time.

Profiling flet across all agenda files

Without flet

(elp-profile 5 (org-agenda-ng--agenda
                :files org-agenda-files
                :pred (lambda ()
                        (and (org-agenda-ng--todo-p)
                             (or (org-agenda-ng--date-p :deadline '<= (org-today))
                                 (org-agenda-ng--date-p :scheduled '<= (org-today)))
                             (not (apply #'org-agenda-ng--todo-p org-done-keywords-for-agenda))))))

With flet

(elp-profile 5 (org-agenda-ng--agenda
                :files org-agenda-files
                :pred (lambda ()
                        (and (todo)
                             (or (date :deadline '<= (org-today))
                                 (date :scheduled '<= (org-today)))
                             (not (apply #'todo org-done-keywords-for-agenda))))))

Profiling flet on a single file

This shows that the difference between them, if any, is so small as to be irrelevant. The convenience and clarity are a big win.

Without flet

(elp-profile 5 (org-agenda-ng--agenda
                :files "~/org/main.org"
                :pred (lambda ()
                        (and (org-agenda-ng--todo-p)
                             (or (org-agenda-ng--date-p :deadline '<= (org-today))
                                 (org-agenda-ng--date-p :scheduled '<= (org-today)))
                             (not (apply #'org-agenda-ng--todo-p org-done-keywords-for-agenda))))))

With flet

(elp-profile 5 (org-agenda-ng--agenda
                :files "~/org/main.org"
                :pred (lambda ()
                        (and (todo)
                             (or (date :deadline '<= (org-today))
                                 (date :scheduled '<= (org-today)))
                             (not (apply #'todo org-done-keywords-for-agenda))))))

Profiling tags matching

ng

(elp-profile 1 nil
  (org-agenda-ng "~/org/main.org"
    (tags "computer")))
FunctionTimes calledTotal timeAverage time
mapcar421712.6127164550.0029909216
org-agenda-ng–agenda19.7214106519.721410651
org-get-tags-at18457.47938603890.0040538677
org-up-heading-safe93616.46226740190.0006903394
re-search-backward250015.33998662390.0002135909
org-agenda-ng–filter-buffer14.8745988544.874598854
org-agenda-ng–tags-p12384.80676234300.0038826836
org-agenda-ng–format-element6073.63256266090.0059844524
org-outline-level174841.02989244595.890…e-05
org-add-props20740.83055492590.0004004604
org-element-headline-parser6070.20926648290.0003447553
org-back-to-heading118130.12521129601.059…e-05
outline-back-to-heading118130.11006937809.317…e-06
org-end-of-subtree6070.07219863400.0001189433
outline-on-heading-p118130.06752610305.716…e-06
outline-next-heading12390.06279809995.068…e-05
re-search-forward32730.06124466201.871…e-05
org-agenda-finalize-entries10.0418462740.041846274
buffer-substring-no-properties63290.03087169794.877…e-06
line-end-position9030.02804849503.106…e-05

ng without inheritance

(elp-profile 1 nil
  (org-agenda-ng "~/org/main.org"
    (tags "computer")))
FunctionTimes calledTotal timeAverage time
mapcar421712.5802468390.0029832219
org-agenda-ng–agenda18.7777760598.777776059
org-get-tags-at18458.28535032990.0044907047
org-up-heading-safe93617.27109818890.0007767437
re-search-backward250015.33600820600.0002134317
org-agenda-ng–filter-buffer14.8656026894.865602689
org-agenda-ng–tags-p12384.79837543100.0038759090
org-agenda-ng–format-element6073.62738251000.0059759184
org-outline-level174841.02844179195.882…e-05
org-back-to-heading118130.93905344797.949…e-05
org-split-string49400.8338250870.0001687905
string-match91020.82316291009.043…e-05
org-element-headline-parser6070.20343058190.0003351409
outline-back-to-heading118130.10961201899.278…e-06
org-end-of-subtree6070.07108025590.0001171009
outline-on-heading-p118130.06700293595.671…e-06
outline-next-heading12390.06223235195.022…e-05
re-search-forward32730.06031025191.842…e-05
org-agenda-finalize-entries10.0372864960.037286496
buffer-substring-no-properties63290.02858186894.516…e-06

original

  (elp-profile 1 nil
    (with-current-buffer "main.org"
(org-tags-view nil "computer")))
FunctionTimes calledTotal timeAverage time
org-tags-view12.6205781292.620578129
org-scan-tags11.4488838171.448883817
org-agenda-format-item6070.92738930600.0015278242
org-add-props20420.88772672090.0004347339
org-agenda-finalize10.1445067820.144506782
re-search-forward21540.13670466506.346…e-05
string-match87420.10025172591.146…e-05
org-get-priority6070.09619962200.0001584837
org-agenda-align-tags10.0951664950.095166495
org-agenda-prepare10.0817244720.081724472
org-outline-level12460.07710331706.188…e-05
org-agenda-finalize-entries10.0717074040.071707404
org-agenda-prepare-buffers10.0579039210.057903921
org-get-heading6070.05177843698.530…e-05
mapcar37380.04186411101.119…e-05
org-agenda-highlight-todo6070.02731230704.499…e-05
mapconcat6090.0247433054.062…e-05
sort20.021170690.010585345
org-activate-plain-links1320.02035589800.0001542113
org-activate-bracket-links780.01985896800.0002546021

More profiling

[2018-05-10 Thu 15:02] I think these are decent improvements.

(elp-profile 1 nil (org-agenda-ng "~/org/main.org"
                      (or (habit)
                          (and (or (date '= (org-today))
                                   (deadline '<=)
                                   (scheduled '<= (org-today)))
                               (not (apply #'todo org-done-keywords-for-agenda)))
                          (and (todo "DONE" "CANCELLED")
                               (closed '= (org-today))))))
FunctionTimes calledTotal timeAverage time
mapcar1641.50045852900.0091491373
org-agenda-ng–agenda11.3482312471.348231247
org-agenda-ng–filter-buffer11.13911898791.1391189879
org-agenda-ng–date-plain-p12670.61985710400.0004892321
org-entry-get39830.29793373707.480…e-05
org-is-habit-p13650.20491011090.0001501172
org–property-local-values13650.19406141500.0001421695
org-agenda-ng–habit-p12720.19110091790.0001502365
org-agenda-ng–format-element520.1779654110.0034224117
org-get-property-block13650.17600045190.0001289380
org-get-tags-at520.13628249690.0026208172
org-agenda-ng–date-p38800.13511766293.482…e-05
org-up-heading-safe2260.12767476090.0005649325
re-search-backward20280.11442110705.642…e-05
org-entry-properties26180.08486609993.241…e-05
org-agenda-ng–todo-p13190.0819526536.213…e-05
org-get-todo-state13190.07968368106.041…e-05
re-search-forward37540.07398037391.970…e-05
org-inlinetask-in-task-p13650.06578293304.819…e-05
org-agenda-ng–scheduled-p12470.06194978504.967…e-05

Intersecting query results

An idea that might be helpful for performance in some cases, depending on the query, the data, and whether the query has a preamble. But it looks like it would very rarely be helpful.

(cl-defun org-ql-agenda-intersection (buffers-files queries &key entries sort buffer narrow super-groups)
  "Like `org-ql-agenda', but intersects multiple queries."
  (declare (indent defun))
  (let* ((org-ql-cache (ht))
         (entries (->> queries
                       (--map (org-ql-select buffers-files
                                it
                                :action 'element-with-markers
                                :narrow narrow
                                :sort sort))
                       (-reduce #'-intersection))))
    (org-ql-agenda--agenda buffers-files queries
      :entries entries :super-groups super-groups)))

(bench-multi-lexical :times 1
  :forms (("intersection" (let ((org-use-tag-inheritance nil))
                            (org-ql-agenda-intersection (org-agenda-files)
                              '((todo "TODO")
                                (tags "Emacs"))
                              :sort '(priority deadline)
                              :super-groups org-super-agenda-groups)))
          ("normal" (let ((org-use-tag-inheritance nil))
                      (org-ql-agenda (org-agenda-files)
                        (and (todo "TODO")
                             (tags "Emacs"))
                        :sort (priority deadline)
                        :super-groups org-super-agenda-groups)))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
normal3.700.23314700
intersectionslowest0.86251200

Alternative approach

[2019-09-01 Sun 08:17] This is very experimental, but the results are surprising. When the action function returns a fairly simple list, the intersection is very slightly faster. When returning full elements, the intersection is much slower, so that it more than doubles the runtime. I wonder if the element list comparison is short-circuiting, or if it looks at the whole lists, because it seems like it shouldn’t take more than 4-5 list elements before it realizes that two lists don’t match.

Anyway, looks like this approach isn’t viable, at least not without a much more complicated implementation, which probably wouldn’t be worth it.

(let* ((action-fn (lambda ()
                    (list (current-buffer)
                          (point)
                          (substring-no-properties (org-get-heading t t)))))
       (files '("~/org/main.org")))
  ;; NOTE: Careful to use the same files and action in each one.  I duplicated
  ;; the variable in each form to make individual testing easier.
  (bench-multi-lexical :times 1 :ensure-equal t
    :forms (("normal" (->> (let ((org-ql-cache (ht))
                                 (action-fn (lambda ()
                                              (list (current-buffer)
                                                    (point)
                                                    (substring-no-properties (org-get-heading t t)))))
                                 (files '("~/org/main.org")))
                             (org-ql-select files
                               '(and (not (done))
                                     (or (habit)
                                         (deadline auto)
                                         (scheduled :to today)
                                         (ts-active :on today)
                                         (closed :on today)))
                               :action action-fn))))
            ("Testing" (let* ((org-ql-cache (ht))
                              (files '("~/org/main.org"))
                              (action-fn (lambda ()
                      (list (current-buffer)
                            (point)
                            (substring-no-properties (org-get-heading t t)))))
                              (and-queries '(not (done)))
                              (or-queries '((habit)
                                            (deadline auto)
                                            (scheduled :to today)
                                            (ts-active :on today)
                                            (closed :on today)))
                              (and-results (org-ql-select files
                                             and-queries
                                             :action action-fn))
                              (or-results (cl-loop for query in or-queries
                                                   append (org-ql-select files
                                                            query
                                                            :action action-fn))))
                         (seq-intersection and-results
                                           (->> or-results
                                                -uniq)))))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
Testing1.150.24837600
normalslowest0.28489700
;; With caching enabled
  (let* ((action-fn (lambda ()
                      (list (current-buffer)
                            (point)
                            (substring-no-properties (org-get-heading t t)))))
         (files '("~/org/main.org")))
    (bench-multi-lexical :times 1 :ensure-equal t
      :forms (("normal" (->> (org-ql-select files
                               '(and (not (done))
                                     (or (habit)
                                         (deadline auto)
                                         (scheduled :to today)
                                         (ts-active :on today)
                                         (closed :on today)))
                               :action action-fn)))
              ("Testing" (let* ((files '("~/org/main.org"))
                                (and-queries '(not (done)))
                                (or-queries '((habit)
                                              (deadline auto)
                                              (scheduled :to today)
                                              (ts-active :on today)
                                              (closed :on today)))
                                (and-results (org-ql-select files
                                               and-queries
                                               :action action-fn))
                                (or-results (cl-loop for query in or-queries
                                                     append (org-ql-select files
                                                              query
                                                              :action action-fn))))
                           (seq-intersection and-results
                                             (->> or-results
                                                  -uniq)))))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
normal13.720.00231100
Testingslowest0.03170700

Using full views:

(let* ((action-fn (lambda ()
                    (list (current-buffer)
                          (point)
                          (substring-no-properties (org-get-heading t t)))))
       (files '("~/org/main.org")))
  (bench-multi-lexical :times 1
    :forms (("normal" (->> (let ((org-ql-cache (ht))
                                 (files '("~/org/main.org")))
                             (org-ql-search files
                               '(and (not (done))
                                     (or (habit)
                                         (deadline auto)
                                         (scheduled :to today)
                                         (ts-active :on today)
                                         (closed :on today)))))))
            ("Testing" (let* ((org-ql-cache (ht))
                              (files '("~/org/main.org"))
                              (and-queries '(not (done)))
                              (or-queries '((habit)
                                            (deadline auto)
                                            (scheduled :to today)
                                            (ts-active :on today)
                                            (closed :on today)))
                              (and-results (org-ql-select files
                                             and-queries
                                             :action 'element-with-markers))
                              (or-results (cl-loop for query in or-queries
                                                   append (org-ql-select files
                                                            query
                                                            :action 'element-with-markers)))
                              (final-results (seq-intersection and-results
                                                               (->> or-results
                                                                    -uniq))))
                         (org-ql-agenda--agenda nil nil
                           :entries final-results)
                       
                         )))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
normal1.740.53474200
Testingslowest0.93189700

Just gathering results, but using elements:

(let* ((action-fn 'element-with-markers)
       (files '("~/org/main.org")))
  ;; NOTE: Careful to use the same files and action in each one.  I duplicated
  ;; the variable in each form to make individual testing easier.
  (bench-multi-lexical :times 1 :ensure-equal t
    :forms (("normal" (->> (let ((org-ql-cache (ht)))
                             (org-ql-select files
                               '(and (not (done))
                                     (or (habit)
                                         (deadline auto)
                                         (scheduled :to today)
                                         (ts-active :on today)
                                         (closed :on today)))
                               :action action-fn))))
            ("Testing" (let* ((org-ql-cache (ht))
                              (and-queries '(not (done)))
                              (or-queries '((habit)
                                            (deadline auto)
                                            (scheduled :to today)
                                            (ts-active :on today)
                                            (closed :on today)))
                              (and-results (org-ql-select files
                                             and-queries
                                             :action action-fn))
                              (or-results (cl-loop for query in or-queries
                                                   append (org-ql-select files
                                                            query
                                                            :action action-fn))))
                         (seq-intersection and-results
                                           (->> or-results
                                                -uniq)))))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
normal2.270.31421800
Testingslowest0.71458700

[2019-08-29 Thu 06:24] Benchmarking org-ql compared to re-search-forward for getting headings in buffer

Minimal difference, and that’s a very large file, too. On smaller files it’s thousandths of a second.

(with-current-buffer (find-buffer-visiting "~/org/inbox.org")
  (bench-multi-lexical :times 1 :ensure-equal t
    :forms (("org-ql" (org-ql-select (current-buffer)
                        '(level 1)
                        :action '(progn
                                   (font-lock-ensure (line-beginning-position) (line-end-position))
                                   (cons (org-get-heading t) (point)))))
            ("re-search-forward" (org-with-wide-buffer
                                  (goto-char (point-min))
                                  (when (org-before-first-heading-p)
                                    (outline-next-heading))
                                  (cl-loop while (re-search-forward (rx bol "*" (1+ blank)) nil t)
                                           do (font-lock-ensure (line-beginning-position) (line-end-position))
                                           collect (cons (org-get-heading t) (match-beginning 0))))))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
re-search-forward1.170.52037500
org-qlslowest0.60828100

Caching of inherited tags

[2019-09-05 Thu 07:59] Implemented a per-buffer tags cache that seems to significantly speed up tags queries that use tag inheritance. It persists as long as the buffer remains unmodified, and it’s usable from any code as a single function that automatically uses caching. It also returns inherited tags and local tags separately, which could be useful for having separate selectors, one for inherited tags, one for local tags, and one for both.

(defvar org-ql-tags-cache (ht)
  "Per-buffer tags cache.
Keyed by buffer.  Each value is a cons of the buffer's modified
tick, and another hash table keyed on buffer position, whose
values are a list of two lists, inherited tags and local tags, as
strings.")

(defun org-ql--tags-at (position)
  "Return tags for POSITION in current buffer.
Returns cons (INHERITED-TAGS . LOCAL-TAGS)."
  ;; I'd like to use `-if-let*', but it doesn't leave non-nil variables
  ;; bound in the else clause, so destructured variables that are non-nil,
  ;; like found caches, are not available in the else clause.
  (if-let* ((buffer-cache (gethash (current-buffer) org-ql-tags-cache))
            (modified-tick (car buffer-cache))
            (tags-cache (cdr buffer-cache))
            (buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
            (cached-result (gethash position tags-cache)))
      ;; Found in cache: return them.
      (pcase cached-result
        ('org-ql-nil nil)
        (_ cached-result))
    ;; Not found in cache: get tags and cache them.
    (let* ((local-tags (or (org-get-tags position 'local)
                           'org-ql-nil))
           (inherited-tags (or (save-excursion
                                 (when (org-up-heading-safe)
                                   (-let* (((inherited local) (org-ql--tags-at (point)))
                                           (inherited-tags (when (or inherited local)
                                                             (cond ((and (listp inherited)
                                                                         (listp local))
                                                                    (append inherited local))
                                                                   ((cond ((listp inherited) inherited)
                                                                          ((listp local) local)))))))
                                     (when inherited-tags
                                       (->> inherited-tags -non-nil -uniq)))))
                               'org-ql-nil))
           (all-tags (list inherited-tags local-tags)))
      ;; Check caches again, because they may have been set now.
      ;; TODO: Is there a clever way we could avoid doing this, or is it inherently necessary?
      (setf buffer-cache (gethash (current-buffer) org-ql-tags-cache)
            modified-tick (car buffer-cache)
            tags-cache (cdr buffer-cache)
            buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
      (cond ((or (not buffer-cache)
                 (not buffer-unmodified-p))
             ;; Buffer-local tags cache empty or invalid: make new one.
             (puthash (current-buffer)
                      (cons (buffer-modified-tick)
                            (let ((table (make-hash-table)))
                              (puthash position all-tags table)
                              table))
                      org-ql-tags-cache)
             ;; Return tags, not the cons put on the buffer-cache.
             all-tags)
            ;; Buffer-local tags cache found, but no result: store this one.
            (t (puthash position all-tags tags-cache))))))

(org-ql--defpred tags-cached (&rest tags)
  "Return non-nil if current heading has one or more of TAGS (a list of strings)."
  ;; TODO: Try to use `org-make-tags-matcher' to improve performance.  It would be nice to not have
  ;; to run `org-get-tags' for every heading, especially with inheritance.
  (cl-macrolet ((tags-p (tags)
                        `(and ,tags
                              (not (eq 'org-ql-nil ,tags)))))
    (-let* (((inherited local) (org-ql--tags-at (point))))
      (cl-typecase tags
        (null (or (tags-p inherited)
                  (tags-p local)))
        (otherwise (or (when (tags-p inherited)
                         (seq-intersection tags inherited))
                       (when (tags-p local)
                         (seq-intersection tags local))))))))

Benchmark results:

(let* ((buffers '("~/org/main.org"))
       (tags '("Emacs")))
  (bench-multi-lexical :times 1 :ensure-equal t
    :forms (("uncached" (let ((org-ql-cache (ht)))
                          (org-ql-select buffers
                            `(tags ,@tags))))
            ("cached" (let ((org-ql-cache (ht))
                            (org-ql-tags-cache (ht)))
                        (org-ql-select buffers
                          `(tags-cached ,@tags)))))))
Formx faster than nextTotal runtime# of GCsTotal GC runtime
cached6.510.51987100
uncachedslowest3.38667900

Testing

[2020-01-08 Wed 07:15] Subtree moved from tests/data.org.

(cl-defun ap/org-tweak-timestamps (&key (offset 0) epoch-ts)
  "Advance all timestamps in the current buffer as if the earliest one was on today.
OFFSET changes which timestamp (in chronological order) is set to
today.  Or, if EPOCH-TS is non-nil, use it as the new zero-point
for today."
  (let* ((tss (->> (org-with-wide-buffer
                    (goto-char (point-min))
                    (cl-loop while (re-search-forward org-ts-regexp-both nil t)
                             collect (ts-parse-org (match-string 0))))
                   (-sort #'ts<)))
         (epoch-ts (if epoch-ts
                       (ts-parse-org epoch-ts)
                     (nth offset tss)))
         (difference-secs (ts-diff (ts-now) epoch-ts))
         (days (floor (/ difference-secs 86400))))
    (org-with-wide-buffer
     (goto-char (point-min))
     (while (re-search-forward org-ts-regexp-both nil t)
       (let* ((ts (ts-parse-org (match-string 0)))
              (timed-p (string-match-p (rx (repeat 2 digit) ":" (repeat 2 digit) (or ">" "]")) (match-string 0)))
              (type (cond ((string-prefix-p "<" (match-string 0)) 'active)
                          ((string-prefix-p "[" (match-string 0)) 'inactive)
                          (t (error "Unknown ts type"))))
              (brackets (cl-ecase type
                          ('active (cons "<" ">"))
                          ('inactive (cons "[" "]"))))
              (format-string (concat (car brackets)
                                     (if timed-p
                                         "%Y-%m-%d %a %H:%M"
                                       "%Y-%m-%d %a")
                                     (cdr brackets)))
              (new-ts (ts-adjust 'day days ts))
              (new-ts-string (ts-format format-string new-ts)))
         (replace-match new-ts-string t t nil 0))))
    (message "Tweaked from epoch: %s" (ts-format epoch-ts))))
(org-time-string-to-absolute (org-entry-get (point) "SCHEDULED"))
;; Setup code
(require 'org-super-agenda)
(org-super-agenda-mode 1)
(require 'org-habit)
(setq org-todo-keywords
      '((sequence "TODO(t!)" "TODAY(a!)" "NEXT(n!)" "STARTED(s!)" "IN-PROGRESS(p!)" "UNDERWAY(u!)" "WAITING(w@)" "SOMEDAY(o!)" "MAYBE(m!)" "|" "DONE(d@)" "CANCELED(c@)")
        (sequence "CHECK(k!)" "|" "DONE(d@)")
        (sequence "TO-READ(r!)" "READING(R!)" "|" "HAVE-READ(d@)")
        (sequence "TO-WATCH(!)" "WATCHING(!)" "SEEN(!)")))
(with-current-buffer "test.org" (revert-buffer))

(defmacro with-org-today-date (date &rest body)
  "Run BODY with the `org-today' function set to return simply DATE.
DATE should be a date-time string (both date and time must be included)."
  (declare (indent defun))
  `(let ((day (date-to-day ,date))
         (orig (symbol-function 'org-today)))
     (unwind-protect
         (progn
           (fset 'org-today (lambda () day))
           ,@body)
       (fset 'org-today orig))))
(defun diary-sunrise ()
  (let ((dss (diary-sunrise-sunset)))
    (with-temp-buffer
      (insert dss)
      (goto-char (point-min))
      (search-forward ",")
      (buffer-substring (point-min) (match-beginning 0)))))

(defun diary-sunset ()
  (let ((dss (diary-sunrise-sunset))
        start end)
    (with-temp-buffer
      (insert dss)
      (goto-char (point-min))
      (search-forward ", ")
      (setq start (match-end 0))
      (search-forward " at")
      (setq end (match-beginning 0))
      (goto-char start)
      (capitalize-word 1)
      (buffer-substring start end))))

Note: Removing tests from here as they’re added to test.el.

(org-super-agenda--test-with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/emacs/org-super-agenda/test/test.org"))
        (org-agenda-span 'day)
        (org-super-agenda-groups
         '((:name "Time grid items in all-uppercase with RosyBrown1 foreground"
                  :time-grid t
                  :transformer (--> it
                                    (upcase it)
                                    (propertize it 'face '(:foreground "RosyBrown1"))))
           (:name "Priority >= C items underlined, on black background"
                  :face (:background "black" :underline t)
                  :not (:priority>= "C")
                  :order 100))))
    (org-agenda nil "a")))
(org-super-agenda--test-with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/emacs/org-super-agenda/test/test.org"))
        (org-agenda-span 'day)
        (org-super-agenda-groups
         '((:name none
                  :time-grid t)
           (:name "Should be all-uppercase RosyBrown1 on black"
                  :face (:background "black" :foreground "RosyBrown1")
                  :transformer #'upcase
                  :not (:priority>= "C")
                  :order 100))))
    (org-agenda nil "a")))

(with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
        (org-agenda-span 'day)
        (org-super-agenda-groups
         '((:name none
                  :time-grid t)
           (:name none
                  :not (:priority>= "C")
                  :order 100))))
    (org-agenda nil "a")))

(with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
        (org-agenda-span 'day)
        (org-super-agenda-groups
         '((:name "Items with child TODOs"
                  :children todo))))
    (org-agenda nil "a")))

(with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
        (org-agenda-span 'day)
        (org-super-agenda-groups
         '((:name "Items with child TODOs"
                  :children "CHECK"))))
    (org-agenda nil "a")))

(with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
        (org-agenda-span 'day)
        (org-agenda-custom-commands
         '(("u" "Super view"
            ((agenda "" ((org-super-agenda-groups
                          '((:name "Today"
                                   :time-grid t
                                   :scheduled today
                                   :deadline today)))))
             (todo "" ((org-super-agenda-groups
                        '((:name "Projects"
                                 :children t)
                          (:discard (:anything t)))))))))))
    (org-agenda nil "u")))

(with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
        (org-agenda-span 'day)
        (org-agenda-custom-commands
         '(("u" "Super view"
            ((agenda "" ((org-super-agenda-groups
                          '((:name "Today"
                                   :time-grid today)))))
             (todo "" ((org-super-agenda-groups
                        '((:name "Projects"
                                 :children t)
                          (:discard (:anything t)))))))))))
    (org-agenda nil "u")))

(with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
        (org-agenda-span 'day)
        (org-agenda-custom-commands
         '(("u" "Super view"
            ((agenda "" ((org-super-agenda-groups
                          '((:name "Schedule"
                                   :time-grid t
                                   :date today)
                            (:name "Due today"
                                   :deadline today)
                            (:name "Due soon"
                                   :deadline t)))))
             (todo "" ((org-agenda-overriding-header "")
                       (org-super-agenda-groups
                        '((:name "Projects"
                                 :children t)
                          (:discard (:anything t)))))))))))
    (org-agenda nil "u")))

(with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
        (org-agenda-span 'day)
        (org-super-agenda-groups
         '((:scheduled (before "2017-07-06")))))
    (org-agenda nil "a")))
  (with-org-today-date "2017-07-05 00:00"
    (let ((org-super-agenda-groups
           '((:todo "WAITING")))
          (org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
      (org-todo-list)))

  (with-org-today-date "2017-07-05 00:00"
    (let ((org-super-agenda-groups
           '((:todo "SOMEDAY")))
          (org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
      (org-tags-view nil "Emacs")))

  (with-org-today-date "2017-07-05 00:00"
    (let ((org-super-agenda-groups
           '((:todo "CHECK")))
          (org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
;; org-search-view doesn't seem to set the todo-state property, so the matcher doesn't work
      (org-search-view nil "Emacs")))

  (with-org-today-date "2017-07-05 00:00"
    (let ((org-super-agenda-groups
           '((:regexp ("moon" "mars"))))
          (org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
      (org-search-view nil "space")))

  (with-org-today-date "2017-07-05 00:00"
    (let ((org-super-agenda-groups
           '((:todo "SOMEDAY")))
          (org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
      (org-agenda-list nil nil 'day)))

Agenda examining

This helps a lot.

(defun data-debug-show-string-with-properties (s)
  (with-current-buffer (get-buffer-create "argh")
    (erase-buffer)
    (print s (current-buffer))

    ;; Convert string reader representations to plain lists that can be set
    (cl-loop for (match replace) in '(("#(" "'(")
                                      ("#<" "'(")
                                      (">" ")"))
             do (progn
                  (goto-char (point-min))
                  (while (search-forward match nil 'noerror)
                    (replace-match replace 'fixedcase 'literal))))

    ;; Surround content in a list which `argh' is set to, then eval
    ;; the buffer to do it
    (goto-char (point-min))
    (insert "(setq argh (list '")
    (delete-forward-char 2)
    (goto-char (point-max))
    (insert "))")

    ;; Okay, sure, eval'ing the buffer is dangerous and bad and wrong.
    ;; But this is the only way I can find to make this work.  (Maybe
    ;; `text-properties-at' could be used to get actual lists...)
    (eval-buffer)

    (data-debug-show-stuff argh "argh")
    ;;  (switch-to-buffer (current-buffer))
    ))

(defun data-debug-show-current-line-with-properties ()
  (interactive)
  (data-debug-show-string-with-properties (buffer-substring (line-beginning-position) (line-end-position))))

(with-current-buffer "*Org Agenda*"
  (data-debug-show-string-with-properties (seq-subseq (split-string (buffer-string) "\n")
                                                      0 5)))

Agenda censoring

For sharing screenshots of the agenda without revealing private data.

(defun org-agenda-sharpie ()
  "Censor the text of items in the agenda."
  (interactive)
  (let (regexp old-heading new-heading properties)
    ;; Save face properties of line in agenda to reapply to changed text
    (setq properties (text-properties-at (point)))

    ;; Go to source buffer
    (org-with-point-at (org-find-text-property-in-string 'org-marker
                                                         (buffer-substring (line-beginning-position)
                                                                           (line-end-position)))
      ;; Save old heading text and ask for new text
      (line-beginning-position)
      (unless (org-at-heading-p)
        ;; Not sure if necessary
        (org-back-to-heading))
      (setq old-heading (when (looking-at org-complex-heading-regexp)
                          (match-string 4))))
    (setq new-heading (read-from-minibuffer "Overwrite visible heading with: "))
    (add-text-properties 0 (length new-heading) properties new-heading)
    ;; Back to agenda buffer
    (save-excursion
      (when (and old-heading new-heading)
        ;; Replace agenda text
        (let ((inhibit-read-only t))
          (goto-char (line-beginning-position))
          (when (search-forward old-heading (line-end-position))
            (replace-match new-heading 'fixedcase 'literal)))))))

Auto grouping

(with-org-today-date "2017-07-05 00:00"
  (let ((org-super-agenda-groups
         '((:auto-group t)))
        (org-agenda-files (list "~/src/org-super-agenda/test/test.org")))
    (org-agenda-list nil nil 'day)))

Auto categories

(let ((org-super-agenda-groups
       '((:auto-category t))))
  (org-agenda-list nil nil 'day))

Date

(with-org-today-date "2017-07-05 00:00"
  (-let* ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
          (org-agenda-span 'day)
          ((sec minute hour day month year dow dst utcoff) (list 0 0 0 5 7 2017 3 t nil))
          (last-day-of-month
           ;; A hack that seems to work fine
           (1+ (calendar-last-day-of-month month year)))
          (target-date (format "%d-%02d-%02d" year month last-day-of-month))
          (org-super-agenda-groups
           `((:deadline (before ,target-date))
             (:discard (:anything t)))))
    (org-todo-list)))

(with-org-today-date "2017-07-05 00:00"
  (-let* ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
          (org-agenda-span 'day)
          ((sec minute hour day month year dow dst utcoff) (list 0 0 0 5 7 2017 3 t nil))
          (last-day-of-month  (calendar-last-day-of-month month year))
          (target-date (format "%d-%02d-%02d" year month last-day-of-month))
          (org-super-agenda-groups
           `((:deadline (before ,target-date))
             (:discard (:anything t)))))
    (org-todo-list)))

Effort

(with-org-today-date "2017-07-05 00:00"
  (let ((org-agenda-files (list "~/src/org-super-agenda/test/test.org"))
        (org-super-agenda-groups
         '((:effort< "0:06"))))
    (org-agenda-list nil nil 'day)))

Misc

let-plist

I don’t need this right now, but it might come in handy here or elsewhere.

(defmacro osa/let-plist (keys plist &rest body)
  "`cl-destructuring-bind' without the boilerplate for plists."
  ;; See https://emacs.stackexchange.com/q/22542/3871

  ;; I really don't understand why Emacs doesn't have this already.
  ;; So many things come close to it: pcase, pcase-let, map-let,
  ;; cl-destructuring-bind, -let...but none of them let you simply
  ;; bind all the values of a plist to variables with the same name as
  ;; their keys. You always have to type the name of the key twice.

  ;; For example, compare these two forms:

  ;; (-let (((&keys :from from :to to :date date :subject subject) email))
  ;;   (list from to date subject))

  ;; (osa/let-plist (:from :to :date :subject) email
  ;;   (list from to date subject))

  ;; Now, sure, sometimes you need to bind values to differently named
  ;; variables. But when you don't, I know which one I prefer.
  (declare (indent defun))
  (setq keys (cl-loop for key in keys
                      collect (intern (replace-regexp-in-string (rx bol ":") ""
                                                                (symbol-name key)))))
  `(cl-destructuring-bind
       (&key ,@keys &allow-other-keys)
       ,plist
     ,@body))

Profiling

(defmacro profile-it (times &rest body)
  `(let (output)
     (dolist (p '("org-super-agenda-" "map" "org-" "string-" "s-" "buffer-" "append" "delq" "map" "list" "car" "save-" "outline-" "delete-dups" "sort" "line-" "nth" "concat" "char-to-string" "rx-" "goto-" "when" "search-" "re-"))
       (elp-instrument-package p))
     (dotimes (x ,times)
       ,@body)
     (elp-results)
     (elp-restore-all)
     (point-min)
     (forward-line 20)
     (delete-region (point) (point-max))
     (setq output (buffer-substring-no-properties (point-min) (point-max)))
     (kill-buffer)
     (delete-window)
     output))