mirror of
https://github.com/rivo/tview.git
synced 2026-04-26 21:35:54 +03:00
[GH-ISSUE #148] panic: tview.(*Table).Draw.func1 #116
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/tview#116
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @joegrasse on GitHub (Jul 31, 2018).
Original GitHub issue: https://github.com/rivo/tview/issues/148
I am getting a panic in tview. It looks like it might be in table.go line 469, but it is really hard to troubleshoot because of the way the panic is being printed.
(Click image for bigger view)
@ardnew commented on GitHub (Jul 31, 2018):
This will be basically impossible to debug without more information. You'll have to post some relevant source code for any chance of help.
If you're having trouble reading the panic message, you can trap the panic using
defer()andrecover()(see: Defer, Panic, and Recover, and Handling panics), and then from there print the panic message and any other debug info (e.g. the call stack) safely to a log file or somewhere.@joegrasse commented on GitHub (Aug 1, 2018):
As stated above the source code is tview table.go I believe. I have added a link above.
I will see if I can capture more information via
defer()andrecover(). Thanks for the recommendation.@ardnew commented on GitHub (Aug 1, 2018):
table.go:469 is only the point at which the actual fault occurs. If you look at the source, you'll see that line is the body of a closure -- for a function ref
getCellthat is called from 4 different locations. So with only that source reference, we have no way of knowing where the fault originated. You need to obtain the call stack to determine how it arrived at that fault location.But even with the call stack in hand, it's more likely the real problem is in the table construction (your code, not the library code). So if you can share any code showing how you're constructing the table, that would be very helpful.
It would also be helpful if you could of course describe how the fault appears. Does it happen immediately when the table is first drawn? After you select a cell? Remove a cell? Does it happen seemingly randomly?
@joegrasse commented on GitHub (Aug 1, 2018):
I have added the
defer()andrecover(). This issue happens at random. Hopefully the next time it happens I will have more information to provide.@rivo commented on GitHub (Aug 4, 2018):
Are you calling
Table.Draw()directly from some goroutine? TheApplication.Draw()function recovers any panics and prints it after closing thetcellsession. Three things make me think you're callingTable'sDraw()function by yourself:tcellsession has not been properly terminated.Applicationwhere panics are recovered.I could be wrong but maybe you can clarify what you're doing.
@joegrasse commented on GitHub (Aug 6, 2018):
No I am not calling
Table.Draw(). The only draw method I am calling is theapplication.Draw()function.@rivo commented on GitHub (Aug 7, 2018):
Ok, well, then let's see what your next
recover()comes up with.@joegrasse commented on GitHub (Aug 14, 2018):
@rivo I have not had the problem since adding
defer()andrecover(), and normally I would have had it several times by now. Digging into why that change might have helped, I noticed I might have picked up this change in the recompile. Does it make sense that this might have fixed my issue?@rivo commented on GitHub (Aug 15, 2018):
There's a good chance that this fixed it. I thought you had already included this commit.
I'm closing this issue now. If you find that you still have problems, I'll reopen it.
@joegrasse commented on GitHub (Sep 4, 2018):
@rivo This issue ended up happening again. Just not quite as often. I had to edit table.go to capture the panic.
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x6a0440]
goroutine 6 [running]:
github.com/rivo/tview.(*Table).Draw.func1(0x8232e0, 0xc042126d80)
D:/gowork/src/github.com/rivo/tview/table.go:459 +0x86
panic(0x77b820, 0x9ccda0)
C:/Go/src/runtime/panic.go:502 +0x237
github.com/rivo/tview.(*Table).Draw.func2(...)
D:/gowork/src/github.com/rivo/tview/table.go:478
github.com/rivo/tview.(*Table).Draw(0xc042059360, 0x8232e0, 0xc042126d80)
D:/gowork/src/github.com/rivo/tview/table.go:792 +0x1eb0
github.com/rivo/tview.(*Pages).Draw(0xc0420f9d40, 0x8232e0, 0xc042126d80)
D:/gowork/src/github.com/rivo/tview/pages.go:246 +0xfd
github.com/rivo/tview.(*Grid).Draw(0xc042122090, 0x8232e0, 0xc042126d80)
D:/gowork/src/github.com/rivo/tview/grid.go:631 +0x150b
github.com/rivo/tview.(*Pages).Draw(0xc0420f9d10, 0x8232e0, 0xc042126d80)
D:/gowork/src/github.com/rivo/tview/pages.go:246 +0xfd
github.com/rivo/tview.(*Application).Draw(0xc042108af0, 0x0)
D:/gowork/src/github.com/rivo/tview/application.go:259 +0xda
github.com/joegrasse/goburrow/goburrow/ui.NewApp.func2(0xc0421092d0)
D:/gowork/src/github.com/joegrasse/goburrow/goburrow/ui/app.go:184 +0x4f
created by github.com/joegrasse/goburrow/goburrow/ui.NewApp
D:/gowork/src/github.com/joegrasse/goburrow/goburrow/ui/app.go:181 +0xdc0
@rivo commented on GitHub (Sep 5, 2018):
Could you provide me with a commit hash of the version you're using? I've made changes to
table.goand the line numbers don't line up anymore. Thanks!@joegrasse commented on GitHub (Sep 5, 2018):
the line numbers don't match because i had to do the following to capture the panic.
@joegrasse commented on GitHub (Sep 5, 2018):
Here is the commit hash I am using.
commit
21f50f5bc4@rivo commented on GitHub (Sep 5, 2018):
Thanks. To be honest, I cannot explain this. The message is
invalid memory address or nil pointer dereferenceand it happens here:The problem doesn't appear to be the slice indices as (A) I check for them and (B) the error message would be different. So
tseems to benil. But how can that be? It's provided to the function by Go and it's never modified:What I also find somewhat odd is that
PagescallsGridwhich callsPagesagain. Do you have aPagesobject in aGridobject which is again in aPagesobject? (It's possible but unusual, I guess.)You mentioned that you're not calling
Draw()from any other goroutine.I don't want to rule out that this is actually a compiler bug. But then, I've never come across one myself in decades of programming so the likelihood of that is low.
I can offer to take a look at your code. But it doesn't appear to be public so I'm not sure how we're going to do that.
@joegrasse commented on GitHub (Sep 5, 2018):
I am not calling
Table.Draw, but I am callingApplication.Draw()from multiple go routines. This is actually how I got it to happen more often, by callingApplication.Draw()more often from those go routines.Yes
I do plan on publishing my code to github. I might just have to do that sooner than I wanted.
Let me see if I can help trace this down first, rather than rushing the publishing.
@rivo commented on GitHub (Sep 5, 2018):
Application.Draw()holds a lock on the application object during its execution (see here) so I don't see how this is a problem.I have a feeling there's something in plain sight which I'm simply not seeing...
@joegrasse commented on GitHub (Sep 6, 2018):
It looks like
Table.cellsis not guarded against concurrent writing/reading. There are several functions that can modifyTable.cells(for exampleTable.Clear(),Table.SetCell(...)), while other functions are accessing it.@rivo commented on GitHub (Sep 7, 2018):
Yes, that is correct. It was a deliberate decision not to wrap access to all member variables with mutex locks. (Although I'm certainly open to discussing this.) Some of the classes like
TableCellgive you direct access to their contents so it wouldn't even be possible without hiding them.This is usually not a problem if changes to these objects are made in response to events generated by
tview, e.g. keyboard events. In that case, your application essentially remains single-threaded.But if changes are made in separate goroutines, you need to start syncing them. There are some functions which are thread-safe, like
Application.Draw()or theWriterinterface ofTextView, for example. But the rest is basically up to you.I was thinking about documenting this fact more and maybe providing examples on how to resolve common issues. Haven't gotten around to it yet.
Regarding your issue, however, the error message that you see still seems strange to me. But maybe race conditions can have unpredictable effects. I'm not familiar enough with Go's internals to be able to tell.
@joegrasse commented on GitHub (Sep 7, 2018):
So, to give you a little more background about my application. I have a table that represents "things" running in the background. These "things" can change state. So, I have a goroutine that refreshes the table periodically to reflect the changing states. The end user can also modify the "things" in the table through the GUI by adding, remove, or modifing them.
So, while I am only calling
Application.Draw()and no other Draw methods, I am inadvertently causing a race by callingApplication.Draw()(which under the covers callsTable.Draw()) in one goroutine and the end user (or another goroutine) modifies the table contents in another. One goroutine could clearTable.cells(Table.Clear()), while another goroutine (Application.Draw()->Table.Draw()) is trying to useTable.cellsto draw it.Back to the issue at hand, I think this would be really hard for a user of your library to grasp this intricacy, and understand when and where they need to sync. I believe you wouldn't have to wrap all member variables. you probably only need to wrap
Table.cellsand maybeTable.*Boxin read and write mutexes. if you did this, this would be a major win for end users of your library. They wouldn't have to really understand the inner workings of a table to use it concurrently.@joegrasse commented on GitHub (Sep 7, 2018):
I have submitted a pull request for this issue.
@rivo commented on GitHub (Sep 26, 2018):
I wrote a reply to your thread by posted it in the wrong issue. Please see my response there: https://github.com/rivo/tview/issues/145#issuecomment-424438234.
I don't think it's enough to just sync the functions of
Table. Users could modifyTableCellas well and it will lead to race conditions (same withListItemandTreeNode).@3cb suggests an
Updaterinterface which I'm not sure yet what it would look like. But it could be explored to solve issues like yours. Maybe something like this:Then, whenever your application updates in a separate goroutine, instead of calling functions on
Tabledirectly, you do this:It will then be called right before everything is redrawn (during the call to
app.Draw()), in a thread-safe fashion.Any thoughts?
@joegrasse commented on GitHub (Sep 26, 2018):
While I understand why you are hesitating on how to implement this, I would say that I am not quite a fan of the
Updaterinterface as presented above. This seems like it causes more of a burden on the users of your library to understand the intricacy of how to use your library. This would cause questions like, "When do I need to use theUpdaterinterface?", "What functions do I need to use with the Updater interface?", etc. Which in the end I think, would lead to more issues and hand holding.For ease of use, I think the syncing should be done behind the scenes. Now how you do it behind the scenes (Updater, Sync), doesn't really matter to me.
/cc @3cb
@3cb commented on GitHub (Sep 27, 2018):
I actually like passing functions around better than implementing an interface. I think it's easier for the user. An update function could look like this:
I'm not so sure about the
QueueUpdateapproach though. If you were to store the functions in a slice of functions then range over the slice inapp.Draw()it would work for simple use cases but wouldn't be thread-safe across the board. It would only work when all modifications of all the used primitives were initiated byapp.Draw(). For example, I have previously gotten this output from the race detector:The above changes wouldn't prevent this. It's not a critical problem for what I'm doing but I'm not sure it wouldn't be for another use case. There may be other gotchas like this one lurking out there.
What if we added a channel select block to the main event loop within
app.Run():https://gist.github.com/3cb/faa10a07d2c138b81db56776afbf91cd
That would bring all updates back to the main goroutine and allow us to avoid adding mutexes to all the primitive types. I haven't tried to get this running yet but I will when I get a chance.
@rivo commented on GitHub (Oct 10, 2018):
Sorry for replying so late. I haven't forgotten about this but have been rather busy lately.
From a point of view of the developer using this package, I totally agree with @joegrasse that all of this should be handled behind the scenes. Your solution, as implemented in #172, which was also my suggestion, will put the burden on the developer. I'd need to provide clear documentation on how to deal with goroutines in
tview.But apart from the huge amount of boilerplate code this would add, I also can't think of ways to make it waterproof. For example, the structure of a
TreeViewis defined by a tree ofTreeNodeobjects where each node references its children. When you callCollapseAll()(and some other functions), the tree is traversed and all of its children are accessed and modified. Adding a mutex toTreeNodewould not prevent race conditions because it would only affect itself, not its children.So I'm gravitating towards #172 (still have to review it). Unless there are some better suggestions in the next few days, I'll merge it.
Btw, would #172 also get rid of the race condition you posted in your comment?
@3cb commented on GitHub (Oct 10, 2018):
I agree with all that. I originally fixed my own data races by adding mutexes where I needed them but it became more complicated than I realized because I had to add them to
TableCellas well asTable. #172 definitely requires more thought by the user but using closures and callingapp.QueueUpdateisn't horrible. I'd be happy to help with the documentation.Yes. I've run it with the race detector and haven't gotten any warnings. It would be helpful if others tested it also.
If you decide against #172 and go the other route, I can help with that too.
@joegrasse commented on GitHub (Oct 11, 2018):
I still believe the behind the scenes fix would be the better solution. I also agree that it would be more difficult to get correct. If you would like help with that I am also willing and able.
@rivo commented on GitHub (Oct 12, 2018):
I'm open to suggestions for solutions to the problems mentioned above.
@rivo commented on GitHub (Oct 28, 2018):
I merged #172. For implementation details, see https://github.com/rivo/tview/pull/172#issuecomment-433702294. For details on how to deal with concurrency in
tview, please check out the package documentation.Even now, after I dove into this topic in detail, I don't see how this could have been made thread-safe in a self-contained way. No primitive stands on its own so a "global" solution was needed. Anyway, using
QueueUpdate()should not impose too much overhead on users.