Nevertheless using just a single thread to simulate everything seems like a great approach.
Reminds me of an InfoQ post about simulating robot swarms in a single thread: https://www.infoq.com/articles/java-robot-swarms/ (links to Java libraries for that in the comments)
Being able to run everything in a single thread allows to:
1) Do a lot of tests quickly (with virtual time flowing as fast as possible), or arbitrarily slowly if you prefer slow-motion in some contexts.
2) Have determinism. Great for debugging: can reproduce bugs at will, even with full logs on (one trick is to enable them just some time before the bug occurs, when the bug is far from start time).
3) More easily figure out whether a bug is in domain code or a threading issue.
4) Use single-threaded execution as a bench of domain code, and see how much multi-threading/distribution can make them faster or do make them slower.
One constraint is that it rules out some programming styles, since the code must never use waiting constructs, like futures (if the single thread starts to wait, it will wait forever since nothing happens outside of it).
The constraint here is for testing/simulation to be able to supply their own implementation of waiting constructs, not that waiting constructs cannot be used, of course they can.
Inside of the technical layers you use to run it in multiple threads, there are of course wait/notify mechanisms (or similar).
Maybe you thought about wait implementations that would not wait but that would "help", and go on with other computations while the condition is not yet met? If not then I would like you to expand on what you mean, ideally with a few lines of code as a sample to make it clear.
(*) When doing deterministic virtual time scheduling, computations are scheduled to take place at given times, and are processed exactly at the time they are supposed to be. The time can only change once everything that had to be computed at current time has been computed (if in "as fast as possible" mode, the scheduler then just jumps the clock to the next time something is scheduled to happen) (if you want to read more about that, see "time advance request" and "time advance grant" in the HLA norm).
Relatedly, FoundationDB has a distributed testing framework called “Simulation”, which can simulate distributed failures on a single machine (thread!). Quoting from https://pierrezemb.fr/posts/notes-about-foundationdb/ :
> We wanted FoundationDB to survive failures of machines, networks, disks, clocks, racks, data centers, file systems, etc., so we created a simulation framework closely tied to Flow. By replacing physical interfaces with shims, replacing the main epoll-based run loop with a time-based simulation, and running multiple logical processes as concurrent Flow Actors, Simulation is able to conduct a deterministic simulation of an entire FoundationDB cluster within a single-thread! Even better, we are able to execute this simulation in a deterministic way, enabling us to reproduce problems and add instrumentation ex post facto. This incredible capability enabled us to build FoundationDB exclusively in simulation for the first 18 months and ensure exceptional fault tolerance long before it sent its first real network packet. For a database with as strong a contract as the FoundationDB, testing is crucial, and over the years we have run the equivalent of a trillion CPU-hours of simulated stress testing.