branch: externals/el-job commit 334636175859d55beec9b962f42d7aa975da0b0c Author: Martin Edström <meedstro...@gmail.com> Commit: Martin Edström <meedstro...@gmail.com>
Get rid of :anonymous --- el-job.el | 179 +++++++++++++++++++++++++++++++------------------------------- 1 file changed, 89 insertions(+), 90 deletions(-) diff --git a/el-job.el b/el-job.el index 08a1ddf7c1..201e61be68 100644 --- a/el-job.el +++ b/el-job.el @@ -273,25 +273,6 @@ Usually the number of logical cores on your machine minus 1.") (defvar el-jobs (make-hash-table :test #'eq) "Table of all el-job objects.") -(defun el-job--launch-anonymous ( load-features - inject-vars - funcall-per-input - inputs - callback ) - "Launch an anonymous job. -See `el-job-launch' for arguments." - (let* ((id (intern (format-time-string "%FT%H%M%S%N"))) - (job (puthash id (el-job--make :id id - :anonymous t - :cores-to-use el-job--machine-cores - :callback callback - :queued-inputs inputs) - el-jobs))) - (when (functionp inputs) - (setq inputs (funcall inputs))) - (el-job--spawn-processes job load-features inject-vars funcall-per-input) - (el-job--exec job))) - (defmacro el-job--with (job slots &rest body) "Make SLOTS expand into object accessors for `el-job' JOB inside BODY. Cf. `with-slots' in the eieio library, or `let-alist'. @@ -311,7 +292,6 @@ with one character of your choosing, such as a dot." (:copier nil) (:conc-name el-job:)) id - anonymous (sig 0) (cores-to-use 1) callback @@ -358,27 +338,27 @@ elements: (road1 road2) (museum1 museum2 museum3)) -which is why it's important that FUNCALL-PER-INPUT always returns a list with a -fixed-in-advance number of sub-lists, enabling this merge. Of course, -these sub-lists are allowed to be empty, i.e. nil. +which is why it's important that FUNCALL-PER-INPUT always returns a list +with a fixed-in-advance number of sub-lists, enabling this merge. +These sub-lists are allowed to be empty, i.e. nil, but not absent. -Alternatively, FUNCALL-PER-INPUT may always return nil. +The fixed-in-advance number can also be zero, i.e. FUNCALL-PER-INPUT may +be designed to always return nil. -FUNCALL-PER-INPUT is a function symbol known to be defined in an Emacs Lisp file. -It is the only mandatory argument, but rarely useful on its own. +FUNCALL-PER-INPUT is a symbol known to be defined in some Emacs Lisp +file as a function of one argument. -Usually, you would need to pass the symbol LOAD to indicate where to -find that Emacs Lisp file; that file should end with a `provide' call on -the same symbol. LOAD can also be a list of several symbols. +Usually, it would be a function you have written yourself, and you pass +LOAD-FEATURES to indicate where to find that Emacs Lisp file, plus any +dependencies not built into Emacs. -While subprocesses do not inherit `load-path', it is the mother Emacs -process that locates that file \(by inspecting `load-history', via -`el-job--ensure-compiled-lib'), then gives the file to the subprocess. +LOAD-FEATURES is a list of symbols like those in `features'; the files +in question should end with a `provide' call on the same symbols. -Due to the absence of `load-path', be careful writing `require' -statements into that Emacs Lisp file. You can pass `load-path' via -INJECT-VARS, but consider that fewer dependencies means faster spin-up. +The subprocesses do not inherit `load-path', it is the current Emacs +process that locates files \(by inspecting `load-history', via +`el-job--ensure-compiled-lib'), then gives them to each subprocess. INPUTS is a list that will be split by up to the output of @@ -413,9 +393,10 @@ A non-nil ID has several purposes: - Allow repeated calls on the same inputs to optimize how those inputs are split, thanks to benchmarks from previous calls. -- The associated process buffers stick around after death and can be - inspected for debugging purposes. Seek buffer names that start with - \" *el-job-\" \(note leading space). +- When there is both a non-nil ID and the value of `el-job--debug-level' + is nonzero, the associated process buffers stick around after death + and can be inspected. Seek buffer names that start with \" *el-job-\" + \(note leading space). IF-BUSY comes into effect when the previous launch with the same ID is @@ -436,7 +417,7 @@ still at work. IF-BUSY may take on one of three symbols: (setq callback wrapup)) (when-let* ((load (plist-get deprecated-args :load))) (message "el-job-launch: Obsolete argument :load now named :load-features") - (setq load-features load)) + (setq load-features (ensure-list load))) (when-let* ((funcall (plist-get deprecated-args :funcall))) (message "el-job-launch: Obsolete argument :funcall now named :funcall-per-input") (setq funcall-per-input funcall)) @@ -445,12 +426,22 @@ still at work. IF-BUSY may take on one of three symbols: (when callback (unless (and (symbolp callback) (functionp callback)) (error "Argument CALLBACK must be a symbol with a function definition"))) - (setq load-features (ensure-list load-features)) + (unless (proper-list-p load-features) + (error "el-job-launch: Argument LOAD-FEATURES must be a list")) (setq if-busy (or if-busy 'wait)) (unless el-job--machine-cores (setq el-job--machine-cores (max 1 (1- (num-processors))))) (if (null id) - (el-job--launch-anonymous load-features inject-vars funcall-per-input inputs callback) + (let ((anonymous-job + (el-job--make :cores-to-use el-job--machine-cores + :callback callback + :queued-inputs + (if (functionp inputs) (funcall inputs) inputs)))) + (el-job--spawn-processes anonymous-job + load-features + inject-vars + funcall-per-input) + (el-job--exec anonymous-job)) (let ((arg-signature (+ (sxhash load-features) (sxhash inject-vars) (sxhash funcall-per-input) @@ -502,7 +493,7 @@ still at work. IF-BUSY may take on one of three symbols: (defun el-job--spawn-processes (job load-features inject-vars funcall-per-input) "Spin up processes for JOB, standing by for input. For the rest of the arguments, see `el-job-launch'." - (el-job--with job (.stderr .id .cores-to-use .ready) + (el-job--with job (.stderr .id .cores-to-use .ready .spawn-args) (let* ((print-length nil) (print-level nil) (print-circle t) @@ -525,33 +516,35 @@ For the rest of the arguments, see `el-job-launch'." "--eval" (format "(el-job-child--work #'%S)" funcall-per-input))) ;; Ensure the working directory is not remote (messes things up) (default-directory invocation-directory) - proc) + (ident (or .id (number-to-string (abs (sxhash .spawn-args)))))) (setf .stderr (with-current-buffer (get-buffer-create (format " *el-job-%s:err*" .id) t) + (setq-local el-job-here job) (erase-buffer) (current-buffer))) (condition-case err (dotimes (i .cores-to-use) - (setq proc (make-process - :name (format "el-job:%s:%d" .id i) - :noquery t - :connection-type 'pipe - ;; https://github.com/jwiegley/emacs-async/issues/165 - :coding 'utf-8-emacs-unix - :stderr .stderr - :buffer (get-buffer-create (format " *el-job-%s:%d*" .id i) t) - :command command - :sentinel #'ignore)) + (let ((proc (make-process + :name (format "el-job:%s:%d" ident i) + :noquery t + :connection-type 'pipe + ;; https://github.com/jwiegley/emacs-async/issues/165 + :coding 'utf-8-emacs-unix + :stderr .stderr + :buffer (get-buffer-create + (format " *el-job-%s:%d*" ident i) t) + :command command + :sentinel #'ignore)))) (when (string-suffix-p ">" (process-name proc)) (el-job--dbg 1 "Unintended duplicate process id for %s" proc)) - (with-current-buffer (process-buffer proc) - (setq-local el-job-here job) - (add-hook 'after-change-functions #'el-job--check-done nil t)) (process-send-string proc vars) (process-send-string proc "\n") (process-send-string proc libs) (process-send-string proc "\n") + (with-current-buffer (process-buffer proc) + (setq-local el-job-here job) + (add-hook 'after-change-functions #'el-job--check-done nil t)) (push proc .ready)) ;; https://github.com/meedstrom/org-node/issues/75 (( file-error ) @@ -565,9 +558,8 @@ This puts them to work. Each successful child will print output \(even nil output) to its associated process buffer, whereupon something should trigger `el-job--handle-output'." (el-job--with job - ( .ready .busy .input-sets .result-sets .queued-inputs .cores-to-use .past-elapsed - .timestamps .finish-times - .id .timeout ) + ( .ready .busy .input-sets .result-sets .queued-inputs .cores-to-use + .past-elapsed .timestamps .finish-times .id .stderr .timeout ) (cancel-timer .timeout) (setf .result-sets nil) (setf .finish-times nil) @@ -595,17 +587,21 @@ should trigger `el-job--handle-output'." (add-hook 'after-change-functions #'el-job--check-done nil t))))) (setf .queued-inputs nil) (plist-put .timestamps :work-begun (current-time)) - (setf .timeout (run-with-timer 30 nil #'el-job--timeout .id)))) - -(defun el-job--timeout (id) - "Disable job by ID, and print that it timed out. -If the job was idle, then do not print, just reap the processes." - (when-let* ((job (gethash id el-jobs))) - (let ((busy (el-job:busy job))) + (setf .timeout (run-with-timer 30 nil #'el-job--timeout .stderr)))) + +(defun el-job--timeout (stderr-buf) + "Disable job corresponding to STDERR-BUF, and print that it timed out. +If the job was idle, just reap the processes and print nothing." + (when-let* ((buf (get-buffer stderr-buf)) + (job (buffer-local-value 'el-job-here buf))) + (let* ((was-busy (el-job:busy job)) + (desc (or (el-job:id job) + (format "once-off job that calls %S" + (car (last (el-job:spawn-args job))))))) (el-job--disable job) - (if busy - (message "el-job: Timed out, was busy for 30+ seconds: %s" id) - (el-job--dbg 2 "Reaped idle processes for %s" id))))) + (if was-busy + (message "el-job: Timed out, was busy for 30+ seconds: %s" desc) + (el-job--dbg 2 "Reaped idle processes for %s" desc))))) ;; REVIEW: We use `process-send-string' to send a \n when sending more input (in ;; `el-job--exec'). @@ -620,6 +616,7 @@ Can be called in a process buffer at any time." (if (eq (char-before) ?\n) (el-job--handle-output))) +;; TODO: remove arg (defun el-job--handle-output (&optional dead-process) "Handle output in current buffer. @@ -650,7 +647,7 @@ object. If nil, infer it from the buffer, if process is still alive." (when results (el-job--with job ( .busy .ready .input-sets .past-elapsed .result-sets .queued-inputs - .timestamps .id .anonymous .finish-times + .timestamps .id .finish-times .timeout .callback .merged-results ) (push finish-time .finish-times) ;; Record time spent by FUNCALL-PER-INPUT on each item in INPUTS, @@ -674,29 +671,31 @@ object. If nil, infer it from the buffer, if process is still alive." (setf .merged-results (el-job--zip-all .result-sets)) (when .callback (funcall .callback .merged-results job)) - ;; Cleanup - (when .anonymous - (el-job--disable job) - (remhash .id el-jobs)) - (when .queued-inputs - (el-job--exec job)))))) - t) + (if .id + (when .queued-inputs + (el-job--exec job)) + ;; Cleanup process buffers of anonymous job + ;; TODO: actually just let timeout do it... + (el-job--disable job))))))) (defun el-job--disable (job) - p "Kill processes in JOB and revert some state variables. -This kills all process buffers, but does not deregister the ID from -`el-jobs' nor clear queued input." - (cancel-timer (el-job:timeout job)) - (dolist (proc (append (el-job:busy job) - (el-job:ready job))) - (let ((buf (process-buffer proc))) - (kill-buffer buf) - (el-job--kill-quietly proc))) - (setf (el-job:busy job) nil) - (setf (el-job:ready job) nil) - (when-let* ((stderr (el-job:stderr job))) - (kill-buffer stderr))) + "Kill processes in JOB and associated process buffers. +This does not deregister the job ID. That means the next launch with +same ID still has the benchmarks table and possibly queued input." + (el-job--with job (.id .timeout .busy .ready .stderr) + (cancel-timer .timeout) + (let ((preserve (and .id (> el-job--debug-level 0)))) + (dolist (proc (append .busy .ready)) + (let ((buf (process-buffer proc))) + (el-job--kill-quietly proc) + (and (buffer-live-p buf) (not preserve) (kill-buffer buf)))) + (setf .busy nil) + (setf .ready nil) + (and (buffer-live-p .stderr) (not preserve) (kill-buffer .stderr))))) + +;; TODO Maybe now we can use only `delete-process' since sentinel is always +;; ignore. (defun el-job--kill-quietly (proc) "Kill PROC while disabling its sentinel and filter." (set-process-filter proc #'ignore)