Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize TUI performance via sqlite tuning + caching #290

Merged
merged 7 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/hctx/hctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func OpenLocalSqliteDb() (*gorm.DB, error) {
}
db.AutoMigrate(&data.HistoryEntry{})
db.Exec("PRAGMA journal_mode = WAL")
db.Exec("pragma mmap_size = 268435456")
db.Exec("CREATE INDEX IF NOT EXISTS start_time_index ON history_entries(start_time)")
db.Exec("CREATE INDEX IF NOT EXISTS end_time_index ON history_entries(end_time)")
db.Exec("CREATE INDEX IF NOT EXISTS entry_id_index ON history_entries(entry_id)")
Expand Down
81 changes: 76 additions & 5 deletions client/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/ddworken/hishtory/client/data"
"github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/client/lib"
"github.com/ddworken/hishtory/client/tui"
"github.com/ddworken/hishtory/shared"
"github.com/ddworken/hishtory/shared/ai"
"github.com/ddworken/hishtory/shared/testutils"
Expand Down Expand Up @@ -3146,17 +3147,20 @@ func BenchmarkQuery(b *testing.B) {
numImported, err := lib.ImportHistory(ctx, false, true)
require.NoError(b, err)
require.GreaterOrEqual(b, numImported, numSyntheticEntries)
db := hctx.GetDb(ctx)
for i := range 1000 {
e := testutils.MakeFakeHistoryEntry(strings.Repeat(fmt.Sprintf("this is a long command %d", i), 100))
require.NoError(b, db.Create(e).Error)
}

// Benchmark it
for n := 0; n < b.N; n++ {
ctx := hctx.MakeContext()
// Benchmarked code:
b.StartTimer()
ctx := hctx.MakeContext()
err := lib.RetrieveAdditionalEntriesFromRemote(ctx, "tui")
require.NoError(b, err)
_, err = lib.Search(ctx, hctx.GetDb(ctx), "echo", 100)
require.NoError(b, err)
_, err := lib.Search(ctx, hctx.GetDb(ctx), "this is a long command 123", 100)
b.StopTimer()
require.NoError(b, err)
}
}

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

func TestTuiBench(t *testing.T) {
if testutils.IsGithubAction() {
t.Skip("Skipping benchmarking test in Github Actions")
}
// Setup
defer testutils.BackupAndRestore(t)()
createSyntheticImportEntries(t, 10000)
tester, _, _ := setupTestTui(t, Online)
ctx := hctx.MakeContext()
numImported, err := lib.ImportHistory(ctx, false, true)
require.NoError(t, err)
require.GreaterOrEqual(t, numImported, 10000)
db := hctx.GetDb(ctx)
for i := range 1000 {
e := testutils.MakeFakeHistoryEntry(strings.Repeat(fmt.Sprintf("this is a long command %d", i), 2))
require.NoError(t, db.Create(e).Error)
}

for range 30 {
out := captureTerminalOutputWithShellNameAndDimensions(t, tester, tester.ShellName(), 100, 20, []TmuxCommand{
{Keys: "hishtory SPACE tquery ENTER", ExtraDelay: 1},
{Keys: "t", NoSleep: true},
{Keys: "h", NoSleep: true},
{Keys: "i", NoSleep: true},
{Keys: "s", NoSleep: true},
{Keys: "SPACE", NoSleep: true},
{Keys: "i", NoSleep: true},
{Keys: "s", NoSleep: true},
{Keys: "SPACE", NoSleep: true},
{Keys: "1", NoSleep: true},
{Keys: "2", NoSleep: true},
{Keys: "3", NoSleep: true},
})
testutils.CompareGoldens(t, out, "TestTuiBench-Query")
}
}

func BenchmarkGetRows(b *testing.B) {
b.StopTimer()
// Setup with an install with a lot of entries
tester := zshTester{}
defer testutils.BackupAndRestore(b)()
testutils.ResetLocalState(b)
installHishtory(b, tester, "")
numSyntheticEntries := 100_000
createSyntheticImportEntries(b, numSyntheticEntries)
ctx := hctx.MakeContext()
numImported, err := lib.ImportHistory(ctx, false, true)
require.NoError(b, err)
require.GreaterOrEqual(b, numImported, numSyntheticEntries)
db := hctx.GetDb(ctx)
for i := range 1000 {
e := testutils.MakeFakeHistoryEntry(strings.Repeat(fmt.Sprintf("this is a long command %d", i), 100))
require.NoError(b, db.Create(e).Error)
}
config := hctx.GetConf(ctx)

// Benchmark it
for n := 0; n < b.N; n++ {
// Benchmarked code:
b.StartTimer()
_, _, err := tui.TestOnlyGetRows(ctx, config.DisplayedColumns, "bash", "", "this is a long command 123", 100)
b.StopTimer()
require.NoError(b, err)
}
}

