microsoft / vscode

Visual Studio Code
https://code.visualstudio.com
MIT License
164.12k stars 29.27k forks source link

Optimize editor for input latency #161622

Closed Tyriar closed 1 year ago

Tyriar commented 2 years ago

I recently did an exploration into input latency and found it can get pretty bad on slower machines. A lot of the problem is related to how we use synchronous event emitters/listeners work, performing their work after keypress but before the animation frame.

image

My proposal to improve is:

  1. Review the most time consuming event listeners. If they are clearly safe to be performed at a later time without worrying about race conditions being introduced., move them to use an asynchronous event listener to be performed after the animation frame. For example UI updates like activity bar badge and tab indicator when an editor becomes dirty
  2. Review all events and their listeners, defer as much as possible to after the animation frame. One way of doing this is for each important event to have both a sync and an async event. It's unclear how much we can move exactly, but when we do this we need to be extremely careful to not introduce text buffer-related race conditions as there are some assumptions made that we may be breaking by doing this
  3. Setup development tools and/or telemetry to easily track measuring latency, I created https://github.com/microsoft/vscode/tree/tyriar/measure_latency to demonstrate a technique to approximate input latency
  4. Come up with a plan for how we can prevent regressions for this critical path code

Tentatively assigning to October

bpasero commented 2 years ago

The core listener for text editors to react on content changes is:

These drive a ton of things on top such as:

I wonder how an async emitter would help here: yes, it would take away lag from the first character typing when the editor transitions into being dirty, but eventually we have to pay the price, so the lag would just happen later? Or is the idea to delay the event literally on idle time?

Tyriar commented 2 years ago

@bpasero oh I missed the question there. The idea is to delay it until shortly after via setTimeout, so the text change should appear asap and the dirty indicator (as an example) would appear 1 or 2 frames later. ie:

Current:

Desired:

There's some nuance here in what should be handled in the keypress task. For example currently the suggest widget is moved and updated in the keypress task. What we probably want is for the suggest widget to move in the keypress task but defer updating as it can be very expensive. Things like this we'll need to experiment with to see if splitting it up ends up with a worse UX and should be on the critical path.

We may be able to optimize the list's splice method as well to help here, haven't looked at the impl yet but it seems to do a lot of work and also affects search performance/ui responsiveness I saw last week.

Tyriar commented 2 years ago

Realized one of my laptops has a CPU similar to the average users (though a better GPU). Here's a screenshot of typing this into TerminalInstance.ctor which validates my assumptions that latency is pretty terrible on lower hardware (up to 100ms in this case):

image

This is with a i7-8750H @ 2.2 GHz, didn't turn off turbo boost which can push it up to 4.10 GHz. Not entirely sure how that works but I think I can disable it in BIOS if needed.

Though I haven't tested thoroughly on the laptop, I'm quite surprised that it seems to actually perform much worse than 4x CPU throttle on my primary machine (i7-12700KF @ 3.61 GHz, boost 5.00 GHz). I was expecting it to be the other way around.

Tyriar commented 2 years ago

I drilled into a profile on my macbook to understand some parts a little more. Here are the details

TLDR: An enormous amount of work seems to be spent just scheduling things, Event.defer will probably be an easy solution to those.

image

Latency

High level parts

Key press

15.43ms (51% of critical path)

Most expensive bottom up parts