changeset 698: | 96958d3eb5b0 |
parent: | 12287fab15d0 |
author: | Richard Westhaver <ellis@rwest.io> |
date: | Fri, 04 Oct 2024 22:04:59 -0400 |
permissions: | -rw-r--r-- |
description: | fixes |
215 | 1 | ;;; lib/rt/tracing.lisp --- Tracing Framework |
2 | ||
3 | ;; This package provides utilities for tracing Lisp code and |
|
4 | ;; displaying traces to the user. In addition to extending the |
|
5 | ;; built-in TRACE macro and SB-DEBUG functionality we have a tracer |
|
6 | ;; which works with Chrome's built-in tracer: chrome://trace. |
|
7 | ||
8 | ;; The chrome tracer is a slightly modernized version of TeMPOraL's |
|
9 | ;; work available here: https://github.com/TeMPOraL/tracer. |
|
10 | ||
11 | ;; ref: https://www.chromium.org/developers/how-tos/trace-event-profiling-tool/ |
|
12 | ||
13 | ;; - sb-debug manual: https://www.sbcl.org/manual/#Debugger |
|
14 | ||
15 | ;; - sb-debug notes: https://gist.github.com/nikodemus/659495 |
|
16 | ||
17 | ;;; Code: |
|
18 | (in-package :rt/tracing) |
|
19 | ||
20 | (defmacro traced-flet (functions &body body) |
|
21 | (flet ((add-traces (function) |
|
22 | (destructuring-bind |
|
23 | (name lambda-list &body body) function |
|
24 | `(,name ,lambda-list |
|
25 | ,@(cons `(format t |
|
26 | "Calling ~a with ~@{~a=~a, ~}~%" |
|
27 | ',name |
|
28 | ,@(loop for symbol in lambda-list |
|
29 | collecting `(quote ,symbol) |
|
30 | collecting symbol)) |
|
31 | body))))) |
|
32 | `(flet ,(mapcar #'add-traces functions) ,@body))) |
|
33 | ||
34 | ;;; This is an implementation of a chromium-based Lisp tracer authored |
|
35 | ;;; by TeMPOraL. The source is available here: |
|
36 | ;;; https://github.com/TeMPOraL/tracer/tree/master |
|
37 | ||
38 | (defvar *trace-events* nil "A list of trace entries, pushed onto from the beginning.") |
|
39 | ||
40 | (defvar *original-trace-start-breakpoint-fun* #'sb-debug::trace-start-breakpoint-fun "Original SBCL function being overwritten by the tracer.") |
|
41 | (defvar *original-trace-end-breakpoint-fun* #'sb-debug::trace-end-breakpoint-fun "Original SBCL function being overwritten by the tracer.") |
|
42 | ||
43 | (defvar *clock-reset-fun* nil) |
|
44 | (defvar *clock-get-time-fun* nil) |
|
45 | ||
46 | (defvar *trace-event-default-pid* 1 "The default value for PID for the trace events. This library is currently intended for use within a single process only.") |
|
47 | ||
48 | (defvar +arg-converter-ignore-all+ (constantly nil) "A converter that rejects all parameters.") |
|
49 | (defvar +arg-converter-passthrough+ (lambda (&rest args) args) "A converter that remembers all args as is, without modifying them.") |
|
50 | (defvar +arg-converter-store-only-simple-objects+ (lambda (&rest args) |
|
51 | (mapcar (lambda (arg) |
|
52 | (typecase arg |
|
53 | ((or boolean character number symbol) |
|
54 | arg) |
|
55 | (t |
|
56 | (type-of arg)))) |
|
57 | args)) |
|
58 | "A converter that remembers directly only objects of simple types, that cannot or are very unlikely to be destructively modified.") |
|
59 | (defvar +arg-converter-store-only-simple-objects-and-strings+ (lambda (&rest args) |
|
60 | (mapcar (lambda (arg) |
|
61 | (typecase arg |
|
62 | ((or boolean character number symbol string) |
|
63 | arg) |
|
64 | (t |
|
65 | (type-of arg)))) |
|
66 | args)) |
|
67 | "Like `+ARG-CONVERTER-STORE-ONLY-SIMPLE-OBJECTS+', but also records strings as-is, hoping they don't get destructively modified too much.") |
|
68 | ||
69 | (defvar *default-arg-converter* +arg-converter-ignore-all+) |
|
70 | (defvar *tracing-arg-converters* (make-hash-table :test 'equal)) |
|
71 | ||
72 | ||
73 | ||
74 | ;;; The format of trace event; created primarily for reference, though later on we might upgrade to vector storage, and then it'll come helpful. |
|
75 | (defstruct (trace-event (:type list)) |
|
76 | "A single event being traced. " |
|
77 | (phase :undefined :type keyword) |
|
78 | (name nil :type (or symbol cons)) |
|
79 | (thread 0 :type t) |
|
80 | (timestamp 0 :type fixnum) |
|
81 | (args nil :type t) |
|
82 | (duration 0 :type (or null (unsigned-byte 62))) |
|
83 | (id nil :type t)) |
|
84 | ||
85 | ;;; TODO: define accessors manually, to save performance? or somehow optimize it. -- Jacek Złydach, 2019-11-04 |
|
86 | ||
87 | (declaim (inline convert-args)) |
|
88 | (defun convert-args (traced-fn-name args) |
|
89 | "Depending on the function being traced, named `TRACED-FN-NAME', and the value of `*DEFAULT-ARG-CONVERTER*' |
|
90 | convert the list of arguments `ARGS' to a form suitable for storing with the trace event, using a converter |
|
91 | registered under `*TRACING-ARG-CONVERTERS*'. |
|
92 | Returns the representation of `ARGS' to store." |
|
93 | (declare (optimize (speed 3))) |
|
94 | (apply (the function (gethash traced-fn-name *tracing-arg-converters* *default-arg-converter*)) |
|
95 | args)) |
|
96 | ||
97 | (declaim (inline make-trace-event-fast)) |
|
98 | (defun make-trace-event-fast (phase name thread timestamp args duration id) |
|
99 | "Like `MAKE-TRACE-EVENT', but inlined, unsafe and without typechecking." |
|
100 | (declare (optimize (speed 3))) |
|
101 | (list phase name thread timestamp (convert-args name args) duration id)) |
|
102 | ||
103 | ;;; Timer |
|
104 | ||
105 | ;;; TODO: make it so that user can plug a high-resolution timer here. -- Jacek Złydach, 2019-10-18 |
|
106 | ||
107 | (sb-ext:defglobal *hack-clock-jitter* 0 "A crude hack because our clock has too little resolution.") |
|
108 | (declaim (type fixnum *hack-clock-jitter*)) |
|
109 | ||
110 | ;;; TODO: this needs to be a function that can be changed between invocations of tracing! |
|
111 | ;;; I want to allow for injecting higher resolution clocks if available. |
|
112 | ;;; -- Jacek Złydach, 2019-11-01 |
|
113 | ||
448 | 114 | (defun get-current-time-usec* () |
215 | 115 | "Get current time with microsecond resolution." |
116 | (sb-ext:atomic-incf *hack-clock-jitter*) |
|
448 | 117 | (the (unsigned-byte 62) |
118 | (+ (* (get-internal-real-time) 1000) |
|
119 | *hack-clock-jitter*))) |
|
215 | 120 | |
448 | 121 | (declaim (ftype (function () (unsigned-byte 62)) get-current-time-usec*) |
122 | (inline get-current-time-usec*)) |
|
215 | 123 | (defun get-current-time-usec-nojitter () |
124 | "Get current time with microsecond resolution. No extra jitter involved." |
|
125 | (declare (optimize (speed 3))) |
|
126 | (the (unsigned-byte 62) (* (get-internal-real-time) 1000))) |
|
127 | ||
128 | ;;; XXX: below is our new, usec clock -- Jacek Złydach, 2019-11-02 |
|
129 | (let ((clock-offset 0)) |
|
130 | (declare (type (unsigned-byte 62) clock-offset)) |
|
131 | (defun %%start-clock () |
|
132 | (setf clock-offset (sb-kernel::get-time-of-day))) |
|
133 | (defun %%get-time-usec () |
|
134 | (multiple-value-bind (sec usec) |
|
135 | (sb-kernel::get-time-of-day) |
|
136 | (+ (* (- sec clock-offset) 1000000) usec))) |
|
137 | (defun %%time (thunk) |
|
138 | (let ((start (%%get-time-usec))) |
|
139 | (funcall thunk) |
|
140 | (- (%%get-time-usec) start))) |
|
141 | (setf *clock-reset-fun* #'%%start-clock |
|
142 | *clock-get-time-fun* #'%%get-time-usec)) |
|
143 | ||
144 | (declaim (ftype (function () (values (unsigned-byte 62) &optional)) get-current-time) |
|
145 | (inline get-current-time)) |
|
146 | (defun get-current-time () |
|
147 | (funcall *clock-get-time-fun*)) |
|
148 | ||
149 | (defun post-process-entries (entries &key correct-zero-duration) |
|
150 | "Destructively modify `ENTRIES', making it more compact and, if `CORRECT-ZERO-DURATION' is T, |
|
151 | changing zero-length events to have 1us length, also modifying other times to offset for that. |
|
152 | `ENTRIES' is expected to be in order entries were added. The function maintain separate offsets per (process, thread) pair. |
|
153 | Returns a processed list, to replace old value `ENTRIES'. As additional values, returns the total accumulated clock offset, |
|
154 | and the stacks containing unclosed duration entries, keyed by thread." |
|
155 | (let ((offset 0) |
|
156 | (stacks (make-hash-table :test 'equal))) |
|
157 | (dolist (entry entries entries) |
|
158 | ;; Always update event time to account for clock offset. |
|
159 | (incf (trace-event-timestamp entry) offset) |
|
160 | ||
161 | ;; Match starting and ending events to offset clock in case of zero-length events, and to convert |
|
162 | ;; matching pairs of Duration events into Complete events. |
|
163 | (let ((entry-ht-id (cons 1 (trace-event-thread entry)))) ;1 is the currently supported PID |
|
164 | (ecase (trace-event-phase entry) |
|
165 | (:enter |
|
166 | ;; Push the :enter entry to stack. |
|
167 | (push entry (gethash entry-ht-id stacks))) |
|
168 | (:exit |
|
169 | (let ((begin-event (first (gethash entry-ht-id stacks)))) |
|
170 | (if (equalp (trace-event-name begin-event) |
|
171 | (trace-event-name entry)) |
|
172 | (progn |
|
173 | ;; Actual post-processing happens here. |
|
174 | ;; If zero-length and correct-zero-duration is on, update close time and offset. |
|
175 | (when (and correct-zero-duration |
|
176 | (= (trace-event-timestamp begin-event) |
|
177 | (trace-event-timestamp entry))) |
|
178 | (incf (trace-event-timestamp entry)) |
|
179 | (incf offset)) |
|
180 | ||
181 | ;; Convert task into complete task + counter |
|
182 | (setf (trace-event-phase begin-event) :complete |
|
183 | (trace-event-phase entry) :skip ;TODO: counters, if any, go here -- Jacek Złydach, 2019-11-04 |
|
184 | (trace-event-duration begin-event) (- (trace-event-timestamp entry) (trace-event-timestamp begin-event)) |
|
185 | (trace-event-args begin-event) `(:in ,(trace-event-args begin-event) :out ,(trace-event-args entry))) |
|
186 | ||
187 | ;; Pop the updated entry from stack. |
|
188 | (pop (gethash entry-ht-id stacks))) |
|
189 | (warn "Recorded entries misordered; expected ~A, got ~A." (trace-event-name begin-event) (trace-event-name entry)))))))) |
|
190 | ;; Go over the list again, and remove "skip" entries. |
|
191 | (deletef entries :skip :key #'trace-event-phase) |
|
192 | (values entries |
|
193 | offset |
|
194 | stacks))) |
|
195 | ||
196 | ;;; Tracing process |
|
197 | ||
198 | (defun %trace-start-breakpoint-fun (info) |
|
199 | (let (conditionp) |
|
200 | (values |
|
201 | (lambda (frame bpt &rest args) |
|
202 | (declare (ignore bpt)) |
|
203 | (sb-debug::discard-invalid-entries frame) |
|
204 | (let ((condition (sb-debug::trace-info-condition info)) |
|
205 | (wherein (sb-debug::trace-info-wherein info))) |
|
206 | (setq conditionp |
|
207 | (and (not sb-debug::*in-trace*) |
|
208 | (or (not condition) |
|
209 | (apply (cdr condition) frame args)) |
|
210 | (or (not wherein) |
|
211 | (sb-debug::trace-wherein-p frame wherein nil))))) |
|
212 | (when conditionp |
|
213 | (when (sb-debug::trace-info-encapsulated info) |
|
214 | (sb-ext:atomic-push (make-trace-event-fast :enter |
|
215 | (sb-debug::trace-info-what info) |
|
216 | sb-thread:*current-thread* |
|
217 | (get-current-time) |
|
218 | args |
|
219 | nil |
|
220 | nil) |
|
221 | *trace-events*)) |
|
222 | ;; TODO: perhaps remove this, it seems unneeded -- Jacek Złydach, 2019-11-05 |
|
223 | (with-standard-io-syntax |
|
224 | (apply #'sb-debug::trace-maybe-break info (sb-debug::trace-info-break info) "before" |
|
225 | frame args)))) |
|
226 | (lambda (frame cookie) |
|
227 | (declare (ignore frame)) |
|
228 | (push (cons cookie conditionp) sb-debug::*traced-entries*))))) |
|
229 | ||
230 | (declaim (ftype (function (sb-debug::trace-info) function) %trace-end-breakpoint-fun)) |
|
231 | (defun %trace-end-breakpoint-fun (info) |
|
232 | (lambda (frame bpt values cookie) |
|
233 | (declare (ignore bpt)) |
|
234 | (unless (eq cookie (caar sb-debug::*traced-entries*)) |
|
235 | (setf sb-debug::*traced-entries* |
|
236 | (member cookie sb-debug::*traced-entries* :key #'car))) |
|
237 | ||
238 | (let ((entry (pop sb-debug::*traced-entries*))) |
|
239 | (when (and (not (sb-debug::trace-info-untraced info)) |
|
240 | (or (cdr entry) |
|
241 | (let ((cond (sb-debug::trace-info-condition-after info))) |
|
242 | (and cond (apply #'funcall (cdr cond) frame values))))) |
|
243 | (sb-ext:atomic-push (make-trace-event-fast :exit |
|
244 | (sb-debug::trace-info-what info) |
|
245 | sb-thread:*current-thread* |
|
246 | (get-current-time) |
|
247 | values |
|
248 | nil |
|
249 | nil) |
|
250 | *trace-events*) |
|
251 | ||
252 | (apply #'sb-debug::trace-maybe-break info (sb-debug::trace-info-break-after info) "after" |
|
253 | frame values))))) |
|
254 | ||
255 | (defun install-tracing-overrides () |
|
256 | (sb-ext:unlock-package (find-package 'sb-debug)) |
|
257 | (setf (symbol-function 'sb-debug::trace-start-breakpoint-fun) #'%trace-start-breakpoint-fun |
|
258 | (symbol-function 'sb-debug::trace-end-breakpoint-fun) #'%trace-end-breakpoint-fun) |
|
259 | (sb-ext:lock-package (find-package 'sb-debug))) |
|
260 | ||
261 | (defun uninstall-tracing-overrides () |
|
262 | (sb-ext:unlock-package (find-package 'sb-debug)) |
|
263 | (setf (symbol-function 'sb-debug::trace-start-breakpoint-fun) *original-trace-start-breakpoint-fun* |
|
264 | (symbol-function 'sb-debug::trace-end-breakpoint-fun) *original-trace-end-breakpoint-fun*) |
|
265 | (sb-ext:lock-package (find-package 'sb-debug))) |
|
266 | ||
679
12287fab15d0
rocksdb load opts and env updates
Richard Westhaver <ellis@rwest.io>
parents:
448
diff
changeset
|
267 | (defun start-tracing (specs) |
215 | 268 | (install-tracing-overrides) |
269 | `(progn |
|
270 | (trace :encapsulate t :methods t ,@specs))) |
|
271 | ||
272 | (defun stop-tracing () |
|
273 | (untrace) |
|
274 | (uninstall-tracing-overrides) |
|
275 | #+nil(setf *trace-events* (nreverse *trace-events*)) |
|
276 | (multiple-value-bind (events offset stacks) |
|
277 | (post-process-entries (nreverse *trace-events*)) |
|
278 | (declare (ignore offset stacks)) |
|
279 | (setf *trace-events* events)) |
|
280 | ;; TODO: report offsets and stacks -- Jacek Złydach, 2019-11-05 |
|
281 | (values)) |
|
282 | ||
283 | (defun reset-tracing () |
|
284 | (setf *trace-events* nil |
|
285 | *hack-clock-jitter* 0)) |
|
286 | ||
287 | (defun get-tracing-report-data () |
|
288 | *trace-events*) |
|
289 | ||
290 | ;;; Trace operations: |
|
291 | ;;; 1. Reset |
|
292 | ;;; 2. Trace |
|
293 | ;;; 2.5 snapshot tracing? |
|
294 | ;;; 3. Stop tracing |
|
295 | ;;; 4. Save report |
|
296 | ||
297 | (defvar *tracing-p* nil "Is currently tracing activity happening?") |
|
298 | ||
299 | ;;; Trace info entry type, for function call |
|
300 | ;;; - Timestamp |
|
301 | ;;; - Function name |
|
302 | ;;; - Function args maybe? (trace-with-args), on enter |
|
303 | ;;; - Function return value, on exit |
|
304 | ;;; - Beginning or ending |
|
305 | ;;; - Thread ID |
|
306 | ||
307 | ||
308 | ||
309 | ;;; This prints a representation of the return values delivered. |
|
310 | ;;; First, this checks to see that cookie is at the top of |
|
311 | ;;; *TRACED-ENTRIES*; if it is not, then we need to adjust this list |
|
312 | ;;; to determine the correct indentation for output. We then check to |
|
313 | ;;; see whether the function is still traced and that the condition |
|
314 | ;;; succeeded before printing anything. |
|
315 | ||
316 | (defmacro with-tracing ((&rest specs) &body body) |
|
317 | `(unwind-protect |
|
318 | (progn |
|
319 | (reset-tracing) |
|
679
12287fab15d0
rocksdb load opts and env updates
Richard Westhaver <ellis@rwest.io>
parents:
448
diff
changeset
|
320 | (start-tracing ',specs) |
215 | 321 | (progn |
322 | ,@body)) |
|
323 | (stop-tracing))) |
|
324 | ||
325 | ||
326 | ||
327 | (defun function-name->name-and-category (function-name) |
|
328 | (etypecase function-name |
|
329 | (symbol |
|
330 | (values (symbol-name function-name) (package-name (symbol-package function-name)))) |
|
331 | (cons |
|
332 | (ecase (first function-name) |
|
333 | (setf |
|
334 | (values (format nil "~S" function-name) (package-name (symbol-package (second function-name))))) |
|
335 | ;; TODO investigate |
|
336 | ((method sb-pcl::combined-method) |
|
337 | (values (remove #\Newline (format nil "~S" function-name)) |
|
338 | (if (consp (second function-name)) |
|
339 | (package-name (symbol-package (second (second function-name)))) |
|
340 | (package-name (symbol-package (second function-name)))))))))) |
|
341 | ||
342 | (defgeneric post-process-arg (arg) |
|
343 | (:method ((arg t)) |
|
344 | "Passthrough method." |
|
345 | (or (ignore-errors |
|
346 | (prin1-to-string arg)) |
|
347 | "!!Error printing argument!!")) |
|
348 | (:documentation "A hook useful for changing the printed representation of input and return values.")) |
|
349 | ||
350 | (defmethod post-process-arg ((arg sequence)) |
|
351 | (if (every (lambda (el) (typep el 'number)) arg) |
|
352 | (format nil "[~{~F~^, ~}]" (coerce arg 'list)) |
|
353 | (call-next-method))) |
|
354 | ||
355 | ;;; FIXME: Something breaks if not collecting args, and :skip-args is NIL. Probably the getf in printing. -- Jacek Złydach, 2019-11-05 |
|
356 | (defun trace-event->json (trace-event &key (skip-args nil)) |
|
357 | (flet ((sanitize-and-format-args-list (argslist) |
|
358 | (if skip-args "\"_\"" |
|
359 | (substitute #\Space #\Newline (format nil "[~{~S~^, ~}]" (mapcar #'post-process-arg argslist)))))) |
|
360 | (ecase (trace-event-phase trace-event) |
|
361 | (:enter |
|
362 | (multiple-value-bind (name category) |
|
363 | (function-name->name-and-category (trace-event-name trace-event)) |
|
364 | (format nil |
|
365 | "{ \"name\" : ~S, \"cat\" : ~S, \"ph\" : \"B\", \"pid\" : 1, \"tid\" : ~D, \"ts\" : ~D, \"args\" : { \"in\" : ~A }}" |
|
366 | name |
|
367 | category |
|
368 | (sb-impl::get-lisp-obj-address (trace-event-thread trace-event)) |
|
369 | (trace-event-timestamp trace-event) |
|
370 | (sanitize-and-format-args-list (trace-event-args trace-event))))) |
|
371 | (:exit |
|
372 | (multiple-value-bind (name category) |
|
373 | (function-name->name-and-category (trace-event-name trace-event)) |
|
374 | (format nil |
|
375 | "{ \"name\" : ~S, \"cat\" : ~S, \"ph\" : \"E\", \"pid\" : 1, \"tid\" : ~D, \"ts\" : ~D, \"args\" : { \"out\" : ~A }}" |
|
376 | name |
|
377 | category |
|
378 | (sb-impl::get-lisp-obj-address (trace-event-thread trace-event)) |
|
379 | (trace-event-timestamp trace-event) |
|
380 | (sanitize-and-format-args-list (trace-event-args trace-event))))) |
|
381 | (:complete |
|
382 | (multiple-value-bind (name category) |
|
383 | (function-name->name-and-category (trace-event-name trace-event)) |
|
384 | (format nil |
|
385 | "{ \"name\" : ~S, \"cat\" : ~S, \"ph\" : \"X\", \"pid\" : 1, \"tid\" : ~D, \"ts\" : ~D, \"dur\" : ~D, \"args\" : { \"in\" : ~A, \"out\" : ~A }}" |
|
386 | name |
|
387 | category |
|
388 | (sb-impl::get-lisp-obj-address (trace-event-thread trace-event)) |
|
389 | (trace-event-timestamp trace-event) |
|
390 | (trace-event-duration trace-event) |
|
391 | (sanitize-and-format-args-list (getf (trace-event-args trace-event) :in)) |
|
392 | (sanitize-and-format-args-list (getf (trace-event-args trace-event) :out)))))))) |
|
393 | ||
394 | (defun thread->json (thread) |
|
395 | (format nil |
|
396 | "{ \"name\" : \"thread_name\", \"ph\" : \"M\", \"pid\" : 1, \"tid\" : ~D, \"args\" : { \"name\" : ~S }}" |
|
397 | (sb-impl::get-lisp-obj-address thread) |
|
398 | (sb-thread:thread-name thread))) |
|
399 | ||
400 | (defun extract-threads (events) |
|
401 | (loop |
|
402 | with uniques-ht = (make-hash-table :test #'eq) |
|
403 | for event in events |
|
404 | do |
|
405 | (setf (gethash (trace-event-thread event) uniques-ht) t) |
|
406 | finally |
|
407 | (return (hash-table-keys uniques-ht)))) |
|
408 | ||
409 | ;;; FIXME: save with streams instead? -- Jacek Złydach, 2019-10-14 |
|
410 | (defun save-report (output-file-name &key (skip-args t)) |
|
411 | (with-open-file (stream output-file-name :direction :output :if-exists :supersede) |
|
412 | ;; TODO: preamble -- Jacek Złydach, 2019-10-14 |
|
413 | (format stream "{~%\"traceEvents\" : [~%") |
|
414 | (loop |
|
415 | for (entry . restp) on *trace-events* |
|
416 | do |
|
417 | (write-string (trace-event->json entry :skip-args skip-args) stream) |
|
418 | (when restp |
|
419 | (write-string "," stream) |
|
420 | (terpri stream))) |
|
421 | (loop |
|
422 | for (thread . restp) on (extract-threads *trace-events*) |
|
423 | initially |
|
424 | (write-string "," stream) |
|
425 | (terpri stream) |
|
426 | do |
|
427 | (write-string (thread->json thread) stream) |
|
428 | (when restp |
|
429 | (write-string "," stream) |
|
430 | (terpri stream))) |
|
431 | ||
432 | (format stream "~&], |
|
433 | \"displayTimeUnit\" : \"ms\", |
|
434 | \"application\" : \"FIXME\", |
|
435 | \"version\" : \"FIXME\", |
|
436 | \"traceTime\" : ~S |
|
437 | }" |
|
438 | " TODO local-time independent time" |
|
439 | ;;(local-time:format-timestring nil (local-time:now)) |
|
440 | )) |
|
441 | (values)) |
|
442 | ||
443 | ||
444 | ||
445 | ;;; Helper function for blacklisting symbols when tracing whole packages. |
|
446 | (defun package-symbols-except (name &rest exceptions) |
|
447 | (let (symbols |
|
448 | (package (sb-impl::find-undeleted-package-or-lose name))) |
|
449 | (do-all-symbols (symbol (find-package name)) |
|
450 | (when (eql package (symbol-package symbol)) |
|
451 | (when (and (fboundp symbol) |
|
452 | (not (macro-function symbol)) |
|
453 | (not (special-operator-p symbol))) |
|
454 | (push symbol symbols)) |
|
455 | (let ((setf-name `(setf ,symbol))) |
|
456 | (when (fboundp setf-name) |
|
457 | (push setf-name symbols))))) |
|
458 | (set-difference symbols exceptions :key (lambda (x) |
|
459 | (if (consp x) |
|
460 | (string (second x)) |
|
461 | (string x))) :test #'string-equal))) |