-
Notifications
You must be signed in to change notification settings - Fork 149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rapid input causes LocalProcessTerminalView hosting REPL processes to hang #294
Comments
I am unable to reproduce this. I suspect there is a race condition and this is why you are able to reproduce and not me. Would you be so kind to stop the process when it hangs and get the stack traces for all the threads involved? |
Your are most certainly right about the problem being a race condition. I"m building an editor for music live coding. Users can, at any rate, send multiline strings of any size into a GHCI process I"m hosting with Here"s the function I"m using that calls .send: func runBlock() async {
guard await bootTidal() else {
log("tidal session was stopped")
return
}
let blockContent = self.getCurrentBlockContent()
let formattedBlockContent = ":{\n\(blockContent)\n:}\n"
// todo: find a way to get a callback when this finishes, or queue calls here
self.ghciSessionView?.send(txt: formattedBlockContent)
} One potential solution from my end could be to implement a queue, but it would greatly simplify things if callers of send could be notified upon its completion or if they could pass a completion handler along with the text. In your opinion, would this be a useful feature for other situations as well, and do you think it could justify a change on your end? Or perhaps you could recommend another way to address this issue? |
oh the issue is you are sending the data as opposed to the reading of data? That could explain why I didn’t reproduce. How much data is being sent? This might be even be simpler, just a deadlock based on buffer sizes. Annoying to address, but easier to reason about. |
I ran a quick test, with this multiline string (tidal cycles expression):
When I called It"s important to note that any size of input is okay, as long as you don"t try to call |
Thanks, let me try to reproduce |
Ok, I tried something like that, but can not reproduce. I think the best thing to do is get a trace of all the threads once it get stuck. In Xcode, pause the program from the debug menu and then on the debugger console type the
|
Here"s the stack trace just after the terminal view freezes:
If this doesn"t help I"ll try to come up with a minimal example we can both run and verify in the coming days. Do you have GHC installed? If not I"ll try it with a Python REPL. |
Mhm. This stack trace does not show a deadlock. |
I"ve come up with this quick&dirty example using a python REPL. On my machine I can reproduce the issue every time after pressing the button. You can tweak send call delay time and numbers in the import SwiftUI
import SwiftTerm
struct Constants {
static let python3Path = "/usr/bin/python3"
// delay between .send calls in seconds
static let sendDelay = 0.05
// number of times to trigger .send
static let sendCount = 4
// dummy python code to send into repl
static let blockContent = """
import time
def is_prime(n):
if n <= 1:
return False
elif n <= 3:
return True
elif n % 2 == 0 or n % 3 == 0:
return False
i = 5
while i * i <= n:
if n % i == 0 or n % (i + 2) == 0:
return False
i += 6
return True
def print_first_n_primes(n):
count = 0
num = 2
while count < n:
if is_prime(num):
print(num)
count += 1
num += 1
start = time.time()
print_first_n_primes(10000)
end = time.time()
print(f"Time taken: {end - start} seconds")
"""
}
@main
struct FreezingREPLApp: App {
@StateObject var replViewManager = REPLViewManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(replViewManager)
}
}
}
struct ContentView: View {
@EnvironmentObject var replViewManager: REPLViewManager
var body: some View {
VStack {
REPLView()
Button(action: {
replViewManager.sendTextToREPL()
}) {
Text("freeze the repl")
}
}
.padding()
}
}
class REPLViewManager: ObservableObject {
private var replView: LocalProcessTerminalView?
func setupREPLView(_ view: LocalProcessTerminalView) {
view.font = .monospacedSystemFont(ofSize: 10, weight: .regular)
replView = view
replView?.startProcess(executable: Constants.python3Path)
}
func sendTextToREPL() {
guard let repl = replView else {
print("REPLView is not initialized")
return
}
for i in 0..<Constants.sendCount {
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.sendDelay * Double(i)) {
repl.send(txt: Constants.blockContent)
}
}
}
}
struct REPLView: NSViewRepresentable {
@EnvironmentObject var replViewManager: REPLViewManager
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> LocalProcessTerminalView {
let view = LocalProcessTerminalView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
replViewManager.setupREPLView(view)
return view
}
func updateNSView(_ nsView: LocalProcessTerminalView, context: Context) {}
class Coordinator: NSObject {
var parent: REPLView
init(_ parent: REPLView) {
self.parent = parent
}
}
} After pressing the button I can see some output in the terminal - until it quickly stops. Then it becomes unresponsive to further input. If you watch the Activity Monitor before and after clicking the button you"ll see that the python process doesn"t crash - but somehow the |
This is fabulous! Thank you, I’ll take a look! |
#294 It seems like the problem is the use of the convenience DispatchQueue read/write APIs that use a file descriptor as a parameter. They would stop delivering data even if data was avaialble, blocking the child process. I created a dedicated channel for reading and it works fine now (might consider doing this for writing to). Additionally, there is a new dispatchQueue introduced, this is not necessary to fix this particular bug, but while I was testing, I noticed a nicer scrolling behavior with a lot of output if I received the data on a queue, and then dispatched the results to the main queue than if I processed this directly in the main queue, which had a visual behavior of "waves".
Fixed: your test case was invaluable, thank you so much for taking the time to put it together. |
I can only give that back, thank you for the quick fix! |
I"m using your package in SwiftUI on macOS to host a GHCI process. When rapidly sending large amounts of text to LocalProcessTerminalView, the view becomes unresponsive.
Steps to Reproduce
Expected Behavior
The REPL process within the LocalProcessTerminalView should properly handle the incoming data and remain responsive, regardless of the input rate or size.
Actual Behavior
After rapidly sending large amounts of text, the LocalProcessTerminalView freezes and the hosted REPL process becomes unresponsive to further text input (manual or via
send
).I"ve noticed the implementation of the
send
method uses DispatchIO. I"m wondering if it might be beneficial to provide a mechanism to notify the caller when the send operation is complete. This might potentially enable us to ensure that send is not called while it"s already running, particularly in the context of a SwiftUI@MainActor
situation. This could be one route towards a resolution, although of course I understand there may be complexities that I"m not aware of.The text was updated successfully, but these errors were encountered: