Deadlock fix: no channel sends during frame rendering while mutex is locked.

This commit is contained in:
Greg Pomerantz 2026-06-02 11:32:57 -04:00
parent b838b4c75f
commit 367e6e9052
2 changed files with 24 additions and 4 deletions

View File

@ -29,13 +29,16 @@ func main() {
} }
func run(w *app.Window) error { func run(w *app.Window) error {
log.Printf("run: starting")
var ops op.Ops var ops op.Ops
shaper := text.NewShaper(text.WithCollection(gofont.Collection())) shaper := text.NewShaper(text.WithCollection(gofont.Collection()))
logic := editor.NewLogic() logic := editor.NewLogic()
renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State()) renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State())
var mu sync.Mutex var mu sync.Mutex
var elems []ui.Element var elems []ui.Element
log.Printf("run: starting frameReceiver")
go frameReceiver(w, &mu, &elems, logic.FrameChan()) go frameReceiver(w, &mu, &elems, logic.FrameChan())
log.Printf("run: starting logic.Run")
go logic.Run() go logic.Run()
for { for {
@ -49,34 +52,43 @@ func run(w *app.Window) error {
PixelHeight: e.Config.Size.Y, PixelHeight: e.Config.Size.Y,
} }
case app.FrameEvent: case app.FrameEvent:
log.Printf("run: FrameEvent")
gtx := app.NewContext(&ops, e) gtx := app.NewContext(&ops, e)
newScale := gtx.Metric.PxPerDp newScale := gtx.Metric.PxPerDp
curScale := logic.State().Scale() curScale := logic.State().Scale()
if newScale != curScale { if newScale != curScale {
logic.ConfigChan() <- editor.ScaleEvent{newScale} logic.ConfigChan() <- editor.ScaleEvent{newScale}
} }
// Deadlock risk: ensure no channel sends while lock is held
mu.Lock() mu.Lock()
currentElems := elems currentElems := elems
renderer.Draw(gtx, currentElems) renderer.Draw(gtx, currentElems)
logic.DisplayLineChan() <- int(renderer.LastLineY()) lastLineY := int(renderer.LastLineY())
// Send search query update to the logic goroutine when it changes. // Send search query update to the logic goroutine when it changes.
// The logic goroutine handles filtering and triggers a new frame. // The logic goroutine handles filtering and triggers a new frame.
newQuery := logic.State().Browser.SearchEditor.Text() newQuery := logic.State().Browser.SearchEditor.Text()
sendQuery := false
if newQuery != logic.State().Browser.Query { if newQuery != logic.State().Browser.Query {
logic.SearchQueryChan() <- newQuery sendQuery = true
} }
if events := renderer.CheckGestures(e.Source, gtx.Metric); len(events) > 0 { if events := renderer.CheckGestures(e.Source, gtx.Metric); len(events) > 0 {
logic.InputChan() <- events logic.InputChan() <- events
} }
e.Frame(&ops) e.Frame(&ops)
mu.Unlock() mu.Unlock()
if sendQuery {
logic.SearchQueryChan() <- newQuery
}
logic.DisplayLineChan() <- lastLineY
} }
} }
} }
func frameReceiver(w *app.Window, mu *sync.Mutex, elems *[]ui.Element, frameChan <-chan []ui.Element) { func frameReceiver(w *app.Window, mu *sync.Mutex, elems *[]ui.Element, frameChan <-chan []ui.Element) {
log.Printf("frameReceiver: loop starting")
for { for {
frame := <-frameChan frame := <-frameChan
log.Printf("frameReceiver: received frame")
mu.Lock() mu.Lock()
*elems = frame *elems = frame
w.Invalidate() w.Invalidate()

View File

@ -1,6 +1,7 @@
package editor package editor
import ( import (
"log"
"sync" "sync"
"pad/internal/browser" "pad/internal/browser"
@ -81,8 +82,8 @@ func NewLogic() *Logic {
configChan: make(chan ConfigUpdate), configChan: make(chan ConfigUpdate),
frameChan: make(chan []ui.Element), frameChan: make(chan []ui.Element),
inputChan: make(chan []ui.InputEvent), inputChan: make(chan []ui.InputEvent),
lastLineYChan: make(chan int), lastLineYChan: make(chan int),
resultChan: make(chan ResultEvent), resultChan: make(chan ResultEvent),
searchQueryChan: make(chan string), searchQueryChan: make(chan string),
openFileChan: make(chan string), // Initialized openFileChan: make(chan string), // Initialized
workerPool: wp, workerPool: wp,
@ -136,6 +137,7 @@ func (l *Logic) Scale() float32 {
// Run runs the logic goroutine loop. // Run runs the logic goroutine loop.
func (l *Logic) Run() { func (l *Logic) Run() {
// Dispatch initial directory index build on startup // Dispatch initial directory index build on startup
log.Printf("Logic: Dispatching BuildIndexTask")
l.workerPool.Dispatch(pool.NewBuildIndexTask(l.state.Browser.CurrentPath, l.mockFS)) l.workerPool.Dispatch(pool.NewBuildIndexTask(l.state.Browser.CurrentPath, l.mockFS))
for { for {
@ -143,6 +145,7 @@ func (l *Logic) Run() {
case <-l.done: case <-l.done:
return return
case update := <-l.configChan: case update := <-l.configChan:
log.Printf("Logic: ConfigEvent")
update.apply(l.state) update.apply(l.state)
l.frameChan <- l.state.layout(l.browserManager) l.frameChan <- l.state.layout(l.browserManager)
case y := <-l.lastLineYChan: case y := <-l.lastLineYChan:
@ -151,11 +154,13 @@ func (l *Logic) Run() {
l.frameChan <- l.state.layout(l.browserManager) l.frameChan <- l.state.layout(l.browserManager)
} }
case events := <-l.inputChan: case events := <-l.inputChan:
log.Printf("Logic: InputEvents")
for _, evt := range events { for _, evt := range events {
evt.Handler(evt.Data) evt.Handler(evt.Data)
} }
l.frameChan <- l.state.layout(l.browserManager) l.frameChan <- l.state.layout(l.browserManager)
case query := <-l.searchQueryChan: case query := <-l.searchQueryChan:
log.Printf("Logic: SearchQuery")
if query != l.state.Browser.Query { if query != l.state.Browser.Query {
l.state.Browser.Query = query l.state.Browser.Query = query
if l.state.page == BrowserPage { if l.state.page == BrowserPage {
@ -164,9 +169,11 @@ func (l *Logic) Run() {
} }
l.frameChan <- l.state.layout(l.browserManager) l.frameChan <- l.state.layout(l.browserManager)
case path := <-l.openFileChan: case path := <-l.openFileChan:
log.Printf("Logic: OpenFileChan %s", path)
// Dispatch ReadFileTask to worker pool // Dispatch ReadFileTask to worker pool
l.workerPool.Dispatch(pool.NewReadFileTask(path, l.mockFS)) l.workerPool.Dispatch(pool.NewReadFileTask(path, l.mockFS))
case res := <-l.workerPool.ResultChan(): case res := <-l.workerPool.ResultChan():
log.Printf("Logic: WorkerResult %s", res.TaskType)
l.handleWorkerResult(res) l.handleWorkerResult(res)
case <-l.resultChan: case <-l.resultChan:
l.frameChan <- l.state.layout(l.browserManager) l.frameChan <- l.state.layout(l.browserManager)
@ -179,6 +186,7 @@ func (l *Logic) handleWorkerResult(res pool.Result) {
if res.IsBrowserResult() { if res.IsBrowserResult() {
l.browserManager.HandleResult(res) l.browserManager.HandleResult(res)
} else if res.TaskType == pool.TypeReadFile { } else if res.TaskType == pool.TypeReadFile {
log.Printf("Logic: TypeReadFile result success=%v", res.Success)
if res.Success { if res.Success {
if content, ok := res.Data.([]byte); ok { if content, ok := res.Data.([]byte); ok {
l.state.ActiveFileContent = string(content) l.state.ActiveFileContent = string(content)