;;; ---------------------------------------------------------------------------
;;; StopWatch.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; Reports how long every AutoCAD command takes to run.
;;;
;;; Toggle it on and each command you use prints its elapsed time when it
;;; finishes. This is the practical way to find out what is actually making a
;;; drawing feel slow, rather than guessing - run the suspect commands with the
;;; timer on and read the numbers.
;;;
;;; HOW IT WORKS
;;; A command reactor fires two callbacks: one the instant a command starts,
;;; one the instant it ends. The start callback stamps the clock, the end
;;; callback stamps it again and prints the difference.
;;;
;;; Two clocks are read, because they answer different questions:
;;;   MILLISECS - wall clock. What the user actually waited.
;;;   CPUTICKS  - processor time. Distinguishes genuine computation from time
;;;               spent waiting on a file, a network share or the user.
;;; A command with a long wall time but few ticks is waiting on something
;;; external; one with both high is genuinely working hard.
;;;
;;; COMMAND:  STOPWATCH  - toggle command timing on and off
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; A NOTE ON THE GLOBAL VARIABLES BELOW
;;;
;;; These three are deliberately NOT localised, and cannot be. A reactor lives
;;; on after the command that created it has returned, and its callbacks are
;;; invoked by AutoCAD long afterwards. Anything the callbacks rely on must
;;; therefore survive at global scope, or the reactor would be firing into
;;; variables that no longer exist.
;;;
;;; They are prefixed *StopWatch: to keep them clearly namespaced and to make
;;; a collision with another routine's globals effectively impossible.
;;; ---------------------------------------------------------------------------
(setq *StopWatch:Reactor* nil    ; the reactor object, nil when timing is off
      *StopWatch:Wall*    0.0    ; MILLISECS reading at command start
      *StopWatch:Ticks*   0.0    ; CPUTICKS  reading at command start
)

;; ---------------------------------------------------------------------------
;; StopWatch:Start  -  :vlr-commandWillStart callback
;; ---------------------------------------------------------------------------
;; Stamps both clocks as the command begins. The two arguments are supplied by
;; the reactor - the reactor object and a list whose car is the command name -
;; and neither is needed here, but the signature must still accept them.
;; ---------------------------------------------------------------------------
(defun StopWatch:Start ( reactor args )
    (setq *StopWatch:Wall*  (getvar "MILLISECS")
          *StopWatch:Ticks* (getvar "CPUTICKS")
    )
    (princ)
)

;; ---------------------------------------------------------------------------
;; StopWatch:Stop  -  :vlr-commandEnded callback
;; ---------------------------------------------------------------------------
;; Stamps both clocks again and reports the elapsed figures.
;;
;; args is a list whose first element is the command name. Elapsed wall time is
;; divided by 1000 to read in seconds, at six decimal places so that commands
;; finishing in well under a millisecond still show a meaningful figure.
;; ---------------------------------------------------------------------------
(defun StopWatch:Stop ( reactor args / wall ticks )
    (setq wall  (getvar "MILLISECS")
          ticks (getvar "CPUTICKS")
    )
    (princ
        (strcat "\nSTOPWATCH> " (car args) ": "
                (rtos (/ (- wall *StopWatch:Wall*) 1000.0) 2 6) " sec  ("
                (rtos (- ticks *StopWatch:Ticks*) 2 0) " ticks)"
        )
    )
    (princ)
)

;; ---------------------------------------------------------------------------
;; c:STOPWATCH  -  main routine, toggles the reactor
;; ---------------------------------------------------------------------------
(defun c:STOPWATCH ( / *error* )

    ;; -----------------------------------------------------------------------
    ;; No system variables are changed and nothing in the drawing is modified,
    ;; so there is no state to save and no undo group to open - wrapping a
    ;; reactor toggle in an undo group would put an empty entry on the undo
    ;; stack for no benefit. The handler exists only to report failures
    ;; cleanly.
    ;; -----------------------------------------------------------------------
    (defun *error* ( msg )
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** STOPWATCH error: " msg " **"))
        )
        (princ)
    )

    (if *StopWatch:Reactor*

        ;; ------------------------------------------------------------------
        ;; Timing is on - tear the reactor down and clear the handle. Removing
        ;; the reactor without clearing the variable would leave a dead object
        ;; behind that the next toggle would mistake for a live one.
        ;; ------------------------------------------------------------------
        (progn
            (vlr-remove *StopWatch:Reactor*)
            (setq *StopWatch:Reactor* nil)
            (princ "\nSTOPWATCH> timing OFF")
        )

        ;; ------------------------------------------------------------------
        ;; Timing is off - build the reactor. vl-load-com is called first
        ;; because the reactor functions are not available until the ActiveX
        ;; layer has been loaded, and a fresh drawing session may not have
        ;; loaded it yet.
        ;; ------------------------------------------------------------------
        (progn
            (vl-load-com)
            (setq *StopWatch:Reactor*
                (vlr-command-reactor "StopWatch"
                    (list (cons :vlr-commandWillStart 'StopWatch:Start)
                          (cons :vlr-commandEnded     'StopWatch:Stop)
                    )
                )
            )
            (princ "\nSTOPWATCH> timing ON - run STOPWATCH again to switch off")
        )
    )

    (princ)
)

(princ "\nStopWatch loaded - type STOPWATCH to toggle command timing.")
(princ)
