;;; 99-external-misc.el --- Miscellaneous functions made by others -*- lexical-binding: t -*- ;; This file is not part of GNU Emacs ;;; Commentary: ;; This file contains various useful functions and macros that I definitely ;; didn't write and took from somewhere as-is. ;;; Code: (defun second (item) (nth 1 item)) (defun third (item) (nth 2 item)) (defun fourth (item) (nth 3 item)) ;; Source: https://phillord.github.io/m-buffer-el/#sec-5-1-2 ;;;###autoload (cl-defmacro bench (&optional (times 100000) &rest body) "Call `benchmark-run-compiled' on BODY with TIMES iterations, returning list suitable for Org source block evaluation. Garbage is collected before calling `benchmark-run-compiled' to avoid counting existing garbage which needs collection." (declare (indent defun)) `(progn (garbage-collect) (list '("Total runtime" "# of GCs" "Total GC runtime") 'hline (benchmark-run-compiled ,times (progn ,@body))))) ;; Source: alphapapa.github.io/emacs-package-dev-handbook/#outline-container-bench-multi-macros ;;;###autoload (cl-defmacro bench-multi (&key (times 1) forms ensure-equal raw) "Return Org table as a list with benchmark results for FORMS. Runs FORMS with `benchmark-run-compiled' for TIMES iterations. When ENSURE-EQUAL is non-nil, the results of FORMS are compared, and an error is raised if they aren't `equal'. If the results are sequences, the difference between them is shown with `seq-difference'. When RAW is non-nil, the raw results from `benchmark-run-compiled' are returned instead of an Org table list. If the first element of a form is a string, it's used as the form's description in the bench-multi-results; otherwise, forms are numbered from 0. Before each form is run, `garbage-collect' is called. See source for a usage example." ;; MAYBE: Since `bench-multi-lexical' byte-compiles the file, I'm not sure if ;; `benchmark-run-compiled' is necessary over `benchmark-run', or if it matters. (declare (indent defun)) (let*((keys (gensym "keys")) (result-times (gensym "result-times")) (header '(("Form" "x fastest" "Total runtime" "# of GCs" "Total GC runtime") hline)) ;; Copy forms so that a subsequent call of the macro will get the original forms. (forms (cl-copy-list forms)) (descriptions (cl-loop for form in forms for i from 0 collect (if (stringp (car form)) (prog1 (car form) (setf (nth i forms) (cadr (nth i forms)))) i)))) `(unwind-protect (progn (defvar bench-multi-results nil) (let* ((bench-multi-results (make-hash-table)) (,result-times (sort (list ,@(cl-loop for form in forms for i from 0 for description = (nth i descriptions) collect `(progn (garbage-collect) (cons ,description (benchmark-run-compiled ,times ,(if ensure-equal `(puthash ,description ,form bench-multi-results) form)))))) (lambda (a b) (< (nth 1 a) (nth 1 b)))))) ,(when ensure-equal `(cl-loop with ,keys = (hash-table-keys bench-multi-results) for i from 0 to (- (length ,keys) 2) unless (equal (gethash (nth i ,keys) bench-multi-results) (gethash (nth (1+ i) ,keys) bench-multi-results)) do (if (sequencep (gethash (car (hash-table-keys bench-multi-results)) bench-multi-results)) (let* ((k1) (k2) ;; If the difference in one order is nil, try in other order. (difference (or (setq k1 (nth i ,keys) k2 (nth (1+ i) ,keys) difference (seq-difference (gethash k1 bench-multi-results) (gethash k2 bench-multi-results))) (setq k1 (nth (1+ i) ,keys) k2 (nth i ,keys) difference (seq-difference (gethash k1 bench-multi-results) (gethash k2 bench-multi-results)))))) (user-error "Forms' bench-multi-results not equal: difference (%s - %s): %S" k1 k2 difference)) ;; Not a sequence (user-error "Forms' bench-multi-results not equal: %s:%S %s:%S" (nth i ,keys) (nth (1+ i) ,keys) (gethash (nth i ,keys) bench-multi-results) (gethash (nth (1+ i) ,keys) bench-multi-results))))) ;; Add factors to times and return table (if ,raw ,result-times (append ',header (bench-multi-process-results ,result-times))))) (unintern 'bench-multi-results nil)))) ;; Use like this: ;; (bench-multi ;; :forms (("org-map-entries" (sort (org-map-entries (lambda () ;; (nth 4 (org-heading-components))) ;; "/+MAYBE" 'agenda) ;; #'string<)) ;; ("regexp" (sort (-flatten ;; (-non-nil ;; (mapcar (lambda (file) ;; (let ((case-fold-search t)) ;; (with-current-buffer (find-buffer-visiting file) ;; (org-with-wide-buffer ;; (goto-char (point-min)) ;; (cl-loop with regexp = (format org-heading-keyword-regexp-format "MAYBE") ;; while (re-search-forward regexp nil t) ;; collect (nth 4 (org-heading-components))))))) ;; (org-agenda-files)))) ;; #'string<)))) (defun bench-multi-process-results (results) "Return sorted RESULTS with factors added." (setq results (sort results (-on #'< #'second))) (cl-loop with length = (length results) for i from 0 below length for description = (car (nth i results)) for factor = (pcase i (0 "fastest") (_ (format "%.2f" (/ (second (nth i results)) (second (nth 0 results)))))) collect (append (list description factor) (list (format "%.6f" (second (nth i results))) (third (nth i results)) (if (> (fourth (nth i results)) 0) (format "%.6f" (fourth (nth i results))) 0))))) ;;;###autoload (cl-defmacro bench-multi-lexical (&key (times 1) forms ensure-equal raw) "Return Org table as a list with benchmark results for FORMS. Runs FORMS from a byte-compiled temp file with `lexical-binding' enabled, using `bench-multi', which see. Afterward, the temp file is deleted and the function used to run the benchmark is uninterned." (declare (indent defun)) `(let* ((temp-file (concat (make-temp-file "bench-multi-lexical-") ".el")) (fn (gensym "bench-multi-lexical-run-"))) (with-temp-file temp-file (insert ";; -*- lexical-binding: t; -*-" "\n\n" "(defvar bench-multi-results)" "\n\n" (format "(defun %s () (bench-multi :times %d :ensure-equal %s :raw %s :forms %S))" fn ,times ,ensure-equal ,raw ',forms))) (unwind-protect (if (byte-compile-file temp-file 'load) (funcall (intern (symbol-name fn))) (user-error "Error byte-compiling and loading temp file")) (delete-file temp-file) (unintern (symbol-name fn) nil)))) ;;; 99-external-misc.el ends here