diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | gomuks.go | 6 | ||||
-rw-r--r-- | matrix/matrix.go | 29 | ||||
-rw-r--r-- | matrix/rooms/room.go | 108 | ||||
-rw-r--r-- | matrix/rooms/roomcache.go | 32 | ||||
-rw-r--r-- | ui/command-processor.go | 1 | ||||
-rw-r--r-- | ui/commands.go | 52 | ||||
-rw-r--r-- | ui/message-view.go | 17 | ||||
-rw-r--r-- | ui/room-list.go | 5 | ||||
-rw-r--r-- | ui/room-view.go | 23 |
10 files changed, 218 insertions, 56 deletions
@@ -7,3 +7,4 @@ gomuks coverage.out coverage.html deb/usr +*.prof @@ -58,8 +58,6 @@ func NewGomuks(uiProvider ifc.UIProvider, configDir, cacheDir string) *Gomuks { // Save saves the active session and message history. func (gmx *Gomuks) Save() { gmx.config.SaveAll() - //debug.Print("Saving history...") - //gmx.ui.MainView().SaveAllHistory() } // StartAutosave calls Save() every minute until it receives a stop signal @@ -70,7 +68,9 @@ func (gmx *Gomuks) StartAutosave() { for { select { case <-ticker.C: - gmx.Save() + if gmx.config.AuthCache.InitialSyncDone { + gmx.Save() + } case val := <-gmx.stop: if val { return diff --git a/matrix/matrix.go b/matrix/matrix.go index de99801..8c19b24 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -221,7 +221,15 @@ func (c *Container) OnLogin() { c.syncer.InitDoneCallback = func() { debug.Print("Initial sync done") c.config.AuthCache.InitialSyncDone = true - c.config.SaveAuthCache() + debug.Print("Updating title caches") + for _, room := range c.config.Rooms.Map { + room.GetTitle() + } + debug.Print("Cleaning cached rooms from memory") + c.config.Rooms.ForceClean() + debug.Print("Saving all data") + c.config.SaveAll() + debug.Print("Adding rooms to UI") c.ui.MainView().SetRooms(c.config.Rooms) c.ui.Render() } @@ -294,17 +302,21 @@ func (c *Container) SendPreferencesToMatrix() { // HandleMessage is the event handler for the m.room.message timeline event. func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) { - if source&EventSourceLeave != 0 || source&EventSourceState != 0 { + room := c.GetOrCreateRoom(evt.RoomID) + if source&EventSourceLeave != 0 { + room.HasLeft = true + return + } else if source&EventSourceState != 0 { return } - room := c.GetOrCreateRoom(evt.RoomID) err := c.history.Append(room, []*mautrix.Event{evt}) if err != nil { debug.Printf("Failed to add event %s to history: %v", evt.ID, err) } - if !c.config.AuthCache.InitialSyncDone { + if !c.config.AuthCache.InitialSyncDone || !room.Loaded() { + room.LastReceivedMessage = time.Unix(evt.Timestamp/1000, evt.Timestamp%1000*1000) return } @@ -327,7 +339,7 @@ func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) { c.ui.Render() } } else { - debug.Printf("Parsing event %s type %s %v from %s in %s failed (ParseEvent() returned nil).", evt.ID, evt.Type, evt.Content.Raw, evt.Sender, evt.RoomID) + debug.Printf("Parsing event %s type %s %v from %s in %s failed (ParseEvent() returned nil).", evt.ID, evt.Type.String(), evt.Content.Raw, evt.Sender, evt.RoomID) } } @@ -335,6 +347,9 @@ func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) { func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { isLeave := source&EventSourceLeave != 0 isTimeline := source&EventSourceTimeline != 0 + if isLeave { + c.GetOrCreateRoom(evt.RoomID).HasLeft = true + } isNonTimelineLeave := isLeave && !isTimeline if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave { return @@ -437,7 +452,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool continue } - room := c.GetRoom(roomID) + room := c.GetOrCreateRoom(roomID) if room != nil && !room.HasLeft { directChats[room] = true } @@ -473,7 +488,7 @@ func (c *Container) HandlePushRules(source EventSource, evt *mautrix.Event) { // HandleTag is the event handler for the m.tag account data event. func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) { - room := c.config.GetRoom(evt.RoomID) + room := c.GetOrCreateRoom(evt.RoomID) newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags)) index := 0 diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index 87c3780..972a32a 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -115,6 +115,12 @@ type Room struct { cache *RoomCache // Lock for state and other room stuff. lock sync.RWMutex + // Function to call when room state is unloaded. + onUnload func() bool + // Function to call when room state is loaded. + onLoad func() bool + // Whether or not the room state has changed + changed bool // Room state cache linked list. prev *Room @@ -133,10 +139,13 @@ func (room *Room) Loaded() bool { } func (room *Room) Load() { + room.cache.TouchNode(room) if room.Loaded() { return } - room.cache.TouchNode(room) + if room.onLoad != nil && !room.onLoad() { + return + } room.lock.Lock() room.load() room.lock.Unlock() @@ -146,7 +155,7 @@ func (room *Room) load() { if room.Loaded() { return } - debug.Print("Loading state for room", room.ID) + debug.Print("Loading state for room", room.ID, "from disk") room.state = make(map[mautrix.EventType]map[string]*mautrix.Event) file, err := os.OpenFile(room.path, os.O_RDONLY, 0600) if err != nil { @@ -168,9 +177,17 @@ func (room *Room) load() { if err = dec.Decode(&room.state); err != nil { debug.Print("Failed to decode room state:", err) } + room.changed = false } -func (room *Room) Unload() { +func (room *Room) Touch() { + room.cache.TouchNode(room) +} + +func (room *Room) Unload() bool { + if room.onUnload != nil && !room.onUnload() { + return false + } debug.Print("Unloading", room.ID) room.Save() room.state = nil @@ -179,14 +196,27 @@ func (room *Room) Unload() { room.canonicalAliasCache = "" room.firstMemberCache = nil room.secondMemberCache = nil + return true +} + +func (room *Room) SetOnUnload(fn func() bool) { + room.onUnload = fn +} + +func (room *Room) SetOnLoad(fn func() bool) { + room.onLoad = fn } func (room *Room) Save() { if !room.Loaded() { - debug.Print("Failed to save room state: room not loaded") + debug.Print("Failed to save room", room.ID, "state: room not loaded") + return + } + if !room.changed { + debug.Print("Not saving", room.ID, "as state hasn't changed") return } - debug.Print("Saving state for room", room.ID) + debug.Print("Saving state for room", room.ID, "to disk") file, err := os.OpenFile(room.path, os.O_WRONLY|os.O_CREATE, 0600) if err != nil { debug.Print("Failed to open room state file for writing:", err) @@ -298,40 +328,51 @@ func (room *Room) UpdateState(event *mautrix.Event) { room.Load() room.lock.Lock() defer room.lock.Unlock() + room.changed = true _, exists := room.state[event.Type] if !exists { room.state[event.Type] = make(map[string]*mautrix.Event) } switch event.Type { case mautrix.StateRoomName: - room.NameCache = "" + room.NameCache = event.Content.Name + room.nameCacheSource = ExplicitRoomName case mautrix.StateCanonicalAlias: if room.nameCacheSource <= CanonicalAliasRoomName { - room.NameCache = "" + room.NameCache = event.Content.Alias + room.nameCacheSource = CanonicalAliasRoomName } - room.canonicalAliasCache = "" + room.canonicalAliasCache = event.Content.Alias case mautrix.StateAliases: if room.nameCacheSource <= AliasRoomName { room.NameCache = "" } room.aliasesCache = nil case mautrix.StateMember: - room.memberCache = nil - room.firstMemberCache = nil - room.secondMemberCache = nil + if room.memberCache != nil { + userID := event.GetStateKey() + if event.Content.Membership == mautrix.MembershipLeave || event.Content.Membership == mautrix.MembershipBan { + delete(room.memberCache, userID) + } else if event.Content.Membership == mautrix.MembershipInvite || event.Content.Membership == mautrix.MembershipJoin { + member := room.eventToMember(userID, &event.Content) + existingMember, ok := room.memberCache[userID] + if ok { + *existingMember = *member + } else { + room.memberCache[userID] = member + room.updateNthMemberCache(userID, member) + } + } + } if room.nameCacheSource <= MemberRoomName { room.NameCache = "" } case mautrix.StateTopic: - room.topicCache = "" + room.topicCache = event.Content.Topic } - stateKey := "" - if event.StateKey != nil { - stateKey = *event.StateKey - } if event.Type != mautrix.StateMember { - debug.Printf("Updating state %s#%s for %s", event.Type, stateKey, room.ID) + debug.Printf("Updating state %s#%s for %s", event.Type.String(), event.GetStateKey(), room.ID) } if event.StateKey == nil { @@ -477,6 +518,25 @@ func (room *Room) GetTitle() string { return room.NameCache } +func (room *Room) eventToMember(userID string, content *mautrix.Content) *mautrix.Member { + member := &content.Member + member.Membership = content.Membership + if len(member.Displayname) == 0 { + member.Displayname = userID + } + return member +} + +func (room *Room) updateNthMemberCache(userID string, member *mautrix.Member) { + if userID != room.SessionUserID { + if room.firstMemberCache == nil { + room.firstMemberCache = member + } else if room.secondMemberCache == nil { + room.secondMemberCache = member + } + } +} + // createMemberCache caches all member events into a easily processable MXID -> *Member map. func (room *Room) createMemberCache() map[string]*mautrix.Member { if len(room.memberCache) > 0 { @@ -489,20 +549,10 @@ func (room *Room) createMemberCache() map[string]*mautrix.Member { room.secondMemberCache = nil if events != nil { for userID, event := range events { - member := &event.Content.Member - member.Membership = event.Content.Membership - if len(member.Displayname) == 0 { - member.Displayname = userID - } - if userID != room.SessionUserID { - if room.firstMemberCache == nil { - room.firstMemberCache = member - } else if room.secondMemberCache == nil { - room.secondMemberCache = member - } - } + member := room.eventToMember(userID, &event.Content) if member.Membership == mautrix.MembershipJoin || member.Membership == mautrix.MembershipInvite { cache[userID] = member + room.updateNthMemberCache(userID, member) } } } diff --git a/matrix/rooms/roomcache.go b/matrix/rooms/roomcache.go index 03e3ad8..6fc400c 100644 --- a/matrix/rooms/roomcache.go +++ b/matrix/rooms/roomcache.go @@ -106,7 +106,7 @@ func (cache *RoomCache) LoadList() error { func (cache *RoomCache) SaveLoadedRooms() { cache.Lock() defer cache.Unlock() - cache.clean() + cache.clean(false) for node := cache.head; node != nil; node = node.prev { node.Save() } @@ -194,8 +194,7 @@ func (cache *RoomCache) GetOrCreate(roomID string) *Room { func (cache *RoomCache) get(roomID string) *Room { node, ok := cache.Map[roomID] - if ok && node != nil && node.Loaded() { - cache.touch(node) + if ok && node != nil { return node } return nil @@ -273,18 +272,29 @@ func (cache *RoomCache) llPush(node *Room) { cache.tail = node } cache.size++ - cache.clean() + cache.clean(false) +} + +func (cache *RoomCache) ForceClean() { + cache.Lock() + cache.clean(true) + cache.Unlock() } -func (cache *RoomCache) clean() { +func (cache *RoomCache) clean(force bool) { origSize := cache.size maxTS := time.Now().Unix() - cache.maxAge for cache.size > cache.maxSize { - if cache.tail.touch > maxTS { + if cache.tail.touch > maxTS && !force { break } - cache.tail.Unload() - cache.llPop(cache.tail) + ok := cache.tail.Unload() + node := cache.tail + cache.llPop(node) + if !ok { + debug.Print("Unload returned false, pushing node back") + cache.llPush(node) + } } if cleaned := origSize - cache.size; cleaned > 0 { debug.Print("Cleaned", cleaned, "rooms") @@ -295,7 +305,11 @@ func (cache *RoomCache) Unload(node *Room) { cache.Lock() defer cache.Unlock() cache.llPop(node) - node.Unload() + ok := node.Unload() + if !ok { + debug.Print("Unload returned false, pushing node back") + cache.llPush(node) + } } func (cache *RoomCache) newRoom(roomID string) *Room { diff --git a/ui/command-processor.go b/ui/command-processor.go index 96b1ada..65b704f 100644 --- a/ui/command-processor.go +++ b/ui/command-processor.go @@ -108,6 +108,7 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "rainbow": cmdRainbow, "invite": cmdInvite, "hprof": cmdHeapProfile, + "cprof": cmdCPUProfile, }, } } diff --git a/ui/commands.go b/ui/commands.go index cd770b6..5d92969 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -21,8 +21,11 @@ import ( "fmt" "os" "runtime" + dbg "runtime/debug" "runtime/pprof" + "strconv" "strings" + "time" "unicode" "github.com/lucasb-eyer/go-colorful" @@ -71,15 +74,56 @@ var rainbow = GradientTable{ } func cmdHeapProfile(cmd *Command) { + dbg.FreeOSMemory() runtime.GC() - memProfile, err := os.Create("gomuks.prof") + memProfile, err := os.Create("gomuks.heap.prof") if err != nil { - debug.Print(err) + debug.Print("Failed to open gomuks.heap.prof:", err) + return } - defer memProfile.Close() + defer func() { + err := memProfile.Close() + if err != nil { + debug.Print("Failed to close gomuks.heap.prof:", err) + } + }() if err := pprof.WriteHeapProfile(memProfile); err != nil { - debug.Print(err) + debug.Print("Heap profile error:", err) + } +} + +func cmdCPUProfile(cmd *Command) { + if len(cmd.Args) == 0 { + cmd.Reply("Usage: /cprof <seconds>") + return + } + dur, err := strconv.Atoi(cmd.Args[0]) + if err != nil || dur < 0 { + cmd.Reply("Usage: /cprof <seconds>") + return } + cpuProfile, err := os.Create("gomuks.cpu.prof") + if err != nil { + debug.Print("Failed to open gomuks.cpu.prof:", err) + return + } + err = pprof.StartCPUProfile(cpuProfile) + if err != nil { + _ = cpuProfile.Close() + debug.Print("CPU profile error:", err) + return + } + cmd.Reply("Started CPU profiling for %d seconds", dur) + go func() { + time.Sleep(time.Duration(dur) * time.Second) + pprof.StopCPUProfile() + cmd.Reply("CPU profiling finished.") + + err := cpuProfile.Close() + if err != nil { + debug.Print("Failed to close gomuks.cpu.prof:", err) + } + }() } // TODO this command definitely belongs in a plugin once we have a plugin system. diff --git a/ui/message-view.go b/ui/message-view.go index eb7098c..81a6d7c 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -89,6 +89,23 @@ func NewMessageView(parent *RoomView) *MessageView { } } +func (view *MessageView) Unload() { + debug.Print("Unloading message view", view.parent.Room.ID) + view.messagesLock.Lock() + view.msgBufferLock.Lock() + view.messageIDLock.Lock() + view.messageIDs = make(map[string]*messages.UIMessage) + view.msgBuffer = make([]*messages.UIMessage, 0) + view.messages = make([]*messages.UIMessage, 0) + view.initialHistoryLoaded = false + view.ScrollOffset = 0 + view._widestSender = 5 + view.prevMsgCount = -1 + view.messagesLock.Unlock() + view.msgBufferLock.Unlock() + view.messageIDLock.Unlock() +} + func (view *MessageView) updateWidestSender(sender string) { if len(sender) > int(view._widestSender) { if len(sender) > view.MaxSenderWidth { diff --git a/ui/room-list.go b/ui/room-list.go index f0a9703..53337b6 100644 --- a/ui/room-list.go +++ b/ui/room-list.go @@ -423,8 +423,10 @@ func (list *RoomList) OnMouseEvent(event mauview.MouseEvent) bool { switch event.Buttons() { case tcell.WheelUp: list.AddScrollOffset(-WheelScrollOffsetDiff) + return true case tcell.WheelDown: list.AddScrollOffset(WheelScrollOffsetDiff) + return true case tcell.Button1: x, y := event.Position() return list.clickRoom(y, x, event.Modifiers() == tcell.ModCtrl) @@ -486,7 +488,8 @@ func (list *RoomList) clickRoom(line, column int, mod bool) bool { if trl.maxShown < 10 { trl.maxShown = 10 } - break + list.RUnlock() + return true } } // Tag footer diff --git a/ui/room-view.go b/ui/room-view.go index b37ebff..48943a7 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -93,6 +93,17 @@ func NewRoomView(parent *MainView, room *rooms.Room) *RoomView { config: parent.config, } view.content = NewMessageView(view) + view.Room.SetOnUnload(func() bool { + if view.parent.currentRoom == view { + return false + } + view.content.Unload() + return true + }) + view.Room.SetOnLoad(func() bool { + view.loadTyping() + return true + }) view.input. SetBackgroundColor(tcell.ColorDefault). @@ -270,14 +281,20 @@ func (view *RoomView) SetCompletions(completions []string) { view.completions.time = time.Now() } -func (view *RoomView) SetTyping(users []string) { - for index, user := range users { +func (view *RoomView) loadTyping() { + for index, user := range view.typing { member := view.Room.GetMember(user) if member != nil { - users[index] = member.Displayname + view.typing[index] = member.Displayname } } +} + +func (view *RoomView) SetTyping(users []string) { view.typing = users + if view.Room.Loaded() { + view.loadTyping() + } } type completion struct { |