Skip to content

Commit

Permalink
perf(#64): optimize memory and CPU usage (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
hedhyw authored May 27, 2024
1 parent a57f20a commit 144f8b8
Show file tree
Hide file tree
Showing 17 changed files with 312 additions and 102 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ vendor/

# Config
.jlv.jsonc

## Logs
/*.log
9 changes: 8 additions & 1 deletion example.jlv.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,12 @@
"40": "warn",
"50": "error",
"60": "fatal"
}
},
// The number of rows to pre-render.
"prerenderRows": 100,
// The number nanoseconds between manual file reloads.
"reloadThreshold": 1000000000,
// The maximum size of the file in bytes.
// The rest of the file will be ignored.
"maxFileSizeBytes": 1073741824
}
18 changes: 13 additions & 5 deletions internal/app/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package app

import (
"fmt"
"runtime"
"strings"
"time"

"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/table"
Expand All @@ -27,19 +29,23 @@ func (h helper) LoadEntries() tea.Msg {
return events.ErrorOccuredMsg{Err: err}
}

runtime.GC()

return events.LogEntriesLoadedMsg(logEntries)
}

func (h helper) getLogLevelStyle(
logEntries source.LogEntries,
logEntries source.LazyLogEntries,
baseStyle lipgloss.Style,
rowID int,
) lipgloss.Style {
if rowID < 0 || rowID >= len(logEntries) {
return baseStyle
}

color := getColorForLogLevel(h.getLogLevelFromLogEntry(logEntries[rowID]))
entry := logEntries[rowID].LogEntry(h.Config)

color := getColorForLogLevel(h.getLogLevelFromLogEntry(entry))
if color == "" {
return baseStyle
}
Expand Down Expand Up @@ -85,11 +91,13 @@ func (h helper) handleErrorOccuredMsg(msg events.ErrorOccuredMsg) (tea.Model, te

func (h helper) handleLogEntriesLoadedMsg(
msg events.LogEntriesLoadedMsg,
lastReloadAt time.Time,
) (tea.Model, tea.Cmd) {
return initializeModel(newStateViewLogs(
h.Application,
source.LogEntries(msg)),
)
source.LazyLogEntries(msg),
lastReloadAt,
))
}

func (h helper) handleOpenJSONRowRequestedMsg(
Expand All @@ -104,7 +112,7 @@ func (h helper) handleOpenJSONRowRequestedMsg(

return initializeModel(newStateViewRow(
h.Application,
logEntry,
logEntry.LogEntry(h.Config),
previousState,
))
}
Expand Down
66 changes: 66 additions & 0 deletions internal/app/lazytable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package app

import (
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"

"github.com/hedhyw/json-log-viewer/internal/pkg/config"
)

// rowGetter renders the row.
type rowGetter interface {
// Row return a rendered table row.
Row(cfg *config.Config) table.Row
}

// lazyTableModel lazily renders table rows.
type lazyTableModel[T rowGetter] struct {
helper

table table.Model

minRenderedRows int
allEntries []T
lastCursor int

renderedRows []table.Row
}

// Init implements tea.Model.
func (m lazyTableModel[T]) Init() tea.Cmd {
return nil
}

// View implements tea.Model.
func (m lazyTableModel[T]) View() string {
return m.table.View()
}

// Update implements tea.Model.
func (m lazyTableModel[T]) Update(msg tea.Msg) (lazyTableModel[T], tea.Cmd) {
var cmd tea.Cmd

m.table, cmd = m.table.Update(msg)

if m.table.Cursor() != m.lastCursor {
m = m.withRenderedRows()
}

return m, cmd
}

func (m lazyTableModel[T]) withRenderedRows() lazyTableModel[T] {
cursor := m.table.Cursor()

start := max(len(m.renderedRows), cursor)
end := min(cursor+m.minRenderedRows, len(m.allEntries))

for i := start; i < end; i++ {
m.renderedRows = append(m.renderedRows, m.allEntries[i].Row(m.Config))
}

m.table.SetRows(m.renderedRows)
m.lastCursor = m.table.Cursor()

return m
}
32 changes: 20 additions & 12 deletions internal/app/logstable.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import (
type logsTableModel struct {
helper

table table.Model
lazyTable lazyTableModel[source.LazyLogEntry]
lastWindowSize tea.WindowSizeMsg

logEntries source.LogEntries
logEntries source.LazyLogEntries
}

func newLogsTableModel(application Application, logEntries source.LogEntries) logsTableModel {
func newLogsTableModel(application Application, logEntries source.LazyLogEntries) logsTableModel {
helper := helper{Application: application}

const cellIDLogLevel = 1
Expand All @@ -28,7 +28,6 @@ func newLogsTableModel(application Application, logEntries source.LogEntries) lo
)

tableLogs.SetStyles(getTableStyles())
tableLogs.SetRows(logEntries.Rows())

tableStyles := getTableStyles()
tableStyles.RenderCell = func(_ table.Model, value string, position table.CellPosition) string {
Expand All @@ -49,21 +48,30 @@ func newLogsTableModel(application Application, logEntries source.LogEntries) lo

tableLogs.SetStyles(tableStyles)

lazyTable := lazyTableModel[source.LazyLogEntry]{
helper: helper,
table: tableLogs,
minRenderedRows: application.Config.PrerenderRows,
allEntries: logEntries,
lastCursor: 0,
renderedRows: make([]table.Row, 0, application.Config.PrerenderRows*2),
}.withRenderedRows()

return logsTableModel{
helper: helper,
table: tableLogs,
lazyTable: lazyTable,
logEntries: logEntries,
}.handleWindowSizeMsg(application.LastWindowSize)
}

// Init initializes component. It implements tea.Model.
func (m logsTableModel) Init() tea.Cmd {
return nil
return m.lazyTable.Init()
}

// View renders component. It implements tea.Model.
func (m logsTableModel) View() string {
return m.table.View()
return m.lazyTable.View()
}

// Update handles events. It implements tea.Model.
Expand All @@ -76,7 +84,7 @@ func (m logsTableModel) Update(msg tea.Msg) (logsTableModel, tea.Cmd) {
m = m.handleWindowSizeMsg(msg)
}

m.table, cmdBatch = batched(m.table.Update(msg))(cmdBatch)
m.lazyTable, cmdBatch = batched(m.lazyTable.Update(msg))(cmdBatch)

return m, tea.Batch(cmdBatch...)
}
Expand All @@ -85,15 +93,15 @@ func (m logsTableModel) handleWindowSizeMsg(msg tea.WindowSizeMsg) logsTableMode
const heightOffset = 4

x, y := m.BaseStyle.GetFrameSize()
m.table.SetWidth(msg.Width - x*2)
m.table.SetHeight(msg.Height - y*2 - footerSize - heightOffset)
m.table.SetColumns(getColumns(m.table.Width()-10, m.Config))
m.lazyTable.table.SetWidth(msg.Width - x*2)
m.lazyTable.table.SetHeight(msg.Height - y*2 - footerSize - heightOffset)
m.lazyTable.table.SetColumns(getColumns(m.lazyTable.table.Width()-10, m.Config))
m.lastWindowSize = msg

return m
}

// Cursor returns the index of the selected row.
func (m logsTableModel) Cursor() int {
return m.table.Cursor()
return m.lazyTable.table.Cursor()
}
4 changes: 2 additions & 2 deletions internal/app/statefiltered.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type StateFiltered struct {

previousState StateLoaded
table logsTableModel
logEntries source.LogEntries
logEntries source.LazyLogEntries

filterText string
keys KeyMap
Expand Down Expand Up @@ -89,7 +89,7 @@ func (s StateFiltered) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (s StateFiltered) handleLogEntriesLoadedMsg(
msg events.LogEntriesLoadedMsg,
) (tea.Model, tea.Cmd) {
s.logEntries = source.LogEntries(msg)
s.logEntries = source.LazyLogEntries(msg)
s.table = newLogsTableModel(s.Application, s.logEntries)

return s, s.table.Init()
Expand Down
4 changes: 3 additions & 1 deletion internal/app/stateinitial.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package app

import (
"time"

tea "github.com/charmbracelet/bubbletea"

"github.com/hedhyw/json-log-viewer/internal/pkg/events"
Expand Down Expand Up @@ -35,7 +37,7 @@ func (s StateInitial) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case events.ErrorOccuredMsg:
return s.handleErrorOccuredMsg(msg)
case events.LogEntriesLoadedMsg:
return s.handleLogEntriesLoadedMsg(msg)
return s.handleLogEntriesLoadedMsg(msg, time.UnixMilli(0))
case tea.KeyMsg:
return s, tea.Quit
default:
Expand Down
37 changes: 31 additions & 6 deletions internal/app/stateloaded.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package app

import (
"time"

"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
Expand All @@ -15,14 +17,20 @@ type StateLoaded struct {

initCmd tea.Cmd

table logsTableModel
logEntries source.LogEntries
table logsTableModel
logEntries source.LazyLogEntries
lastReloadAt time.Time

keys KeyMap
help help.Model
keys KeyMap
help help.Model
reloading bool
}

func newStateViewLogs(application Application, logEntries source.LogEntries) StateLoaded {
func newStateViewLogs(
application Application,
logEntries source.LazyLogEntries,
lastReloadAt time.Time,
) StateLoaded {
table := newLogsTableModel(application, logEntries)

return StateLoaded{
Expand All @@ -35,6 +43,8 @@ func newStateViewLogs(application Application, logEntries source.LogEntries) Sta

keys: defaultKeys,
help: help.New(),

lastReloadAt: lastReloadAt,
}
}

Expand All @@ -45,6 +55,10 @@ func (s StateLoaded) Init() tea.Cmd {

// View renders component. It implements tea.Model.
func (s StateLoaded) View() string {
if s.reloading {
return s.viewTable() + "\nreloading..."
}

return s.viewTable() + s.viewHelp()
}

Expand All @@ -68,12 +82,16 @@ func (s StateLoaded) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case events.ErrorOccuredMsg:
return s.handleErrorOccuredMsg(msg)
case events.LogEntriesLoadedMsg:
return s.handleLogEntriesLoadedMsg(msg)
return s.handleLogEntriesLoadedMsg(msg, s.lastReloadAt)
case events.ViewRowsReloadRequestedMsg:
return s.handleViewRowsReloadRequestedMsg()
case events.OpenJSONRowRequestedMsg:
return s.handleOpenJSONRowRequestedMsg(msg, s)
case tea.KeyMsg:
if s.reloading {
return s, nil
}

switch {
case key.Matches(msg, s.keys.Back):
return s, tea.Quit
Expand Down Expand Up @@ -119,6 +137,13 @@ func (s StateLoaded) handleRequestOpenJSON() (tea.Model, tea.Cmd) {
}

func (s StateLoaded) handleViewRowsReloadRequestedMsg() (tea.Model, tea.Cmd) {
if time.Since(s.lastReloadAt) < s.Config.ReloadThreshold {
return s, nil
}

s.lastReloadAt = time.Now()
s.reloading = true

return s, s.helper.LoadEntries
}

Expand Down
Loading

0 comments on commit 144f8b8

Please sign in to comment.