branch: externals/el-job
commit 2ede4a3183a49598961d1d586c4724fda11fdf60
Author: Martin Edström <[email protected]>
Commit: Martin Edström <[email protected]>

    Docs comments
---
 README.org   |  58 +++++++++++++++++++++++++++++-
 el-job-ng.el |  30 +++++++++++-----
 el-job.el    |  16 +++++----
 el-job.texi  | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 4 files changed, 199 insertions(+), 21 deletions(-)

diff --git a/README.org b/README.org
index 8c17c01915..cb7c42e3eb 100644
--- a/README.org
+++ b/README.org
@@ -19,7 +19,63 @@ That's it in a nutshell.  You can look at real-world usage 
by searching for "el-
 - 
[[https://raw.githubusercontent.com/meedstrom/org-mem/refs/heads/main/org-mem.el][org-mem.el]]
 - 
[[https://raw.githubusercontent.com/meedstrom/org-roam-async/refs/heads/main/org-roam-async.el][org-roam-async.el]]
 
-* Since 2.5.0
+* 2.7.0
+- New entry point: =el-job-parallel-mapcar=! Read more below.
+- Argument =:inputs= are now split differently, for the sake of a predictable 
order of outputs.  It is likely less optimal, but hopefully not too much.
+- Argument =:require= now accepts strings in addition to symbols.  Strings are 
passed to =load= instead of =require=.
+- Value of =temporary-file-directory= no longer added to =:inject-vars= for 
you.
+  - Now the only values added are =load-path= and =native-comp-eln-load-path=.
+- Function =el-job-ng-job= renamed to =el-job-ng-get-job=.
+  - New class =el-job-ng-job=.
+- Delete many aliases to el-job-old.el.
+
+** New entry point: =el-job-parallel-mapcar=
+Until now, we had to make do with an unwieldy =el-job-ng-run= with ~5 keyword 
arguments.  And we had to understand asynchronous programming to use it.
+
+I've long dreamed to be able to brainlessly rewrite a form =(mapcar #'FN 
INPUTS)= to something like =(multicore-mapcar #'FN INPUTS)= and have it Just 
Work and Just Be Faster.
+
+Finally, it is done!
+
+The calling convention is
+
+: (el-job-parallel-mapcar FN INPUTS &optional INJECT-VARS)
+
+Please see the docstring for what you need to know.
+
+*** Weaknesses
+Do not use =el-job-parallel-mapcar= with overly trivial functions.  It adds 
some overhead per item, so it actually slows you down if the function itself 
only takes microseconds or nanoseconds per invocation.
+
+Example of a bad use-case:
+
+#+BEGIN_SRC elisp
+(let ((spam (make-list 1000000 "spam")))
+  (list (benchmark-elapse (mapcar #'upcase spam))
+        (benchmark-elapse (el-job-parallel-mapcar #'upcase spam))))
+#+END_SRC
+
+Return value:
+
+: (0.482017027 4.793430311)
+
+Another bad use-case is when plain =mapcar= would've been fast enough.  There 
is some constant overhead related to spinning up subprocesses.
+
+Changing 1000000 from the earlier expression to just 100 reveals the constant 
to be ~210ms on my machine:
+
+#+BEGIN_SRC elisp
+(let ((spam (make-list 100 "spam")))
+  (list (benchmark-elapse (mapcar #'upcase spam))
+        (benchmark-elapse (el-job-parallel-mapcar #'upcase spam))))
+#+END_SRC
+
+Return value:
+
+: (0.000041974 0.219570936)
+
+* 2.6.0
+
+- For  =el-job-old-launch=, new argument =:eval=
+
+* 2.5.0
 
 Released [2025-10-06 Mon], v2.5.0 comes with a variant library "el-job-ng".
 
diff --git a/el-job-ng.el b/el-job-ng.el
index 8fb2211a4d..c13076e8f1 100644
--- a/el-job-ng.el
+++ b/el-job-ng.el
@@ -39,7 +39,7 @@ if making too many processes, so capping it can help."
   :group 'processes)
 
 
-;;; Subroutines
+;;;; Subroutines
 
 (defvar el-job-ng--debug-level 0
   "Increase this to 1 or 2 to see more debug messages.")
@@ -138,7 +138,7 @@ Unlike `locate-library', this can actually find the .eln."
         (error "el-job-ng: Library not found: %S" name))))
 
 
-;;; Entry point
+;;;; Entry point
 
 (defvar el-job-ng--jobs (make-hash-table :test 'eq))
 (defclass el-job-ng-job ()
@@ -172,22 +172,26 @@ At a glance:
    that to CALLBACK, a function called precisely once.
    In other words, CALLBACK should be expected to receive one list that
    is equal in length to INPUTS.
+   Also, the order of values is preserved.
 
 Details:
 - INJECT-VARS is an alist of symbols and values to pass to `set'.
   It has some default members, including `load-path'.
-- REQUIRE is a list of symbols for `require' or strings for `load'.
+- REQUIRE is a list of symbols for `require', or strings for `load'.
 - EVAL is a list of quoted forms.
 - FUNCALL-PER-INPUT must be a symbol with a function definition,
   not an anonymous lambda.
+  That definition must be discoverable via `load-path' or REQUIRE.
   It is passed two arguments: the current item, and the remaining items.
   \(You probably will not need the second argument.\)
 
 Finally, ID is an optional symbol.  Passing an ID has two effects:
 - Automatically cancel a running job with the same ID, before starting.
 - Use benchmarks from previous runs to better balance the INPUTS split.
+  See `el-job-ng--split-optimally'.
 
 ID can also be passed to these helpers:
+- `el-job-ng-get-job'
 - `el-job-ng-await'
 - `el-job-ng-await-or-die'
 - `el-job-ng-ready-p'
@@ -279,10 +283,12 @@ ID can also be passed to these helpers:
           (setf process-outputs (nreverse process-outputs)))))))
 
 
-;;; Code used by child processes
+;;;; Code used by child processes
 
 (defvar el-job-ng--child-args 2)
 (defun el-job-ng--child-work ()
+  "Read a few lines from stdin, then work according to that info.
+Finally print to stdout and die."
   (let* ((coding-system-for-write 'utf-8-emacs-unix)
          (coding-system-for-read  'utf-8-emacs-unix)
          (vars   (read-from-minibuffer "" nil nil t))
@@ -310,7 +316,7 @@ ID can also be passed to these helpers:
       (print (nreverse benchmarked-outputs)))))
 
 
-;;; Sentinel; receiving what the child printed
+;;;; Sentinel; receiving what the child printed
 
 (defun el-job-ng--sentinel (proc event)
   "Handle changed state of a child process.
@@ -350,6 +356,10 @@ and run `el-job-ng--handle-finished-child'."
            (el-job-ng-kill-keep-bufs id)))))
 
 (defun el-job-ng--handle-finished-child (proc buf job)
+  "Handle output returned by PROC, presuming that is in buffer BUF.
+Then kill BUF.
+Once this has handled all outputs for JOB, run the CALLBACK function
+specified in `el-job-ng-run'."
   (with-slots (id process-outputs callback benchmarks do-bench) job
     (with-current-buffer buf
       (unless (and (eobp) (> (point) 2) (eq (char-before) ?\n))
@@ -371,7 +381,7 @@ and run `el-job-ng--handle-finished-child'."
           (el-job-ng--dbg 0 "Quit while executing :callback for %s" id))))))
 
 
-;;; API
+;;;; API
 
 (defmacro el-job-ng-sit-until (test max-secs &optional message)
   "Block until form TEST evaluates to non-nil, or MAX-SECS elapse.
@@ -399,12 +409,14 @@ A typical TEST would check if something in the 
environment has changed."
        ,last)))
 
 (defun el-job-ng-await (id max-secs &optional message)
-  "Like `el-job-ng-sit-until' but take ID and return t if job finishes."
+  "Like `el-job-ng-sit-until' but take ID and return t if job finishes.
+MAX-SECS and MESSAGE as in `el-job-ng-sit-until'."
   (el-job-ng-sit-until (el-job-ng-ready-p id) max-secs message))
 
 (defun el-job-ng-await-or-die (id max-secs &optional message)
   "Like `el-job-ng-await', but kill the job on timeout or any signal.
-Otherwise, a keyboard quit would let it continue in the background."
+Otherwise, a keyboard quit would let it continue in the background.
+ID, MAX-SECS and MESSAGE as in `el-job-ng-await'."
   (condition-case sig
       (if (el-job-ng-await id max-secs message)
           t
@@ -440,6 +452,7 @@ Otherwise, a keyboard quit would let it continue in the 
background."
     (delete-process proc)))
 
 (defun el-job-ng-stderr (id)
+  "Get the stderr buffer for ID."
   (let ((job (el-job-ng-get-job id)))
     (and job (oref job stderr))))
 
@@ -450,6 +463,7 @@ Otherwise, a keyboard quit would let it continue in the 
background."
                          (mapcar #'car (oref job process-outputs))))))
 
 (defun el-job-ng-get-job (id-or-process)
+  "Get the job object associated with ID-OR-PROCESS."
   (if (processp id-or-process)
       (cl-loop for job being each hash-value of el-job-ng--jobs
                when (assq id-or-process (oref job process-outputs))
diff --git a/el-job.el b/el-job.el
index fa86620874..97c69ecc83 100644
--- a/el-job.el
+++ b/el-job.el
@@ -53,10 +53,11 @@
   "Apply FN to LIST like `mapcar' in one or more parallel processes.
 
 Function FN must be known in `load-history' to be defined in some file.
-The parallel processes inherit `load-path' and then load that file.
+At spin-up, the parallel processes inherit `load-path', then load that
+file \(even if it is not on `load-path'\), and then get to work.
 
-Function FN must not depend on side effects from previous invocations of
-itself, because each process gets a different subset of LIST.
+Function FN should not depend on side effects from previous invocations
+of itself, because each process gets a different subset of LIST.
 
 Unlike the more general `el-job-ng-run', this is meant as a close
 drop-in for `mapcar'.  It behaves like a synchronous function by
@@ -76,9 +77,12 @@ since FN runs in external processes.
 That means FN will not see let-bindings, runtime variables and the like,
 that you might have meant to have in effect where
 `el-job-parallel-mapcar' is invoked.
-Nor can it mutate such variables for you -- the only way it can affect
-the current Emacs session is if the caller of
-`el-job-parallel-mapcar' does something with the return value."
+That is why you may need INJECT-VARS.
+
+N/B: The aforementioned loss of scope also means that FN cannot set or
+mutate any variables for you -- the only way it can affect the current
+Emacs session is if the caller of `el-job-parallel-mapcar' does
+something with the return value."
   (let* (result
          (vars (el-job-ng-vars (cons '(el-job-ng--child-args . 1) 
inject-vars)))
          (id (intern (format "parallel-mapcar.%S.%d" fn (sxhash vars)))))
diff --git a/el-job.texi b/el-job.texi
index 4a6bf1c963..83937728e3 100644
--- a/el-job.texi
+++ b/el-job.texi
@@ -40,13 +40,23 @@ That's it in a nutshell.  You can look at real-world usage 
by searching for "el-
 @end ifnottex
 
 @menu
-* Since 2.5.0: Since 250. 
+* 2.7.0: 270. 
+* 2.6.0: 260. 
+* 2.5.0: 250. 
 * README for 2.4.8: README for 248. 
 
 @detailmenu
 --- The Detailed Node Listing ---
 
-Since 2.5.0
+2.7.0
+
+* New entry point @samp{el-job-parallel-mapcar}::
+
+New entry point: @samp{el-job-parallel-mapcar}
+
+* Weaknesses::
+
+2.5.0
 
 * Future work::
 
@@ -69,10 +79,104 @@ Design rationale
 @end detailmenu
 @end menu
 
-@node Since 250
-@chapter Since 2.5.0
+@node 270
+@chapter 2.7.0
+
+@itemize
+@item
+New entry point: @samp{el-job-parallel-mapcar}! Read more below.
+@item
+Argument @samp{:inputs} are now split differently@comma{} for the sake of a 
predictable order of outputs.  It is likely less optimal@comma{} but hopefully 
not too much.
+@item
+Argument @samp{:require} now accepts strings in addition to symbols.  Strings 
are passed to @samp{load} instead of @samp{require}.
+@item
+Value of @samp{temporary-file-directory} no longer added to 
@samp{:inject-vars} for you.
+@itemize
+@item
+Now the only values added are @samp{load-path} and 
@samp{native-comp-eln-load-path}.
+@end itemize
+@item
+Function @samp{el-job-ng-job} renamed to @samp{el-job-ng-get-job}.
+@itemize
+@item
+New class @samp{el-job-ng-job}.
+@end itemize
+@item
+Delete many aliases to el-job-old.el.
+@end itemize
+
+@menu
+* New entry point @samp{el-job-parallel-mapcar}::
+@end menu
+
+@node New entry point @samp{el-job-parallel-mapcar}
+@section New entry point: @samp{el-job-parallel-mapcar}
+
+Until now@comma{} we had to make do with an unwieldy @samp{el-job-ng-run} with 
~5 keyword arguments.  And we had to understand asynchronous programming to use 
it.
+
+I've long dreamed to be able to brainlessly rewrite a form @samp{(mapcar #'FN 
INPUTS)} to something like @samp{(multicore-mapcar #'FN INPUTS)} and have it 
Just Work and Just Be Faster.
+
+Finally@comma{} it is done!
+
+The calling convention is
+
+@example
+(el-job-parallel-mapcar FN INPUTS &optional INJECT-VARS)
+@end example
+
+Please see the docstring for what you need to know.
+
+@menu
+* Weaknesses::
+@end menu
+
+@node Weaknesses
+@subsection Weaknesses
+
+Do not use @samp{el-job-parallel-mapcar} with overly trivial functions.  It 
adds some overhead per item@comma{} so it actually slows you down if the 
function itself only takes microseconds or nanoseconds per invocation.
+
+Example of a bad use-case:
+
+@lisp
+(let ((spam (make-list 1000000 "spam")))
+  (list (benchmark-elapse (mapcar #'upcase spam))
+        (benchmark-elapse (el-job-parallel-mapcar #'upcase spam))))
+@end lisp
+
+Return value:
+
+@example
+(0.482017027 4.793430311)
+@end example
+
+Another bad use-case is when plain @samp{mapcar} would've been fast enough.  
There is some constant overhead related to spinning up subprocesses.
+
+Changing 1000000 from the earlier expression to just 100 reveals the constant 
to be ~210ms on my machine:
+
+@lisp
+(let ((spam (make-list 100 "spam")))
+  (list (benchmark-elapse (mapcar #'upcase spam))
+        (benchmark-elapse (el-job-parallel-mapcar #'upcase spam))))
+@end lisp
+
+Return value:
+
+@example
+(0.000041974 0.219570936)
+@end example
+
+@node 260
+@chapter 2.6.0
+
+@itemize
+@item
+For  @samp{el-job-old-launch}@comma{} new argument @samp{:eval}
+@end itemize
+
+@node 250
+@chapter 2.5.0
 
-Released @emph{<2025-Oct-06>}@comma{} v2.5.0 comes with a variant library 
"el-job-ng".
+Released @emph{[2025-10-06 Mon]}@comma{} v2.5.0 comes with a variant library 
"el-job-ng".
 
 I find it simpler and easier to reason about.  400 lines of code instead of 
800.
 
@@ -108,7 +212,7 @@ I may write yet another variant.
 
 Something that came with experience is that it's best to make a new variant 
library for a narrow use-case@comma{} rather than complicate one library with 
different code flows.  When it comes to this type of library@comma{} you really 
want to keep it easy to reason about!
 
-Ideas as of @emph{<2025-Oct-06>}:
+Ideas as of @emph{[2025-10-06 Mon]}:
 
 @table @asis
 @item File IPC

Reply via email to