// TODO: somehow test/confirm that hishtory works even if only bash/only zsh is installed
56 changes: 55 additions & 1 deletion client/lib/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import (
"github.com/ddworken/hishtory/shared"

"github.com/araddon/dateparse"
"github.com/dgraph-io/ristretto"
"github.com/eko/gocache/lib/v4/cache"
"github.com/eko/gocache/lib/v4/store"
ristretto_store "github.com/eko/gocache/store/ristretto/v4"
"github.com/google/uuid"
"github.com/schollz/progressbar/v3"
"golang.org/x/exp/slices"
Expand Down Expand Up @@ -769,7 +773,7 @@ func where(tx *gorm.DB, s string, args ...any) *gorm.DB {

func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*gorm.DB, error) {
tokens := tokenize(query)
tx := db.Model(&data.HistoryEntry{}).Where("true")
tx := db.Model(&data.HistoryEntry{}).WithContext(ctx).Where("true")
for _, token := range tokens {
if strings.HasPrefix(token, "-") {
if token == "-" {
Expand Down Expand Up @@ -807,6 +811,56 @@ func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*
return tx, nil
}

type searchQuery struct {
query string
limit int
}

type searchResult struct {
results []*data.HistoryEntry
err error
}

var SEARCH_CACHE *cache.LoadableCache[*searchResult]

func ClearSearchCache(ctx context.Context) error {
if SEARCH_CACHE == nil {
return nil
}
return SEARCH_CACHE.Clear(ctx)
}

func SearchWithCache(ctx context.Context, db *gorm.DB, query string, limit int) ([]*data.HistoryEntry, error) {
if SEARCH_CACHE == nil {
loadFunction := func(ctx context.Context, key any) (*searchResult, []store.Option, error) {
sq := key.(searchQuery)
results, err := Search(ctx, db, sq.query, sq.limit)
return &searchResult{results, err}, []store.Option{store.WithCost(1), store.WithExpiration(time.Second * 3)}, nil
}

ristrettoCache, err := ristretto.NewCache(&ristretto.Config{
NumCounters: 1000,
MaxCost: 100,
BufferItems: 64,
})
if err != nil {
panic(err)
}
ristrettoStore := ristretto_store.NewRistretto(ristrettoCache)

cacheManager := cache.NewLoadable[*searchResult](
loadFunction,
cache.New[*searchResult](ristrettoStore),
)
SEARCH_CACHE = cacheManager
}
res, err := SEARCH_CACHE.Get(ctx, searchQuery{query, limit})
if err != nil {
return nil, fmt.Errorf("failed to get from cache: %w", err)
}
return res.results, res.err
}

func Search(ctx context.Context, db *gorm.DB, query string, limit int) ([]*data.HistoryEntry, error) {
return SearchWithOffset(ctx, db, query, limit, 0)
}
Expand Down
2 changes: 1 addition & 1 deletion client/posttest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var NUM_TEST_RETRIES map[string]int

var UNUSED_GOLDENS []string = []string{
"testCustomColumns-query-isAction=false", "testCustomColumns-tquery-bash",
"testCustomColumns-tquery-zsh",
"testCustomColumns-tquery-zsh", "TestTuiBench-Query",
}

func main() {
Expand Down
16 changes: 16 additions & 0 deletions client/testdata/TestTuiBench-Query
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Search Query: > this is 123
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│──────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:53:41 P… 3s 2 this is a long co… │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
hiSHtory: Search your shell history • ctrl+h help
33 changes: 25 additions & 8 deletions client/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,21 @@ func runQueryAndUpdateTable(m model, forceUpdateTable, maintainCursor bool) tea.
query = *m.runQuery
}
queryId := allocateQueryId()
conf := hctx.GetConf(m.ctx)
defaultFilter := conf.DefaultFilter
if m.queryInput.Prompt == "" {
// The default filter was cleared for this session, so don't apply it
defaultFilter = ""
}

// Kick off an async query to getRows() so that we can start our DB query in the background
// before bubbletea actually invokes our tea.Msg. This reduces latency between key presses
// and results being displayed.
go func() {
_, _, _ = getRows(m.ctx, conf.DisplayedColumns, m.shellName, defaultFilter, query, getNumEntriesNeeded(m.ctx))
}()

return func() tea.Msg {
conf := hctx.GetConf(m.ctx)
defaultFilter := conf.DefaultFilter
if m.queryInput.Prompt == "" {
// The default filter was cleared for this session, so don't apply it
defaultFilter = ""
}
rows, entries, searchErr := getRows(m.ctx, conf.DisplayedColumns, m.shellName, defaultFilter, query, getNumEntriesNeeded(m.ctx))
return asyncQueryFinishedMsg{queryId, rows, entries, searchErr, forceUpdateTable, maintainCursor, nil, false}
}
Expand Down Expand Up @@ -504,13 +512,17 @@ func getRowsFromAiSuggestions(ctx context.Context, columnNames []string, shellNa
return rows, entries, nil
}

func TestOnlyGetRows(ctx context.Context, columnNames []string, shellName, defaultFilter, query string, numEntries int) ([]table.Row, []*data.HistoryEntry, error) {
return getRows(ctx, columnNames, shellName, defaultFilter, query, numEntries)
}

func getRows(ctx context.Context, columnNames []string, shellName, defaultFilter, query string, numEntries int) ([]table.Row, []*data.HistoryEntry, error) {
db := hctx.GetDb(ctx)
config := hctx.GetConf(ctx)
if config.AiCompletion && strings.HasPrefix(query, "?") && len(query) > 1 {
return getRowsFromAiSuggestions(ctx, columnNames, shellName, query)
}
searchResults, err := lib.Search(ctx, db, defaultFilter+" "+query, numEntries)
searchResults, err := lib.SearchWithCache(ctx, db, defaultFilter+" "+query, numEntries)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -824,7 +836,12 @@ func deleteHistoryEntry(ctx context.Context, entry data.HistoryEntry) error {
dr.Messages.Ids = append(dr.Messages.Ids,
shared.MessageIdentifier{DeviceId: entry.DeviceId, EndTime: entry.EndTime, EntryId: entry.EntryId},
)
return lib.SendDeletionRequest(ctx, dr)
err := lib.SendDeletionRequest(ctx, dr)
if err != nil {
return err
}

return lib.ClearSearchCache(ctx)
}

func configureColorProfile(ctx context.Context) {
Expand Down
Loading
Loading