
;; Poor man's stack-trace-on-exceptions/profiler.
;; See doc.txt for information.

(module errortrace-lib mzscheme
  (require "stacktrace.ss"
           "errortrace-key.ss"
           (lib "list.ss")
           (lib "unitsig.ss"))

  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;; Test coverage run-time support
  (define test-coverage-enabled (make-parameter #f))

  (define test-coverage-info (make-hash-table))

  (define (initialize-test-coverage-point key expr)
    (hash-table-put! test-coverage-info key (list #f expr)))

  (define (test-covered key)
    (let ([v (hash-table-get test-coverage-info key)])
      (set-car! v #t)))

  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;; Profiling run-time support

  (define profile-thread #f)
  (define profile-key (gensym))

  (define profiling-enabled (make-parameter #f))
  (define profiling-record-enabled (make-parameter #t))
  (define profile-paths-enabled (make-parameter #f))

  (define profile-info (make-hash-table))

  (define (initialize-profile-point key name expr)
    (hash-table-put! profile-info key
                     (list (box #f) 0 0 (and name (syntax-e name)) expr null)))

  (define (register-profile-start key)
    (and (profiling-record-enabled)
         (let ([v (hash-table-get profile-info key)])
           (let ([b (car v)]
                 [v (cdr v)])
             (set-car! v (add1 (car v)))
             (when (profile-paths-enabled)
               (let ([v (cdddr v)])
                 (set-car! v (cons (current-continuation-marks profile-key)
                                   (car v)))))
             (if (unbox b)
                 #f
                 (begin
                   (set-box! b #t)
                   (current-process-milliseconds)))))))

  (define (register-profile-done key start)
    (when start
      (let ([v (hash-table-get profile-info key)])
        (let ([b (car v)]
              [v (cddr v)])
          (set-box! b #f)
          (let ([v (cddr (hash-table-get profile-info key))])
            (set-car! v (+ (- (current-process-milliseconds) start)
                           (car v))))))))

  (define (get-profile-results)
    (hash-table-map profile-info
      (lambda (key val)
        (let ([count (cadr val)]
              [time (caddr val)]
              [name (cadddr val)]
              [expr (cadddr (cdr val))]
              [cmss (cadddr (cddr val))])
          (list count time name expr
                (map (lambda (cms)
                       (map (lambda (k)
                              (let ([v (cdr (hash-table-get profile-info k))])
                                (list (caddr v) (cadddr v))))
                            cms))
                     cmss))))))

  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;; Stacktrace instrumenter

  (define dynamic-errortrace-key
    (dynamic-require '(lib "errortrace-key-syntax.ss" "errortrace")
                     'errortrace-key-syntax))

  ;; with-mark : stx stx -> stx
  (define (with-mark mark expr)
    (with-syntax ([expr expr]
                  [loc (make-st-mark mark)]
                  [et-key dynamic-errortrace-key])
      (execute-point
       mark
       (syntax
        (with-continuation-mark
            et-key
            loc
          expr)))))

  (define-values/invoke-unit/sig
    stacktrace^ stacktrace@ #f stacktrace-imports^)

  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;; Execute counts

  (define execute-info (make-hash-table))

  (define execute-counts-enabled (make-parameter #f))

  (define (register-executed-once key)
    (let ([i (hash-table-get execute-info key)])
      (set-cdr! i (add1 (cdr i)))))

  (define (execute-point mark expr)
    (if (execute-counts-enabled)
        (let ([key (gensym)])
          (hash-table-put! execute-info key (cons mark 0))
          (with-syntax ([key (datum->syntax-object #f key (quote-syntax here))]
                        [expr expr]
                        [register-executed-once register-executed-once]);<- 3D!
            (syntax
             (begin
               (register-executed-once 'key)
               expr))))
        expr))

  (define (get-execute-counts)
    (hash-table-map execute-info (lambda (k v) v)))

  (define (annotate-executed-file name)
    (let ([name (path->complete-path name (current-directory))])
      (let ([here (filter (lambda (s)
                            (and (equal? name (syntax-source (car s)))
                                 (syntax-position (car s))))
                          (get-execute-counts))])
        (let ([sorted
               (quicksort
                here
                (lambda (a b)
                  (let ([ap (syntax-position (car a))]
                        [bp (syntax-position (car b))])
                    (or (< ap bp) ; earlier first
                        (and (= ap bp)
                             (let ([as (syntax-span (car a))]
                                   [bs (syntax-span (car b))])
                               (or (> as bs) ; wider first at same pos
                                   (and (= as bs)
                                        ;; less called for same region last
                                        (> (cdr a) (cdr b))))))))))]
              [pic (make-string (file-size name) #\space)])
          ;; fill out picture:
          (for-each (lambda (s)
                      (let ([pos (sub1 (syntax-position (car s)))]
                            [span (syntax-span (car s))]
                            [key (let ([c (cdr s)])
                                   (cond
                                    [(zero? c) #\^]
                                    [(= c 1) #\.]
                                    [else #\,]))])
                        (let loop ([p pos])
                          (unless (= p (+ pos span))
                            (string-set! pic p key)
                            (loop (add1 p))))))
                    sorted)
          ;; Write annotated file
          (with-input-from-file name
            (lambda ()
              (let loop ()
                (let ([pos (file-position (current-input-port))]
                      [line (read-line (current-input-port) 'any)])
                  (unless (eof-object? line)
                    (printf "~a~n" line)
                    (let ([w (string-length line)])
                      ;; Blank out leading spaces in pic:
                      (let loop ([i 0])
                        (cond
                         [(and (< i w)
                               (char-whitespace? (string-ref line i)))
                          (string-set! pic (+ pos i) (string-ref line i))
                          (loop (add1 i))]))
                      (printf "~a~n" (substring pic pos (+ pos w))))
                    (loop))))))))))

  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;; Eval handler, exception handler

  (define instrumenting-enabled
    (make-parameter #t))
  (define error-context-display-depth
    (make-parameter 10000 (lambda (x) (and (integer? x) x))))

  ;; port exn -> void
  ;; effect: prints out the context surrounding the exception
  (define (print-error-trace p x)
    (let loop ([n (error-context-display-depth)]
               [l (map st-mark-source
                       (continuation-mark-set->list (exn-continuation-marks x)
                                                    errortrace-key))])
      (cond
        [(or (zero? n) (null? l)) (void)]
        [(pair? l)
         (let* ([stx (car l)]
                [source (syntax-source stx)]
                [file (cond
                       [(string? source) source]
                       [(path? source)
                        (path->string source)]
                       [(not source)
                        #f]
                       [else
                        (format "~a" source)])]
                [line (syntax-line stx)]
                [col (syntax-column stx)]
                [pos (syntax-position stx)])
           (fprintf p "~a~a: ~e~n"
                    (or file "[unknown source]")
                    (cond
                     [line (format ":~a:~a" line col)]
                     [pos (format "::~a" pos)]
                     [else ""])
                    (syntax-object->datum stx))
           (loop (- n 1) (cdr l)))])))

  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;; Profile printer

  (define (output-profile-results paths? sort-time?)
    (profiling-enabled #f)
    (error-print-width 50)
    (printf "Sorting profile data...~n")
    (let* ([sel (if sort-time? cadr car)]
           [counts (quicksort (filter (lambda (c) (positive? (car c)))
                                      (get-profile-results))
                              (lambda (a b) (< (sel a) (sel b))))]
           [total 0])
      (for-each
       (lambda (c)
         (set! total (+ total (sel c)))
         (printf "=========================================================~n")
         (printf "time = ~a : no. = ~a : ~e in ~s~n"
                 (cadr c) (car c) (caddr c) (cadddr c))
         ;; print call paths
         (when paths?
           (for-each
            (lambda (cms)
              (unless (null? cms)
                (printf "  VIA ~e" (caar cms))
                (for-each
                 (lambda (cm)
                   (printf " <- ~e" (car cm)))
                 (cdr cms))
                (printf "~n")))
            (cadddr (cdr c)))))
       counts)
      (printf "Total samples: ~a~n" total)))

  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

  (define errortrace-annotate
    (lambda (top-e)
      (define (normal e)
        (let ([ex (expand-syntax e)])
          (annotate-top ex #f)))
      (syntax-case top-e (begin module)
        [(module name . reste)
         (if (eq? (syntax-e #'name) 'errortrace-key)
             top-e
             (let ([top-e (expand-syntax top-e)])
               (syntax-case top-e (module #%plain-module-begin)
                 [(module name init-import (#%plain-module-begin body ...))
                  (normal
                   #`(module name init-import
                       (#%plain-module-begin
                        #,((make-syntax-introducer)
                           #'(require (lib "errortrace-key.ss" "errortrace")))
                        #,((make-syntax-introducer)
                           #'(require-for-syntax
                              (lib "errortrace-key.ss" "errortrace")))
                        body ...)))])))]
        [_else
         (normal top-e)])))

  (define errortrace-compile-handler
    (let ([orig (current-compile)]
          [ns (current-namespace)])
      (lambda (e immediate-eval?)
        (orig
         (if (and (instrumenting-enabled)
                  (eq? ns (current-namespace))
                  (not (compiled-expression? (if (syntax? e)
                                                 (syntax-e e)
                                                 e))))
             (let ([e2 (errortrace-annotate
                        (if (syntax? e)
                            e
                            (namespace-syntax-introduce
                             (datum->syntax-object #f e))))])
               e2)
             e)
         immediate-eval?))))

  (define errortrace-error-display-handler
    (let ([orig (error-display-handler)])
      (lambda (msg exn)
        (if (exn? exn)
            (let ([p (open-output-string)])
              (display (exn-message exn) p)
              (newline p)
              (print-error-trace p exn)
              (orig (get-output-string p) exn))
            (orig msg exn)))))

  (provide errortrace-compile-handler
           errortrace-error-display-handler
           errortrace-annotate

           print-error-trace
           error-context-display-depth

           instrumenting-enabled

           profiling-enabled
           profiling-record-enabled
           profile-paths-enabled
           get-profile-results
           output-profile-results

           execute-counts-enabled
           get-execute-counts
           annotate-executed-file

           annotate-top))

