Skip to content

Commit 641e87c

Browse files
authored
Optimize TUI performance via sqlite tuning + caching (#290)
* Optimize TUI performance via sqlite tuning + caching * Disable TestTuiBench in GH actions since it is just meant for benchmarking * Update golden * Fix go errcheck finding * Revert "Update golden" This reverts commit 7c8865c. * Clear the search cache after deletions * Allowlist TestTuiBench-Query as an unused golden since it is not used on GH actions
1 parent 380fb2a commit 641e87c

File tree

8 files changed

+274
-239
lines changed

8 files changed

+274
-239
lines changed

Diff for: client/hctx/hctx.go

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func OpenLocalSqliteDb() (*gorm.DB, error) {
110110
}
111111
db.AutoMigrate(&data.HistoryEntry{})
112112
db.Exec("PRAGMA journal_mode = WAL")
113+
db.Exec("pragma mmap_size = 268435456")
113114
db.Exec("CREATE INDEX IF NOT EXISTS start_time_index ON history_entries(start_time)")
114115
db.Exec("CREATE INDEX IF NOT EXISTS end_time_index ON history_entries(end_time)")
115116
db.Exec("CREATE INDEX IF NOT EXISTS entry_id_index ON history_entries(entry_id)")

Diff for: client/integration_test.go

+76-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/ddworken/hishtory/client/data"
2222
"github.com/ddworken/hishtory/client/hctx"
2323
"github.com/ddworken/hishtory/client/lib"
24+
"github.com/ddworken/hishtory/client/tui"
2425
"github.com/ddworken/hishtory/shared"
2526
"github.com/ddworken/hishtory/shared/ai"
2627
"github.com/ddworken/hishtory/shared/testutils"
@@ -3146,17 +3147,20 @@ func BenchmarkQuery(b *testing.B) {
31463147
numImported, err := lib.ImportHistory(ctx, false, true)
31473148
require.NoError(b, err)
31483149
require.GreaterOrEqual(b, numImported, numSyntheticEntries)
3150+
db := hctx.GetDb(ctx)
3151+
for i := range 1000 {
3152+
e := testutils.MakeFakeHistoryEntry(strings.Repeat(fmt.Sprintf("this is a long command %d", i), 100))
3153+
require.NoError(b, db.Create(e).Error)
3154+
}
31493155

31503156
// Benchmark it
31513157
for n := 0; n < b.N; n++ {
3158+
ctx := hctx.MakeContext()
31523159
// Benchmarked code:
31533160
b.StartTimer()
3154-
ctx := hctx.MakeContext()
3155-
err := lib.RetrieveAdditionalEntriesFromRemote(ctx, "tui")
3156-
require.NoError(b, err)
3157-
_, err = lib.Search(ctx, hctx.GetDb(ctx), "echo", 100)
3158-
require.NoError(b, err)
3161+
_, err := lib.Search(ctx, hctx.GetDb(ctx), "this is a long command 123", 100)
31593162
b.StopTimer()
3163+
require.NoError(b, err)
31603164
}
31613165
}
31623166

@@ -3634,4 +3638,71 @@ func TestDefaultSearchColumnsAddDelete(t *testing.T) {
36343638
require.Equal(t, out, "command current_working_directory \n")
36353639
}
36363640

3641+
func TestTuiBench(t *testing.T) {
3642+
if testutils.IsGithubAction() {
3643+
t.Skip("Skipping benchmarking test in Github Actions")
3644+
}
3645+
// Setup
3646+
defer testutils.BackupAndRestore(t)()
3647+
createSyntheticImportEntries(t, 10000)
3648+
tester, _, _ := setupTestTui(t, Online)
3649+
ctx := hctx.MakeContext()
3650+
numImported, err := lib.ImportHistory(ctx, false, true)
3651+
require.NoError(t, err)
3652+
require.GreaterOrEqual(t, numImported, 10000)
3653+
db := hctx.GetDb(ctx)
3654+
for i := range 1000 {
3655+
e := testutils.MakeFakeHistoryEntry(strings.Repeat(fmt.Sprintf("this is a long command %d", i), 2))
3656+
require.NoError(t, db.Create(e).Error)
3657+
}
3658+
3659+
for range 30 {
3660+
out := captureTerminalOutputWithShellNameAndDimensions(t, tester, tester.ShellName(), 100, 20, []TmuxCommand{
3661+
{Keys: "hishtory SPACE tquery ENTER", ExtraDelay: 1},
3662+
{Keys: "t", NoSleep: true},
3663+
{Keys: "h", NoSleep: true},
3664+
{Keys: "i", NoSleep: true},
3665+
{Keys: "s", NoSleep: true},
3666+
{Keys: "SPACE", NoSleep: true},
3667+
{Keys: "i", NoSleep: true},
3668+
{Keys: "s", NoSleep: true},
3669+
{Keys: "SPACE", NoSleep: true},
3670+
{Keys: "1", NoSleep: true},
3671+
{Keys: "2", NoSleep: true},
3672+
{Keys: "3", NoSleep: true},
3673+
})
3674+
testutils.CompareGoldens(t, out, "TestTuiBench-Query")
3675+
}
3676+
}
3677+
3678+
func BenchmarkGetRows(b *testing.B) {
3679+
b.StopTimer()
3680+
// Setup with an install with a lot of entries
3681+
tester := zshTester{}
3682+
defer testutils.BackupAndRestore(b)()
3683+
testutils.ResetLocalState(b)
3684+
installHishtory(b, tester, "")
3685+
numSyntheticEntries := 100_000
3686+
createSyntheticImportEntries(b, numSyntheticEntries)
3687+
ctx := hctx.MakeContext()
3688+
numImported, err := lib.ImportHistory(ctx, false, true)
3689+
require.NoError(b, err)
3690+
require.GreaterOrEqual(b, numImported, numSyntheticEntries)
3691+
db := hctx.GetDb(ctx)
3692+
for i := range 1000 {
3693+
e := testutils.MakeFakeHistoryEntry(strings.Repeat(fmt.Sprintf("this is a long command %d", i), 100))
3694+
require.NoError(b, db.Create(e).Error)
3695+
}
3696+
config := hctx.GetConf(ctx)
3697+
3698+
// Benchmark it
3699+
for n := 0; n < b.N; n++ {
3700+
// Benchmarked code:
3701+
b.StartTimer()
3702+
_, _, err := tui.TestOnlyGetRows(ctx, config.DisplayedColumns, "bash", "", "this is a long command 123", 100)
3703+
b.StopTimer()
3704+
require.NoError(b, err)
3705+
}
3706+
}
3707+
36373708
// TODO: somehow test/confirm that hishtory works even if only bash/only zsh is installed

Diff for: client/lib/lib.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import (
2828
"github.com/ddworken/hishtory/shared"
2929

3030
"github.com/araddon/dateparse"
31+
"github.com/dgraph-io/ristretto"
32+
"github.com/eko/gocache/lib/v4/cache"
33+
"github.com/eko/gocache/lib/v4/store"
34+
ristretto_store "github.com/eko/gocache/store/ristretto/v4"
3135
"github.com/google/uuid"
3236
"github.com/schollz/progressbar/v3"
3337
"golang.org/x/exp/slices"
@@ -769,7 +773,7 @@ func where(tx *gorm.DB, s string, args ...any) *gorm.DB {
769773

770774
func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*gorm.DB, error) {
771775
tokens := tokenize(query)
772-
tx := db.Model(&data.HistoryEntry{}).Where("true")
776+
tx := db.Model(&data.HistoryEntry{}).WithContext(ctx).Where("true")
773777
for _, token := range tokens {
774778
if strings.HasPrefix(token, "-") {
775779
if token == "-" {
@@ -807,6 +811,56 @@ func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*
807811
return tx, nil
808812
}
809813

814+
type searchQuery struct {
815+
query string
816+
limit int
817+
}
818+
819+
type searchResult struct {
820+
results []*data.HistoryEntry
821+
err error
822+
}
823+
824+
var SEARCH_CACHE *cache.LoadableCache[*searchResult]
825+
826+
func ClearSearchCache(ctx context.Context) error {
827+
if SEARCH_CACHE == nil {
828+
return nil
829+
}
830+
return SEARCH_CACHE.Clear(ctx)
831+
}
832+
833+
func SearchWithCache(ctx context.Context, db *gorm.DB, query string, limit int) ([]*data.HistoryEntry, error) {
834+
if SEARCH_CACHE == nil {
835+
loadFunction := func(ctx context.Context, key any) (*searchResult, []store.Option, error) {
836+
sq := key.(searchQuery)
837+
results, err := Search(ctx, db, sq.query, sq.limit)
838+
return &searchResult{results, err}, []store.Option{store.WithCost(1), store.WithExpiration(time.Second * 3)}, nil
839+
}
840+
841+
ristrettoCache, err := ristretto.NewCache(&ristretto.Config{
842+
NumCounters: 1000,
843+
MaxCost: 100,
844+
BufferItems: 64,
845+
})
846+
if err != nil {
847+
panic(err)
848+
}
849+
ristrettoStore := ristretto_store.NewRistretto(ristrettoCache)
850+
851+
cacheManager := cache.NewLoadable[*searchResult](
852+
loadFunction,
853+
cache.New[*searchResult](ristrettoStore),
854+
)
855+
SEARCH_CACHE = cacheManager
856+
}
857+
res, err := SEARCH_CACHE.Get(ctx, searchQuery{query, limit})
858+
if err != nil {
859+
return nil, fmt.Errorf("failed to get from cache: %w", err)
860+
}
861+
return res.results, res.err
862+
}
863+
810864
func Search(ctx context.Context, db *gorm.DB, query string, limit int) ([]*data.HistoryEntry, error) {
811865
return SearchWithOffset(ctx, db, query, limit, 0)
812866
}

Diff for: client/posttest/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var NUM_TEST_RETRIES map[string]int
2222

2323
var UNUSED_GOLDENS []string = []string{
2424
"testCustomColumns-query-isAction=false", "testCustomColumns-tquery-bash",
25-
"testCustomColumns-tquery-zsh",
25+
"testCustomColumns-tquery-zsh", "TestTuiBench-Query",
2626
}
2727

2828
func main() {

Diff for: client/testdata/TestTuiBench-Query

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Search Query: > this is 123
2+
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
3+
│ Hostname CWD Timestamp Runtime Exit Code Command │
4+
│──────────────────────────────────────────────────────────────────────────────────────────────│
5+
│ localhost /tmp/ Oct 17 2022 21:53:41 P… 3s 2 this is a long co… │
6+
│ │
7+
│ │
8+
│ │
9+
│ │
10+
│ │
11+
│ │
12+
│ │
13+
│ │
14+
│ │
15+
└──────────────────────────────────────────────────────────────────────────────────────────────┘
16+
hiSHtory: Search your shell history • ctrl+h help

Diff for: client/tui/tui.go

+25-8
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,21 @@ func runQueryAndUpdateTable(m model, forceUpdateTable, maintainCursor bool) tea.
210210
query = *m.runQuery
211211
}
212212
queryId := allocateQueryId()
213+
conf := hctx.GetConf(m.ctx)
214+
defaultFilter := conf.DefaultFilter
215+
if m.queryInput.Prompt == "" {
216+
// The default filter was cleared for this session, so don't apply it
217+
defaultFilter = ""
218+
}
219+
220+
// Kick off an async query to getRows() so that we can start our DB query in the background
221+
// before bubbletea actually invokes our tea.Msg. This reduces latency between key presses
222+
// and results being displayed.
223+
go func() {
224+
_, _, _ = getRows(m.ctx, conf.DisplayedColumns, m.shellName, defaultFilter, query, getNumEntriesNeeded(m.ctx))
225+
}()
226+
213227
return func() tea.Msg {
214-
conf := hctx.GetConf(m.ctx)
215-
defaultFilter := conf.DefaultFilter
216-
if m.queryInput.Prompt == "" {
217-
// The default filter was cleared for this session, so don't apply it
218-
defaultFilter = ""
219-
}
220228
rows, entries, searchErr := getRows(m.ctx, conf.DisplayedColumns, m.shellName, defaultFilter, query, getNumEntriesNeeded(m.ctx))
221229
return asyncQueryFinishedMsg{queryId, rows, entries, searchErr, forceUpdateTable, maintainCursor, nil, false}
222230
}
@@ -504,13 +512,17 @@ func getRowsFromAiSuggestions(ctx context.Context, columnNames []string, shellNa
504512
return rows, entries, nil
505513
}
506514

515+
func TestOnlyGetRows(ctx context.Context, columnNames []string, shellName, defaultFilter, query string, numEntries int) ([]table.Row, []*data.HistoryEntry, error) {
516+
return getRows(ctx, columnNames, shellName, defaultFilter, query, numEntries)
517+
}
518+
507519
func getRows(ctx context.Context, columnNames []string, shellName, defaultFilter, query string, numEntries int) ([]table.Row, []*data.HistoryEntry, error) {
508520
db := hctx.GetDb(ctx)
509521
config := hctx.GetConf(ctx)
510522
if config.AiCompletion && strings.HasPrefix(query, "?") && len(query) > 1 {
511523
return getRowsFromAiSuggestions(ctx, columnNames, shellName, query)
512524
}
513-
searchResults, err := lib.Search(ctx, db, defaultFilter+" "+query, numEntries)
525+
searchResults, err := lib.SearchWithCache(ctx, db, defaultFilter+" "+query, numEntries)
514526
if err != nil {
515527
return nil, nil, err
516528
}
@@ -824,7 +836,12 @@ func deleteHistoryEntry(ctx context.Context, entry data.HistoryEntry) error {
824836
dr.Messages.Ids = append(dr.Messages.Ids,
825837
shared.MessageIdentifier{DeviceId: entry.DeviceId, EndTime: entry.EndTime, EntryId: entry.EntryId},
826838
)
827-
return lib.SendDeletionRequest(ctx, dr)
839+
err := lib.SendDeletionRequest(ctx, dr)
840+
if err != nil {
841+
return err
842+
}
843+
844+
return lib.ClearSearchCache(ctx)
828845
}
829846

830847
func configureColorProfile(ctx context.Context) {

0 commit comments

Comments
 (0)