[personal profile] kpreid

Apple's Sampler is a profiler based on the principle of periodically collecting the entire call stack of the executing threads, then summarizing these stacks to show what occurs frequently; primarily, as a tree, rooted at the bottom of the stack, where each node shows the number of times that call sequence was found on the stack.

SBCL's sb-sprof is a profiler which also collects call stacks, but its summary report is much less useful to me as it does not provide the per-branch counting; just top-of-stack frequencies and a caller/callee graph.

Therefore, I examined Sampler's file format and wrote code to generate it from sb-sprof's record.

Screenshot of Sampler with SB-SPROF data

The file is mixed text/binary, LF line endings. The grammar, as far as I've determined it, is:

  "@supersamplerV1.0" LF
  "@symboltableV1.1" LF
  (TAB int32<id> TAB int32<unknown> 
   TAB text<symbol> 
   TAB text<library-path> TAB text<library-path> LF)*
  "@end" LF
  (
    "@threadV1.0" TAB int16Hex<thread-id> LF
    (
      TAB int32<1> int32<0> int32<1> int32<count of stack-frame> (int32<stack-frame>)* LF
    )*
  )*
  "@end" LF

where by "int32" I mean a big-endian 32-bit (unsigned?) integer (i.e. four not-necessarily-ASCII bytes), and by "int16Hex" I mean a 16-bit integer in hexadecimal (i.e. four ASCII bytes).

"id" is an arbitrary identifier for this symbol. "unknown" is occasionally nonzero, but I don't know what it means. "symbol" is the name of a function/method found on the stack. "library-path" is the pathname to the object file it was loaded from (relative in the case of a standard framework, e.g. "Carbon.framework/HIToolbox.framework/HIToolbox").

"thread-id" is an identifier for the thread, which should occur as an "id" in the symbol table; the upper 16 bits evidently must be 0. Thread symbol table entries have a name and library path which is the string ("Thread_" int16<thread-id>); I have not confirmed whether this is necessary.

Each entry in a @thread block is one sampling of the whole stack of that thread. I do not know what the 1, 0, and 1 mean, but the fourth integer is the number of frames on the stack; immediately after are that many integers, each of which is an id from the symbol table.

Files generated from this structure are accepted by Sampler, but not always by Shark; I don't know why, and my attempt at tracking it down made it seem to depend on the size of the trace file.

Here is code to generate such a file from sb-sprof data; it should be loaded in the SB-SPROF package:

  (defun write-32 (index stream)
    (write-byte (ldb (byte 8 24) index) stream)
    (write-byte (ldb (byte 8 16) index) stream)
    (write-byte (ldb (byte 8 8) index) stream)
    (write-byte (ldb (byte 8 0) index) stream))

  (defun write-sampler-data (stream)
    (format stream "~&@supersamplerV1.0~%")
    (format stream "~&@symboltableV1.1~%")
    (let ((symbols (make-hash-table :test 'equal))
          (buffer))
      (loop with gen = #x00002121
            for i below (- (samples-index *samples*) 2) by 2
            for debug-info = (aref (samples-vector *samples*) i)
            do (unless (or (member debug-info '(trace-start))
                           (gethash debug-info symbols))
                 (let ((index (incf gen)))
                   (setf (gethash debug-info symbols) index)
                   (write-char #\Tab stream)
                   (write-32 index stream)
                   (write-char #\Tab stream)
                   (write-32 0 stream)
                   (let* ((*print-right-margin* nil)
                          (*print-miser-width* nil)
                          (name (princ-to-string 
                                  (if debug-info
                                    (node-name (make-node debug-info))
                                    (format nil "Thread"))))
                          (real-info
                           (when (typep debug-info 'sb-di::compiled-debug-fun)
                             (sb-di::compiled-debug-fun-debug-info debug-info)))
                          (source
                           (when real-info
                             (sb-c::debug-info-source real-info)))
                          (pathname
                            (when (and source
                                       (eql (sb-c::debug-source-from source) :file))
                              (sb-c::debug-source-name source))))
                     (nsubstitute #\/ #\Newline name)
                     (format stream "~C~A~C~A~C~A~%"
                             #\Tab name
                             #\Tab pathname
                             #\Tab pathname)))))
      (format stream "~&@threadV1.0~C~4,'0X~%" #\Tab (gethash nil symbols))
      (flet ((out ()
               (when buffer
                 (write-char #\Tab stream)
                 (write-32 1 stream)
                 (write-32 0 stream)
                 (write-32 1 stream)
                 (write-32 (length buffer) stream)
                 (dolist (sym buffer)
                   (write-32 sym stream))
                 (terpri stream)
                 (setf buffer nil))))
        (loop for i downfrom (- (samples-index *samples*) 2) to 0 by 2
              for debug-info = (aref (samples-vector *samples*) i)
              do (cond
                   ((eql debug-info 'trace-start)
                    (out))
                   (t
                    (push (or (gethash debug-info symbols)
                              (error "No symbol assigned to ~S" debug-info))
                          buffer))))
        (out)))
    (format stream "~&@end~%"))

Usage:

(with-open-file (s "sampler.trace" :direction :output 
                                   :element-type :default
                                   :if-exists :supersede)
  (sb-sprof::write-sampler-data s))

This code generates a noninteractive Sampler-style tree report from SB-SPROF data.

  (defun make-bucket ()
    (list 0 (make-hash-table :test 'equal)))

  (defun put (bucket name)
    (let ((h (second bucket)))
      (or (gethash name h)
          (setf (gethash name h) (make-bucket)))))

  (defun build-tree ()
    (loop with root = (make-bucket)
          with current = (list root)
          for i downfrom (- (samples-index *samples*) 2) to 0 by 2
          for debug-info = (aref (samples-vector *samples*) i)
          do (cond
               ((eql debug-info 'trace-start)
                (dolist (bucket current)
                  (incf (first bucket)))
                (setf current (list root)))
               (t
                (let ((name (node-name (make-node debug-info)) #|kludge|#))
                  (push (put (first current) name) current))))
          finally (return root)))

  (defun twiddle-tree (bucket)
    (let (entries)
      (maphash (lambda (k v) (push (list* :i k (twiddle-tree v)) entries))
               (second bucket))
      (list (first bucket) entries)))

(Usage for printing a report: (twiddle-tree (build-tree)).)

This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org