My Windows app uses sourcekit-lsp. It processes requests from my app. There are not many requests, and if there is no input from the user in my app, there are no requests at all (I checked, my application is not at fault). Despite this, the sourcekit-lsp process constantly consumes ~24% of CPU power all the time, even when idle. What can cause this? What intensive calculations it does when nothing is requested from it?
I am using Swift 5.9.2. Just tried both Swift 5.10 Development and Trunk Development (main) which shows version number 5.11-something. Unfortunately the problem persists in both
Swift version CPU usage by sourcekit-lsp
5.6.1 0%
5.6.2 not tested
5.6.3 not tested
5.7 not tested
5.7.1 not tested
5.8 0%
5.8.1 0%
5.9 24%
5.9.1 not tested
5.9.2 24%
5.10 dev 24%
trunk dev 24%
It seems the bug was introduced in version 5.9 and is still not fixed in either Swift 5.10 Development or Trunk Development (main).
This is definitely interesting. I wonder if we can isolate this a bit further. I think that it might be possible to isolate libdispatch (though it is unlikely to be the cause as it doesn't change very much). I wonder if the issue is in SourceKit-LSP, sourcekitd, or somewhere else. Hopefully @z2oh might have some time to look into this.
Do you know if a corresponding issue with self-contained reproduction steps tracking this exists at Issues · apple/sourcekit-lsp · GitHub? If it doesn't, would you mind filing one?
As of April 15 2025 the fix has been merged. I was happily waiting for Swift 6.2 release, but today… the issue has been reopened since it's been reverted from main (and being reverted from 6.2) as well.
Please someone familiar with the issue and the codebase, fix it again.
It really is impossible to fix. The problem is that anonymous pipes on Windows do not function as they do on Unix. There is no mechanism to know the amount of free space in the pipe buffer at any given moment, and thus we cannot perform the partial write to the pipe synchronously. We can do the complete write, but that would block and break the semantics of dispatch’s pipe handling.
I think that one option that we may have at our disposal is for someone in the community to step up and help with the problem in an alternative direction: refactor sourcekit-lsp to not use pipes for communication. We could use a named pipe or some other mechanism where we do not need to rely on dispatch to do the communication.
Edit:
To be clear, I am suggesting that we remove the stdio communication channel and require a TCP or a named pipe connection always.
The Windows pipe synchronization thread, _dispatch_pipe_monitor_thread, uses a blocking 0-byte read as a synchronization mechanism to signal to the actual reader threads that there is data waiting in the pipe. Of course, for this to work, the read must actually be blocking. With a PIPE_NOWAIT pipe, the Read will not block, causing the thread to spin endlessly and constantly wake the actual reader thread to perform 0 byte reads. This spinning thread is the root source of the problem in this issue.
Can we get away without trying to read 0 bytes, and instead call PeekNamedPipe right away (which is called there later) to find out dwBytesAvailable ?
IIRC, that is from my original implementation and no, that is not reasonable to remove. That 0-byte read is the trick to not have the high CPU usage. By reading 0-bytes, we can effectively suspend the monitor and avoid any CPU activity. When that succeeds, we rely on some implementation specifics to determine how much we can safely read. That works great for the reader, but that causes problems for the writer, because we do not know how much free space there is in the writer and that effectively means that all writers, including anyone expecting a non-blocking call should become blocking because I can block on any non-zero-sized write.
As far as I understand, this was true for PIPE_WAIT pipes, that is for Swift 5.8.1 and earlier. Since Swift 5.9 these pipes were changed to PIPE_NOWAIT and we have the exact opposite: The sourcekit-lsp.exe consumes ~50% CPU on 2-core machines even when idle (after the init request).
As far as I understand, this is why the pipes were changed to PIPE_NOWAIT since Swift 5.9.
Correct; the conversion to PIPE_NOWAIT was to avoid another issue - we would sometimes fail to write the content and effectively fail to send the message if the buffer was ever filled and we were trying to perform a write. The problem is that the pipe model does not work on Windows because the pipe model is designed for Unix pipes. I suspect that there is no good solution which will solve both sides of the problem.
My recommendation is to simply migrate away from anonymous pipes for the communication channel and switch to a RPC mechanism using COM or gRPC. That should avoid the excessive CPU utilization and do it in a consistent manner across the platforms.
In an attempt to fix the issue the pipes were briefly changed to PIPE_WAIT, but these changes did not make it to any release, and were reverted back to PIPE_NOWAIT, IIRC due to problems on the write side.
~25% CPU load for 4-core machines or ~50% for 2-core machines indicates that one core is 100% busy. This is a serious performance issue, and everyone seems to ignore it.
Support for Windows is generally dependent on a very tiny number of engineers. For a long time, it has been just myself, and even now, is primarily ~2.5 engineers. I too would like to see this problem resolved, but it will be when the time is right for the current engineering resources to be spent on it. However, if this is a blocker for you, you are welcome to provide engineering resources to work on the problem.
This has been brought up before. I’m more than happy to form a workgroup. However, I don’t particularly see the value in forming the workgroup if there is not an active set of toolchain developers. The workgroup is valuable when there is the need for more technical conversations which need to occur or coordination needed.
There have been some occasional drive by contributions for windows, but if there were more consistent contributions to windows (e.g. improvements to the prologue/epilogue inserter, improvements to the swift linking model, major design work like removal of exec from the driver, etc), I think that a workgroup would become far more interesting.
Apple could form the workgroup of its employees. It is in their best interest to ensure that their product works perfectly on all officially supported platforms.