The client is partially to blame at times, but we share some blame too. I've been checking out 68+ content the past several days without botting, and the actual client issues I see in maps are disappointing. I tend to desync quite often (in lockstep), skill gems causes FPS spikes when highlighting or hovering over them still, and there's very specific cases of the client locking up due to certain events (Blacksmith leap slam + zombie summon always freezes my client for 1/2 a second). Then what other users have said, sometimes the bot magically performs a lot better after certain patches, then is back to being even worse after others (when there's no real changes on our end).
On our side though, take a high level character into normal, and run the bot will NullRoutine and look at the client performance. Now re-run the same area using OldRoutine and compare.
You should see a massive difference.
I know what the core problem is, but it's a very complicated problem to solve correctly. Basically, this game now has too many objects active at once, and that number continues to grow each major update. Caching has been used in the past to help increase performance by reducing what needs to be updated every frame, but that model is no longer sufficient for this game.
Instead, we have to use an entirety new model of spreading update logic across multiple frames, then allowing code to execute, in an attempt to ensure no one frame takes too long to execute due to updates, which is what those spikes that you see are. While we have a way to enforce this though GreyMagic, it results in the bot breaking and tons of executions being thrown, because memory is now invalid because the bot is no longer synced with the client.
This is why our setup inherently affects the FPS of the client; we're fully synchronized with the client on render so when you access client info, you know it's going to be correct and can act upon it accordingly. If we ran desynchronized from the client, you'd see much less performance overhead, but you'd also see an insane amount of memory exceptions being thrown by the bot and other oddities because the memory is so volatile in this game. An API driven setup such as ours just wouldn't work reliably that way. We know that from experience because EB was originally implemented that way, and we saw tons of problems doing things like that.
I have some experimental code I'll be trying out for the expansion to see if the problem can be reduced a lot more, but it only applies to coroutine logic. However, it won't solve the core problem in general, because anything that executes in Tick that ends up taking a lot of frametime will still result in FPS spikes (so 3rd party code needs to be aware of that). Solving that issue is more complicated than coroutines because it requires multi-frame caching of client data, which our design doesn't do; we cache data per frame instead. It's possible, but requires redesigning and rewriting the API around that fact, so that won't be done anytime soon really.