#lang racket/base

;; The core profiler sample collector
;; (This module is a private tool for collecting profiling data, and should not
;; be used as is.)

(provide create-sampler)

(require errortrace/errortrace-key)

;; (cons sexp srcloc) -> (cons sexp srcloc)
;; abbreviate the expression for concise reports
;; we take the first symbol we can find, and wrap it in a stub expression
(define (errortrace-preprocess frame)
  (cons (and (car frame)
             (let loop ([e (car frame)])
               (cond [(symbol? e) (list e '...)]
                     [(pair? e) (loop (car e))]
                     [else (error 'errortrace-preprocess
                                  "unexpected frame: ~a" frame)])))
        (and (cdr frame)
             (apply srcloc (cdr frame)))))

;; create-sampler : creates a sample collector thread, which tracks the given
;; `to-track' value every `delay' seconds.
;; Uses errortrace annotations when #:use-errortrace? is specified, otherwise
;;   uses the native stack traces provided by `cms->context`.
;; * The input value can be either a thread (track just that thread), a
;;   custodian (track all threads managed by the custodian), or a list of
;;   threads and/or custodians.  If a custodian is given, it must be
;;   subordinate to the current custodian or to the given super custodian.
;;   Also optionally takes a list of continuation mark keys, which will be
;;   monitored in addition to the stack trace continuation mark key.
;; * The collected values are (<thread-id> <thread-time> . <stack>), where
;;   - The <thread-id> is an integer number identifying the thread, starting
;;     from 0.  If the collected data has thread ids in a 0..N range
;;     (exclusive) , then there were N threads that were observed.  (This can
;;     be relevant when tracking a custodian, where threads can change
;;     dynamically.)
;;   - The <thread-time> is the result of `current-process-milliseconds' for
;;     the thread that this sample came from.  Note that these numbers will not
;;     start at 0, since threads are likely to run before a sample is
;;     collected.
;;   - Finally, the <stack> part is a snapshot of the thread's stack, as
;;     grabbed by `continuation-mark-set->context'.  The values in these
;;     snapshots are interned to reduce memory load.
;;   The results are collected sequentially, so they're always sorted from the
;;   newest to the oldest.  Remember that these results should be considered
;;   private for the profiler collection, and can change when more information
;;   needs to be collected.
;; * Returns a "controller" function that accepts messages to control the
;;   sampler thread.  The current set of messages that the controller
;;   understands are:
;;   - 'pause and 'resume: stops or resumes collecting samples.  These messages
;;     can be nested.  Note that the thread will continue running it just won't
;;     collect snapshots.
;;   - 'stop: kills the collector thread.  Should be called when you have your
;;     data.  (There is no message to start a new sampler thread, although
;;     adding one will not be difficult.)
;;   - 'set-tracked! <new>: changes the thread/s and/or custodian/s to track.
;;     (Custodians should still be subordinate to the original one or to the
;;     given argument.)
;;   - 'set-delay! <new>: changes the sampling delay.  This means that we won't
;;     have a direct correlation between the number of samples and the time
;;     they represent -- but the samples are statistical snapshots anyway, and
;;     the results are not formulated in terms of time spent.  (The time spent
;;     could be added of course, but it is best to do that in terms of the
;;     start/stop times)
;;   - 'get-snapshots: returns the currently collected list of snapshots.  Note
;;     that this can be called multiple times, each will return the data that
;;     is collected up to that point in time.
;;   - 'get-custom-snapshots: returns the currently collected list of custom
;;     key snapshots. Returns a list of samples, where each sample is in the
;;     same format as the output of continuation-mark-set->list*.
(define (create-sampler to-track delay
                        [super-cust  (current-custodian)]
                        [custom-keys #f]
                        #:use-errortrace? [do-errortrace #f])
  ;; the collected data
  (define snapshots '())
  ;; listof (cons continuation-mark-key value/#f)
  (define custom-snapshots '())
  ;; intern the entries (which are (cons id/#f srcloc/#f))
  (define entry-table (make-hash))
  (define (intern-entry entry)
    (define key (or (cdr entry) (car entry)))
    (define en  (hash-ref entry-table key #f))
    (if en
      ;; ELI: is this sanity check needed?
      ;; (if (equal? en entry)
      ;;   en
      ;;   (error 'profile "internal error: assumption invalid"))
      en
      (begin (hash-set! entry-table key entry) entry)))
  (define (validate to-track who)
    (unless (or (not custom-keys) (list? custom-keys))
      (raise-type-error
       who "list of continuation mark keys" custom-keys))
    (let loop ([t to-track])
      (cond
        [(thread? t)]
        [(list? t) (for-each loop t)]
        [(not (custodian? t))
         (raise-type-error
          who "thread, custodian, or a list of threads/custodians" to-track)]
        ;; test that it's subordinate
        [(with-handlers ([exn:fail:contract? (λ (_) #t)])
           (custodian-managed-list t super-cust) #f)
         (error who "got an insubordinate custodian")])))
  (define paused 0)
  (define thread-id
    (let ([next-id 0] [t (make-weak-hasheq)])
      (λ (thd)
        (or (hash-ref t thd #f)
            (let ([id next-id])
              (set! next-id (add1 next-id))
              (hash-set! t thd id)
              id)))))

  ;; The sampler thread must hold its watched threads weakly
  ;; This way if the thread it's watching is dead---which means
  ;; it'll never run again---then it too can die.
  (define weak-to-track
    (let loop ([t to-track])
      (cond [(thread? t) (make-weak-box t)]
            [(list? t) (map loop t)]
            ;; cannot assume that it's a list: we might get other values from
            ;; a custodian managed list
            [else t])))

  (define (sampler)
    (sleep delay)
    (define run-again?
      (let loop ([t weak-to-track])
        (cond [(paused . > . 0) #t]
              [(eq? t sampler-thread) #f]
              [(thread? t)
               (when custom-keys
                 (set! custom-snapshots
                       (cons (continuation-mark-set->list*
                              (continuation-marks t)
                              custom-keys) ; frames
                             custom-snapshots)))
               (set! snapshots
                     (cons (list* (thread-id t)
                                  (current-process-milliseconds t)
                                  (if do-errortrace
                                      (for/list ([frame (in-list
                                                         (continuation-mark-set->list
                                                          (continuation-marks t)
                                                          errortrace-key))])
                                        (intern-entry (errortrace-preprocess frame)))
                                      (map intern-entry
                                           (continuation-mark-set->context
                                            (continuation-marks t)))))
                           snapshots))
               #t]
              [(custodian? t)
               (for-each loop (custodian-managed-list t super-cust))
               ;; Custodians may spawn new threads, which we will then track
               #t]
              ;; If weak box value is GC'd, we fall through and ignore it
              [(weak-box? t) (loop (weak-box-value t))]
              [(list? t) (ormap loop t)])))
    (when run-again?
      (sampler)))
  (define cpu-time   0)
  (define start-time (current-process-milliseconds))
  (define (add-time)
    (when (paused . <= . 0)
      (define cur (current-process-milliseconds))
      (set! cpu-time (+ cpu-time (- cur start-time)))
      (set! start-time cur)))
  (define (ignore-time)
    (when (paused . <= . 0)
      (set! start-time (current-process-milliseconds))))
  (define sampler-thread
    (begin (validate to-track 'create-sampler)
           (thread sampler)))
  ;; use a sema to avoid mutations from different threads, the sampler thread
  ;; is only reading these values so it doesn't use it.
  (define sema (make-semaphore 1))
  (define (sampler-controller msg [arg #f])
    (define-syntax-rule (w/sema body ...)
      (call-with-semaphore sema (λ () body ...)))
    (case msg
      [(pause)  (w/sema (add-time) (set! paused (add1 paused)))]
      [(resume) (w/sema (set! paused (sub1 paused)) (ignore-time))]
      [(stop)   (kill-thread sampler-thread) (add-time) (set! paused +inf.0)]
      [(set-tracked!) (validate arg 'sampler-controller)
                      (w/sema (set! to-track arg))]
      [(set-delay!)   (w/sema (set! delay arg))]
      [(get-snapshots) (add-time) (cons cpu-time snapshots)]
      [(get-custom-snapshots) custom-snapshots]
      [else (error 'sampler-controller "unknown message: ~e" msg)]))
  sampler-controller)
