mirror of
git://git.sv.gnu.org/emacs.git
synced 2025-12-06 06:20:55 -08:00
* src/igc.c (Figc__arena_step): * lisp/emacs-lisp/igc.el (igc--current-idle-time, igc--on-idle): Doc fixes.
552 lines
20 KiB
EmacsLisp
552 lines
20 KiB
EmacsLisp
;;; igc.el --- Support functions for IGC -*- lexical-binding: t -*-
|
||
|
||
;; Copyright (C) 2024-2025 Free Software Foundation, Inc.
|
||
|
||
;; Keywords: GC
|
||
|
||
;; This file is part of GNU Emacs.
|
||
|
||
;; GNU Emacs is free software: you can redistribute it and/or modify
|
||
;; it under the terms of the GNU General Public License as published by
|
||
;; the Free Software Foundation, either version 3 of the License, or
|
||
;; (at your option) any later version.
|
||
|
||
;; GNU Emacs is distributed in the hope that it will be useful,
|
||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
;; GNU General Public License for more details.
|
||
|
||
;; You should have received a copy of the GNU General Public License
|
||
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
;;; Commentary:
|
||
|
||
;;; Code:
|
||
|
||
(require 'cl-lib)
|
||
(require 'sqlite)
|
||
|
||
(declare-function igc-info "igc.c")
|
||
(declare-function igc--collect "igc.c")
|
||
(declare-function igc--roots "igc.c")
|
||
|
||
(defun igc--diff (i1 i2)
|
||
(cl-loop for (t1 n1 s1) in i1
|
||
for (_t2 n2 s2) in i2
|
||
when (and n1 n2 (/= n1 n2))
|
||
collect (list t1 (- n1 n2) (and s1 (- s1 s2)))))
|
||
|
||
(defvar igc--a nil
|
||
"IGC snapshot A. Used for debugging.")
|
||
(defvar igc--b nil
|
||
"IGC snapshot B. Used for debugging.")
|
||
(defvar igc--display-mode 'a
|
||
"IGC stats current snapshot, \='a or \='b.")
|
||
(defvar igc--number-format 'human-readable
|
||
"Format for displaying numbers.
|
||
Should be either the symbol human-readable or nil.")
|
||
|
||
(defun igc-snapshot ()
|
||
"Take snapshot A or B depending on igc--display-mode."
|
||
(interactive)
|
||
(if (eq igc--display-mode 'a)
|
||
(setq igc--a (igc-info))
|
||
(setq igc--b (igc-info)))
|
||
(igc-stats))
|
||
|
||
(defun igc--info-to-display ()
|
||
(cl-ecase igc--display-mode
|
||
(diff (igc--diff igc--b igc--a))
|
||
(a igc--a)
|
||
(b igc--b)))
|
||
|
||
(defun igc-display-diff ()
|
||
"Display the difference between IGC snapshots A and B."
|
||
(interactive)
|
||
(setq igc--display-mode 'diff)
|
||
(igc-stats))
|
||
|
||
(defun igc-display-a ()
|
||
"Display IGC stats snapshot A. See igc-stats-mode."
|
||
(interactive)
|
||
(setq igc--display-mode 'a)
|
||
(igc-stats))
|
||
|
||
(defun igc-display-b ()
|
||
"Display IGC stats snapshot B. See igc-stats-mode."
|
||
(interactive)
|
||
(setq igc--display-mode 'b)
|
||
(igc-stats))
|
||
|
||
;;;###autoload
|
||
(defun igc-collect ()
|
||
"Perform a full GC.
|
||
This triggers an immediate garbage collection of the entire memory
|
||
used for Lisp objects, recycling any unreachable objects whose
|
||
memory can be freed and attempting to reduce the size of
|
||
the memory used for objects."
|
||
(interactive)
|
||
(let ((garbage-collection-messages t))
|
||
(igc--collect)))
|
||
|
||
(defun igc-clear ()
|
||
"Reset snapshots A and B."
|
||
(interactive)
|
||
(setq igc--a nil igc--b nil)
|
||
(igc-stats))
|
||
|
||
(defun igc-cycle-number-format ()
|
||
"Switch between human-readable and plain number format."
|
||
(interactive)
|
||
(setq igc--number-format (cl-ecase igc--number-format
|
||
(human-readable nil)
|
||
((nil) 'human-readable)))
|
||
(igc-stats))
|
||
|
||
(define-derived-mode igc-stats-mode special-mode "Statistics"
|
||
"Display memory statistics from `igc-info', about incremental GC.
|
||
You can display two snapshots A and B containing the info from `igc-info'
|
||
at different times. These can be displayed either as-is, or by showing the
|
||
difference between them. Type \\`a' to switch to snapshot A
|
||
and \\`b' to switch to snapshot B. To take a snapshot, type \\`s'.
|
||
To reset the current snapshot, type \\`s'.
|
||
To display the difference between A and B, type \\`d'.
|
||
To perform a full GC, type \\`c'.
|
||
Type \\`g' to refresh the the difference view.
|
||
Type \\`?' to see the mode's help.
|
||
|
||
The IGC memory statistics is a list of elements describing the various
|
||
statistics of the incremental GC. The elements are of the
|
||
form (NAME NOBJECTS NBYTES LARGEST), where:
|
||
- NAME is a string describing the kind of objects this entry represents,
|
||
- NOBJECTS is the number of objects of this type,
|
||
- NBYTES is the number of bytes used by objects of this type,
|
||
- LARGEST is the largest object of this type.
|
||
|
||
In addition, there are several pseudo-objects which provide overall
|
||
IGC statistics:
|
||
- committed -- the amount of committed memory in bytes
|
||
- commit-limit -- max amount of memory the arena is allowed to commit
|
||
- spare-committed -- memory which remains committed and which the
|
||
arena is managing as free memory
|
||
- reserved -- total address space reserved by the arena
|
||
- spare -- spare commit limit fraction
|
||
- pause-time -- max amount of time GC operations may pause Emacs."
|
||
(keymap-local-set "a" #'igc-display-a)
|
||
(keymap-local-set "b" #'igc-display-b)
|
||
(keymap-local-set "c" #'igc-collect)
|
||
(keymap-local-set "d" #'igc-display-diff)
|
||
(keymap-local-set "s" #'igc-snapshot)
|
||
(keymap-local-set "x" #'igc-clear)
|
||
(keymap-local-set "n" #'igc-cycle-number-format)
|
||
(display-line-numbers-mode -1)
|
||
(setq header-line-format
|
||
'((:eval (format " %-35s %10s %15s %10s %13s"
|
||
(concat "Display "
|
||
(symbol-name igc--display-mode))
|
||
"Objects"
|
||
"Bytes"
|
||
"Avg"
|
||
"Largest"))))
|
||
(setq-local revert-buffer-function
|
||
(lambda (&rest _)
|
||
(setq igc--display-mode 'diff)
|
||
(igc-snapshot)
|
||
(igc-stats))))
|
||
|
||
(defun igc--format-bytes-human-readable (n)
|
||
(let* ((negative (< n 0))
|
||
(n (abs n)))
|
||
(concat (if negative "-" "")
|
||
(file-size-human-readable n))))
|
||
|
||
(defun igc--format-bytes (n)
|
||
(cl-ecase igc--number-format
|
||
(human-readable
|
||
(cl-etypecase n
|
||
(integer (igc--format-bytes-human-readable n))
|
||
(float (number-to-string n))))
|
||
((nil) (number-to-string n))))
|
||
|
||
(defun igc--format-cell (x f)
|
||
(cl-etypecase x
|
||
(number (funcall f x))
|
||
(string x)
|
||
(null "")))
|
||
|
||
(defun igc--format-bytes-cell (x)
|
||
(igc--format-cell x #'igc--format-bytes))
|
||
|
||
(defun igc--format-avg-cell (bytes n)
|
||
(cond ((not (and bytes n)) "")
|
||
((zerop n) "0")
|
||
(t (igc--format-bytes-cell (abs (/ bytes n))))))
|
||
|
||
(defun igc--compute-column-widths (rows)
|
||
(let* ((widths (make-vector (length (car rows)) 0)))
|
||
(dolist (row rows)
|
||
(cl-loop for col in row
|
||
for i from 0
|
||
do (aset widths i (max (length col) (aref widths i)))))
|
||
widths))
|
||
|
||
(defun igc--insert-info-row (row widths)
|
||
(let ((title (car row)))
|
||
(insert title)
|
||
(insert-char ?\s (- (aref widths 0) (length title))))
|
||
(cl-loop for col in (cdr row)
|
||
for i from 1 do
|
||
(insert " ")
|
||
(insert-char ?\s (- (aref widths i) (length col)))
|
||
(insert col))
|
||
(insert "\n"))
|
||
|
||
(defun igc--insert-info (info)
|
||
(let* ((header (list (format "Display %s" igc--display-mode)
|
||
"Objects" "Bytes" "Avg" "Largest"))
|
||
(rows (cl-loop for (title n bytes largest) in info
|
||
collect (list title
|
||
(igc--format-cell n #'number-to-string)
|
||
(igc--format-bytes-cell bytes)
|
||
(igc--format-avg-cell bytes n)
|
||
(igc--format-bytes-cell largest))))
|
||
(widths (igc--compute-column-widths (cons header rows)))
|
||
(sorted-rows (sort rows :key #'car :lessp #'string<)))
|
||
(igc--insert-info-row header widths)
|
||
(setq header-line-format
|
||
(buffer-substring-no-properties (point-min) (1- (point))))
|
||
(delete-region (point-min) (point))
|
||
(dolist (row sorted-rows)
|
||
(igc--insert-info-row row widths))))
|
||
|
||
;;;###autoload
|
||
(defun igc-stats ()
|
||
"Display memory statistics from `igc-info'.
|
||
You can display two snapshots A and B containing the info from `igc-info'
|
||
at different times. These can be displayed either as-is, or by showing the
|
||
difference between them. \\<igc-stats-mode-map>Type \\[igc-display-a] to switch to snapshot A and \\[igc-display-b] to switch to
|
||
snapshot B. To take a snapshot, type \\[igc-snapshot].
|
||
To reset the current snapshot, type \\[igc-clear].
|
||
To display the difference between A and B, type \\[igc-display-diff].
|
||
Type \\`g' to refresh the the difference view.
|
||
Type \\`?' to see the mode's help."
|
||
(interactive)
|
||
(with-current-buffer (get-buffer-create "*igc*")
|
||
(igc-stats-mode)
|
||
(setq buffer-read-only t buffer-file-name nil)
|
||
(let ((old-line (line-number-at-pos))
|
||
(info (igc--info-to-display))
|
||
(inhibit-read-only t)
|
||
(inhibit-modification-hooks t)
|
||
(standard-output (current-buffer)))
|
||
(erase-buffer)
|
||
(delete-all-overlays)
|
||
(igc--insert-info info)
|
||
(goto-char (point-min))
|
||
(forward-line (1- old-line))))
|
||
(display-buffer "*igc*")
|
||
(message "Type `?' in the *igc* buffer for help"))
|
||
|
||
(defun igc--roots-diff (i1 i2)
|
||
(cl-loop for (t1 n1 s1) in i1
|
||
for (_t2 n2 s2) in i2
|
||
unless (= n1 n2)
|
||
collect (list t1 (- n1 n2) (and s1 (- s1 s2)))))
|
||
|
||
(defvar igc--roots-a nil
|
||
"IGC roots snapshot A. Used for debugging statistics.")
|
||
(defvar igc--roots-b nil
|
||
"IGC roots snapshot B. Used for debugging statistics.")
|
||
(defvar igc--roots-display-mode 'a)
|
||
|
||
(defun igc-roots-display-diff ()
|
||
"Display the difference between IGC roots snapshots A and B."
|
||
(interactive)
|
||
(setq igc--roots-display-mode 'diff)
|
||
(igc-roots-stats))
|
||
|
||
(defun igc-roots-display-a ()
|
||
"Display IGC roots for snapshot A."
|
||
(interactive)
|
||
(setq igc--roots-display-mode 'a)
|
||
(igc-roots-stats))
|
||
|
||
(defun igc-roots-display-b ()
|
||
"Display IGC roots for snapshot B."
|
||
(interactive)
|
||
(setq igc--roots-display-mode 'b)
|
||
(igc-roots-stats))
|
||
|
||
(defun igc--roots-info ()
|
||
(let ((h (make-hash-table :test 'equal)))
|
||
(cl-loop for (label type start end) in (igc--roots)
|
||
for (found _ n size) = (gethash label h) do
|
||
(if found
|
||
(puthash label
|
||
(list label type (1+ n)
|
||
(and size
|
||
(+ size (- end start))) h)
|
||
h)
|
||
(puthash label
|
||
(list label type 1 (and end (- end start)))
|
||
h)))
|
||
(cl-loop for i being the hash-values of h collect i)))
|
||
|
||
(defun igc--roots-snapshot ()
|
||
"Display roots snapshots a or b."
|
||
(interactive)
|
||
(if (eq igc--roots-display-mode 'a)
|
||
(setq igc--roots-a (igc--roots-info))
|
||
(setq igc--roots-b (igc--roots-info)))
|
||
(igc-roots-stats))
|
||
|
||
(defun igc--roots-info-to-display ()
|
||
(cl-ecase igc--roots-display-mode
|
||
(diff (igc--roots-diff igc--b igc--a))
|
||
(a igc--roots-a)
|
||
(b igc--roots-b)))
|
||
|
||
(defun igc-roots-clear ()
|
||
"GC, then set snapshot B to current `igc-info'."
|
||
(interactive)
|
||
(setq igc--roots-a nil igc--roots-b nil)
|
||
(igc-roots-stats))
|
||
|
||
(define-derived-mode igc-roots-mode special-mode "Roots"
|
||
"Display root statistics from `igc--roots'.
|
||
You can display two snapshots A and B containing the info from `igc--roots'
|
||
at different times. These can be displayed either as-is, or by showing the
|
||
difference between them. \\<igc-roots-mode-map>Type \\[igc-roots-display-a] to switch to snapshot A and \\[igc-roots-display-b] to switch to
|
||
snapshot B. To take a snapshot, type \\[igc--roots-snapshot].
|
||
To reset the current snapshot, type \\[igc-roots-clear].
|
||
To display the difference between A and B, type \\[igc-roots-display-diff].
|
||
Type \\`g' to refresh the the difference view.
|
||
Type \\`?' to see the mode's help.
|
||
|
||
The list of IGC roots is a list of elements, one each for every
|
||
root. Each element has the form (LABEL TYPE START END), where
|
||
- LABEL is the label of the root
|
||
- TYPE is either `ambig' or `exact'
|
||
- START is the start address
|
||
- END is either the end address or nil."
|
||
(keymap-local-set "a" #'igc-roots-display-a)
|
||
(keymap-local-set "b" #'igc-roots-display-b)
|
||
(keymap-local-set "c" #'igc-collect)
|
||
(keymap-local-set "d" #'igc-roots-display-diff)
|
||
(keymap-local-set "s" #'igc--roots-snapshot)
|
||
(keymap-local-set "x" #'igc-roots-clear)
|
||
(display-line-numbers-mode -1)
|
||
(setq header-line-format
|
||
'((:eval (format " %-25s %10s %15s %15s"
|
||
(concat "Display "
|
||
(symbol-name igc--roots-display-mode))
|
||
"Type" "N" "Bytes"))))
|
||
(setq-local revert-buffer-function
|
||
(lambda (&rest _)
|
||
(setq igc--roots-display-mode 'diff)
|
||
(igc--roots-snapshot)
|
||
(igc-roots-stats))))
|
||
|
||
;;;###autoload
|
||
(defun igc-roots-stats ()
|
||
"Display root statistics from `igc--roots'.
|
||
You can display two snapshots A and B containing the info from `igc--roots'
|
||
at different times. These can be displayed either as-is, or by showing the
|
||
difference between them. \\<igc-roots-mode-map>Type \\[igc-roots-display-a] to switch to snapshot A and \\[igc-roots-display-b] to switch to
|
||
snapshot B. To take a snapshot, type \\[igc--roots-snapshot].
|
||
To reset the current snapshot, type \\[igc-roots-clear].
|
||
To display the difference between A and B, type \\[igc-roots-display-diff].
|
||
Type \\`g' to refresh the the difference view.
|
||
Type \\`?' to see the mode's help."
|
||
(interactive)
|
||
(with-current-buffer (get-buffer-create "*igc roots*")
|
||
(igc-roots-mode)
|
||
(setq buffer-read-only t buffer-file-name nil)
|
||
(let ((info (igc--roots-info-to-display))
|
||
(inhibit-read-only t)
|
||
(inhibit-modification-hooks t)
|
||
(standard-output (current-buffer)))
|
||
(erase-buffer)
|
||
(delete-all-overlays)
|
||
(when info
|
||
(cl-loop for (label type n size) in info
|
||
do (insert (format "%-25s %10s %15s %15s\n"
|
||
label type n size)))
|
||
(sort-lines nil (point-min) (point-max)))
|
||
(goto-char (point-min))))
|
||
(display-buffer "*igc roots*")
|
||
(message "Type `?' in the *igc roots* buffer for help"))
|
||
|
||
(defvar igc--collect-timer nil)
|
||
(defvar igc--collect-file nil)
|
||
(defvar igc--sqlite nil)
|
||
|
||
;;;###autoload
|
||
(defun igc-stop-collecting-stats ()
|
||
"Stop collecting IGC stats."
|
||
(interactive)
|
||
(when igc--collect-timer
|
||
(cancel-timer igc--collect-timer)
|
||
(setq igc--collect-timer nil)
|
||
(when (sqlitep igc--sqlite)
|
||
(sqlite-close igc--sqlite)
|
||
(setq igc--sqlite nil))))
|
||
|
||
(defvar igc-stats-time-format "%T.%3N"
|
||
"Format for exporting time stamps in IGC stats, e.g. to csv or sqlite.
|
||
Used in calls to `format-time-string'.")
|
||
|
||
(defun igc--collect-stats-csv ()
|
||
"Collect IGC statistics from `igc-info' into CSV file.
|
||
This function is called from a timer; see `igc-start-collecting-stats'."
|
||
(let ((buffer (get-file-buffer igc--collect-file)))
|
||
(when buffer
|
||
(with-current-buffer buffer
|
||
(goto-char (point-max))
|
||
(when (= (point-min) (point-max))
|
||
(insert (format "\"%s\",\"%s\",\"%s\",\"%s\"\n"
|
||
"Time" "Type" "N" "Bytes")))
|
||
(cl-loop with time = (format-time-string igc-stats-time-format)
|
||
for (title n bytes) in (igc-info) do
|
||
(insert (format "\"%s\",\"%s\",\"%s\",\"%s\"\n"
|
||
time title n bytes))))
|
||
(save-buffer))))
|
||
|
||
(defun igc--collect-stats-sqlite ()
|
||
"Collect IGC statistics from `igc-info' into sqlite database.
|
||
This function is called from a timer; see `igc-start-collecting-stats'."
|
||
(let ((values (cl-loop with time = (format-time-string igc-stats-time-format)
|
||
for (title n bytes) in (igc-info)
|
||
collect (format "(\"%s\",\"%s\",\"%s\",\"%s\")"
|
||
time title n bytes))))
|
||
(with-sqlite-transaction igc--sqlite
|
||
(sqlite-execute igc--sqlite
|
||
(format "INSERT INTO igc VALUES %s"
|
||
(mapconcat #'identity values ","))))))
|
||
|
||
(defun igc--open-sqlite (db)
|
||
(let ((con (or (sqlite-open db)
|
||
(error "Error opening sqlite database %s" db))))
|
||
(with-sqlite-transaction con
|
||
(sqlite-execute con (concat "CREATE TABLE IF NOT EXISTS igc "
|
||
"(time, type, n, bytes)"))
|
||
;; Which indices we need depends, and has to be seen with time.
|
||
(sqlite-execute con (concat "CREATE INDEX IF NOT EXISTS igc_time_index "
|
||
"ON igc (time)"))
|
||
(sqlite-execute con (concat "CREATE INDEX IF NOT EXISTS igc_type_index "
|
||
"ON igc (type)")))
|
||
con))
|
||
|
||
;;;###autoload
|
||
(defun igc-start-collecting-stats (type file secs)
|
||
"Start collecting IGC statistics."
|
||
(interactive
|
||
(let* ((completion-ignore-case t)
|
||
(type (completing-read "Output to: " '("SQLite DB" "CSV") nil t))
|
||
(file (if (string-equal-ignore-case type "CSV")
|
||
(read-file-name "CSV file: ")
|
||
(unless (sqlite-available-p)
|
||
(error "Your Emacs is not built with SQLite support"))
|
||
(read-file-name "Sqlite database file: ")))
|
||
(secs (read-number "Interval seconds: " 10)))
|
||
(list (if (string-equal-ignore-case type "CSV") 'csv 'sqlite)
|
||
file secs)))
|
||
(unless (> secs 0)
|
||
(error "Invalid interval seconds"))
|
||
(igc-stop-collecting-stats)
|
||
(cond ((eq type 'csv)
|
||
(setq igc--collect-file file)
|
||
(find-file-noselect file)
|
||
(setq igc--collect-timer
|
||
(run-at-time nil secs #'igc--collect-stats-csv)))
|
||
(t
|
||
(setq igc--sqlite (igc--open-sqlite file))
|
||
(setq igc--collect-timer
|
||
(run-at-time nil secs #'igc--collect-stats-sqlite)))))
|
||
|
||
|
||
;;; Opportunistic GC
|
||
|
||
(defvar igc--idle-timer nil
|
||
"Idle timer to trigger opportunistic GC.")
|
||
|
||
(defvar igc--idle-delay 1.0
|
||
"Time, in seconds, to wait for `igc--idle-timer'.")
|
||
|
||
(defvar igc--idle-repetitions 6
|
||
"How many times to wait.")
|
||
|
||
(defvar igc--step-interval 0.01
|
||
"Time, in seconds, MPS is allowed to use for one step.")
|
||
|
||
;;;###autoload
|
||
(defun igc-start-idle-timer ()
|
||
"Start a timer to do GC work while Emacs is idle."
|
||
(cl-assert (< igc--step-interval igc--idle-delay))
|
||
(when igc--idle-timer
|
||
(cancel-timer igc--idle-timer))
|
||
(setq igc--idle-timer
|
||
(run-with-idle-timer igc--idle-delay t
|
||
#'igc--on-idle (vector nil) 0)))
|
||
|
||
(defun igc--current-idle-time ()
|
||
"Time since the last command finished, in seconds as a float.
|
||
This calls `current-idle-time', then converts the return value
|
||
to a float number of seconds.
|
||
See `igc--on-idle'."
|
||
(let ((idle-time (current-idle-time)))
|
||
(if idle-time (float-time idle-time) 0)))
|
||
|
||
(defun igc--predict-idle-time ()
|
||
"Predict available time until the next command starts.
|
||
See `igc--on-idle'."
|
||
(* (igc--current-idle-time) 0.80))
|
||
|
||
;; The igc-idle-timer works a bit like the blink-cursor-timer. It can
|
||
;; call 'igc--on-idle' multiple times per idle cycle either until some GC
|
||
;; work is done or until `igc--idle-repetitions' is reached. We do this
|
||
;; because our idle time prediction primarily depends on the
|
||
;; `current-idle-time', i.e. our predicted idle time gets larger the
|
||
;; larger `current-idle-time' gets.
|
||
;;
|
||
;; We try to recover from too optimistic predictions quicky, by calling
|
||
;; `igc--arena-step' with a relatively short `igc--step-interval' and
|
||
;; use `accept-process-output' to check if some output arrived, before
|
||
;; calling 'igc--arena-step' again.
|
||
|
||
(defun igc--on-idle (state repetition)
|
||
"Timer function to do GC in idle Emacs state.
|
||
Calls `igc--arena-step', wrapper for mps_arena_step for this very purpose.
|
||
See `igc-start-idle-timer'."
|
||
(let ((timer2 (aref state 0)))
|
||
(when timer2
|
||
(cancel-timer timer2)))
|
||
(let* ((available-time (igc--predict-idle-time))
|
||
(interval igc--step-interval)
|
||
(multiplier (floor (/ available-time interval))))
|
||
(named-let step ((n multiplier))
|
||
(let* ((work-to-do (igc--arena-step interval n)))
|
||
(cond ((and work-to-do
|
||
(> n 0)
|
||
(not (accept-process-output nil 0)))
|
||
(step (1- n)))
|
||
((and (not work-to-do)
|
||
(= n multiplier)
|
||
(< repetition igc--idle-repetitions))
|
||
(aset state 0 (run-with-idle-timer
|
||
(+ (igc--current-idle-time)
|
||
(* igc--idle-delay (ash 1 repetition)))
|
||
nil #'igc--on-idle state (1+ repetition)))))))))
|
||
|
||
|
||
(defun igc-describe-arena ()
|
||
"Describe MPS internals in a separate window."
|
||
(interactive)
|
||
(with-output-to-temp-buffer "*Arena*"
|
||
(with-current-buffer standard-output
|
||
(insert (igc--describe-arena))
|
||
(setq truncate-lines t))))
|
||
|
||
(provide 'igc)
|
||
|
||
;;; igc.el ends here.
|