This PR completely rewrites the internals of the turtle crate. This work was done to address a number of issues and set the stage for many exciting features that will be added in the future (see below). This is essentially a more robust version of the work done in #31. Three years after doing that work in a hurry while trying to address a showstopper bug, I have finally had some time to redesign turtle (almost) from scratch.
Overview
The new architecture is a little more complicated than the old one, but still largely the same at a high-level:
We have two "processes" - the "main process" and the "server process"
The "server process" (or "renderer server process") is where the window is managed and turtle drawing commands are run
The "main process" is where the user writes normally looking Rust code to interact with the server process and draw pictures
The reason it's more complicated now is that we've introduced several platform-specific backends. These deal with issues like #27 while still allowing for more flexibility on platforms where that issue doesn't exist. There are currently three backends:
multiprocessed (used on MacOS) - The main process and server process are actually separate processes which communicate via IPC
multithreaded (used on Windows and Linux) - The main process and the server process are separate tasks (basically separate threads) and communicate via channels in the same process
test (used for running tests) - The server process does not actually create any windows or manage events, etc.
Goals
There were several key goals with this rewrite:
Enable multiple turtle (#16) and async turtle (#17) support
Have zero CPU usage when nothing is being drawn (#99)
Use ipc-channel instead of JSON over stdin/stdout (#50)
Have an instant speed that is actually "instant", at least for small programs (#44)
Rewrite the renderer to use pathfinder (#45)
Address other issues that have been around for a while (#30, #49)
I chose these goals because I knew that they had the potential to impact the stable public API of the crate. I needed to fully explore the multiple turtle and async turtle features in order to know whether our public API would still work when those features are finally released. I also needed to fix a number of bugs and deal with issues regarding our windowing/graphics APIs so I could know which parts of the API to keep unstable after v1.0.0 is released.
(I've actually been trying to release a turtle v1.0.0 for the past several years. That's why you've seen so many 1.0.0-{alpha,rc} versions...)
Does all of this work ended up being a great exercise because I found several major bugs in turtle and even ended up making some breaking changes. I am definitely glad I put in the time to explore these features even though it will still take time for them to be publically exposed in the crate.
For a full list of the breaking changes, see CHANGELOG.md
The turtle crate is now entirely asynchronous under the hood. This really helps with (#99) since we now only do work when needed and wait for things to happen in all other cases. The turtle crate has 0 CPU usage unless something is drawing, and even then we only use the bare minimum. The Turtle struct is now just a blocking interface over a new AsyncTurtle struct. AsyncTurtle will be made public when the async turtles feature is completed.
(Shout out to tokio, the great async library that the turtle crate relies on. The tokio maintainers are very nice and I received a great deal of help from the people on their Discord. Really enjoyed using the library and I'm glad we have it!)
Multiple Turtles
To enable multiple turtle support, I rewrote the renderer process to track an arbitrary number of turtles and their drawings. Each Turtle now remembers its TurtleId (not exposed publically) and uses that to tell the renderer process to draw things on behalf of that turtle. As part of #44, all animation is now performed on the renderer process. So turtles no longer need to use IPC to update the "temporary path". Everything is done in the server process asynchronously.
Having multiple turtles enables a lot of opportunities for concurrency and parallelism. Our previous approach of running one drawing command at a time doesn't really work, especially now that animations are handled in the server process. If one turtle is drawing a line, we don't want other turtles to have to wait for that if they don't have to. A lot of work was done to enable this kind of concurrency while also preserving operations that have to be sequentially consistent (e.g. clear all drawings).
This PR completely rewrites the internals of the turtle crate. This work was done to address a number of issues and set the stage for many exciting features that will be added in the future (see below). This is essentially a more robust version of the work done in #31. Three years after doing that work in a hurry while trying to address a showstopper bug, I have finally had some time to redesign turtle (almost) from scratch.
Overview
The new architecture is a little more complicated than the old one, but still largely the same at a high-level:
The reason it's more complicated now is that we've introduced several platform-specific backends. These deal with issues like #27 while still allowing for more flexibility on platforms where that issue doesn't exist. There are currently three backends:
multiprocessed
(used on MacOS) - The main process and server process are actually separate processes which communicate via IPCmultithreaded
(used on Windows and Linux) - The main process and the server process are separate tasks (basically separate threads) and communicate via channels in the same processtest
(used for running tests) - The server process does not actually create any windows or manage events, etc.Goals
There were several key goals with this rewrite:
ipc-channel
instead of JSON over stdin/stdout (#50)I chose these goals because I knew that they had the potential to impact the stable public API of the crate. I needed to fully explore the multiple turtle and async turtle features in order to know whether our public API would still work when those features are finally released. I also needed to fix a number of bugs and deal with issues regarding our windowing/graphics APIs so I could know which parts of the API to keep unstable after v1.0.0 is released.
(I've actually been trying to release a turtle v1.0.0 for the past several years. That's why you've seen so many
1.0.0-{alpha,rc}
versions...)Does all of this work ended up being a great exercise because I found several major bugs in turtle and even ended up making some breaking changes. I am definitely glad I put in the time to explore these features even though it will still take time for them to be publically exposed in the crate.
For a full list of the breaking changes, see CHANGELOG.md
Issues Addressed
Fully Asynchronous Internals
The turtle crate is now entirely asynchronous under the hood. This really helps with (#99) since we now only do work when needed and wait for things to happen in all other cases. The turtle crate has 0 CPU usage unless something is drawing, and even then we only use the bare minimum. The
Turtle
struct is now just a blocking interface over a newAsyncTurtle
struct.AsyncTurtle
will be made public when the async turtles feature is completed.(Shout out to tokio, the great async library that the turtle crate relies on. The tokio maintainers are very nice and I received a great deal of help from the people on their Discord. Really enjoyed using the library and I'm glad we have it!)
Multiple Turtles
To enable multiple turtle support, I rewrote the renderer process to track an arbitrary number of turtles and their drawings. Each
Turtle
now remembers itsTurtleId
(not exposed publically) and uses that to tell the renderer process to draw things on behalf of that turtle. As part of #44, all animation is now performed on the renderer process. So turtles no longer need to use IPC to update the "temporary path". Everything is done in the server process asynchronously.(Source)
Having multiple turtles enables a lot of opportunities for concurrency and parallelism. Our previous approach of running one drawing command at a time doesn't really work, especially now that animations are handled in the server process. If one turtle is drawing a line, we don't want other turtles to have to wait for that if they don't have to. A lot of work was done to enable this kind of concurrency while also preserving operations that have to be sequentially consistent (e.g. clear all drawings).