branch: externals/futur
commit b3234c5ef479294cc194bb540e84f822fa937ac6
Author: Stefan Monnier <[email protected]>
Commit: Stefan Monnier <[email protected]>

    Make the concurrency bound non-internal; release as 1.1
    
    * futur.el (futur--background): Bind `inhibit-quit` since this runs
    code that's probably not directly related to the current command.
    (futur-concurrency-bound): New non-internal name.
    Rename vars and functions.
    
    * futur-tests.el (futur-process-bounded): Adjust accordingly.
---
 futur-tests.el |  8 +++---
 futur.el       | 86 ++++++++++++++++++++++++++++++++++++++++++----------------
 2 files changed, 66 insertions(+), 28 deletions(-)

diff --git a/futur-tests.el b/futur-tests.el
index e1ef14adeb..4c1084ac43 100644
--- a/futur-tests.el
+++ b/futur-tests.el
@@ -165,16 +165,16 @@
 (ert-deftest futur-process-bounded ()
   (let* ((futures ())
          (start (float-time))
-         (futur-process-max 2))
+         (futur-concurrency-bound 2))
     (dotimes (_ 10)
-      (push (futur--process-bounded #'futur-timeout 0.1) futures))
+      (push (futur-concurrency-bound #'futur-timeout 0.1) futures))
     (futur-blocking-wait-to-get-result (apply #'futur-list futures))
     (should (<= 0.5 (- (float-time) start) 0.6)))
   (let* ((futures ())
          (start (float-time))
-         (futur-process-max 3))
+         (futur-concurrency-bound 3))
     (dotimes (_ 10)
-      (push (futur--process-bounded #'futur-timeout 0.1) futures))
+      (push (futur-concurrency-bound #'futur-timeout 0.1) futures))
     (futur-blocking-wait-to-get-result (apply #'futur-list futures))
     (should (<= 0.4 (- (float-time) start) 0.5))))
 
diff --git a/futur.el b/futur.el
index cc10430a13..f6e8169280 100644
--- a/futur.el
+++ b/futur.el
@@ -3,7 +3,7 @@
 ;; Copyright (C) 2026  Free Software Foundation, Inc.
 
 ;; Author: Stefan Monnier <[email protected]>
-;; Version: 1.0
+;; Version: 1.1
 ;; Keywords: concurrency, async, promises, futures
 
 ;; This program is free software; you can redistribute it and/or modify
@@ -125,9 +125,10 @@
 
 ;;; News:
 
-;; Since 1.0:
+;; Version 1.1:
 
 ;; - New functions: `futur-race', `futur-sit-for', `futur-url-retrieve'.
+;; - New function `futur-concurrency-bound' when you need to limit concurrency.
 ;; - Rename `futur-error' to `futur-failed'.
 ;; - Rename `futur-register-callback' to `futur--register-callback'.
 ;; - Rename `futur-ize' to `futur--ize'.
@@ -196,7 +197,8 @@ that it is not empty."
       (with-no-warnings ,@body))))
 
 (defun futur--background ()
-  (let ((futur--in-background t))
+  (let ((futur--in-background t)
+        (inhibit-quit t))
     (while t
       (let ((pending
              (with-mutex futur--pending-mutex
@@ -227,9 +229,10 @@ time or order of execution."
       (apply #'run-with-timer 0 nil args)
     (with-mutex futur--pending-mutex
       (futur--queue-enqueue futur--pending args)
-      ;; FIXME: Maybe we should have combination
+      ;; FIXME: Maybe we should have a combo
       ;; `mutex-unlock+condition-notify', i.e. a variant of
-      ;; `condition-notify' which doesn't regrab the lock?
+      ;; `condition-notify' which doesn't regrab the lock, since it's
+      ;; common to do `condition-notify' at the end of a critical section.
       (condition-notify futur--pending-condition))))
 
 (defvar futur--idle-loop-bug80286
@@ -641,43 +644,77 @@ Returns non-nil if it waited the full TIME."
   ;; that's already run (or been canceled).
   (unless (timer--triggered timer) (cancel-timer timer)))
 
-;;;; Processes
-
-(defvar futur-process-max
+;;;; Bounding concurrent resource usage
+
+;; This is a crude support to limit the amount of concurrency.
+;;
+;; We could limit it automatically within `futur-process-call'
+;; and/or `url-retrieve', but in practice it can be preferable
+;; to apply the limit "higher up" in the call-tree.
+;; E.g. for `smerge-refine' rather than prepare right away the inputs
+;; to all the `diff' processes and then bound the number of concurrent
+;; processes, it can be preferable to apply the "delay until CPU is
+;; available" not just to the actual process but also to the preparation
+;; of the input files (e.g. so we don't uselessly prepare all those
+;; input files if we end up aborting the futures before the `diff's
+;; get to run).
+;;
+;; There's a lot more we could do, but I don't think we have enough
+;; experience to be sure what we'll need, really.
+;; Possible future needs:
+;;
+;; - Support for things like `nice' when launching processes.
+;; - Distinguish different resources.  E.g. rather than just N concurrent
+;;   futures, each future could specify what it consumes (as in CPU-vs-network
+;;   when comparing `url-retrieve' and `call-process').
+;; - Distinguish quantitative needs: the background native compiler
+;;   might prefer to bound its concurrency to 50% of the available CPUs,
+;;   while `diff-refine' might prefer to use more CPUs to reduce latency.
+
+(defvar futur-concurrency-bound
   (if (fboundp 'num-processors) ;; Emacs-28
       (num-processors) 2)
   "Maximum number of concurrent subprocesses.")
 
-(defvar futur--process-active nil
+(defvar futur--concurrency-bound-active nil
   "List of active process-futures.")
 
-(defvar futur--process-waiting (futur--queue)
+(defvar futur--concurrency-bound-waiting (futur--queue)
   "Queue of futures waiting to start
 Each element is of the form (FUTURE FUN . ARGS).")
 
-(defun futur--process-bounded (&rest args)
-  (if (< (length futur--process-active) futur-process-max)
-      (futur--process-bounded-start args)
+(defun futur-concurrency-bound (func &rest args)
+  "Call FUNC with ARGS while limiting the amount of concurrency.
+FUNC should also return a `futur'.  Returns a `futur' with the same value.
+The amount of concurrently active futures is determined by the variable
+`futur-concurrency-bound' and considers only those futures constructed
+via the function `futur-concurrency-bound'."
+  (if (< (length futur--concurrency-bound-active) futur-concurrency-bound)
+      (futur--concurrency-bound-start func args)
     (let ((new (futur--waiting 'waiting)))
-      (futur--queue-enqueue futur--process-waiting (cons new args))
+      (futur--queue-enqueue futur--concurrency-bound-waiting
+                            `(,new ,func . ,args))
       new)))
 
-(defun futur--process-bounded-start (args)
-  (let ((new (apply #'funcall args)))
-    (push new futur--process-active)
+(defun futur--concurrency-bound-start (func args)
+  ;; FIXME: Call FUNC in an "empty" dynamic context!
+  (let ((new (apply func args)))
+    (push new futur--concurrency-bound-active)
     (futur--register-callback
-     new (oclosure-lambda (futur--aux) (_ _) (futur--process-next new)))
+     new (oclosure-lambda (futur--aux) (_ _)
+           (futur--concurrency-bound-next new)))
     new))
 
-(defun futur--process-next (done)
-  (setq futur--process-active (delq done futur--process-active))
+(defun futur--concurrency-bound-next (done)
+  (setq futur--concurrency-bound-active
+        (delq done futur--concurrency-bound-active))
   (cl-block nil
-    (while (not (futur--queue-empty-p futur--process-waiting))
+    (while (not (futur--queue-empty-p futur--concurrency-bound-waiting))
       (pcase-let ((`(,fut . ,call)
-                   (futur--queue-dequeue futur--process-waiting)))
+                   (futur--queue-dequeue futur--concurrency-bound-waiting)))
         (pcase fut
           ((futur--waiting)
-           (let ((new (futur--process-bounded-start call)))
+           (let ((new (futur--concurrency-bound-start (car call) (cdr call))))
              (futur--register-callback
               new (lambda (err val) (futur--deliver fut err val)))
              (cl-return))))))))
@@ -685,6 +722,8 @@ Each element is of the form (FUTURE FUN . ARGS).")
 (cl-defmethod futur-blocker-abort ((_ (eql 'waiting)) _error)
   nil)
 
+;;;; Processes
+
 (defun futur--process-completed-p (proc)
   (memq (process-status proc) '(exit signal closed failed)))
 
@@ -736,7 +775,6 @@ The DISPLAY argument is ignored: redisplay always happens."
                  :filter (if (bufferp destination) nil
                            #'futur-process-call--filter)))
          (proc (pcase-exhaustive futur ((futur--waiting blocker) blocker))))
-    (push futur futur--process-active)
     (when (stringp destination)
       (write-region "" nil destination nil 'silent))
     (pcase-exhaustive infile

Reply via email to