(require 'cl-lib) (require 'package) (require 'backtrace)
(require 'snitch-custom)
(require 'snitch-timer)
(defvar snitch--site-lisp-dir-cache nil
"Cache a list of the Emacs site-lisp directories.")
(defvar snitch--site-lisp-root-cache nil
"Cache a list of the Emacs site-lisp root directories.")
(defvar snitch--function-to-file-cache nil
"Cache of function-to-file mappings.
Hash table cache of function names to the file the functions are
defined in.")
(defvar snitch--package-dirs-cache '()
"Cache of elisp package directories.
Hash table cache mapping elisp directories to active packages.")
(defun snitch--fn-hash-cmp (a b)
"Hash comparison for function/package cache.
Hash comparison function for function/package hash table,
since functions can be either function objects or strings and
require different comparisons.
Return t if A equals B."
(if (and (functionp a) (functionp b))
(eq a b)
(equal a b)))
(defun snitch--find-function-file (fn)
"Find file owning function FN.
Look up the file a function is defined in, caching it in a
hash table for quicker subsequent accesses."
(unless snitch--function-to-file-cache
(define-hash-table-test 'snitch-fn-hash-cmp
#'snitch--fn-hash-cmp #'sxhash-equal)
(setq snitch--function-to-file-cache
(make-hash-table :test 'snitch-fn-hash-cmp)))
(let ((stored-file (gethash fn snitch--function-to-file-cache)))
(if stored-file (if (eq stored-file 'notfound) nil
stored-file)
(let ((file (find-lisp-object-file-name fn 'defun)))
(if file
(puthash fn file snitch--function-to-file-cache)
(progn
(puthash fn 'notfound snitch--function-to-file-cache)
nil))))))
(defun snitch--site-lisp-dirs ()
"Find site-lisp directories.
Find all directories in elisp load path that are not in the user
dir."
(if (not snitch--site-lisp-dir-cache)
(let* ((user-dir (expand-file-name user-emacs-directory))
(pkg-dir (expand-file-name package-user-dir))
(dirs
(cl-loop for dir in (elisp-load-path-roots)
unless (or
(string-prefix-p user-dir dir)
(string-prefix-p pkg-dir dir)
(string-prefix-p package-user-dir dir)
(string-prefix-p user-emacs-directory dir))
collect dir)))
(setq snitch--site-lisp-dir-cache dirs)
dirs)
snitch--site-lisp-dir-cache))
(defun snitch--site-lisp-roots ()
"Find the root site-lisp directories.
Find the 'root' directories, hopefully a list of
system-wide/non-user base directories containing elisp files."
(if (not snitch--site-lisp-root-cache)
(let ((dirs
(cl-loop for dir in (snitch--site-lisp-dirs)
if (or (string-equal "lisp" (file-name-base dir))
(string-equal "site-lisp" (file-name-base dir)))
collect dir)))
(setq snitch--site-lisp-root-cache dirs)
dirs)
snitch--site-lisp-root-cache))
(defun snitch--dir-in-site-lisp (dir)
"Check if DIR is in a site-lisp directory.
Check if directory DIR is a subdirectory of one of the
system-wide elisp directories found by
`snitch--site-lisp-roots'."
(not (null (cl-loop for site-dir in (snitch--site-lisp-roots)
if (string-prefix-p site-dir dir)
collect site-dir))))
(defun snitch--fill-package-dirs-cache ()
"Fill package directory cache.
Cache package directories in a hash table for faster subsequent
accesses."
(setq snitch--package-dirs-cache
(make-hash-table :test 'equal :size (length (package--alist))))
(cl-loop for (pkgname . pkgdesc) in (package--alist)
do
(puthash (file-name-as-directory (package-desc-dir (car pkgdesc)))
pkgname
snitch--package-dirs-cache))
(hash-table-count snitch--package-dirs-cache))
(defun snitch--package-from-dir (dir)
"Find package that owns directory DIR.
Given a directory DIR, returns a package that owns the files in
that directory."
(when (null snitch--package-dirs-cache)
(snitch--fill-package-dirs-cache))
(gethash (file-name-as-directory dir) snitch--package-dirs-cache))
(defun snitch--package-from-path (path)
"Try to guess a package name for PATH, a full path to a file.
Returns a symbol, which is either an installed package name, or
one of the following special values:
- `built-in' -- registered as a built-in package
- `site-lisp' -- found in a system-wide elisp directory
- `user' -- unknown source"
(let* ((dir (file-name-directory path))
(base (file-name-base (file-name-base path)))
(package (snitch--package-from-dir dir)))
(if package
package
(if (package-built-in-p (intern base))
'built-in
(if (snitch--dir-in-site-lisp dir)
'site-lisp
'user)))))
(defun snitch--maybe-add-timer-backtrace (bt timer)
"Try to add a saved timer backtrace to current backtrace.
If the given backtrace BT terminates in the timer execution
handler, check if snitch has cached the backtrace for the
executing timer, TIMER, and append that backtrace to BT."
(let ((last-fn (nth 0 (car bt)))
(reverse-bt (nreverse bt)))
(if (eq last-fn #'timer-event-handler)
(let ((t-bt (snitch--get-timer-backtrace timer)))
(nconc reverse-bt t-bt))
reverse-bt)))
(defun snitch--backtrace (&optional follow-timer)
"Return a backtrace usable by snitch.
Return a full list of backtrace entries (the full function call
stack) where each entry is a list containing (FUNCTION PATH
PACKAGE). Entries related to the snitch callstack are filtered
out.
FUNCTION is a function symbol if available, or one of the special
symbols ‘lambda’, ‘macro’, or ‘compiled-function’ otherwise.
PATH is the full path to the file FUNCTION is defined in, if
known.
PACKAGE is the package that FUNCTION is defined in, or one of the
special symbols ‘built-in’, ‘site-lisp’, ‘user’, or nil if
unknown.
FOLLOW-TIMER tells snitch to attempt to reconstruct a longer
backtrace if this one originated from a timer callback.
‘snitch-trace-timers’ must be t for this to have any effect. If
it is enabled, and a matching timer is found, the backtraces are
concatenated together."
(let* ((stack '())
(timer-args nil)
(frames (backtrace-get-frames))
(skip-frames (if (eq 'let* (backtrace-frame-fun (nth 1 frames)))
4
3)))
(dotimes (idx (length frames))
(if (> idx skip-frames)
(let* ((frame (nth idx frames))
(fun (backtrace-frame-fun frame))
(clean-fun (cond
((and (listp fun)
(eq (car fun) 'lambda))
'lambda)
((and (listp fun)
(eq (car fun) 'closure))
'lambda)
((macrop fun) 'macro)
((byte-code-function-p fun)
'compiled-function)
(t fun)))
(path (snitch--find-function-file fun))
(package (if path (snitch--package-from-path path) nil)))
(if (eq fun #'timer-event-handler)
(setq timer-args (car (backtrace-frame-args frame))))
(push (list clean-fun path package) stack))))
(if follow-timer
(snitch--maybe-add-timer-backtrace stack timer-args)
(nreverse stack))))
(defun snitch--package-type-more-important (a b)
"Determine if A is a more important package than B.
Return t if package type of 'a' is more important than the
package type of b, where:
- nil > nil
- built-in > nil, built-in
- site-lisp > nil, built-in, site-lisp
- user > nil, built-in, site-lisp
- package > nil, built-in, site-lisp, user
Noting that the first three are more important than themselves.
This is due to long chains of nil/built-in/site-lisp packages in
every backtrace, where typically the earliest one is the one that
started the chain.
On the other hand, for packages, we really want to focus on the
very last function that was responsible for triggering the rest
of the Emacs internal activity."
(cond
((null a) (member b (list nil)))
((eq 'built-in a) (member b (list nil 'built-in)))
((eq 'site-lisp a) (member b (list nil 'built-in 'site-lisp)))
((eq 'user a) (member b (list nil 'site-lisp 'built-in)))
((symbolp a) (member b (list nil 'site-lisp 'built-in 'user)))
(t nil)))
(defun snitch--responsible-caller (backtrace)
"Determine entry in backtrace responsible for the event.
Return a single entry from BACKTRACE which is snitch’s best guess
for which function on the stack frame should be considered
’responsible’ for causing this event. snitch uses this to assign
one single function/file/package as the responsible party for an
event, for use in filtering.
This is inherently fallible, based on prioritizing certain
function types and locations over others with some very primitive
heuristics. It is, however, deterministic."
(cl-loop for caller in backtrace with result = nil
when (and (snitch--package-type-more-important
(nth 2 caller)
(if (null result) nil
(nth 2 (car result))))
(not (and (eq (nth 2 caller) 'site-lisp)
(string-suffix-p "/startup.el" (nth 1 caller)))))
do
(push caller result)
finally return (car result)))
(provide 'snitch-backtrace)