Coverage report: /home/ellis/comp/core/lib/rt/tracing.lisp
Kind | Covered | All | % |
expression | 90 | 484 | 18.6 |
branch | 0 | 20 | 0.0 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; lib/rt/tracing.lisp --- Tracing Framework
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.
8
;; The chrome tracer is a slightly modernized version of TeMPOraL's
9
;; work available here: https://github.com/TeMPOraL/tracer.
11
;; ref: https://www.chromium.org/developers/how-tos/trace-event-profiling-tool/
13
;; - sb-debug manual: https://www.sbcl.org/manual/#Debugger
15
;; - sb-debug notes: https://gist.github.com/nikodemus/659495
18
(in-package :rt/tracing)
20
(defmacro traced-flet (functions &body body)
21
(flet ((add-traces (function)
23
(name lambda-list &body body) function
26
"Calling ~a with ~@{~a=~a, ~}~%"
28
,@(loop for symbol in lambda-list
29
collecting `(quote ,symbol)
32
`(flet ,(mapcar #'add-traces functions) ,@body)))
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
38
(defvar *trace-events* nil "A list of trace entries, pushed onto from the beginning.")
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.")
43
(defvar *clock-reset-fun* nil)
44
(defvar *clock-get-time-fun* nil)
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.")
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)
53
((or boolean character number symbol)
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)
62
((or boolean character number symbol string)
67
"Like `+ARG-CONVERTER-STORE-ONLY-SIMPLE-OBJECTS+', but also records strings as-is, hoping they don't get destructively modified too much.")
69
(defvar *default-arg-converter* +arg-converter-ignore-all+)
70
(defvar *tracing-arg-converters* (make-hash-table :test 'equal))
72
;;; The format of trace event; created primarily for reference, though later on we might upgrade to vector storage, and then it'll come helpful.
73
(defstruct (trace-event (:type list))
74
"A single event being traced. "
75
(phase :undefined :type keyword)
76
(name nil :type (or symbol cons))
78
(timestamp 0 :type fixnum)
80
(duration 0 :type (or null (unsigned-byte 62)))
83
;;; TODO: define accessors manually, to save performance? or somehow optimize it. -- Jacek Złydach, 2019-11-04
85
(declaim (inline convert-args))
86
(defun convert-args (traced-fn-name args)
87
"Depending on the function being traced, named `TRACED-FN-NAME', and the value of `*DEFAULT-ARG-CONVERTER*'
88
convert the list of arguments `ARGS' to a form suitable for storing with the trace event, using a converter
89
registered under `*TRACING-ARG-CONVERTERS*'.
90
Returns the representation of `ARGS' to store."
91
(declare (optimize (speed 3)))
92
(apply (the function (gethash traced-fn-name *tracing-arg-converters* *default-arg-converter*))
95
(declaim (inline make-trace-event-fast))
96
(defun make-trace-event-fast (phase name thread timestamp args duration id)
97
"Like `MAKE-TRACE-EVENT', but inlined, unsafe and without typechecking."
98
(declare (optimize (speed 3)))
99
(list phase name thread timestamp (convert-args name args) duration id))
103
;;; TODO: make it so that user can plug a high-resolution timer here. -- Jacek Złydach, 2019-10-18
105
(sb-ext:defglobal *hack-clock-jitter* 0 "A crude hack because our clock has too little resolution.")
106
(declaim (type fixnum *hack-clock-jitter*))
108
;;; TODO: this needs to be a function that can be changed between invocations of tracing!
109
;;; I want to allow for injecting higher resolution clocks if available.
110
;;; -- Jacek Złydach, 2019-11-01
112
(defun get-current-time-usec* ()
113
"Get current time with microsecond resolution."
114
(sb-ext:atomic-incf *hack-clock-jitter*)
115
(the (unsigned-byte 62)
116
(+ (* (get-internal-real-time) 1000)
117
*hack-clock-jitter*)))
119
(declaim (ftype (function () (unsigned-byte 62)) get-current-time-usec*)
120
(inline get-current-time-usec*))
121
(defun get-current-time-usec-nojitter ()
122
"Get current time with microsecond resolution. No extra jitter involved."
123
(declare (optimize (speed 3)))
124
(the (unsigned-byte 62) (* (get-internal-real-time) 1000)))
126
;;; XXX: below is our new, usec clock -- Jacek Złydach, 2019-11-02
127
(let ((clock-offset 0))
128
(declare (type (unsigned-byte 62) clock-offset))
129
(defun %%start-clock ()
130
(setf clock-offset (sb-kernel::get-time-of-day)))
131
(defun %%get-time-usec ()
132
(multiple-value-bind (sec usec)
133
(sb-kernel::get-time-of-day)
134
(+ (* (- sec clock-offset) 1000000) usec)))
135
(defun %%time (thunk)
136
(let ((start (%%get-time-usec)))
138
(- (%%get-time-usec) start)))
139
(setf *clock-reset-fun* #'%%start-clock
140
*clock-get-time-fun* #'%%get-time-usec))
142
(declaim (ftype (function () (values (unsigned-byte 62) &optional)) get-current-time)
143
(inline get-current-time))
144
(defun get-current-time ()
145
(funcall *clock-get-time-fun*))
147
(defun post-process-entries (entries &key correct-zero-duration)
148
"Destructively modify `ENTRIES', making it more compact and, if `CORRECT-ZERO-DURATION' is T,
149
changing zero-length events to have 1us length, also modifying other times to offset for that.
150
`ENTRIES' is expected to be in order entries were added. The function maintain separate offsets per (process, thread) pair.
151
Returns a processed list, to replace old value `ENTRIES'. As additional values, returns the total accumulated clock offset,
152
and the stacks containing unclosed duration entries, keyed by thread."
154
(stacks (make-hash-table :test 'equal)))
155
(dolist (entry entries entries)
156
;; Always update event time to account for clock offset.
157
(incf (trace-event-timestamp entry) offset)
159
;; Match starting and ending events to offset clock in case of zero-length events, and to convert
160
;; matching pairs of Duration events into Complete events.
161
(let ((entry-ht-id (cons 1 (trace-event-thread entry)))) ;1 is the currently supported PID
162
(ecase (trace-event-phase entry)
164
;; Push the :enter entry to stack.
165
(push entry (gethash entry-ht-id stacks)))
167
(let ((begin-event (first (gethash entry-ht-id stacks))))
168
(if (equalp (trace-event-name begin-event)
169
(trace-event-name entry))
171
;; Actual post-processing happens here.
172
;; If zero-length and correct-zero-duration is on, update close time and offset.
173
(when (and correct-zero-duration
174
(= (trace-event-timestamp begin-event)
175
(trace-event-timestamp entry)))
176
(incf (trace-event-timestamp entry))
179
;; Convert task into complete task + counter
180
(setf (trace-event-phase begin-event) :complete
181
(trace-event-phase entry) :skip ;TODO: counters, if any, go here -- Jacek Złydach, 2019-11-04
182
(trace-event-duration begin-event) (- (trace-event-timestamp entry) (trace-event-timestamp begin-event))
183
(trace-event-args begin-event) `(:in ,(trace-event-args begin-event) :out ,(trace-event-args entry)))
185
;; Pop the updated entry from stack.
186
(pop (gethash entry-ht-id stacks)))
187
(warn "Recorded entries misordered; expected ~A, got ~A." (trace-event-name begin-event) (trace-event-name entry))))))))
188
;; Go over the list again, and remove "skip" entries.
189
(deletef entries :skip :key #'trace-event-phase)
196
(defun %trace-start-breakpoint-fun (info)
199
(lambda (frame bpt &rest args)
200
(declare (ignore bpt))
201
(sb-debug::discard-invalid-entries frame)
202
(let ((condition (sb-debug::trace-info-condition info))
203
(wherein (sb-debug::trace-info-wherein info)))
205
(and (not sb-debug::*in-trace*)
207
(apply (cdr condition) frame args))
209
(sb-debug::trace-wherein-p frame wherein nil)))))
211
(when (sb-debug::trace-info-encapsulated info)
212
(sb-ext:atomic-push (make-trace-event-fast :enter
213
(sb-debug::trace-info-what info)
214
sb-thread:*current-thread*
220
;; TODO: perhaps remove this, it seems unneeded -- Jacek Złydach, 2019-11-05
221
(with-standard-io-syntax
222
(apply #'sb-debug::trace-maybe-break info (sb-debug::trace-info-break info) "before"
224
(lambda (frame cookie)
225
(declare (ignore frame))
226
(push (cons cookie conditionp) sb-debug::*traced-entries*)))))
228
(declaim (ftype (function (sb-debug::trace-info) function) %trace-end-breakpoint-fun))
229
(defun %trace-end-breakpoint-fun (info)
230
(lambda (frame bpt values cookie)
231
(declare (ignore bpt))
232
(unless (eq cookie (caar sb-debug::*traced-entries*))
233
(setf sb-debug::*traced-entries*
234
(member cookie sb-debug::*traced-entries* :key #'car)))
236
(let ((entry (pop sb-debug::*traced-entries*)))
237
(when (and (not (sb-debug::trace-info-untraced info))
239
(let ((cond (sb-debug::trace-info-condition-after info)))
240
(and cond (apply #'funcall (cdr cond) frame values)))))
241
(sb-ext:atomic-push (make-trace-event-fast :exit
242
(sb-debug::trace-info-what info)
243
sb-thread:*current-thread*
250
(apply #'sb-debug::trace-maybe-break info (sb-debug::trace-info-break-after info) "after"
253
(defun install-tracing-overrides ()
254
(sb-ext:unlock-package (find-package 'sb-debug))
255
(setf (symbol-function 'sb-debug::trace-start-breakpoint-fun) #'%trace-start-breakpoint-fun
256
(symbol-function 'sb-debug::trace-end-breakpoint-fun) #'%trace-end-breakpoint-fun)
257
(sb-ext:lock-package (find-package 'sb-debug)))
259
(defun uninstall-tracing-overrides ()
260
(sb-ext:unlock-package (find-package 'sb-debug))
261
(setf (symbol-function 'sb-debug::trace-start-breakpoint-fun) *original-trace-start-breakpoint-fun*
262
(symbol-function 'sb-debug::trace-end-breakpoint-fun) *original-trace-end-breakpoint-fun*)
263
(sb-ext:lock-package (find-package 'sb-debug)))
265
(defun start-tracing (specs)
266
(install-tracing-overrides)
268
(trace :encapsulate :default :methods t ,@specs)))
270
(defun stop-tracing ()
272
(uninstall-tracing-overrides)
273
#+nil(setf *trace-events* (nreverse *trace-events*))
274
(multiple-value-bind (events offset stacks)
275
(post-process-entries (nreverse *trace-events*))
276
(declare (ignore offset stacks))
277
(setf *trace-events* events))
278
;; TODO: report offsets and stacks -- Jacek Złydach, 2019-11-05
281
(defun reset-tracing ()
282
(setf *trace-events* nil
283
*hack-clock-jitter* 0))
285
(defun get-tracing-report-data ()
288
;;; Trace operations:
291
;;; 2.5 snapshot tracing?
295
(defvar *tracing-p* nil "Is currently tracing activity happening?")
297
;;; Trace info entry type, for function call
300
;;; - Function args maybe? (trace-with-args), on enter
301
;;; - Function return value, on exit
302
;;; - Beginning or ending
305
;;; This prints a representation of the return values delivered.
306
;;; First, this checks to see that cookie is at the top of
307
;;; *TRACED-ENTRIES*; if it is not, then we need to adjust this list
308
;;; to determine the correct indentation for output. We then check to
309
;;; see whether the function is still traced and that the condition
310
;;; succeeded before printing anything.
312
(defmacro with-tracing (specs &body body)
316
(start-tracing ',specs)
321
(defun function-name->name-and-category (function-name)
322
(etypecase function-name
324
(values (symbol-name function-name) (package-name (symbol-package function-name))))
326
(ecase (first function-name)
328
(values (format nil "~S" function-name) (package-name (symbol-package (second function-name)))))
330
((method sb-pcl::combined-method)
331
(values (remove #\Newline (format nil "~S" function-name))
332
(if (consp (second function-name))
333
(package-name (symbol-package (second (second function-name))))
334
(package-name (symbol-package (second function-name))))))))))
336
(defgeneric post-process-arg (arg)
338
"Passthrough method."
340
(prin1-to-string arg))
341
"!!Error printing argument!!"))
342
(:documentation "A hook useful for changing the printed representation of input and return values."))
344
(defmethod post-process-arg ((arg sequence))
345
(if (every (lambda (el) (typep el 'number)) arg)
346
(format nil "[~{~F~^, ~}]" (coerce arg 'list))
349
;;; FIXME: Something breaks if not collecting args, and :skip-args is NIL. Probably the getf in printing. -- Jacek Złydach, 2019-11-05
350
(defun trace-event->json (trace-event &key (skip-args nil))
351
(flet ((sanitize-and-format-args-list (argslist)
352
(if skip-args "\"_\""
353
(substitute #\Space #\Newline (format nil "[~{~S~^, ~}]" (mapcar #'post-process-arg argslist))))))
354
(ecase (trace-event-phase trace-event)
356
(multiple-value-bind (name category)
357
(function-name->name-and-category (trace-event-name trace-event))
359
"{ \"name\" : ~S, \"cat\" : ~S, \"ph\" : \"B\", \"pid\" : 1, \"tid\" : ~D, \"ts\" : ~D, \"args\" : { \"in\" : ~A }}"
362
(sb-impl::get-lisp-obj-address (trace-event-thread trace-event))
363
(trace-event-timestamp trace-event)
364
(sanitize-and-format-args-list (trace-event-args trace-event)))))
366
(multiple-value-bind (name category)
367
(function-name->name-and-category (trace-event-name trace-event))
369
"{ \"name\" : ~S, \"cat\" : ~S, \"ph\" : \"E\", \"pid\" : 1, \"tid\" : ~D, \"ts\" : ~D, \"args\" : { \"out\" : ~A }}"
372
(sb-impl::get-lisp-obj-address (trace-event-thread trace-event))
373
(trace-event-timestamp trace-event)
374
(sanitize-and-format-args-list (trace-event-args trace-event)))))
376
(multiple-value-bind (name category)
377
(function-name->name-and-category (trace-event-name trace-event))
379
"{ \"name\" : ~S, \"cat\" : ~S, \"ph\" : \"X\", \"pid\" : 1, \"tid\" : ~D, \"ts\" : ~D, \"dur\" : ~D, \"args\" : { \"in\" : ~A, \"out\" : ~A }}"
382
(sb-impl::get-lisp-obj-address (trace-event-thread trace-event))
383
(trace-event-timestamp trace-event)
384
(trace-event-duration trace-event)
385
(sanitize-and-format-args-list (getf (trace-event-args trace-event) :in))
386
(sanitize-and-format-args-list (getf (trace-event-args trace-event) :out))))))))
388
(defun thread->json (thread)
390
"{ \"name\" : \"thread_name\", \"ph\" : \"M\", \"pid\" : 1, \"tid\" : ~D, \"args\" : { \"name\" : ~S }}"
391
(sb-impl::get-lisp-obj-address thread)
392
(sb-thread:thread-name thread)))
394
(defun extract-threads (events)
396
with uniques-ht = (make-hash-table :test #'eq)
399
(setf (gethash (trace-event-thread event) uniques-ht) t)
401
(return (hash-table-keys uniques-ht))))
403
;;; FIXME: save with streams instead? -- Jacek Złydach, 2019-10-14
404
(defun save-report (output-file-name &key (skip-args t))
405
(with-open-file (stream output-file-name :direction :output :if-exists :supersede)
406
;; TODO: preamble -- Jacek Złydach, 2019-10-14
407
(format stream "{~%\"traceEvents\" : [~%")
409
for (entry . restp) on *trace-events*
411
(write-string (trace-event->json entry :skip-args skip-args) stream)
413
(write-string "," stream)
416
for (thread . restp) on (extract-threads *trace-events*)
418
(write-string "," stream)
421
(write-string (thread->json thread) stream)
423
(write-string "," stream)
427
\"displayTimeUnit\" : \"ms\",
428
\"application\" : \"FIXME\",
429
\"version\" : \"FIXME\",
432
(time:format-timestring nil (time:now))