A vision of a multi-threaded Emacs
coredumped.dev
coredumped.dev
The various race conditions that would appear in a multithreaded context would be a nightmare to debug because they would likely only occur sporadically and would be hard to reproduce. I would love to be wrong about this, but I see it as a major obstacle that will likely need to be sandboxed (as was done with lexical scoping).
For an example of how hard these kinds of issues could be to debug look no further than the incredible difficult that the undo-tree author had in debugging a race condition with the garbage collector. It took years.
In the context of this piece, having a buffer mutex makes certain use cases for multi-threading impossible, for example dividing the buffer up into chunk equal to the number of cores and running a regexp over each chunk.
It might be naive of me, but why not limit the process execution to the state and buffer that its working in ?
I know that this does not answer the multi processor per buffer problem, but its a better start.
Finally for LSP, I always recommend Eglot over LSP Mode, as Eglot is written by an Emacs contributor that has deep knowledge of Emacs and Emacs Lisp. LSP Mode is written by volunteers on Github, and the code is nowhere near as good.
Given the (enormous amount / ever-increasing number) of Emacs Lisp packages out there, a lot of them being substandard, being able to make these sort of qualitative calls (and also increasingly dive into Emacs internals) will pay off. Assuming moderate effort, this will come with experience and time spent using Emacs.
> if you're experiencing I/O hangs, it's either misconfiguration or bad code in a package that you loaded
I mean...yes. That's the issue. The issue is that it's possible to have i/o hangs caused by misconfiguration or elisp packages.
The solution isn't to tell the user to do something different, it's to make it so that elisp execution can't cause i/o hangs.
edit: to a be a bit more precise with language: "i/o hang" should be interpreted as "making the UI unresponsive or preventing other tasks from completing".
I think you're missing the point. Emacs already contains all the support necessary to make LSP not lock up the UI, LSP simply doesn't use it. So if you say "LSP is a great use case for multi-threaded Emacs", that's not correct.
The problem with LSP specifically is that Emacs doesn't really know what the language server is going to do, and if it let you update the text while the language server was also updating the text, it would have to have some way to merge the changes that you made while the language server was busy. There would need to be some spec for that, or more likely, another trip to the language server to figure out what to do. You could, of course, edit the text while that second trip was happening, and this algorithm never converges to a finished state. So, it locks the UI so you can't make any edits until the text from the language server comes back. That's the easy way out, but at least it results in an algorithm that terminates.
The way to fix this is to figure out where the time is being spent. If it's on JSON marshaling/unmarshaling, speed that up. If it's because the language server is slow, speed that up. The only compromise right now would be to let you edit buffers in a different project while the language server is processing something. I don't think anyone wants to do that; "I don't want LSP to lock up Emacs" really means "I want my language server to perform all operations instantly." So get out that profiler for your language server, I think.
I don't. "The only thing that matters in software is the experience of the user." The Emacs user experience is full of I/O hangs; the VSCode experience is not. This is a problem that needs to be solved if Emacs is ever to have a significant installed base again.
I can't remember the last time I had an I/O hang in Emacs. I'm also fairly active in the mailing list and various forums, I/O hangs are not really what's being discussed day to day.
So I have to ask, are you projecting or what makes you say something like that?
My emacs likes to crash sometimes, usually a few times a week. Hopefully I’ll be able to take some time to bisect my config and see what’s going on. Looks like a memory leak.
Emacs behaves in a different but still incorrect way. It writes the entire fmt.Println statement at the wrong indentation level, locks up for a while, and decides that there was no formatting to do. VS Code will be fast after a while, but Emacs never gets fast; every reformat request locks up the UI for several seconds and doesn't actually reformat. VS Code remains responsive, but it's difficult to get the reformatted text on to disk. (Sometimes it does it, though.)
My main conclusion is that you're screwed if you have to work on k8s day to day. But on a more serious note, this isn't really an issue that can be solved with a UX tweak; the class of problem is "the optimal algorithm for providing instant hints on a codebase that is 214k lines hasn't been discovered yet". Yes, editors can let you mutate the text while it's working on something in the background; and if you turn of language server support that's fine. The key problem is merging your edits with its set of assumptions, and that's what's going to be slow.
My intent was to highlight on what a good solution looks like from a user perspective.
Off the top of my head, in an incredibly hand-wavey fashion:
A quick win would be to set a timeout on every single i/o action, with the default timeout being quite low. That doesn't remove the problem, but it'd probably be a good start towards easing the user pain. Bringing an indefinitely long hang down to 1s is a win.
For a total solution, I'd consider looking into implementing a concurrency model with a preemptive scheduler, probably using erlang as inspiration. have updates to the ui becoming a message sent to the ui inbox, and the routine for rendering the ui just reading from the inbox and applying the given command.
This is without thought to how to actually implement that, and what implications it would have on legacy elisp, or how legacy elisp could be brought into a CSP world without breakage.
So just some armchair architecting on a day off for a project I've never worked on :)
"The code is nowhere near as good" according to you? Give a concrete example of where the LSP code has real deficiencies; Eglot isn't as fully featured, is more opinionated, and its stans are more annoying.
Sounds to me like the LSP project is helping new hackers learn Emacs, and Eglot is one bus accident from being unmaintained, frankly.
Both projects are very good and everyone should give both a shot and see which they prefer, but spreading misinformation about one project because you prefer another is stupid and childish.
A quick peek at the github contributors insights also shows that the overwhelming majority of code is contributed by 3 people and not "volunteers on github", but even if it was mostly worked on by volunteers it would make no difference at all.
Code quality is not something that can be trivially disproved like the volunteer thing, but as someone who has spent a bit of time poking around in both projects, neither look any better code quality wise to me. The main thing is that lsp-mode is just much, much larger and prefers to use non-native APIs and packages at times. This is purely a difference in style and opinion rather than "quality".
This isn't really true at all, but even if it was, lsp-mode has a larger scope and has many more features than eglot. It's also much more actively maintained/developed than eglot is.
Anyways, most FOSS code is written by volunteers. I really don't think that's a great argument against a FOSS project.
Also see:
https://github.com/emacs-lsp/lsp-mode/graphs/contributors
As an emacs user and package developer of 20 years I can definitely say you are totally wrong. This is the kind of dismissive, unhelpful, and frankly extremely discouraging attitude that is slowly killing emacs.
Instead of taking responsibility for the fact that emacs is incredibly slow because of poor architectural decisions (like an insistence to not using fast native libraries and an incredibly slow language), the solution is apparently to blame the people who create packages. If developers they are creating packages that are slow, it is not their fault. It is the fault of emacs developers in refusing to modernize the architecture in a way that enables people to make efficient code.
> like an insistence to not using fast native libraries
How? Even disregarding graphics and font stack which almost entirely leverages native libraries, my Emacs build links against gmp, libpng, librsvg, zlib, alsa-lib, libxml2 and so on. 27.1 introduced jansson for native json parsing over pure elisp, from 28.1 we can basically AoT compile elisp to native code using libgccjit, master branch (29) even bundles sqlite these days. That is not to mention module system had been stable for a while, and lot of packages already use this to offload computation heavy stuff to native code.
It terrifies me how often new users today are gasping for breath on the shores, ungainly four limbed tetrapods gasping for breath...this isn't progress and it's not evolution, there's so much more of everything but it's not at all higher quality for novice users than I grew up on forty years ago.
> more high quality packages emerging each year than ever before
I question my own ability to quantify modern software quality. Compared with what? My working lifetime has been dominated by corporate shovelware and that's absorbed multiple generations of misguided human and fiscal wealth creating structural deficit immeasurable compared to the Cambrian bounty of my early years. Edited last sentence for clarity.
Emacs seems to be doing just fine if not thriving.
> Instead of taking responsibility for the fact that emacs is incredibly slow because of poor architectural decisions (like an insistence to not using fast native libraries and an incredibly slow language)
For me, Emacs is both incredibly fast and incredibly stable. For an "Emacs user and package developer of 20 years", you seem either too quick to jump to conclusions or are still barely scratching the surface of what's possible which makes me question the depth of your expertise and/or how efficiently your time with Emacs was invested.
The only way an lsp-client could be slow is by making a request to the server and then blocking on the response - which is wrong and the package writer's fault because they are not using the asynchronous api emacs provides for communicating with a process.
Languages are not slow or fast; their implementations are. There's absolutely nothing wrong with Emacs being programmed in Lisp and it sure as hell isn't "a poor architectural decision". If Emacs is slow because of its implementation of Emacs Lisp, it's time to fix its implementation of Emacs Lisp.
Right, but languages have finite extant implementations. If a language has no fast implementations then it's reasonable to call it slow.
I love Scheme and Lisp. I've probably written several million lines of code in both Scheme and Lisp and I've contributed code to several Lisp and Scheme compilers.
But languages are absolutely slow or fast! There are fast Schemes and Lisps and there are slow ones. Emacs has probably the slowest one by far. For example, Emacs still relies on dynamic binding after all of this time. That's a performance disaster.
There is simply no efficient implementation of Elisp possible as it stands and as it is used today. Even with JIT compiling Elisp performance is simply wretched.
BTW you do realize that this would put you at (sustained, average) several hundred lines of code per day (every day) over your lifetime? That sounds almost depressingly bleak to me. How does one have a life outside of that?
Grad school sure puts you on the right track of getting good numbers like this! :) And the answer was, not having a life outside of grad school.
Also, 40 years? 1000000/40/52 ~> You're assuming the average developer writes 500 lines of code per week? That seems absurdly slow.
You are contradicting yourself. If languages instead of implementations were slow or fast, then their would not be fast and slow Schemes and Lisps. Scheme would be either slow or fast.
Actually, I would go even further than saying only implementations are slow or fast, they are slow or fast for certain use cases.
> There is simply no efficient implementation of Elisp possible as it stands and as it is used today. Even with JIT compiling Elisp performance is simply wretched.
I agree in so far as there will --- probably --- never be an Elisp implementation which can run numeric intensive code as fast as the best Fortran or C compiler for this task. But it is certainly possible to improve the performance of ELisp even more than native-comp branch did. The question is whether there are people capable and willing to invest the time to develop that and whether spending that time on the language implementation is the most useful way to spend that time.
You are unfamiliar with the huge differences between what counts as a "Scheme" or a "Lisp". Neither "Scheme" or "Lisp" are a language, they're language families. The difference between Schemes is larger than the difference between say C and Ocaml, or Haskell and Javascript.
Depending on what features a particular Scheme or Lisp has, it can range from insanely fast (like, faster than an optimizing C compiler), to mediocre (like Python), to dirt slow, like current Elisp. It all depends on what the language has, and how it encourages you to write code. For example, having a type system tends to make your language very fast.
> Actually, I would go even further than saying only implementations are slow or fast, they are slow or fast for certain use cases.
Compilers are not magic. And compiler technology has not fundamentally advanced for decades now. There are basic limits to what a compiler can do. Some language features simply destroy performance, and the lack of some language features prevent compilers from getting good performance. And then, yes, there is also style. For example, relying heavily on language features that are inherently slow, will do you no favors.
Elisp is the pinnacle of a terrible language used in a terrible way. It lacks all features that make languages fast. It has plenty of features to defeat optimizations and preclude having any fast implementations. And people write Elisp in a gory style that makes everything worse.
> I agree in so far as there will --- probably --- never be an Elisp implementation which can run numeric intensive code as fast as the best Fortran or C compiler for this task. But it is certainly possible to improve the performance of ELisp even more than native-comp branch did. The question is whether there are people capable and willing to invest the time to develop that and whether spending that time on the language implementation is the most useful way to spend that time.
I don't care about numerical code. I care about the fact that emacs is dirt slow.
It's been many decades now. Elisp performance has barely budged. Even the native branch only marginally improved things in some edges cases. There is zero evidence that more effort for faster implementations will go anywhere.
In general. More developer time doesn't meaningfully speed up a language even on the timespan of decades; this means like even say a huge effort, a millenium worth of hours of developer time (100 developers full time for 10 years), makes no serious difference to the performance of the average app running on the JVM (a few percent), it mostly just patches edge cases. It's the same in Haskell. GHC 7, from 10 years ago, is marginally slower than GHC 9.4 if you run the same large app in both.
What makes a difference to performance is changing the language. Adding language features that the compiler can understand (that's why say, Haskell code is far faster today than it was 10 years ago, it's a different language, with different features, that we write in a different way).
I don't have access to $someLargeApp you mrntion, but I find this hard to believe.
Where do you find your package code most throttled? Do you have a top-3 wishlist you would want the core team to put in?
Are we really ready to declare that Lars Magne Ingebrigtsen is a substandard Emacs Lisp coder?
I don't use Gnus so I have never bothered looking at its internals, but if it is locking your emacs for twenty seconds checking email, it is probably not doing much asynchronously. It is notoriously hard to convert existing synchronous code to asynchronous without using threads. Promises/futures/continuations/whatever-you-want-to-call-them are a pretty all in affair. Not having the time investment that would take to convert a 100k line codebase to async does not make Lars Magne Ingebrigtsen a substandard Emacs Lisp coder.
Lars is amazing by the way, his relentless Emacs work and prolific bug killing in the last few years should be praised to high heavens.
This means Emacs always has an up to date view of my email within a ~30 second window of the email being received and all I'm asking it to do is query mu which takes ms.
This is the single biggest reason I don't use emacs. I have yet to get a stable installation with the necessary behaviors and tools I need to make memorizing all the chords worth the effort.
I've tried spacemacs, doom and from scratch, and have never been satisfied or gotten to the point where coding is as productive as VScode for me.
What would be the correct way to configure my .emacs file to e.g. check for package updates in a background process?
You could also make a script that starts emacs as a client only if a server already is running
#!/bin/sh
if [ "$#" -eq 0 ]
then
echo "Starting new Emacs process ..." >&2
nohup emacs > /dev/null 2>&1 &
elif emacsclient -n "$@" 2> /dev/null
then
echo "Opened $@ in Emacs server" >&2
else
echo "Opening $@ in a new Emacs process ..." >&2
nohup emacs "$@" > /dev/null 2>&1 &
fi
(Copied from an emacs starter kit but don't remember which...)From the man page:
-a, --alternate-editor=COMMAND
If the Emacs server is not running, run the specified shell command instead. This can also be specified via the ALTERNATE_EDITOR environment variable. If the value of ALTERNATE_EDITOR is the empty string, run
"emacs --daemon" to start Emacs in daemon mode, and try to connect to it.I’m not saying there’s something wrong with leaving Emacs running. But it’s a workaround. When I used Emacs, startup time was not a problem. But I didn’t use a bunch of packages.
Apart from that, without too many paclages stsrtup time is not an issue imo
> or an acceptance of “fit your workflow around Emacs” than it is truly a solution, which would be to reduce startup times.
I don't see a problem with this. Emacs is a general purpose platform, and it's silly to expect really fast startup times if you're using it as a general purpose platform. People aren't expecting Linux to have an instant startup time if you have a large number of services running - why expect it from Emacs?
For what its worth, I've got at least 23 (service --status-all) daemons already running on this desktop. So leaving things running in the background until they are needed has precedent.
I normally never exit Emacs. This is not because of startup time. To me it's like the web browser; I use it all the time, have a workspace dedicated to it and it's natural to keep lots of buffers open. My muscle memory takes care of jumping between open buffers.
(Of course it doesn't really use no resources - it consumes a finite PID among other things)
Nope, it for sure does not.
I might look at the doom config and try to divine how they're getting such fast loading times. Thank you for the suggestion.
$ grep csv ~/.emacs
'(package-selected-packages '(csv-mode org-evil dash)))
If I want evil all the time but csv only when needed, I should change that to the following? '(package-selected-packages '(org-evil dash)))
'(use-package '(csv-mode)))You can set
(setq use-package-always-defer t)
so anything you load with use-package will only be loaded on-demand. Or you can do it per package with e.g. `(use-package csv-mode :defer t)`.Also make sure you don't set use-package-always-ensure and don't `:ensure t` or it'll try to install anything that's not installed on startup. If you want fast startup, it's better to explicitly install with M-x package-install
(use-package org-evil :ensure t ;; download if not present :defer t ;; defer loading hook (org-mode . org-evil-mode)) ;; start this mode in org buffers
(use-package csv :ensure t :defer t) ;; modes are usually smart enough to know in which buffer they start
and so on for all your packages. If you NEED something to start when Emacs open, you can remove the `:defer t` macro. There are also other useful things like `:init` `:config` `custom`, you should check out the docs!
>Finally for LSP, I always recommend Eglot over LSP Mode
As far as I can tell, Eglot also just blocks waiting for the server response.
Unfortunately, that is the current state of the emacs package ecosystem. Org mode freezes emacs. Magit freezes emacs. Most package writers just don't bother writing their packages with asynchronous I/O in mind. There is no point blaming the user.
And in reverse, using threads won't prevent lock-ups: the opposite is likely. [Pre-emptive] threads compared to external processes require much more careful coordination that is hard to achieve if you are installing dozen of emacs packages written by different people that manipulate the same state.
unrelated, it is one of the arguments in favor of GIL such as in CPython--it is easier to get right than fine-grained locks.
I'm not saying you're not experiencing what you're describing but I find that weird because I use Emacs with LSP and the system is responsive while LSP loads up. And I'm using lsp-mode, not Eglot (I should really try Eglot). I mostly use it for Clojure: I'm using both Cider and lsp-mode and they work fine together. And as soon as I open any Clojure source file, I can keep using Emacs while LSP starts (which is not that slow btw: only about three seconds I'd say). LSP is one of those things that runs in another process, so it's one of those case, for me at least, where Emacs acts like if it was multithreaded. I haven't seen this lock up / freeze while waiting for some LSP server answer.
For example I'll typically open a Cojure file, lsp-mode shall begin loading automatically and then I manually launch Cider: everything happens "simultaneously" and I'm not blocked.
I'll play more attention with other LSP servers (like the CSS one) but nothing strikes me as slow or frozen.
If there's one thing I could complain about is that when I'm busy doing other things (because my Emacs ain't blocked), I see spurious LSP messages telling me where it's at kinda distracting me.
Now I take you're using a native-comp Emacs? And a fast JSON parsing lib?
Emacs 28, native comp, lsp-mode, AMD 3700X, NVMe SSD, 16 GB of RAM. A good machine but nothing crazy fast.
In a Lisp you should have continuations and whatnot to be able to the event-driven programming for that without the inconvenience of having to express all the background tasks as transitions in a state machine.
(setq org-export-in-background 't)
https://orgmode.org/manual/The-Export-Dispatcher.htmlThere's the -pvc option in latexmk that[1], "continuously check all input files for changes and re-compile the whole thing if needed and always display the result," depending on if your PDF viewer will refresh.
That may obviate the need for a bash while loop.
I'd strongly recommend something like Javascript's "web workers", where each thread lives in an almost entirely independent memory space, with sharing limited to explicitly named buffers.
The problem with traditional "everything shared by default" threading is even when you get the basic core working, we found throughout the system there were buffers and caches, which all needed making thread-safe. Even after that, every library needed some fixes -- and while these were often simple these libraries may not have had updates in years.
Parallelizing Emacs needs to be a long-term endaveour, as the maintainers of Emacs already said years ago. I think Emacs could learn something from Clojure's story for parallelism. Make data structures immutable by default, and reference them from as few global variables as possible. The "make data structures immutable by default" ship has already sailed, but introducing additional immutable list/vector/map/set data structures with first class syntax support would allow newly written code to prepare for a more parallel future. Such immutable data structures could then be shared between different "ELisp web workers". The "as few global variables story" ship has already sailed, but again ELisp authors would start to consider reducing the number of global variables if it would enable multi-threading.
By and large, if you don't write blocking code, it doesn't block. Big caveat here is the TRAMP logic to establish connections can block. Not sure why those weren't done in a more async way.
emacs really needs to be hoisted onto another language and runtime. everyone says this but no one can really conceive of doing this and rewriting or at least refactoring all the elisp extensions.
I guess guile emacs is still alive? I should really try installing it
You lost when that code lies beyond some organizational boundary, e.g., a separate project that has is too big in its own right to refactor in one go, or where the maintainers have sufficient clout to resist such a change. Such a transformation would effectively entail a fork of that project.
It's easier to implement as a language and runtime, but to get truly high performance on today's many thread many core machines you need shar d memory concurrency more like Java
JavaScript supports shared memory concurrency.
Shared memory¹ is available in browser WebWorkers these days.²
Atomic ops for building multi-threaded data structures³ are available too⁴.
Including primitives for thread synchronisation⁵⁶ used by mutexes, condvars, producer-consumer queues, etc.
¹ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
² https://caniuse.com/sharedarraybuffer
³ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
⁴ https://caniuse.com/mdn-javascript_builtins_atomics
⁵ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
If you were building something on top of them using wasm then you probably wouldn't even know what the primitive was. But we're not. We're writing Javascript, and it's a damned shame.
When someone takes a really long time to make a really bad solution, there's frequently not enough group will to correct the issue. This will be the bullshit we are stuck with for the next ten years.
Clojure' STM is basically unused. It's a suite of functions and a container type `ref` that allows you to do transactions in memory and keep everything consistent within the view of a transaction. The overhead required to do the book keeping for these transactions is often not worth it for most use cases in practice.
Clojure also, as an alternative, gives you `atom` which is a type that implements a simple thread safe compare-and-swap. This ends up being sufficient for many cases of sharing state between threads. When combined with Clojure's immutable data structures, one can often use them as a simple in memory database.
Those two things are about sharing memory, but has nothing to do with what the article talks about: green threads.
clojure.core.async, as the article succinctly explains, allows you to create "goroutines" or green threads which are then multiplexed across some number of OS threads. This can allow you to create many more threads for computation than you have OS threads, and combined with thread locals and atoms can allow one to build large scale systems.
Clojure's STM and atomics are sort of a non-sequitur from what the article is talking about
Now as for downsides of Clojure, there are plenty of gotchas in practice that can lead people to issues with the three things above:
* STM is slow and rarely useful compared to regular atomics
* atoms CAS assumes you are using immutable data structures
* core.async has a lot of sharp edges, including lack of first-class error handling and the fact that doing synchronous I/O on a go routine can end up starving the thread pool they are running on
I've never built a high performance web server, but I imagine it's possible with Clojure; it would probably require you to probably eschew immutable data structures within the plumbing (for performance) and use an async I/O framework along with core.async to get competitive with a language like go. For shared memory, you probably would end up eschewing compare-and-swap for something more bare bones like locks, which you have access to via Java interop. At that point, you may wonder why you even chose Clojure in the first place instead of using Java directly. :D
Project Loom really only helps solve the problem that core.async is trying to solve, and applications would still make use of atomics like `atom` and basic Java atomic values. My hope is that it makes it easier to do I/O, since it sounds like along with loom fibers the project is adding first class support for suspending and resuming on fundamental I/O operations. Time will tell.