Update Emacs Parallel

This commit is contained in:
Grégoire Jadi 2013-08-19 20:46:52 +02:00
parent c9215889cc
commit 468b183880
4 changed files with 260 additions and 27 deletions

View file

@ -0,0 +1,147 @@
* Emacs Parallel
Emacs Parallel is yet another library to simulate parallel
computations in Emacs (because it lacks threads support in Elisp).
* STARTED HowTo
You can execute a simple function a retrive the result like this:
#+BEGIN_SRC emacs-lisp
(parallel-get-result (parallel-start (lambda () (* 42 42))))
⇒ 1764
#+END_SRC
Though you won't benefit from the parallelism because
~parallel-get-result~ is blocking, that is it waits for the function
to be executed.
So you can use define a callback to be called when the function is
finished:
#+BEGIN_SRC emacs-lisp
(parallel-start (lambda () (sleep-for 4.2) "Hello World")
:post-exec (lambda (results _status)
(message (first results))))
⊣ Hello World
#+END_SRC
Here, why ~(first results)~ and not ~result~? Because you can send
data from the remote instance while it's running with
~parallel-remote-send~:
#+BEGIN_SRC emacs-lisp
(parallel-start (lambda ()
(parallel-remote-send "Hello")
(sleep-for 4.2)
"World")
:post-exec (lambda (results _status)
(message "%s"
(mapconcat #'identity (reverse results) " "))))
⊣ Hello World
#+END_SRC
As you may have noticed the results are pushed in a list, so the
first element is the result returned by the function called, the
second is the last piece of data send, and so on...
And of course you can execute some code when you receive data from
the remote instance:
#+BEGIN_SRC emacs-lisp
(parallel-start (lambda ()
(parallel-remote-send 42)
(sleep-for 4.2) ; heavy computation to compute PI
pi)
:on-event (lambda (data)
(message "Received %S" data)))
⊣ Received 42
⊣ Received 3.141592653589793
#+END_SRC
Because the function is executed in another Emacs instance (in Batch
Mode by default), the environment isn't the same. However you can
send some data with the ~env~ parameter:
#+BEGIN_SRC emacs-lisp
(let ((a 42)
(b 12))
(parallel-get-result (parallel-start (lambda (a b) (+ a b))
:env (list a b))))
⇒ 54
#+END_SRC
By default, the remote Emacs instance is exited when the function is
executed, but you can keep it running with the
~:continue-when-executed~ option and send new code to be executed
with ~parellel-send~.
#+BEGIN_SRC emacs-lisp
(let ((task (parallel-start (lambda () 42)
:continue-when-executed t)))
(sleep-for 4.2)
(parallel-send task (lambda () (setq parallel-continue-when-executed nil) 12))
(parallel-get-results task))
⇒ (12 42)
#+END_SRC
As you can see, to stop the remote instance you have to set the
variable ~parallel-continue-when-executed~ to nil.
* Modules
** Parallel XWidget
[[http://www.emacswiki.org/emacs/EmacsXWidgets][Emacs XWidget]] is an experimental branch which permits to embed GTK+
widget inside Emacs buffers. For instance, it is possible to use it
to render an HTML page using the webkit engine within an Emacs
buffer.
With this module, you can configure your "main" Emacs to use
another one to render web pages.
Let's assume that you've cloned [[https://github.com/jave/xwidget-emacs][the Emacs XWidget repository]] in
~$HOME/src/emacs-xwidget/~. Once you've compiled it, an Emacs
executable is available ~$HOME/src/emacs-xwidget/src/emacs~.
Configure ~parallel-xwidget~ to use it:
#+BEGIN_SRC emacs-lisp
(setq parallel-xwidget-config (list :emacs-path
(concat (getenv "HOME")
"/src/emacs-xwidget/src/emacs")))
#+END_SRC
Then configure your current Emacs to use it:
#+BEGIN_SRC emacs-lisp
(setq browse-url-browser-function 'parallel-xwidget-browse-url)
#+END_SRC
You can check it out with M-x browse-url RET google.com RET.
* Tips & Tricks
If your windows manager is smart enough (like StumpwWM) you can use
it to move graphical windows (Emacs frames) in another desktop.
For example, I use this to move Emacs frames (with the title
"emacs-debug") to the group (aka desktop) 9:
#+BEGIN_SRC lisp
(define-frame-preference "9"
(0 nil t :title "emacs-debug"))
#+END_SRC
And this to specify the title of the frame:
#+BEGIN_SRC emacs-lisp
(parallel-start (lambda () 42)
:no-batch t
:emacs-args '("-T" "emacs-debug"))
#+END_SRC
* TODO How does it work?
* Known limitations
You can only send data to the remote (with the ~env~ parameter) or
from the remote (with ~parallel-send~ and ~parallel-remote-send~)
that have a printed representation (see [[info:elisp#Printed%20Representation][info:elisp#Printed
Representation]]).
So you can pass around numbers, symbols, strings, lists, vectors,
hash-table but you can't pass buffers, windows, frames...
It lacks documentation, tests and probably a clean API, but I'm
working on it!

View file

@ -22,12 +22,15 @@
;;; Code:
(require 'cl)
(defvar parallel-service nil)
(defvar parallel-task-id nil)
(defvar parallel-client nil)
(defvar parallel--executed nil)
(defvar parallel-continue-when-executed nil)
(defun parallel-send (data)
(defun parallel-remote-send (data)
(process-send-string parallel-client
(format "%S " (cons parallel-task-id data))))
@ -39,7 +42,7 @@
:host "localhost"
:family 'ipv4))
(set-process-filter parallel-client #'parallel-remote--filter)
(parallel-send 'code)
(parallel-remote-send 'code)
(when noninteractive ; Batch Mode
;; The evaluation is done in the `parallel--filter' but in Batch
;; Mode, Emacs doesn't wait for the input, it stops as soon as
@ -48,15 +51,30 @@
(sleep-for 10)))) ; arbitrary chosen
(defun parallel-remote--filter (_proc output)
(parallel-send
(if (or noninteractive
(not debug-on-error))
(condition-case err
(eval (read output))
(error err))
(eval (read output))))
(setq parallel--executed t)
(kill-emacs))
(dolist (code (parallel--read-output output))
(parallel-remote-send
(if (or noninteractive
(not debug-on-error))
(condition-case err
(eval code)
(error err))
(eval code))))
(unless parallel-continue-when-executed
(setq parallel--executed t)
(kill-emacs)))
(defun parallel--read-output (output)
"Read lisp forms from output and return them as a list."
(loop with output = (replace-regexp-in-string
"\\`[ \t\n]*" ""
(replace-regexp-in-string "[ \t\n]*\\'" "" output)) ; trim string
with start = 0
with end = (length output)
for ret = (read-from-string output start end)
for data = (first ret)
do (setq start (rest ret))
collect data
until (= start end)))
(provide 'parallel-remote)

View file

@ -0,0 +1,59 @@
;;; parallel-xwidget.el ---
;; Copyright (C) 2013 Grégoire Jadi
;; Author: Grégoire Jadi <gregoire.jadi@gmail.com>
;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation, either version 3 of
;; the License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(require 'parallel)
(require 'browse-url)
(defgroup parallel-xwidget nil
"Browse the web in another emacs instance with XWidget."
:group 'emacs)
(defvar parallel-xwidget--task nil)
(defcustom parallel-xwidget-config nil
"Parallel configuration."
:type 'alist
:group 'parallel-xwidget)
(defun parallel-xwidget--init ()
(setq parallel-xwidget--task
(parallel-start (lambda ()
(require 'xwidget))
:graphical t
:continue-when-executed t
:config parallel-xwidget-config)))
(defun parallel-xwidget-browse-url (url &optional new-session)
"Browse URL in another Emacs instance."
(interactive (browse-url-interactive-arg "xwidget-webkit URL: "))
(unless (and parallel-xwidget--task
(eq 'run (parallel-status parallel-xwidget--task)))
(parallel-xwidget--init))
(parallel-send parallel-xwidget--task
(lambda (url new-session)
(xwidget-webkit-browse-url url new-session))
(url-tidy url) new-session))
(provide 'parallel-xwidget)
;;; parallel-xwidget.el ends here

View file

@ -23,7 +23,7 @@
;;; Code:
(require 'cl)
(require 'find-func)
(require 'parallel-remote)
(defgroup parallel nil
"Execute stuff in parallel"
@ -87,7 +87,7 @@
(defun* parallel-start (exec-fun &key post-exec env timeout
emacs-path library-path emacs-args
graphical debug on-event
graphical debug on-event continue-when-executed
username hostname hostport
config)
(parallel--init-server)
@ -101,6 +101,7 @@
graphical
debug
on-event
continue-when-executed
username
hostname
hostport)
@ -113,7 +114,7 @@
library-path (or library-path
(plist-get config :library-path)
(plist-get parallel-config :library-path)
(find-library-name "parallel-remote")))
(locate-library "parallel-remote")))
(let ((task (parallel--new-task))
proc tunnel ssh-args)
@ -127,6 +128,7 @@
(put task 'on-event on-event))
(put task 'results nil)
(put task 'status 'run)
(put task 'queue nil)
;; We need to get the tunnel if it exists so we can send the right
;; `service' to the remote.
@ -150,6 +152,7 @@
(process-contact parallel--server :service)))
"--eval" (format "(setq parallel-task-id '%S)" task)
"--eval" (format "(setq debug-on-error '%S)" debug)
"--eval" (format "(setq parallel-continue-when-executed '%S)" continue-when-executed)
"-f" "parallel-remote--init"
emacs-args)))
@ -239,23 +242,20 @@ to `funcall' FUN with ENV as arguments."
(defun parallel--filter (connection output)
"Server filter used to retrieve the results send by the remote
process and send the code to be executed by it."
(loop with output = (replace-regexp-in-string
"\\`[ \t\n]*" ""
(replace-regexp-in-string "[ \t\n]*\\'" "" output)) ; trim string
with start = 0
with end = (length output)
for ret = (read-from-string output start end)
for data = (first ret)
do (setq start (rest ret))
do (parallel--process-output connection (first data) (rest data))
until (= start end)))
(dolist (data (parallel--read-output output))
(parallel--process-output connection (first data) (rest data))))
(defun parallel--process-output (connection task result)
(put task 'connection connection)
(cond ((and (not (get task 'initialized))
(eq result 'code))
(process-send-string connection
(parallel--call-with-env (get task 'exec-fun)
(get task 'env)))
(apply #'parallel-send
task
(get task 'exec-fun)
(get task 'env))
(let ((code nil))
(while (setq code (pop (get task 'queue)))
(apply #'parallel-send task (car code) (cdr code))))
(put task 'initialized t))
(t
(push result (get task 'results))
@ -296,6 +296,15 @@ result returned by exec-fun."
"Stop TASK."
(delete-process (get task 'proc)))
(defun parallel-send (task fun &rest env)
"Send FUN to be evaluated by TASK in ENV."
(let ((connection (get task 'connection)))
(if connection
(process-send-string
connection
(parallel--call-with-env fun env))
(push (cons fun env) (get task 'queue)))))
(provide 'parallel)
;;; parallel.el ends here