;;; etaf-performance.el --- Optional ETAF operation recorder -*- lexical-binding: t; -*- ;; SPDX-License-Identifier: GPL-3.0-or-later ;;; Commentary: ;; This optional package records the flat provider reports emitted by one ;; mounted ETAF Runtime. It has no advice registry and owns no timing path: ;; operation boundaries and durations come exclusively from the Runtime ;; observer contract. ;;; Code: (require 'cl-lib) (require 'pp) (require 'ring) (require 'seq) (require 'tabulated-list) (require 'etaf-runtime) (defgroup etaf-performance nil "Operation timing reports emitted by ETAF runtimes." :group 'etaf) (defcustom etaf-performance-max-records 200 "Maximum completed operations retained by the recorder." :type 'integer :group 'etaf-performance) (defconst etaf-performance-report-format-version 2 "Current portable performance report format version.") (cl-defstruct (etaf-performance-stage (:constructor etaf-performance--stage-create)) "One flat provider report belonging to an operation." sequence provider category name duration status metadata) (cl-defstruct (etaf-performance-operation (:constructor etaf-performance--operation-create)) "One completed Runtime operation and its ordered provider reports." id kind label runtime-id buffer-name generation-before generation-after elapsed status gc-count gc-elapsed metadata stages) (cl-defstruct (etaf-performance--attachment (:constructor etaf-performance--attachment-create)) sink pending-keys) (defvar etaf-performance--record-ring nil) (defvar etaf-performance--pending (make-hash-table :test #'equal)) (defvar etaf-performance--attachments (make-hash-table :test #'eq)) (defvar-local etaf-performance--mode-runtime nil) (defconst etaf-performance--report-context-keys '(:format-version :operation-id :sequence :runtime-id :buffer-name :provider :stage :status :duration-ms) "Flat report keys represented by typed recorder fields.") (defun etaf-performance--copy-value (value) "Return a defensive recorder snapshot of VALUE." (cond ((stringp value) (copy-sequence value)) ((consp value) (cons (etaf-performance--copy-value (car value)) (etaf-performance--copy-value (cdr value)))) ((vectorp value) (apply #'vector (mapcar #'etaf-performance--copy-value (append value nil)))) ((hash-table-p value) (let ((copy (copy-hash-table value))) (clrhash copy) (maphash (lambda (key item) (puthash (etaf-performance--copy-value key) (etaf-performance--copy-value item) copy)) value) copy)) (t value))) (defun etaf-performance--copy-stage (stage) "Return an isolated snapshot of recorder STAGE." (etaf-performance--stage-create :sequence (etaf-performance-stage-sequence stage) :provider (etaf-performance-stage-provider stage) :category (etaf-performance-stage-category stage) :name (etaf-performance-stage-name stage) :duration (etaf-performance-stage-duration stage) :status (etaf-performance-stage-status stage) :metadata (etaf-performance--copy-value (etaf-performance-stage-metadata stage)))) (defun etaf-performance--copy-operation (operation) "Return an isolated snapshot of recorder OPERATION." (etaf-performance--operation-create :id (etaf-performance-operation-id operation) :kind (etaf-performance-operation-kind operation) :label (etaf-performance--copy-value (etaf-performance-operation-label operation)) :runtime-id (etaf-performance-operation-runtime-id operation) :buffer-name (etaf-performance--copy-value (etaf-performance-operation-buffer-name operation)) :generation-before (etaf-performance-operation-generation-before operation) :generation-after (etaf-performance-operation-generation-after operation) :elapsed (etaf-performance-operation-elapsed operation) :status (etaf-performance-operation-status operation) :gc-count (etaf-performance-operation-gc-count operation) :gc-elapsed (etaf-performance-operation-gc-elapsed operation) :metadata (etaf-performance--copy-value (etaf-performance-operation-metadata operation)) :stages (mapcar #'etaf-performance--copy-stage (etaf-performance-operation-stages operation)))) (defun etaf-performance-records (&optional runtime-id) "Return newest-first immutable snapshots of completed records. When RUNTIME-ID is non-nil, select only that integer observer report identity, as exposed by `etaf-performance-operation-runtime-id'. This also selects historical records after unmount or reuse of a buffer name. Omitted or nil RUNTIME-ID selects all retained records." (unless (or (null runtime-id) (integerp runtime-id)) (signal 'wrong-type-argument (list 'integerp runtime-id))) (let ((records (and etaf-performance--record-ring (ring-elements etaf-performance--record-ring)))) (mapcar #'etaf-performance--copy-operation (if runtime-id (seq-filter (lambda (operation) (equal runtime-id (etaf-performance-operation-runtime-id operation))) records) records)))) (defun etaf-performance--percentile (samples percentile) "Return nearest-rank PERCENTILE from numeric SAMPLES." (when samples (let* ((ordered (sort (copy-sequence samples) #'<)) (rank (max 1 (ceiling (* percentile (length ordered)))))) (nth (1- rank) ordered)))) (defun etaf-performance--provider-category (provider) "Return the public recorder category for PROVIDER." (if (eq provider 'etaf) 'runtime provider)) (defun etaf-performance--report-metadata (report) "Return REPORT fields not represented by typed recorder fields." (let (metadata) (while report (let ((key (pop report)) (value (pop report))) (unless (memq key etaf-performance--report-context-keys) (setq metadata (append metadata (list key (etaf-performance--copy-value value))))))) metadata)) (defun etaf-performance--stage-from-report (report) "Create one flat recorder stage from provider REPORT." (let ((provider (plist-get report :provider))) (etaf-performance--stage-create :sequence (plist-get report :sequence) :provider provider :category (etaf-performance--provider-category provider) :name (plist-get report :stage) :duration (plist-get report :duration-ms) :status (plist-get report :status) :metadata (etaf-performance--report-metadata report)))) (defun etaf-performance-operation-stage-summary (operation) "Summarize flat provider stages in OPERATION by category. Each result contains `:category', `:count', and `:duration-ms'. Provider durations may overlap and are therefore never presented as exclusive time." (unless (etaf-performance-operation-p operation) (signal 'wrong-type-argument (list 'etaf-performance-operation-p operation))) (let ((table (make-hash-table :test #'eq)) result) (dolist (stage (etaf-performance-operation-stages operation)) (let* ((category (etaf-performance-stage-category stage)) (entry (or (gethash category table) (cons 0 0.0)))) (setcar entry (1+ (car entry))) (setcdr entry (+ (cdr entry) (etaf-performance-stage-duration stage))) (puthash category entry table))) (maphash (lambda (category entry) (push (list :category category :count (car entry) :duration-ms (cdr entry)) result)) table) (sort result (lambda (left right) (> (plist-get left :duration-ms) (plist-get right :duration-ms)))))) (cl-defun etaf-performance-summary (&optional (records (etaf-performance-records))) "Summarize completed RECORDS by operation kind and label. Omitted RECORDS selects all retained records; explicit nil selects none." (let ((groups (make-hash-table :test #'equal)) result) (dolist (operation records) (let ((key (cons (etaf-performance-operation-kind operation) (etaf-performance-operation-label operation)))) (puthash key (cons (etaf-performance-operation-elapsed operation) (gethash key groups)) groups))) (maphash (lambda (key samples) (push (list :kind (car key) :label (cdr key) :count (length samples) :min-ms (apply #'min samples) :p50-ms (etaf-performance--percentile samples 0.50) :p95-ms (etaf-performance--percentile samples 0.95) :max-ms (apply #'max samples) :mean-ms (/ (apply #'+ samples) (float (length samples)))) result)) groups) (sort result (lambda (left right) (> (plist-get left :p95-ms) (plist-get right :p95-ms)))))) (defun etaf-performance--retain (operation) "Retain completed OPERATION under the configured bound." (let ((limit (max 0 etaf-performance-max-records))) (if (zerop limit) (setq etaf-performance--record-ring nil) (unless (and etaf-performance--record-ring (= (ring-size etaf-performance--record-ring) limit)) (let ((existing (etaf-performance-records))) (setq etaf-performance--record-ring (make-ring limit)) (dolist (record (reverse (seq-take existing limit))) (ring-insert etaf-performance--record-ring record)))) (ring-insert etaf-performance--record-ring operation)))) (defun etaf-performance--final-report-p (report) "Return non-nil when REPORT closes one Runtime operation." (and (eq (plist-get report :provider) 'etaf) (eq (plist-get report :stage) 'runtime-operation))) (defun etaf-performance--operation-from-reports (reports) "Create one completed operation from ordered flat REPORTS." (let* ((ordered (sort reports (lambda (left right) (< (plist-get left :sequence) (plist-get right :sequence))))) (final (car (last ordered)))) (etaf-performance--operation-create :id (plist-get final :operation-id) :kind (plist-get final :kind) :label (plist-get final :label) :runtime-id (plist-get final :runtime-id) :buffer-name (plist-get final :buffer-name) :generation-before (plist-get final :generation-before) :generation-after (plist-get final :generation-after) :elapsed (plist-get final :duration-ms) :status (plist-get final :status) :gc-count (or (plist-get final :gc-count) 0) :gc-elapsed (or (plist-get final :gc-duration-ms) 0.0) :metadata (etaf-performance--report-metadata final) :stages (mapcar #'etaf-performance--stage-from-report ordered)))) (defun etaf-performance--pending-key (report) "Return the Runtime and operation correlation key for REPORT." (cons (plist-get report :runtime-id) (plist-get report :operation-id))) (defun etaf-performance--capture (runtime report) "Capture one flat REPORT emitted by RUNTIME." (let* ((attachment (gethash runtime etaf-performance--attachments)) (key (etaf-performance--pending-key report)) (reports (cons (copy-tree report) (gethash key etaf-performance--pending)))) (when attachment (cl-pushnew key (etaf-performance--attachment-pending-keys attachment) :test #'equal)) (if (etaf-performance--final-report-p report) (progn (remhash key etaf-performance--pending) (when attachment (setf (etaf-performance--attachment-pending-keys attachment) (delete key (etaf-performance--attachment-pending-keys attachment)))) (etaf-performance--retain (etaf-performance--operation-from-reports reports)) (when (eq (plist-get report :kind) 'unmount) (remhash runtime etaf-performance--attachments))) (puthash key reports etaf-performance--pending))) nil) (defun etaf-performance--discard-pending (attachment) "Discard unfinished records owned by ATTACHMENT." (dolist (key (etaf-performance--attachment-pending-keys attachment)) (remhash key etaf-performance--pending)) (setf (etaf-performance--attachment-pending-keys attachment) nil)) ;;;###autoload (defun etaf-performance-start (runtime) "Attach the recorder to mounted RUNTIME and return RUNTIME. Attachment is idempotent for this recorder. A Runtime already owned by a different observer is rejected rather than replaced." (setq runtime (etaf-runtime-require-mounted runtime)) (let* ((attachment (gethash runtime etaf-performance--attachments)) (owned-sink (and attachment (etaf-performance--attachment-sink attachment)))) (cond (owned-sink (unless (etaf-runtime-compare-and-set-observer runtime owned-sink owned-sink) (error "ETAF recorder no longer owns the Runtime observer")) runtime) (t (when attachment (remhash runtime etaf-performance--attachments)) (let* ((sink (lambda (report) (etaf-performance--capture runtime report))) (new (etaf-performance--attachment-create :sink sink :pending-keys nil))) (unless (etaf-runtime-compare-and-set-observer runtime nil sink) (error "ETAF runtime already has a different observer")) (puthash runtime new etaf-performance--attachments) runtime))))) ;;;###autoload (defun etaf-performance-stop (runtime) "Detach this recorder from RUNTIME and return RUNTIME. If another observer replaced the recorder sink, leave that observer intact." (unless (etaf-runtime-p runtime) (signal 'wrong-type-argument (list 'etaf-runtime-p runtime))) (when-let* ((attachment (gethash runtime etaf-performance--attachments))) (let ((sink (etaf-performance--attachment-sink attachment))) (condition-case nil (etaf-runtime-compare-and-set-observer runtime sink nil) (etaf-runtime-error nil))) (etaf-performance--discard-pending attachment) (remhash runtime etaf-performance--attachments)) runtime) ;;;###autoload (defun etaf-performance-call-operation (runtime kind label function) "Call FUNCTION as KIND and LABEL through RUNTIME's operation boundary." (etaf-runtime-call-operation runtime kind label function)) (cl-defmacro etaf-performance-with-operation ((runtime kind label) &rest body) "Evaluate BODY through RUNTIME's canonical operation boundary." (declare (indent 1) (debug ((form form form) body))) `(etaf-performance-call-operation ,runtime ,kind ,label (lambda () ,@body))) ;;;###autoload (define-minor-mode etaf-performance-mode "Record operations for the Runtime mounted in the current buffer." :init-value nil :lighter " ETAF-Perf" (if etaf-performance-mode (let ((runtime (etaf-runtime-for-buffer (current-buffer)))) (unless runtime (setq etaf-performance-mode nil) (user-error "Current buffer has no mounted ETAF runtime")) (condition-case condition (progn (etaf-performance-start runtime) (setq etaf-performance--mode-runtime runtime)) (error (setq etaf-performance-mode nil) (signal (car condition) (cdr condition))))) (when etaf-performance--mode-runtime (etaf-performance-stop etaf-performance--mode-runtime) (setq etaf-performance--mode-runtime nil)))) ;;;###autoload (defun etaf-performance-clear () "Clear completed and unfinished performance records." (interactive) (setq etaf-performance--record-ring nil) (clrhash etaf-performance--pending) (when-let* ((panel (get-buffer "*ETAF Performance*"))) (with-current-buffer panel (when (derived-mode-p 'etaf-performance-panel-mode) (tabulated-list-revert)))) nil) (defun etaf-performance--stage-report-data (stage) "Return portable report data for STAGE." (list :sequence (etaf-performance-stage-sequence stage) :provider (etaf-performance-stage-provider stage) :category (etaf-performance-stage-category stage) :stage (etaf-performance-stage-name stage) :duration-ms (etaf-performance-stage-duration stage) :status (etaf-performance-stage-status stage) :metadata (etaf-performance--copy-value (etaf-performance-stage-metadata stage)))) (defun etaf-performance--operation-report-data (operation) "Return portable report data for OPERATION." (list :id (etaf-performance-operation-id operation) :kind (etaf-performance-operation-kind operation) :label (etaf-performance-operation-label operation) :runtime-id (etaf-performance-operation-runtime-id operation) :buffer-name (etaf-performance-operation-buffer-name operation) :generation-before (etaf-performance-operation-generation-before operation) :generation-after (etaf-performance-operation-generation-after operation) :elapsed-ms (etaf-performance-operation-elapsed operation) :status (etaf-performance-operation-status operation) :gc-count (etaf-performance-operation-gc-count operation) :gc-duration-ms (etaf-performance-operation-gc-elapsed operation) :metadata (etaf-performance--copy-value (etaf-performance-operation-metadata operation)) :stage-summary (etaf-performance-operation-stage-summary operation) :stages (mapcar #'etaf-performance--stage-report-data (etaf-performance-operation-stages operation)))) (defun etaf-performance--parse-darwin-power-state (battery-output custom-output) "Return normalized macOS power state from PMSET outputs. BATTERY-OUTPUT identifies the active source; CUSTOM-OUTPUT contains settings." (let* ((source (cond ((string-match-p "Battery Power" (or battery-output "")) 'battery) ((string-match-p "AC Power" (or battery-output "")) 'ac) (t 'unknown))) (target-heading (pcase source ('battery "Battery Power") ('ac "AC Power") (_ nil))) active low-power) (dolist (line (split-string (or custom-output "") "\n" t)) (cond ((string-match "^\\(.+ Power\\):[[:space:]]*$" line) (setq active (and target-heading (string= (match-string 1 line) target-heading)))) ((and active (string-match "^[[:space:]]*lowpowermode[[:space:]]+\\([01]\\)" line)) (setq low-power (if (string= (match-string 1 line) "1") 'on 'off))))) (list :source source :low-power-mode (or low-power 'unknown)))) (defun etaf-performance--command-output (program &rest arguments) "Return PROGRAM output for ARGUMENTS, or nil on failure." (when program (with-temp-buffer (let ((default-directory temporary-file-directory)) (when (eq 0 (apply #'call-process program nil t nil arguments)) (buffer-string)))))) (defun etaf-performance--power-state () "Return a portable local power-state snapshot." (if (not (eq system-type 'darwin)) (list :source 'unknown :low-power-mode 'unknown) (let ((pmset (executable-find "pmset"))) (if (not pmset) (list :source 'unknown :low-power-mode 'unknown) (etaf-performance--parse-darwin-power-state (etaf-performance--command-output pmset "-g" "batt") (etaf-performance--command-output pmset "-g" "custom")))))) (defun etaf-performance-environment-data () "Return the environment snapshot used to interpret latency records." (list :emacs-version emacs-version :system-type system-type :window-system window-system :graphic-display (display-graphic-p) :frame-pixel-size (list (frame-pixel-width) (frame-pixel-height)) :window-body-pixel-size (list (window-body-width nil t) (window-body-height nil t)) :gc-cons-threshold gc-cons-threshold :gc-cons-percentage gc-cons-percentage :native-comp-jit-compilation (and (boundp 'native-comp-jit-compilation) native-comp-jit-compilation) :load-average (load-average t) :power-state (etaf-performance--power-state))) ;;;###autoload (cl-defun etaf-performance-report-data (&optional (records (etaf-performance-records))) "Return a portable report for completed performance RECORDS. Omitted RECORDS selects all retained records; explicit nil selects none." (list :format-version etaf-performance-report-format-version :generated-at (format-time-string "%Y-%m-%dT%H:%M:%S%z") :environment (etaf-performance-environment-data) :summary (etaf-performance-summary records) :operations (mapcar #'etaf-performance--operation-report-data (reverse (copy-sequence records))))) ;;;###autoload (cl-defun etaf-performance-report-string (&optional (records (etaf-performance-records))) "Return completed performance RECORDS as a readable report string. Omitted RECORDS selects all retained records; explicit nil selects none." (concat ";; ETAF performance report\n" (pp-to-string (etaf-performance-report-data records)))) ;;;###autoload (defun etaf-performance-copy-report () "Copy all retained performance records to the kill ring." (interactive) (let* ((record-count (length (etaf-performance-records))) (report (etaf-performance-report-string))) (kill-new report) (message "Copied %d ETAF performance operation%s" record-count (if (= 1 record-count) "" "s")) report)) ;;;###autoload (defun etaf-performance-export (file) "Export all retained performance records to FILE. When called interactively, suggest a timestamped `.eld' file. Refuse to overwrite an existing file." (interactive (list (read-file-name "Export ETAF performance report: " nil (expand-file-name (format "etaf-performance-%s.eld" (format-time-string "%Y%m%d-%H%M%S")) default-directory)))) (let ((report (etaf-performance-report-string))) (write-region report nil file nil 'silent nil 'excl) (message "Exported %d ETAF performance operations to %s" (length (etaf-performance-records)) (abbreviate-file-name file)) file)) (defun etaf-performance--format-object (value) "Format VALUE compactly for the performance table." (if (null value) "-" (format "%s" value))) (defun etaf-performance--tabulated-entries () "Return tabulated entries for retained operations and flat stages." (let (entries) (dolist (operation (reverse (etaf-performance-records))) (let ((id (etaf-performance-operation-id operation))) (push (list (cons 'operation id) (vector (number-to-string id) (symbol-name (etaf-performance-operation-kind operation)) (or (etaf-performance-operation-label operation) "") "operation" "-" "-" (format "%.3f" (etaf-performance-operation-elapsed operation)) (symbol-name (etaf-performance-operation-status operation)) (etaf-performance--format-object (etaf-performance-operation-buffer-name operation)))) entries) (dolist (stage (etaf-performance-operation-stages operation)) (push (list (cons id (etaf-performance-stage-sequence stage)) (vector (format "%s.%s" id (etaf-performance-stage-sequence stage)) "stage" (symbol-name (etaf-performance-stage-name stage)) (symbol-name (etaf-performance-stage-category stage)) (symbol-name (etaf-performance-stage-provider stage)) (number-to-string (etaf-performance-stage-sequence stage)) (format "%.3f" (etaf-performance-stage-duration stage)) (symbol-name (etaf-performance-stage-status stage)) "-")) entries)))) (nreverse entries))) (defun etaf-performance--panel-environment-line (&optional environment) "Return one compact header for performance ENVIRONMENT." (let* ((environment (or environment (etaf-performance-environment-data))) (power (plist-get environment :power-state)) (load (car (plist-get environment :load-average)))) (format " ETAF Performance | Emacs %s | power %s/low:%s | load %.2f " (or (plist-get environment :emacs-version) "unknown") (or (plist-get power :source) 'unknown) (or (plist-get power :low-power-mode) 'unknown) (or load 0.0)))) (define-derived-mode etaf-performance-panel-mode tabulated-list-mode "ETAF-Performance" "Display ETAF operations and their flat provider reports." (setq tabulated-list-format [("ID" 9 t) ("Type" 10 t) ("Operation / Stage" 26 t) ("Category" 12 t) ("Provider" 12 t) ("Sequence" 9 t) ("Duration ms" 13 t) ("Status" 8 t) ("Buffer" 20 t)]) (setq tabulated-list-padding 2 tabulated-list-entries #'etaf-performance--tabulated-entries header-line-format (etaf-performance--panel-environment-line)) (tabulated-list-init-header)) (define-key etaf-performance-panel-mode-map (kbd "c") #'etaf-performance-copy-report) (define-key etaf-performance-panel-mode-map (kbd "w") #'etaf-performance-export) ;;;###autoload (defun etaf-performance-show () "Show retained ETAF operations and stages in a tabulated panel." (interactive) (let ((buffer (get-buffer-create "*ETAF Performance*"))) (with-current-buffer buffer (etaf-performance-panel-mode) (tabulated-list-print t)) (pop-to-buffer buffer))) (provide 'etaf-performance) ;;; etaf-performance.el ends here