(defpackage :swank-trace-dialog (:use :cl) (:import-from :swank :defslimefun :from-string :to-string) (:export #:clear-trace-tree #:dialog-toggle-trace #:dialog-trace #:dialog-traced-p #:dialog-untrace #:dialog-untrace-all #:inspect-trace-part #:report-partial-tree #:report-specs #:report-total #:report-trace-detail #:report-specs #:trace-format #:still-inside #:exited-non-locally #:*record-backtrace* #:*traces-per-report* #:*dialog-trace-follows-trace* #:find-trace-part #:find-trace)) (in-package :swank-trace-dialog) (defparameter *record-backtrace* nil "Record a backtrace of the last 20 calls for each trace. Beware that this may have a drastic performance impact on your program.") (defparameter *traces-per-report* 150 "Number of traces to report to emacs in each batch.") ;;;; `trace-entry' model ;;;; (defvar *traces* (make-array 1000 :fill-pointer 0 :adjustable t)) (defvar *trace-lock* (swank/backend:make-lock :name "swank-trace-dialog lock")) (defvar *current-trace-by-thread* (make-hash-table)) (defclass trace-entry () ((id :reader id-of) (children :accessor children-of :initform nil) (backtrace :accessor backtrace-of :initform (when *record-backtrace* (useful-backtrace))) (spec :initarg :spec :accessor spec-of :initform (error "must provide a spec")) (args :initarg :args :accessor args-of :initform (error "must provide args")) (parent :initarg :parent :reader parent-of :initform (error "must provide a parent, even if nil")) (retlist :initarg :retlist :accessor retlist-of :initform 'still-inside))) (defmethod initialize-instance :after ((entry trace-entry) &rest initargs) (declare (ignore initargs)) (if (parent-of entry) (nconc (children-of (parent-of entry)) (list entry))) (swank/backend:call-with-lock-held *trace-lock* #'(lambda () (setf (slot-value entry 'id) (fill-pointer *traces*)) (vector-push-extend entry *traces*)))) (defmethod print-object ((entry trace-entry) stream) (print-unreadable-object (entry stream) (format stream "~a: ~a" (id-of entry) (spec-of entry)))) (defun completed-p (trace) (not (eq (retlist-of trace) 'still-inside))) (defun find-trace (id) (when (<= 0 id (1- (length *traces*))) (aref *traces* id))) (defun find-trace-part (id part-id type) (let* ((trace (find-trace id)) (l (and trace (ecase type (:arg (args-of trace)) (:retval (swank::ensure-list (retlist-of trace))))))) (values (nth part-id l) (< part-id (length l))))) (defun useful-backtrace () (swank/backend:call-with-debugging-environment #'(lambda () (loop for i from 0 for frame in (swank/backend:compute-backtrace 0 20) collect (list i (swank::frame-to-string frame)))))) (defun current-trace () (gethash (swank/backend:current-thread) *current-trace-by-thread*)) (defun (setf current-trace) (trace) (setf (gethash (swank/backend:current-thread) *current-trace-by-thread*) trace)) ;;;; Control of traced specs ;;; (defvar *traced-specs* '()) (defslimefun dialog-trace (spec) (flet ((before-hook (args) (setf (current-trace) (make-instance 'trace-entry :spec spec :args args :parent (current-trace)))) (after-hook (retlist) (let ((trace (current-trace))) (when trace ;; the current trace might have been wiped away if the ;; user cleared the tree in the meantime. no biggie, ;; don't do anything. ;; (setf (retlist-of trace) retlist (current-trace) (parent-of trace)))))) (when (dialog-traced-p spec) (warn "~a is apparently already traced! Untracing and retracing." spec) (dialog-untrace spec)) (swank/backend:wrap spec 'trace-dialog :before #'before-hook :after #'after-hook) (pushnew spec *traced-specs*) (format nil "~a is now traced for trace dialog" spec))) (defslimefun dialog-untrace (spec) (swank/backend:unwrap spec 'trace-dialog) (setq *traced-specs* (remove spec *traced-specs* :test #'equal)) (format nil "~a is now untraced for trace dialog" spec)) (defslimefun dialog-toggle-trace (spec) (if (dialog-traced-p spec) (dialog-untrace spec) (dialog-trace spec))) (defslimefun dialog-traced-p (spec) (find spec *traced-specs* :test #'equal)) (defslimefun dialog-untrace-all () (untrace) (mapcar #'dialog-untrace *traced-specs*)) (defparameter *dialog-trace-follows-trace* nil) (setq swank:*after-toggle-trace-hook* #'(lambda (spec traced-p) (when *dialog-trace-follows-trace* (cond (traced-p (dialog-trace spec) "traced for trace dialog as well") (t (dialog-untrace spec) "untraced for the trace dialog as well"))))) ;;;; A special kind of trace call ;;; (defun trace-format (format-spec &rest format-args) "Make a string from FORMAT-SPEC and FORMAT-ARGS and as a trace." (let* ((line (apply #'format nil format-spec format-args))) (make-instance 'trace-entry :spec line :args format-args :parent (current-trace) :retlist nil))) ;;;; Reporting to emacs ;;; (defparameter *visitor-idx* 0) (defparameter *visitor-key* nil) (defvar *unfinished-traces* '()) (defun describe-trace-for-emacs (trace) `(,(id-of trace) ,(and (parent-of trace) (id-of (parent-of trace))) ,(spec-of trace) ,(loop for arg in (args-of trace) for i from 0 collect (list i (swank::to-line arg))) ,(loop for retval in (swank::ensure-list (retlist-of trace)) for i from 0 collect (list i (swank::to-line retval))))) (defslimefun report-partial-tree (key) (unless (equal key *visitor-key*) (setq *visitor-idx* 0 *visitor-key* key)) (let* ((recently-finished (loop with i = 0 for trace in *unfinished-traces* while (< i *traces-per-report*) when (completed-p trace) collect trace and do (incf i) (setq *unfinished-traces* (remove trace *unfinished-traces*)))) (new (loop for i from (length recently-finished) below *traces-per-report* while (< *visitor-idx* (length *traces*)) for trace = (aref *traces* *visitor-idx*) collect trace unless (completed-p trace) do (push trace *unfinished-traces*) do (incf *visitor-idx*)))) (list (mapcar #'describe-trace-for-emacs (append recently-finished new)) (- (length *traces*) *visitor-idx*) key))) (defslimefun report-trace-detail (trace-id) (swank::call-with-bindings swank::*inspector-printer-bindings* #'(lambda () (let ((trace (find-trace trace-id))) (when trace (append (describe-trace-for-emacs trace) (list (backtrace-of trace) (swank::to-line trace)))))))) (defslimefun report-specs () (sort (copy-list *traced-specs*) #'string< :key #'princ-to-string)) (defslimefun report-total () (length *traces*)) (defslimefun clear-trace-tree () (setf *current-trace-by-thread* (clrhash *current-trace-by-thread*) *visitor-key* nil *unfinished-traces* nil) (swank/backend:call-with-lock-held *trace-lock* #'(lambda () (setf (fill-pointer *traces*) 0))) nil) ;; HACK: `swank::*inspector-history*' is unbound by default and needs ;; a reset in that case so that it won't error `swank::inspect-object' ;; before any other object is inspected in the slime session. ;; (unless (boundp 'swank::*inspector-history*) (swank::reset-inspector)) (defslimefun inspect-trace-part (trace-id part-id type) (multiple-value-bind (obj found) (find-trace-part trace-id part-id type) (if found (swank::inspect-object obj) (error "No object found with ~a, ~a and ~a" trace-id part-id type)))) (provide :swank-trace-dialog)