Checkpoint progress with adapting POIU to the new 2.26.x simplified ASDF internals.
authorFrancois-Rene Rideau <tunes@google.com>
Sun, 9 Dec 2012 15:36:47 +0000 (10:36 -0500)
committerFrancois-Rene Rideau <tunes@google.com>
Sun, 9 Dec 2012 15:36:47 +0000 (10:36 -0500)
This is a major rewrite of several subsystems of POIU.
NB: it looks like some race condition it making things hard for us on either SBCL or CCL.
This isn't stable. Try POIU 1.027 with ASDF 2.26 for stable results.

poiu.asd
poiu.lisp
test.lisp

index 583c071..390a387 100644 (file)
--- a/poiu.asd
+++ b/poiu.asd
@@ -13,7 +13,7 @@
 
 (let ((old-ver (asdf-version)))
   (load-system :asdf)
-  (let ((min "2.21")
+  (let ((min "2.26.14")
        (ver (asdf-version)))
     (unless (or (version-satisfies old-ver "2.014.8") ; first version to do magic upgrade
                (equal ver old-ver))
@@ -32,5 +32,5 @@ POIU is a variant of ASDF that may operate on your systems in parallel.
 POIU will notably compile each Lisp file in its own forked process,
 in parallel with other operations (compilation or loading).
 However, it will load FASLs serially as they become available."
-    :depends-on ((:version :asdf "2.21")) ; for external-format support.
+    :depends-on ((:version :asdf "2.26.14")) ; for new operation-done-p
     :components ((:file "poiu")))
index 65ccd1b..4968505 100644 (file)
--- a/poiu.lisp
+++ b/poiu.lisp
@@ -1,9 +1,9 @@
-;; -*- Lisp ; coding: utf-8 -*-
+;; -*- mode: Lisp ; coding: utf-8 -*-
 ;;; This is POIU: Parallel Operator on Independent Units
 (cl:in-package :asdf)
 (eval-when (:compile-toplevel :load-toplevel :execute)
-(defparameter *poiu-version* "1.027")
-(defparameter *asdf-version-required-by-poiu* "2.21"))
+(defparameter *poiu-version* "1.28")
+(defparameter *asdf-version-required-by-poiu* "2.26.14"))
 #|
 POIU is a modification of ASDF that may operate on your systems in parallel.
 This version of POIU was designed to work with ASDF no earlier than specified.
@@ -82,9 +82,9 @@ The original copyright and (MIT-style) licence of ASDF (below) applies to POIU:
 ;;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 ;;; WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
-(declaim (optimize debug safety))
-
+(declaim (optimize (speed 1) (debug 3) (safety 3)))
 
+;;; check versions
 (eval-when (:compile-toplevel :load-toplevel :execute)
   #-(or clisp clozure sbcl)
   (error "POIU doesn't support your Lisp implementation (yet). Help port POIU!")
@@ -101,8 +101,7 @@ The original copyright and (MIT-style) licence of ASDF (below) applies to POIU:
             parallel-load-system parallel-compile-system))
   (pushnew :poiu *features*))
 
-
-;; Some general purpose data structures we use
+;;; Some general purpose data structures we use
 (defgeneric table-values (table))
 (defmethod table-values ((table hash-table))
   (loop :for val :being :the :hash-values :of table :collect val))
@@ -158,25 +157,45 @@ The original copyright and (MIT-style) licence of ASDF (below) applies to POIU:
 (defmacro with-queue ((var qvar &optional (qval '(simple-queue))) &body body)
   `(let ((,qvar ,qval)) (call-with-queue (lambda (,var) ,@body) ,qvar)))
 
-(defun ensure-operation (opoid)
-  (etypecase opoid
-    (symbol (make-instance opoid))
-    (operation opoid)))
-
-(defun ensure-operation-name (opoid)
-  (etypecase opoid
-    (symbol opoid)
-    (operation (type-of opoid))))
-
-(defmacro remove-method-if-defined
-    (method-name specializers &optional qualifiers)
-  `(when (find-method (function ,method-name) ',qualifiers
-                      ',specializers
-                      nil)
-     (remove-method (function ,method-name)
-                    (find-method (function ,method-name)
-                                 ',qualifiers
-                                 ',specializers))))
+;;; Reifying and reconstituting objects for sending across processes
+(defun cl-symbol-p (x)
+  (and (symbolp x) (eq (find-package :cl) (symbol-package x))))
+(deftype cl-symbol () '(and symbol (satisfies cl-symbol-p)))
+(defun reify-symbol (sym)
+  (vector (symbol-name sym) (package-name (symbol-package sym))))
+(defun reconstitute-symbol (sym)
+  (intern (aref sym 0) (aref sym 1)))
+(defun reify-simple-sexp (sexp)
+  (etypecase sexp
+    ((or cl-symbol keyword number character simple-string pathname) sexp)
+    (cons (cons (reify-simple-sexp (car sexp)) (reify-simple-sexp (cdr sexp))))
+    (symbol (reify-symbol sexp))))
+(defun reconstitute-simple-sexp (sexp)
+  (etypecase sexp
+    ((or cl-symbol keyword number character simple-string pathname) sexp)
+    (cons (cons (reconstitute-simple-sexp (car sexp)) (reconstitute-simple-sexp (cdr sexp))))
+    ((simple-vector 2) (reconstitute-symbol sexp))))
+
+;;; Extracting undefined-warnings from the compilation-unit
+;;; To be passed through the above reify/reconstitute link, it must be a "simple-sexp"
+(defun undefined-warning-sexp (warning)
+  #-sbcl (declare (ignore warning))
+  #+sbcl
+  (list*
+   (sb-c::undefined-warning-kind warning)
+   (sb-c::undefined-warning-name warning)
+   (sb-c::undefined-warning-count warning)
+   (mapcar
+    #'(lambda (frob)
+        ;; the lexenv slot can be ignored for reporting purposes
+        `(:enclosing-source ,(sb-c::compiler-error-context-enclosing-source frob)
+          :source ,(sb-c::compiler-error-context-source frob)
+          :original-source ,(sb-c::compiler-error-context-original-source frob)
+          :context ,(sb-c::compiler-error-context-context frob)
+          :file-name ,(sb-c::compiler-error-context-file-name frob) ; a pathname
+          :file-position ,(sb-c::compiler-error-context-file-position frob) ; an integer
+          :original-source-path ,(sb-c::compiler-error-context-original-source-path frob)))
+    (sb-c::undefined-warning-warnings warning))))
 
 (defun reconstitute-deferred-warnings (constructor-list)
   #-sbcl (declare (ignore constructor-list))
@@ -191,8 +210,7 @@ The original copyright and (MIT-style) licence of ASDF (below) applies to POIU:
          (setf sb-c::*undefined-warnings*
                (nconc (mapcan
                        #'(lambda (stuff)
-                           (destructuring-bind (kind name count . rest)
-                               (reconstitute-simple-sexp stuff)
+                           (destructuring-bind (kind name count . rest) stuff
                              (if (and (eq kind :function) (fboundp name))
                                  nil
                                  (list
@@ -209,49 +227,13 @@ The original copyright and (MIT-style) licence of ASDF (below) applies to POIU:
         (otherwise
          (set symbol (+ (symbol-value symbol) adjustment)))))))
 
-(defun cl-symbol-p (x)
-  (and (symbolp x) (eq (find-package :cl) (symbol-package x))))
-(deftype cl-symbol () '(and symbol (satisfies cl-symbol-p)))
-(defun reify-symbol (sym)
-  (vector (symbol-name sym) (package-name (symbol-package sym))))
-(defun reconstitute-symbol (sym)
-  (intern (aref sym 0) (aref sym 1)))
-(defun reify-simple-sexp (sexp)
-  (etypecase sexp
-    ((or cl-symbol keyword number character simple-string pathname) sexp)
-    (cons (cons (reify-simple-sexp (car sexp)) (reify-simple-sexp (cdr sexp))))
-    (symbol (reify-symbol sexp))))
-(defun reconstitute-simple-sexp (sexp)
-  (etypecase sexp
-    ((or cl-symbol keyword number character simple-string pathname) sexp)
-    (cons (cons (reconstitute-simple-sexp (car sexp)) (reconstitute-simple-sexp (cdr sexp))))
-    ((simple-vector 2) (reconstitute-symbol sexp))))
-
-(defun reify-undefined-warnings (warning)
-  #+sbcl
-  (reify-simple-sexp
-   (list* (sb-c::undefined-warning-kind warning)
-          (sb-c::undefined-warning-name warning)
-          (sb-c::undefined-warning-count warning)
-          (mapcar
-           #'(lambda (frob)
-               ;; the lexenv slot can be ignored for reporting purposes
-               `(:enclosing-source ,(sb-c::compiler-error-context-enclosing-source frob)
-                 :source ,(sb-c::compiler-error-context-source frob)
-                 :original-source ,(sb-c::compiler-error-context-original-source frob)
-                 :context ,(sb-c::compiler-error-context-context frob)
-                 :file-name ,(sb-c::compiler-error-context-file-name frob) ; a pathname
-                 :file-position ,(sb-c::compiler-error-context-file-position frob) ; an integer
-                 :original-source-path ,(sb-c::compiler-error-context-original-source-path frob)))
-           (sb-c::undefined-warning-warnings warning)))))
-
-(defun get-compilation-unit-report ()
+(defun get-deferred-warnings ()
   #-sbcl nil
   #+sbcl
   (when sb-c::*in-compilation-unit*
     ;; Try to send nothing through the pipe if nothing needs to be accumulated
     `(,@(when sb-c::*undefined-warnings*
-          `((sb-c::*undefined-warnings* . ,(mapcar #'reify-undefined-warnings sb-c::*undefined-warnings*))))
+          `((sb-c::*undefined-warnings* . ,(mapcar #'undefined-warning-sexp sb-c::*undefined-warnings*))))
       ,@(loop for what in '(sb-c::*aborted-compilation-unit-count*
                             sb-c::*compiler-error-count*
                             sb-c::*compiler-warning-count*
@@ -261,262 +243,168 @@ The original copyright and (MIT-style) licence of ASDF (below) applies to POIU:
               when (plusp value)
                 collect `(,what . ,value)))))
 
+(defun reset-deferred-warnings ()
+  #+sbcl
+  (when sb-c::*in-compilation-unit*
+    (setf sb-c::*undefined-warnings* nil
+          sb-c::*aborted-compilation-unit-count* 0
+          sb-c::*compiler-error-count* 0
+          sb-c::*compiler-warning-count* 0
+          sb-c::*compiler-style-warning-count* 0
+          sb-c::*compiler-note-count* 0)))
+
+;;; Toplevel parallel operations
 (defclass parallelizable-operation (operation) ())
-
-(defclass parallel-op (parallelizable-operation)
-  ((operations :initarg :operations :accessor parallel-operations)))
-
 (defclass parallel-compile-op (compile-op parallelizable-operation) ())
-
 (defclass parallel-load-op (load-op parallelizable-operation) ())
 
-
-(defvar *breadcrumb-stream* (make-broadcast-stream)
-  "Stream that records the trail of operations on components.
-As the order of ASDF operations in general and parallel operations in
-particular are randomized, it is necessary to record them to replay &
-debug them later.")
-
-(defvar *breadcrumbs* nil
-  "Actual breadcrumbs found, to override traversal for replay and debugging")
-
-(defgeneric can-run-in-background-p (operation)
-  (:method ((operation symbol))
-    (can-run-in-background-p (make-instance operation)))
-  (:method ((operation parallelizable-operation))
-    nil)
-  (:method ((op parallel-compile-op))
-    t))
-
-(defgeneric run-in-background-p (operation component &key force)
-  (:method ((operation symbol) component &key force)
-    (run-in-background-p (make-instance operation) component :force force))
-  (:method ((operation parallelizable-operation) component &key force)
-    (and (can-run-in-background-p operation)
-         (or force
-             (not (operation-executed-p operation component))))))
-
-(defgeneric dependee-operations-necessary-p (operation component)
-  (:method ((op symbol) component)
-    (dependee-operations-necessary-p (make-instance op) component))
-  (:method ((op compile-op) component)
-    (declare (ignorable op component))
-    t)
-  (:method ((op operation) component)
-    (declare (ignorable op component))
-    nil))
-
 (defgeneric unparallelize-operation (operation))
 (defmethod unparallelize-operation ((op parallel-load-op))
-  (load-time-value (make-instance 'load-op)))
+  (make-sub-operation op 'load-op))
 (defmethod unparallelize-operation ((op compile-op))
-  (load-time-value (make-instance 'compile-op)))
-
-(defmethod operation-done-p ((operation parallelizable-operation) component)
-  (operation-done-p (unparallelize-operation operation) component))
-
-(defgeneric operation-executed-p (operation component)
-  (:documentation "operation-done-p is at planning time.
-Operation-executed-p is at plan execution time.")
-  (:method ((operation symbol) component)
-    (operation-executed-p (make-instance operation) component)))
-
-(defun parallelize-action (action)
-  (case (car action)
-    (compile-op (cons 'parallel-compile-op (cdr action)))
-    (load-op (cons 'parallel-load-op (cdr action)))
-    (otherwise action)))
-
-;; ASDF somehow maintains a dubious distinction between internal dependencies
-;; that trigger a recompilation and external dependencies that don't.
-;; We don't try to maintain that distinction as we deduce parallel dependencies
-;; from serial dependencies.
-(macrolet ((def-depend-method (class base-class)
-             `(defmethod component-depends-on ((operation ,class) c)
-                (mapcar #'parallelize-action
-                        (append
-                         (cdr (assoc ',base-class (component-do-first c)))
-                         (call-next-method))))))
-  (def-depend-method parallel-compile-op compile-op)
-  (def-depend-method parallel-load-op load-op))
-
-(defun component-equal (c1 c2)
-  (or (and (null c1) (null c2))
-      (and c1 c2 (equal (component-name c1) (component-name c2))
-           (component-equal (component-parent c1) (component-parent c2)))))
-
-(defun action-equal (action1 action2)
-  (and (eql (car action1) (car action2))
-       (component-equal (second action1) (second action2))))
-
-(defun ensure-component (parent coid)
-  (etypecase coid
-    (component coid)
-    ((or symbol string)
-     (assert coid)
-     (let ((c (find-component parent (coerce-name coid))))
-       (unless (typep c 'component)
-         (error 'missing-component :requires coid :parent parent))
-       c))))
-
-(defun make-dependency-trees (operation module)
-  (let ((starting-points (simple-queue))
-        ;; component -> dependency map
-        (direct-entries (make-hash-table :test #'equal))
-        ;; dependency -> component map
-        (indirect-entries (make-hash-table :test #'equal)))
-    (labels ((add-to-tree (dependency this-operation)
-               ;; don't record dependencies from component on itself
-               (when (equal dependency this-operation)
-                 (return-from add-to-tree nil))
-               (unless (gethash this-operation direct-entries)
-                 (setf (gethash this-operation direct-entries)
-                       (make-hash-table :test #'equal)))
-               (unless (gethash dependency indirect-entries)
-                 (setf (gethash dependency indirect-entries)
-                       (make-hash-table :test #'equal)))
-               (setf (gethash dependency (gethash this-operation direct-entries)) t)
-               (setf (gethash this-operation (gethash dependency indirect-entries)) t))
-             (is-in-tree-p (dependency)
-               (gethash dependency direct-entries))
-             (normalize-dependencies (parent deps)
-               (let ((queue (simple-queue)))
-                 (enqueue-normalized-dependencies queue parent deps)
-                 (dequeue-all queue)))
-             (enqueue-normalized-dependencies (queue parent deps)
-               (loop :for (op . components) :in deps :do
-                 (enqueue-normalized-dependencies-entry
-                  queue parent (ensure-operation-name op) components)))
-             (enqueue-normalized-dependencies-entry (queue parent op-name components)
-               (loop :for component :in components
-                     :for comp = (ensure-component parent component) :do
-                       (etypecase comp
-                         (module
-                          (enqueue-normalized-dependencies-entry
-                           queue comp op-name (module-components comp)))
-                         (component (enqueue queue (list op-name comp))))))
-             (do-components (operation module additional-dependencies)
-               (dolist (component (module-components module))
-                 (do1 operation component additional-dependencies)))
-             (do1 (operation component additional-dependencies)
-               (let ((operation (ensure-operation-name operation))
-                     (parent (component-parent component)))
-                 (when (is-in-tree-p (list operation component))
-                   (return-from do1))
-                 (etypecase component
-                   (module
-                    (let* ((component-parents
-                             (loop :for parent = component :then (component-parent parent)
-                                   :while parent :collect parent))
-                           (deps (loop :for (op . deps) :in (component-depends-on operation component)
-                                       :for real-deps
-                                         = (set-difference (mapcar (lambda (dep) (ensure-component parent dep)) deps)
-                                                           component-parents)
-                                 :when real-deps :collect `(,op ,@real-deps))))
-                      (do-components
-                        operation component
-                        (append additional-dependencies (normalize-dependencies parent deps)))
-                      (loop :for (op comp) :in deps :do
-                        (do1 op (ensure-component module comp) additional-dependencies))))
-                   (component
-                    (let* ((action (list operation component))
-                           (deps (normalize-dependencies (component-parent component)
-                                                         (component-depends-on operation component)))
-                           (all-deps (append additional-dependencies deps)))
-                      (unless all-deps
-                        (enqueue-new starting-points action :test #'equal))
-                      (loop :for d :in all-deps :do
-                        (add-to-tree d action))
-                      (loop :for (op comp) :in deps :do
-                        (do1 op (ensure-component module comp) additional-dependencies))))))))
-      (do-components (ensure-operation-name operation) module nil)
-      (values starting-points
-              indirect-entries
-              direct-entries))))
-
-(defun mark-as-done (operation component indirect-deps direct-deps)
+  (make-sub-operation op 'compile-op))
+
+(defun parallel-load-system (system &rest args)
+  (apply #'operate 'parallel-load-op system args))
+(defun parallel-compile-system (system &rest args)
+  (apply #'operate 'parallel-compile-op system args))
+
+(defgeneric run-in-background-p (operation component)
+  (:method ((o operation) (c component))
+    ;; We presume that actions that modify the filesystem can run in the background,
+    ;; whereas those that don't are meant to side-effect the current image.
+    (and (output-files o c) t)))
+
+(defclass parallel-plan ()
+  ((starting-points :initform (simple-queue))
+   (children :initform (make-hash-table :test #'equal)) ;; map an action to a (hash)set of "children" that it depends on
+   (parents :initform (make-hash-table :test #'equal)) ;; map an action to a (hash)set of "parents" that depend on it
+   (background-actions :initform 0 :accessor plan-background-actions)
+   (ancestor :initarg :ancestor)))
+
+(defmethod plan-operates-on-p ((plan parallel-plan) (component-path list))
+  (with-slots (starting-points children) plan
+    (let ((component (find-component () component-path)))
+      (remove component (append (queue-contents starting-points) (table-keys children)) :key 'cdr :test-not 'eq))))
+
+(defun record-dependency (parent child parents children)
+  (when parent
+    (unless (gethash parent children)
+      (setf (gethash parent children) (make-hash-table :test #'equal)))
+    (unless (gethash child parents)
+      (setf (gethash child parents) (make-hash-table :test #'equal)))
+    (setf (gethash child (gethash parent children)) t)
+    (setf (gethash parent (gethash child parents)) t)))
+
+(defun mark-as-done (operation component parents children)
   ;; marks the action of operation on component as done in the deps hash-tables,
   ;; returns a list of new actions that are enabled by it being done.
-  (let* ((operation (ensure-operation-name operation))
-         (action (list operation component))
-         (dependees (when (gethash action indirect-deps)
-                      (loop :for dependee :being
-                        :the :hash-keys :in (gethash action indirect-deps)
-                        :collect dependee))))
-    (remhash action direct-deps)
-    (loop :for dependee :in dependees
-      :for dependee-deps = (gethash dependee direct-deps)
-      :do (assert dependee-deps)
-          (remhash action dependee-deps)
-      :when (empty-p dependee-deps)
-      :collect dependee
-      :and :do (remhash dependee direct-deps))))
-
-(defun summarize-direct-deps (dir)
-  (sort (loop :for key :being :the :hash-keys :in dir :using (:hash-value val)
-          :collect (list key
-                         (loop :for innerkey :being :the :hash-keys :in val :using (:hash-value v)
-                           :when v :collect innerkey)))
-        #'< :key (lambda (depl) (length (cdr depl)))))
-
-(defun check-dependency-trees (module starting-points indirect-entries direct-entries)
+  (check-type operation operation)
+  (let* ((action (node-for operation component))
+         (action-parents (aif (gethash action parents) (table-keys it))))
+    (remhash action parents)
+    (loop :for parent :in action-parents
+      :for siblings = (gethash parent children)
+      :do (assert siblings)
+          (remhash action siblings)
+      :when (empty-p siblings)
+        :do (remhash parent children)
+        :and :collect parent)))
+
+(defun make-parallel-plan (operation component)
+  (let ((plan (make-instance 'parallel-plan :ancestor operation)))
+    (with-slots (starting-points children parents ancestor background-actions) plan
+      (labels
+          ((visit (o c stamp parent)
+             (let ((action (node-for o c)))
+               (record-dependency parent action parents children)
+               (multiple-value-bind (s p) (component-visited-p o c)
+                 (if p
+                     (car s)
+                     (with-component-being-visited (o c)
+                       (visit-action
+                        o c
+                        #'(lambda (stamp)
+                            #'(lambda (o c) (visit o c stamp action)))
+                        #'(lambda (o c done-p stamp)
+                            (mark-component-visited
+                             o c (cons stamp
+                                       (when (and (not done-p)
+                                                  (run-in-background-p o c))
+                                         (incf background-actions))))
+                            (cond
+                              (done-p (mark-operation-done o c)
+                                      (mark-as-done o c parents children))
+                              ((not (gethash action children))
+                               (enqueue starting-points (cons o c)))))
+                        stamp)))))))
+        (visit operation component nil nil)
+        plan))))
+
+(defun summarize-plan (plan)
+  (with-slots (starting-points children) plan
+    `((:starting-points
+       ,(loop :for (o . c) :in (queue-contents starting-points)
+              :collect (cons (type-of o) (component-find-path c))))
+      (:dependencies
+       ,(flet ((sexpify (action)
+                 (destructuring-bind (oname . c) action
+                   (cons oname (component-find-path c)))))
+          (sort
+           (loop :for parent :being :the :hash-keys :in children
+                 :using (:hash-value progeny)
+                 :collect `(,(sexpify parent) :-
+                            ,@(loop :for child :being :the :hash-keys :in progeny
+                                    :using (:hash-value v)
+                                    :when v :collect (sexpify child))))
+           #'< :key #'length))))))
+
+(defgeneric check-invariants (object))
+
+(defun node-action (node)
+  (destructuring-bind (opname . comp) node
+    (cons (make-instance opname) comp)))
+
+(defmethod check-invariants ((plan parallel-plan))
   ;; This destructively checks that the dependency tree model is coherent.
-  (with-queue (action action-queue starting-points)
-    (destructuring-bind (op-name component) action
-      (enqueue-many action-queue
-                    (mark-as-done op-name component
-                                  indirect-entries direct-entries))))
-  (unless (empty-p direct-entries)
-    (error "Cycle detected in the dependency graph of ~A. Direct dependencies are:~%~S"
-           module (summarize-direct-deps direct-entries))))
-
-(defun make-checked-dependency-trees (operation module)
-  (multiple-value-call #'check-dependency-trees ;; do it once, destructively check the results
-    module (make-dependency-trees operation module))
-  (make-dependency-trees operation module)) ;; do it again.
-
-(defparameter *max-forks* 16)
-(defparameter *max-actual-forks* 0)
-
-;;; subprocesses: data structure, ipc
-
-(defvar *current-subprocess*)
+  (with-slots (starting-points parents children ancestor) plan
+    (with-queue (action action-queue starting-points)
+      (destructuring-bind (operation . component) action
+        (enqueue-many action-queue
+                      (mapcar 'node-action (mark-as-done operation component parents children)))))
+    (unless (empty-p children)
+      (error "Cycle detected in the dependency graph:~%~S"
+             (summarize-plan plan))))
+  t)
 
-(defparameter *default-process-result*
-  '())
-(defparameter *failed-process-result*
-  '(:failure-p t :performed-p t))
+(defun make-checked-parallel-plan (operation module)
+  (check-invariants (make-parallel-plan operation module)) ;; do it once, destructively check the results
+  (make-parallel-plan operation module)) ;; do it again.
 
-(defclass communicating-subprocess ()
-  ((pid :initarg :pid :accessor process-pid)
-   (data :initarg :data :accessor process-data)
-   (cleanup-thunk :initarg :cleanup :accessor process-cleanup)
-   (status-pipe :accessor status-pipe)))
+(defmethod traverse ((operation parallelizable-operation) system)
+  (make-parallel-plan (unparallelize-operation operation) system))
 
-#|
-(defclass communicating-thread ()
-  ((thread :initarg :thread :accessor process-thread)
-   (data :initarg :data :accessor process-data)
-   (cleanup-thunk :initarg :cleanup :accessor process-cleanup)
-   (lock :initform (ccl:make-lock) :accessor process-lock)
-   (status :initform () :accessor process-status)))
-|#
-
-(defun process-result (exit-status result-pipe)
-  (prog1
-      (or (and (member exit-status '(0 nil))
-               (ignore-errors (read result-pipe)))
-          *failed-process-result*)
-    (close result-pipe)))
+(defparameter *max-forks* 16)
+(defparameter *max-actual-forks* nil)
 
-(defun process-return (proc result)
-  (prin1 result (status-pipe proc)))
+;;; subprocesses: abstraction for the implementation-dependent low-level API
 
 (defun finish-outputs ()
+  ;; This is notably necessary for CCL, that buffers output
   (finish-output *standard-output*)
   (finish-output *error-output*)
   (values))
 
+(defun disable-other-waiters ()
+  ;; KLUDGE: Try to undo problems caused by run-program.
+  ;; There will still be a race condition if some action calls run-program at load-time.
+  ;; But this work-around makes it is safe to call run-program before to invoke poiu
+  ;; (it is of course safe after). The true fix to allow run-program to be invoked
+  ;; at load-time would be to have an API for a process-waiting callbacks.
+  #+sbcl
+  (sb-sys:default-interrupt sb-unix:sigchld)) ; ignore-interrupt is undefined for SIGCHLD.
+
+
 #+sbcl
 (progn
 
@@ -562,8 +450,7 @@ Operation-executed-p is at plan execution time.")
 (defun make-input-stream (fd)
   (sb-sys:make-fd-stream fd :input t))
 
-)      ;#+sbcl
-
+);#+sbcl
 
 #+clozure
 (progn
@@ -599,7 +486,7 @@ Operation-executed-p is at plan execution time.")
 (defun make-input-stream (fd)
   (ccl::make-fd-stream fd :direction :input))
 
-)      ;#+clozure
+);#+clozure
 
 #+clisp ;;; CLISP specific fork support
 (progn
@@ -648,54 +535,170 @@ Operation-executed-p is at plan execution time.")
 
 );#+clisp
 
-(defun make-communicating-subprocess (data continuation cleanup)
-  (multiple-value-bind (read-fd write-fd) (posix-pipe)
-    ;; Try to undo problems caused by sb-ext:run-program. XXX: hack.
-    ;; Will still cause a race condition if an ASDF op calls run-program at load-time.
-    ;; But this work-around makes it is safe to call run-program before to invoke poiu
-    ;; (it is of course safe after). The true fix to allow run-program to be invoked
-    ;; at load-time would be to define and hook into an exported interface for process interaction.
-    #+sbcl
-    (sb-sys:default-interrupt sb-unix:sigchld) ; ignore-interrupt is undefined for SIGCHLD.
-    (finish-outputs)
-    (let* ((pid (posix-fork))
-           (proc (make-instance 'communicating-subprocess
-                    :pid pid
-                    :cleanup cleanup
-                    :data data)))
-      (cond ((zerop pid)
-             ;; don't receive the parent's SIGINTs
-             (posix-setpgrp)
-             ;; close the read end, set the write end to be the status reporter.
-             (posix-close read-fd)
-             (setf (status-pipe proc) (make-output-stream write-fd))
-             (when (find-package :sb-sprof)
-               (funcall (intern "STOP-PROFILING" :sb-sprof)))
-             (let ((*current-subprocess* proc))
-               #+sbcl
-               (progn
-                 (sb-ext:disable-debugger)
-                 ;; If POIU performs some part of a plan serially by compiling in the parent Lisp,
-                 ;; its warnings should not propagate to children.
-                 ;; In fact, the child's warning counters should probably be reset too,
-                 ;; but the more visible brain-damage was the warning list.
-                 (setf sb-c::*undefined-warnings* nil))
-               #+clozure (setf ccl::*batch-flag* t)
-               (unwind-protect (funcall continuation data)
-                 (close (status-pipe proc))
-                 (finish-outputs)
-                 (posix-exit 0))))
-            (t
-             ;; close the write end, set up the read end
-             (posix-close write-fd)
-             (setf (status-pipe proc) (make-input-stream read-fd))
-             proc)))))
+;;; Timing the build process
+
+(defvar *time-spent-waiting* 0)
+
+(defmacro timed-do ((time-accumulator) &body body)
+  (let ((time-before-thing (gensym)))
+    `(let ((,time-before-thing (get-internal-real-time)))
+       (multiple-value-prog1 (progn ,@body)
+              (incf ,time-accumulator (- (get-internal-real-time)
+                                         ,time-before-thing))))))
+
+;;; Handling multiple processes: high-level API
+
+(defclass background-process ()
+  ((pid :initarg :pid :accessor process-pid)
+   (data :initarg :data :accessor process-data)
+   (cleanup :initarg :cleanup :accessor process-cleanup)
+   ;; We pass results through a file: pipes may cause deadlocks due to full buffers and naive event loop.
+   (result-file :initarg :result-file :accessor process-result-file)))
+
+(define-condition process-failed (error)
+  ((exit-status :initarg :exit-status)
+   (condition :initform nil :initarg :condition)))
+
+(defun process-return (result-file result condition)
+  (with-open-file (s result-file
+                     :direction :output :if-exists :rename-and-delete :if-does-not-exist :create)
+    (with-standard-io-syntax
+      (let ((*package* (find-package :cl))
+            (*read-eval* nil)
+            (*print-readably* nil))
+        (write (reify-simple-sexp
+                `(:process-done
+                  ,@(when result `(:result ,result))
+                  ,@(when condition `(:condition ,(princ-to-string condition)))))
+               :stream s)))))
+
+(defun process-result (process status)
+  (block nil
+    (when status
+      (let ((exit-status (posix-wexitstatus status)))
+        (unless (zerop exit-status)
+          (return (values nil (make-condition 'process-failed :exit-status exit-status))))))
+    (multiple-value-bind (form condition)
+        (ignore-errors
+         (with-open-file (s (process-result-file process)
+                            :direction :input :if-does-not-exist :error)
+           (with-standard-io-syntax
+             (let ((*package* (find-package :cl))
+                   (*read-eval* nil)
+                   (*print-readably* nil))
+               (read s)))))
+      (when condition
+        (return (values nil (make-condition 'process-failed :condition "Could not read result file"))))
+      (unless (and (consp form) (eq (car form) :process-done))
+        (return (values nil (make-condition 'process-failed :condition "Invalid result file"))))
+      (destructuring-bind (&key result condition) (cdr form)
+        (return (values result (when condition (make-condition 'process-failed :condition condition))))))))
+
+(defun make-background-process (data function cleanup result-file)
+  (disable-other-waiters)
+  (finish-outputs)
+  (let ((pid (posix-fork)))
+    (cond
+      ((zerop pid) ; in the child
+       ;; don't receive the parent's SIGINTs
+       (posix-setpgrp)
+       #+sbcl
+       (progn
+         (sb-ext:disable-debugger)
+         (when (find-package :sb-sprof)
+           (funcall (intern "STOP-PROFILING" :sb-sprof))))
+       #+clozure (setf ccl::*batch-flag* t)
+       (reset-deferred-warnings)
+       (multiple-value-bind (result condition)
+           (ignore-errors (values (funcall function data t)))
+         (process-return result-file result condition)
+         (finish-outputs)
+         (posix-exit 0)))
+      (t ; in the parent
+       (make-instance 'background-process
+                      :pid pid
+                      :result-file result-file
+                      :cleanup cleanup
+                      :data data)))))
+
+(defun call-queue/forking (fun queue
+                          &key announce cleanup result-file (background-p (constantly t)))
+  ;; assumes a single-threaded parent process
+  (declare (optimize debug))
+  (loop :with processes = (make-hash-table :test 'equal) :do
+    (cond
+      (;; nothing to do or wait for anymore.
+       (and (empty-p queue) (empty-p processes))
+       (return))
+      (;; we've exceeded the subprocess limit. Wait for a few before continuing.
+       (or (>= (hash-table-count processes) *max-forks*)
+           (empty-p queue))
+       (disable-other-waiters)
+       (multiple-value-bind (pid status)
+           (timed-do (*time-spent-waiting*) (posix-wait))
+         (flet ((cleanup (process status)
+                  (multiple-value-bind (result condition)
+                      (process-result process status)
+                    (funcall (process-cleanup process) (process-data process) result condition t))))
+           (if pid
+               (let ((process (gethash pid processes)))
+                 (assert process () "couln't find the pid ~A in processes ~S" pid (table-values processes))
+                 (remhash pid processes)
+                 (cleanup process status))
+               ;; clisp can currently drop signals and get a ENOCHILD...
+               (let ((missed (table-values processes)))
+                 (warn "No child left: we must have dropped a signal!")
+                       ;;;(warn "blah ~S" entries) ;XXX
+                 (clrhash processes)
+                 (dolist (process missed)
+                   (cleanup process nil)))))))
+      (t ;; dequeue an item
+       (let* ((item (dequeue queue))
+              (backgroundp (funcall background-p item)))
+         (funcall announce item backgroundp)
+         (cond
+           (backgroundp
+            (latest-stamp-f *max-actual-forks* (hash-table-count processes))
+            (let ((process (make-background-process item fun cleanup (funcall result-file item))))
+              (setf (gethash (process-pid process) processes) process)))
+           (t
+            (multiple-value-bind (result condition)
+                (ignore-errors (values (funcall fun item nil)))
+              (funcall cleanup item result condition nil))))))
+      :finally
+      (assert (and (empty-p queue) (empty-p processes)) ()
+              "List of processes or list of things to do isn't empty: ~S / ~S~%"
+              (queue-contents queue)
+              (table-values processes)))))
+
+(defmacro doqueue/forking ((queue &key variables
+                                    (background-p t) (announce nil) (cleanup nil) result-file)
+                           &body body)
+  (destructuring-bind (&key item (backgroundp (gensym "BACKGROUNDP")) result condition) variables
+    `(call-queue/forking
+      #'(lambda (,item ,backgroundp) (declare (ignorable ,item ,backgroundp)) ,@body)
+      ,queue
+      :result-file #'(lambda (,item) (declare (ignorable ,item)) ,result-file)
+      :background-p #'(lambda (,item) (declare (ignorable ,item)) ,background-p)
+      :announce #'(lambda (,item ,backgroundp) (declare (ignorable ,item ,backgroundp)) ,announce)
+      :cleanup #'(lambda (,item ,result ,condition ,backgroundp)
+                   (declare (ignorable ,item ,result ,condition ,backgroundp)) ,cleanup))))
+
+#|
+;;; Vague attempt at doing things with threads.
+;;; BUT, compilation takes a global lock in CCL, so it's no go.
+
+(defclass communicating-thread ()
+  ((thread :initarg :thread :accessor process-thread)
+   (data :initarg :data :accessor process-data)
+   (cleanup :initarg :cleanup :accessor process-cleanup)
+   (lock :initform (ccl:make-lock) :accessor process-lock)
+   (status :initform () :accessor process-status)))
 
 #+clozure
 (defparameter *null-stream*
   (open "/dev/null" :direction :io :if-does-not-exist :error :if-exists :append))
 
-#|
 #+clozure
 (defun make-communicating-thread (semaphore data continuation cleanup)
   (let* ((proc (make-instance 'communicating-thread
@@ -705,8 +708,7 @@ Operation-executed-p is at plan execution time.")
                   "worker"
                   (lambda ()
                     (handler-case
-                        (let ((*current-subprocess* proc)
-                              (*standard-input* *null-stream*))
+                        (let ((*standard-input* *null-stream*))
                           (catch :process-return
                             (funcall continuation data)))
                       (t (c)
@@ -728,71 +730,7 @@ Operation-executed-p is at plan execution time.")
 #+clozure
 (defun thread-result (proc)
   (second (process-status proc)))
-|#
-
-;;; Timing the build process
-
-(defvar *time-spent-waiting* 0)
-
-(defmacro timed-do ((time-accumulator) &body body)
-  (let ((time-before-thing (gensym)))
-    `(let ((,time-before-thing (get-internal-real-time)))
-       (multiple-value-prog1 (progn ,@body)
-              (incf ,time-accumulator (- (get-internal-real-time)
-                                         ,time-before-thing))))))
-
-;;; Handling multiple processes
-
-(defun call-queue/forking (thunk queue
-                          &key announce cleanup (background-p (constantly t)))
-  ;; assumes a single-threaded parent process
-  (declare (optimize debug))
-  (let ((elem nil)
-        (processes (make-hash-table :test 'equal)))
-    (loop
-      ;;;(warn "cqf~% count: ~S~% elem: ~S~% map: ~S" (hash-table-count processes) elem (table-values processes))
-      (cond (;; nothing to do or wait for anymore.
-             (and (empty-p queue) (empty-p processes))
-             (return))
-            (;; we've exceeded the subprocess limit. Wait for a few before continuing.
-             (or (>= (hash-table-count processes) *max-forks*)
-                 (empty-p queue))
-             (multiple-value-bind (pid status)
-                 (timed-do (*time-spent-waiting*) (posix-wait))
-               (flet ((cleanup (entry exit-status)
-                        (funcall (process-cleanup entry) (process-data entry)
-                                 (process-result exit-status (status-pipe entry)))))
-                 (if pid
-                     (let ((entry (gethash pid processes)))
-                       (assert entry () "couln't find the pid ~A in processes ~S" pid (table-values processes))
-                       (remhash pid processes)
-                       (cleanup entry (posix-wexitstatus status)))
-                     ;; clisp can currently drop signals and get a ENOCHILD...
-                     (let ((entries (table-values processes)))
-                       (warn "No child left: we must have dropped a signal!")
-                       ;;;(warn "blah ~S" entries) ;XXX
-                       (clrhash processes)
-                       (dolist (entry entries)
-                         (cleanup entry nil))))))))
-      (unless (empty-p queue)
-        (setf elem (dequeue queue))
-        (funcall announce elem)
-        (cond
-          ((funcall background-p elem)
-           (when (> (hash-table-count processes) *max-actual-forks*)
-             (setf *max-actual-forks* (hash-table-count processes)))
-           (let ((process (make-communicating-subprocess elem thunk cleanup)))
-             (setf (gethash (process-pid process) processes) process)))
-          (t
-           (unwind-protect (funcall thunk elem)
-             (funcall cleanup elem *default-process-result*))))))
-    (assert (and (empty-p queue) (empty-p processes)) ()
-            "List of processes or list of things to do isn't empty: ~S / ~S~%"
-            (queue-contents queue)
-            (table-values processes)))
-  nil)
 
-#|
 #+clozure
 (defun call-queue/threading (thunk queue &key cleanup (background-p (constantly t)))
   ;; will use threads instead of fork
@@ -817,8 +755,7 @@ Operation-executed-p is at plan execution time.")
         (setf elem (dequeue queue))
         (cond
           ((funcall background-p elem)
-           (when (> (hash-table-count processes) *max-actual-forks*)
-             (setf *max-actual-forks* (hash-table-count processes)))
+           (latest-stamp-f *max-actual-forks* (hash-table-count processes))
            (let ((thread (make-communicating-thread pending elem thunk cleanup)))
              (setf (gethash thread processes) thread)))
           (t
@@ -831,218 +768,106 @@ Operation-executed-p is at plan execution time.")
   nil)
 |#
 
-(defmacro doqueue/forking ((var queue
-                            &key
-                              (result (gensym "RESULT"))
-                              (background-p t) (announce nil) (cleanup nil))
-                           &body body)
-  `(call-queue/forking
-    #'(lambda (,var) (declare (ignorable ,var)) ,@body)
-    ,queue
-    :cleanup #'(lambda (,var ,result) (declare (ignorable ,var ,result)) ,cleanup)
-    :announce #'(lambda (,var) (declare (ignorable ,var)) ,announce)
-    :background-p #'(lambda (,var) (declare (ignorable ,var)) ,background-p)))
-
-(defmethod perform :after ((operation parallel-compile-op) c)
-  (mark-operation-done (make-instance 'compile-op) c))
-
-(defmethod perform :after ((operation parallel-load-op) c)
-  (mark-operation-done (make-instance 'load-op) c))
-
-(defmethod perform :after ((operation operation) c)
-  "Record the operations and components in a stream of breadcrumbs."
-  (labels ((component-module-path (c)
-             (unless (typep c 'system)
-               (cons (coerce-name (component-name c))
-                     (component-module-path (component-parent c))))))
-    (format *breadcrumb-stream* "~S~%"
-            `(,(type-of operation)
-               ,(coerce-name (component-name (component-system c)))
-               ,@(component-module-path c)))
-    (force-output *breadcrumb-stream*)))
-
-(defun file-compile-action-p (action)
-  (destructuring-bind (op comp &optional necessary-p) action
-    (declare (ignore necessary-p))
-    (and (typep (ensure-operation op) 'parallel-compile-op) (typep comp 'source-file))))
-
-(defmethod perform-with-restarts ((operation parallelizable-operation) (module module))
-  (multiple-value-bind (action-queue ind dir) (make-checked-dependency-trees operation module)
-    (unless (empty-p action-queue)
-      (let ((all-compilation-unit-reports nil)
-            (system-name (coerce-name (component-system module)))
-            (n (length (remove-if-not 'file-compile-action-p (table-keys dir)))))
+;;; Performing a parallel plan
+(defun action-result-file (o c)
+  (let ((p (component-pathname c)))
+    (apply-output-translations
+     (make-pathname :name (format nil "~A.ASDF-~A" (file-namestring p) (type-of o))
+                    :type "process-result" :defaults p))))
+
+(defmethod perform-plan ((plan parallel-plan) &key)
+  (with-slots ((action-queue starting-points) children parents ancestor background-actions) plan
+    (DBG :pp (summarize-plan plan))
+    (let ((all-compilation-unit-reports nil)
+          (ltogo (unless (zerop background-actions) (ceiling (log background-actions 10))))
+          (*package* *package*)
+          (*readtable* *readtable*))
+      (with-compilation-unit ()
         (doqueue/forking
-            (action action-queue
-             :result result
-             :background-p
-             (destructuring-bind (op comp &optional necessary-p) action
-               (run-in-background-p op comp :force necessary-p))
+            (action-queue ;; variable for each action, queue object
+             :variables (:item action :backgroundp backgroundp :result result :condition condition)
+             :background-p (destructuring-bind (o . c) action (run-in-background-p o c))
+             :announce
+             (format t "~&Will try ~A in ~:[foreground~;background~]~%"
+                     (destructuring-bind (o . c) action (operation-description o c)) backgroundp)
+             :result-file
+             (destructuring-bind (o . c) action (action-result-file o c))
+             ;; How we cleanup in the foreground after an action is run
              :cleanup
-             (destructuring-bind (&key input-file compilation-unit-report
-                                    failure-p performed-p &allow-other-keys)
-                 result
-               (when input-file
-                 (format t "~&~@[[~4d to go in ~A] ~]Done compiling ~A~%"
-                         ;; Don't show negatives. (It's good enough for me)
-                         ;; I really don't care that or why I'm counting wrong.
-                         n system-name input-file)
+             (destructuring-bind (o . c) action
+               (cond
+                 (condition
+                  (finish-outputs)
+                  (warn "Failed ~A~:[~; in the background~]. Retrying~:*~:[~; in the foreground~]."
+                        (operation-description o c) backgroundp)
+                  (finish-outputs)
+                  (perform-with-restarts o c))
+                 (t
+                  (mark-operation-done o c)
+                  (destructuring-bind (&key compilation-unit-report &allow-other-keys) result
+                    (when compilation-unit-report
+                      (push compilation-unit-report all-compilation-unit-reports)))))
+               (when backgroundp
+                 (decf background-actions)
+                 (format t "~&[~vd to go] Done ~A~%"
+                         ltogo background-actions (operation-description o c))
                  (finish-outputs))
-               (when (file-compile-action-p action)
-                 (decf n))
-               (when compilation-unit-report
-                 (push compilation-unit-report all-compilation-unit-reports))
-               (destructuring-bind (operation component &optional necessary-p) action
-                 (when failure-p
-                   (finish-outputs)
-                   (warn "Action ~A has failure-p set. Retrying in this process." action)
-                   (finish-outputs)
-                   (perform-with-restarts (ensure-operation operation) component))
-                 (loop :for (opened-op opened-comp) :in (mark-as-done operation component ind dir)
-                       :for opened-necessary-p
-                         = (or necessary-p
-                               (and performed-p
-                                    (dependee-operations-necessary-p operation component)))
-                       :for opened-action = (list opened-op opened-comp opened-necessary-p)
-                       :do (if (can-run-in-background-p opened-op)
-                               (enqueue-in-front action-queue opened-action)
-                               (enqueue action-queue opened-action))))))
-          (destructuring-bind (operation component &optional necessary-p) action
-            (when (or necessary-p
-                      (not (operation-executed-p operation component)))
-              (perform-with-restarts (ensure-operation operation) component))))
-        (mapc #'reconstitute-deferred-warnings all-compilation-unit-reports)))
-    (assert (empty-p dir)
-            (dir ind)
-            "Direct dependency table is not empty - there is a problem ~
-               with the dependency trees:~%~S" (summarize-direct-deps dir))))
-
-(defmethod perform :before ((operation parallel-compile-op) (c source-file))
-  (ensure-all-directories-exist (asdf:output-files operation c)))
-
-(defmethod perform ((op parallel-compile-op) (c cl-source-file))
-  (let* ((source-file (component-pathname c))
-        ;; on some implementations, there are more than one output-file,
-        ;; but the first one should always be the primary fasl that gets loaded.
-        (output-file (first (output-files op c)))
-        (compile-status (list
-                         :input-file source-file
-                         :performed-p t
-                         :output-truename output-file
-                         :warnings-p nil
-                         :failure-p t))
-        warnings-p failure-p output-truename)
-    (unwind-protect (progn
-                      (multiple-value-setq (output-truename warnings-p failure-p)
-                        (call-with-around-compile-hook
-                         c #'(lambda (&rest flags)
-                               (apply *compile-op-compile-file-function* source-file
-                                      :output-file output-file
-                                      :external-format (component-external-format c)
-                                      (append flags (compile-op-flags op))))))
-                      (setf compile-status
-                            (list :input-file source-file
-                                  :performed-p t
-                                  :output-truename output-truename
-                                  :compilation-unit-report (get-compilation-unit-report)
-                                  :warnings-p warnings-p
-                                  :failure-p failure-p)))
-      (finish-outputs)
-      (cond
-        ((boundp '*current-subprocess*)
-         (process-return *current-subprocess* compile-status))
-        (t
-         (when warnings-p
-           (ecase (operation-on-warnings op)
-             (:warn (warn
-                     "~@<COMPILE-FILE warned while performing ~A on ~A.~@:>"
-                     op c))
-             (:error (error 'compile-warned :component c :operation op))
-             (:ignore nil)))
-         (when failure-p
-           (ecase (operation-on-failure op)
-             (:warn (warn
-                     "~@<COMPILE-FILE failed while performing ~A on ~A.~@:>"
-                     op c))
-             (:error (error 'compile-failed :component c :operation op))
-             (:ignore nil)))
-         (unless output-truename
-           (error 'compile-error :component c :operation op)))))))
-
-(defmethod perform-with-restart :around ((operation parallelizable-operation) c)
-  (unless (operation-executed-p operation c)
-    (call-next-method)))
+               (loop :for enabled-node :in (mark-as-done o c parents children)
+                     :for (e-o . e-c) = enabled-node
+                     :for ee-o = (make-sub-operation o e-o)
+                     :for enabled-action = (cons ee-o e-c)
+                     :do (if (run-in-background-p ee-o e-c)
+                             (enqueue-in-front action-queue enabled-action)
+                             (enqueue action-queue enabled-action)))))
+          ;; What we do in each forked process
+          (destructuring-bind (o . c) action
+            (perform o c)
+            (when backgroundp `(:deferred-warnings ,(get-deferred-warnings)))))
+        (mapc #'reconstitute-deferred-warnings all-compilation-unit-reports)
+        (assert (and (empty-p action-queue) (empty-p children))
+                (parents children)
+                "Problem with the dependency graph: ~S"
+                (summarize-plan plan))))))
+
+;;; Breadcrumbs: feature to replay otherwise non-deterministic builds
+(defvar *breadcrumb-stream* (make-broadcast-stream)
+  "Stream that records the trail of operations on components.
+As the order of ASDF operations in general and parallel operations in
+particular are randomized, it is necessary to record them to replay &
+debug them later.")
+(defvar *breadcrumbs* nil
+  "Actual breadcrumbs found, to override traversal for replay and debugging")
 
-(defmethod operation-executed-p ((op parallelizable-operation) (c module))
-  "A lazy operation on a module is done only when the op on all its
-components is done."
-  (labels ((dependency-done-p (op sub-c)
-             (loop :for (dep-op-name . dep-component-names)
-               :in (component-depends-on op sub-c)
-               :for dep-op = (make-instance dep-op-name)
-               :do (loop :for dep-component-name :in dep-component-names
-                     :for dep-c = (ensure-component (component-parent sub-c)
-                                                    dep-component-name)
-                     :do (unless (operation-executed-p dep-op dep-c)
-                           (return-from dependency-done-p nil))))
-             t))
-    (every (lambda (sub-c)
-             (and (dependency-done-p op sub-c)
-                  (operation-executed-p op sub-c)))
-           (module-components c))))
-
-(defmethod operation-executed-p ((operation parallel-load-op) (c static-file))
-  t)
-(defmethod operation-executed-p ((operation parallel-compile-op) (c static-file))
-  t)
-(defmethod operation-executed-p ((operation compile-op) c)
-  (operation-done-p operation c))
-(defmethod operation-executed-p ((operation load-op) c)
-  (operation-done-p operation c))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;; invoking operations
-
-(defun read-breadcrumbs-from (pathname)
-  (labels ((resolve-component-path (component path)
-             (if (null path)
-                 component
-                 (resolve-component-path (ensure-component component (first path))
-                                         (rest path)))))
-    (with-open-file (f pathname)
-      (loop :for (op-name system-name . component-path) = (read f nil nil)
-        :until (null op-name)
-        :collect (cons (make-instance op-name)
-                       (resolve-component-path (find-system system-name)
-                                               component-path))))))
+(defmethod perform :after ((o operation) (c component))
+  "Record the operations and components in a stream of breadcrumbs."
+  (format *breadcrumb-stream* "~S~%" `(,(type-of o) . ,(component-find-path c)))
+  (force-output *breadcrumb-stream*))
+
+(defun read-breadcrumbs-from (operation pathname)
+  (with-open-file (f pathname)
+    (loop :for (op . comp) = (read f nil nil) :while op
+          :collect (cons (make-sub-operation operation op) (find-component () comp)))))
 
 (defun call-recording-breadcrumbs (pathname record-p thunk)
   (if record-p
-      (with-open-file (*breadcrumb-stream*
-                       pathname :direction :output
-                       :if-exists :supersede :if-does-not-exist :create)
-        (funcall thunk))
+      (let ((*breadcrumb-stream*
+              (open pathname :direction :output
+                             :if-exists :rename-and-delete :if-does-not-exist :create)))
+        (format *breadcrumb-stream* ";; Breadcrumbs~%")
+        (unwind-protect
+             (funcall thunk)
+          (close *breadcrumb-stream*)))
       (funcall thunk)))
+
 (defmacro recording-breadcrumbs ((pathname record-p) &body body)
   `(call-recording-breadcrumbs ,pathname ,record-p (lambda () ,@body)))
 
-(defmethod traverse :around ((operation parallelizable-operation) system)
-  (append *breadcrumbs*
-          (remove 'system (call-next-method) :test-not #'eq
-                  :key (lambda (x) (type-of (cdr x))))))
-
-(defmethod operate :around ((operation-class parallelizable-operation) system &key
+(defmethod operate :around (operation system &key
                             (breadcrumbs-to nil record-breadcrumbs-p)
                             ((:using-breadcrumbs-from breadcrumb-input-pathname)
                              (make-broadcast-stream) read-breadcrumbs-p)
                             &allow-other-keys)
   (recording-breadcrumbs (breadcrumbs-to record-breadcrumbs-p)
-    (let ((*breadcrumbs* (when read-breadcrumbs-p
-                           (read-breadcrumbs-from breadcrumb-input-pathname))))
-      (call-next-method))))
-
-(defun parallel-load-system (system &rest args)
-  (apply #'operate 'parallel-load-op system args))
-
-(defun parallel-compile-system (system &rest args)
-  (apply #'operate 'parallel-compile-op system args))
+    (when read-breadcrumbs-p
+      (perform-plan (read-breadcrumbs-from operation breadcrumb-input-pathname)))
+    (call-next-method)))
index d1d9cb9..25d5aaf 100755 (executable)
--- a/test.lisp
+++ b/test.lisp
 
 (in-package :cl-user)
 
-(setf *load-verbose* t
-      *load-print* t
-      *compile-verbose* t
-      *compile-print* t)
+(setf *load-verbose* nil
+      *load-print* nil
+      *compile-verbose* nil
+      *compile-print* nil)
 
 (require "asdf")
 
-(setf *load-verbose* t
-      *load-print* t
-      *compile-verbose* t
-      *compile-print* t)
-
 (in-package :asdf)
 
-(defmacro dbg (tag &rest exprs)
+(defmacro DBG (tag &rest exprs)
   "simple debug statement macro:
 outputs a tag plus a list of source expressions and their resulting values, returns the last values"
   (let ((res (gensym))(f (gensym)))
@@ -38,12 +33,16 @@ outputs a tag plus a list of source expressions and their resulting values, retu
          exprs)
       (apply 'values ,res)))))
 
+(load-system :asdf)
+
 (load-system :poiu :verbose t)
 
 (setf *load-verbose* t
       *load-print* t
       *compile-verbose* t
-      *compile-print* t)
+      *compile-print* t
+      *asdf-verbose* t)
+
 
 (format *error-output* "~&POIU ~A~%" *poiu-version*)
 
@@ -56,14 +55,20 @@ outputs a tag plus a list of source expressions and their resulting values, retu
   #+sbcl
   (sb-debug:backtrace most-positive-fixnum out))
 
-#+(or)
-(trace operate traverse make-checked-dependency-trees
-       run-in-background-p
-       can-run-in-background-p operation-executed-p operation-done-p
-       input-files output-files file-write-date
-       component-operation-time mark-operation-done
-       call-queue/forking make-communicating-subprocess
-       perform perform-with-restarts perform-plan compile-file)
+;;#+(or)
+(trace
+ ;; traverse traverse-component
+ ;; make-checked-dependency-trees
+ ;; run-in-background-p
+ ;; mark-as-done
+ ;; process-return process-result ;; action-result-file
+ ;; input-files output-files file-write-date
+ ;; component-operation-time mark-operation-done
+ ;; call-queue/forking make-communicating-subprocess
+ ;; perform perform-with-restarts
+ ;; compile-file load
+ operate call-recording-breadcrumbs perform-plan
+)
 ;;#+clisp (trace posix-wexitstatus posix-wait)
 
 (defvar *fare* (asdf::user-homedir))
@@ -77,7 +82,14 @@ outputs a tag plus a list of source expressions and their resulting values, retu
                             (format t "~&ERROR:~%~A~%" condition)
                             (finish-output)
                             (return))))
-    (asdf:parallel-load-system :exscribe :verbose t :force t)
+    (asdf:parallel-load-system
+     :iterate :verbose t
+     ;;:force :all
+     :breadcrumbs-to "/tmp/breadcrumbs.text")
+    (asdf:parallel-load-system
+     :exscribe :verbose t
+     ;;:force :all
+     :breadcrumbs-to "/tmp/breadcrumbs.text")
     (funcall (find-symbol "PROCESS-COMMAND-LINE" "EXSCRIBE")
              `("-I" ,(subnamestring *fare* "fare/www/")
                "-o" "-" "-H" ,(subnamestring *fare* "fare/www/index.scr")))))