Skip to content

Commit 144f8b8

Browse files
authored
perf(#64): optimize memory and CPU usage (#69)
1 parent a57f20a commit 144f8b8

File tree

17 files changed

+312
-102
lines changed

17 files changed

+312
-102
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ vendor/
2020

2121
# Config
2222
.jlv.jsonc
23+
24+
## Logs
25+
/*.log

example.jlv.jsonc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,12 @@
6262
"40": "warn",
6363
"50": "error",
6464
"60": "fatal"
65-
}
65+
},
66+
// The number of rows to pre-render.
67+
"prerenderRows": 100,
68+
// The number nanoseconds between manual file reloads.
69+
"reloadThreshold": 1000000000,
70+
// The maximum size of the file in bytes.
71+
// The rest of the file will be ignored.
72+
"maxFileSizeBytes": 1073741824
6673
}

internal/app/helper.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package app
22

33
import (
44
"fmt"
5+
"runtime"
56
"strings"
7+
"time"
68

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

32+
runtime.GC()
33+
3034
return events.LogEntriesLoadedMsg(logEntries)
3135
}
3236

3337
func (h helper) getLogLevelStyle(
34-
logEntries source.LogEntries,
38+
logEntries source.LazyLogEntries,
3539
baseStyle lipgloss.Style,
3640
rowID int,
3741
) lipgloss.Style {
3842
if rowID < 0 || rowID >= len(logEntries) {
3943
return baseStyle
4044
}
4145

42-
color := getColorForLogLevel(h.getLogLevelFromLogEntry(logEntries[rowID]))
46+
entry := logEntries[rowID].LogEntry(h.Config)
47+
48+
color := getColorForLogLevel(h.getLogLevelFromLogEntry(entry))
4349
if color == "" {
4450
return baseStyle
4551
}
@@ -85,11 +91,13 @@ func (h helper) handleErrorOccuredMsg(msg events.ErrorOccuredMsg) (tea.Model, te
8591

8692
func (h helper) handleLogEntriesLoadedMsg(
8793
msg events.LogEntriesLoadedMsg,
94+
lastReloadAt time.Time,
8895
) (tea.Model, tea.Cmd) {
8996
return initializeModel(newStateViewLogs(
9097
h.Application,
91-
source.LogEntries(msg)),
92-
)
98+
source.LazyLogEntries(msg),
99+
lastReloadAt,
100+
))
93101
}
94102

95103
func (h helper) handleOpenJSONRowRequestedMsg(
@@ -104,7 +112,7 @@ func (h helper) handleOpenJSONRowRequestedMsg(
104112

105113
return initializeModel(newStateViewRow(
106114
h.Application,
107-
logEntry,
115+
logEntry.LogEntry(h.Config),
108116
previousState,
109117
))
110118
}

internal/app/lazytable.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package app
2+
3+
import (
4+
"github.com/charmbracelet/bubbles/table"
5+
tea "github.com/charmbracelet/bubbletea"
6+
7+
"github.com/hedhyw/json-log-viewer/internal/pkg/config"
8+
)
9+
10+
// rowGetter renders the row.
11+
type rowGetter interface {
12+
// Row return a rendered table row.
13+
Row(cfg *config.Config) table.Row
14+
}
15+
16+
// lazyTableModel lazily renders table rows.
17+
type lazyTableModel[T rowGetter] struct {
18+
helper
19+
20+
table table.Model
21+
22+
minRenderedRows int
23+
allEntries []T
24+
lastCursor int
25+
26+
renderedRows []table.Row
27+
}
28+
29+
// Init implements tea.Model.
30+
func (m lazyTableModel[T]) Init() tea.Cmd {
31+
return nil
32+
}
33+
34+
// View implements tea.Model.
35+
func (m lazyTableModel[T]) View() string {
36+
return m.table.View()
37+
}
38+
39+
// Update implements tea.Model.
40+
func (m lazyTableModel[T]) Update(msg tea.Msg) (lazyTableModel[T], tea.Cmd) {
41+
var cmd tea.Cmd
42+
43+
m.table, cmd = m.table.Update(msg)
44+
45+
if m.table.Cursor() != m.lastCursor {
46+
m = m.withRenderedRows()
47+
}
48+
49+
return m, cmd
50+
}
51+
52+
func (m lazyTableModel[T]) withRenderedRows() lazyTableModel[T] {
53+
cursor := m.table.Cursor()
54+
55+
start := max(len(m.renderedRows), cursor)
56+
end := min(cursor+m.minRenderedRows, len(m.allEntries))
57+
58+
for i := start; i < end; i++ {
59+
m.renderedRows = append(m.renderedRows, m.allEntries[i].Row(m.Config))
60+
}
61+
62+
m.table.SetRows(m.renderedRows)
63+
m.lastCursor = m.table.Cursor()
64+
65+
return m
66+
}

internal/app/logstable.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import (
1010
type logsTableModel struct {
1111
helper
1212

13-
table table.Model
13+
lazyTable lazyTableModel[source.LazyLogEntry]
1414
lastWindowSize tea.WindowSizeMsg
1515

16-
logEntries source.LogEntries
16+
logEntries source.LazyLogEntries
1717
}
1818

19-
func newLogsTableModel(application Application, logEntries source.LogEntries) logsTableModel {
19+
func newLogsTableModel(application Application, logEntries source.LazyLogEntries) logsTableModel {
2020
helper := helper{Application: application}
2121

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

3030
tableLogs.SetStyles(getTableStyles())
31-
tableLogs.SetRows(logEntries.Rows())
3231

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

5049
tableLogs.SetStyles(tableStyles)
5150

51+
lazyTable := lazyTableModel[source.LazyLogEntry]{
52+
helper: helper,
53+
table: tableLogs,
54+
minRenderedRows: application.Config.PrerenderRows,
55+
allEntries: logEntries,
56+
lastCursor: 0,
57+
renderedRows: make([]table.Row, 0, application.Config.PrerenderRows*2),
58+
}.withRenderedRows()
59+
5260
return logsTableModel{
5361
helper: helper,
54-
table: tableLogs,
62+
lazyTable: lazyTable,
5563
logEntries: logEntries,
5664
}.handleWindowSizeMsg(application.LastWindowSize)
5765
}
5866

5967
// Init initializes component. It implements tea.Model.
6068
func (m logsTableModel) Init() tea.Cmd {
61-
return nil
69+
return m.lazyTable.Init()
6270
}
6371

6472
// View renders component. It implements tea.Model.
6573
func (m logsTableModel) View() string {
66-
return m.table.View()
74+
return m.lazyTable.View()
6775
}
6876

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

79-
m.table, cmdBatch = batched(m.table.Update(msg))(cmdBatch)
87+
m.lazyTable, cmdBatch = batched(m.lazyTable.Update(msg))(cmdBatch)
8088

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

8795
x, y := m.BaseStyle.GetFrameSize()
88-
m.table.SetWidth(msg.Width - x*2)
89-
m.table.SetHeight(msg.Height - y*2 - footerSize - heightOffset)
90-
m.table.SetColumns(getColumns(m.table.Width()-10, m.Config))
96+
m.lazyTable.table.SetWidth(msg.Width - x*2)
97+
m.lazyTable.table.SetHeight(msg.Height - y*2 - footerSize - heightOffset)
98+
m.lazyTable.table.SetColumns(getColumns(m.lazyTable.table.Width()-10, m.Config))
9199
m.lastWindowSize = msg
92100

93101
return m
94102
}
95103

96104
// Cursor returns the index of the selected row.
97105
func (m logsTableModel) Cursor() int {
98-
return m.table.Cursor()
106+
return m.lazyTable.table.Cursor()
99107
}

internal/app/statefiltered.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type StateFiltered struct {
1414

1515
previousState StateLoaded
1616
table logsTableModel
17-
logEntries source.LogEntries
17+
logEntries source.LazyLogEntries
1818

1919
filterText string
2020
keys KeyMap
@@ -89,7 +89,7 @@ func (s StateFiltered) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8989
func (s StateFiltered) handleLogEntriesLoadedMsg(
9090
msg events.LogEntriesLoadedMsg,
9191
) (tea.Model, tea.Cmd) {
92-
s.logEntries = source.LogEntries(msg)
92+
s.logEntries = source.LazyLogEntries(msg)
9393
s.table = newLogsTableModel(s.Application, s.logEntries)
9494

9595
return s, s.table.Init()

internal/app/stateinitial.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package app
22

33
import (
4+
"time"
5+
46
tea "github.com/charmbracelet/bubbletea"
57

68
"github.com/hedhyw/json-log-viewer/internal/pkg/events"
@@ -35,7 +37,7 @@ func (s StateInitial) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3537
case events.ErrorOccuredMsg:
3638
return s.handleErrorOccuredMsg(msg)
3739
case events.LogEntriesLoadedMsg:
38-
return s.handleLogEntriesLoadedMsg(msg)
40+
return s.handleLogEntriesLoadedMsg(msg, time.UnixMilli(0))
3941
case tea.KeyMsg:
4042
return s, tea.Quit
4143
default:

internal/app/stateloaded.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package app
22

33
import (
4+
"time"
5+
46
"github.com/charmbracelet/bubbles/help"
57
"github.com/charmbracelet/bubbles/key"
68
tea "github.com/charmbracelet/bubbletea"
@@ -15,14 +17,20 @@ type StateLoaded struct {
1517

1618
initCmd tea.Cmd
1719

18-
table logsTableModel
19-
logEntries source.LogEntries
20+
table logsTableModel
21+
logEntries source.LazyLogEntries
22+
lastReloadAt time.Time
2023

21-
keys KeyMap
22-
help help.Model
24+
keys KeyMap
25+
help help.Model
26+
reloading bool
2327
}
2428

25-
func newStateViewLogs(application Application, logEntries source.LogEntries) StateLoaded {
29+
func newStateViewLogs(
30+
application Application,
31+
logEntries source.LazyLogEntries,
32+
lastReloadAt time.Time,
33+
) StateLoaded {
2634
table := newLogsTableModel(application, logEntries)
2735

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

3644
keys: defaultKeys,
3745
help: help.New(),
46+
47+
lastReloadAt: lastReloadAt,
3848
}
3949
}
4050

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

4656
// View renders component. It implements tea.Model.
4757
func (s StateLoaded) View() string {
58+
if s.reloading {
59+
return s.viewTable() + "\nreloading..."
60+
}
61+
4862
return s.viewTable() + s.viewHelp()
4963
}
5064

@@ -68,12 +82,16 @@ func (s StateLoaded) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6882
case events.ErrorOccuredMsg:
6983
return s.handleErrorOccuredMsg(msg)
7084
case events.LogEntriesLoadedMsg:
71-
return s.handleLogEntriesLoadedMsg(msg)
85+
return s.handleLogEntriesLoadedMsg(msg, s.lastReloadAt)
7286
case events.ViewRowsReloadRequestedMsg:
7387
return s.handleViewRowsReloadRequestedMsg()
7488
case events.OpenJSONRowRequestedMsg:
7589
return s.handleOpenJSONRowRequestedMsg(msg, s)
7690
case tea.KeyMsg:
91+
if s.reloading {
92+
return s, nil
93+
}
94+
7795
switch {
7896
case key.Matches(msg, s.keys.Back):
7997
return s, tea.Quit
@@ -119,6 +137,13 @@ func (s StateLoaded) handleRequestOpenJSON() (tea.Model, tea.Cmd) {
119137
}
120138

121139
func (s StateLoaded) handleViewRowsReloadRequestedMsg() (tea.Model, tea.Cmd) {
140+
if time.Since(s.lastReloadAt) < s.Config.ReloadThreshold {
141+
return s, nil
142+
}
143+
144+
s.lastReloadAt = time.Now()
145+
s.reloading = true
146+
122147
return s, s.helper.LoadEntries
123148
}
124149

0 commit comments

Comments
 (0)