diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-28 11:22:32 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-28 11:22:32 +0200 |
| commit | 2c1930adf4355b7d0b113f3a2f9573c4f961420e (patch) | |
| tree | 3d6884d50240b8ae6633084adefa286d12706f72 /internal/askcli/task_alias_cache.go | |
| parent | dbc9c19f8306aa095350abb2edef353c3c44d402 (diff) | |
perf: replace linear scan with O(1) map lookup in task alias cache
Add in-memory byUUID and byAlias maps to taskAliasCache so that
ensureAlias, lookupUUIDByAlias, and lookupAliasByUUID all run in O(1)
instead of scanning the full entries slice. Maps are rebuilt after
every mutation (load, prune, append) to avoid stale pointers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/askcli/task_alias_cache.go')
| -rw-r--r-- | internal/askcli/task_alias_cache.go | 53 |
1 files changed, 41 insertions, 12 deletions
diff --git a/internal/askcli/task_alias_cache.go b/internal/askcli/task_alias_cache.go index 0a1f729..3ea5dff 100644 --- a/internal/askcli/task_alias_cache.go +++ b/internal/askcli/task_alias_cache.go @@ -18,9 +18,17 @@ var ( taskAliasCacheRoot = stats.CacheDir ) +// taskAliasCache maps UUIDs to short human-readable aliases for display and +// completion. The Entries slice is persisted as JSON; the byUUID and byAlias +// maps are derived in-memory for O(1) lookups and are rebuilt after every load +// or mutation. type taskAliasCache struct { NextID uint64 `json:"next_id"` Entries []taskAliasCacheEntry `json:"entries"` + + // In-memory lookup maps — not serialized to JSON. + byUUID map[string]*taskAliasCacheEntry + byAlias map[string]*taskAliasCacheEntry } type taskAliasCacheEntry struct { @@ -88,6 +96,9 @@ func loadTaskAliasCache() (taskAliasCache, string, error) { if err := cache.validate(); err != nil { return taskAliasCache{}, "", fmt.Errorf("validate task alias cache: %w", err) } + // Rebuild lookup maps from the deserialized entries so that all subsequent + // operations use O(1) map access rather than a linear scan. + cache.rebuildMaps() return cache, path, nil } @@ -134,6 +145,20 @@ func (c *taskAliasCache) validate() error { return nil } +// rebuildMaps repopulates byUUID and byAlias from the current Entries slice. +// This must be called after any operation that mutates Entries (load, prune, +// add). Pointer stability is guaranteed because entries are addressed by their +// slice index at the time the map is built; both maps are fully rebuilt each +// time rather than patched to avoid stale pointers after slice reallocation. +func (c *taskAliasCache) rebuildMaps() { + c.byUUID = make(map[string]*taskAliasCacheEntry, len(c.Entries)) + c.byAlias = make(map[string]*taskAliasCacheEntry, len(c.Entries)) + for i := range c.Entries { + c.byUUID[c.Entries[i].UUID] = &c.Entries[i] + c.byAlias[c.Entries[i].Alias] = &c.Entries[i] + } +} + func (c *taskAliasCache) prune(now time.Time) bool { if len(c.Entries) == 0 { return false @@ -151,12 +176,16 @@ func (c *taskAliasCache) prune(now time.Time) bool { c.Entries = kept if changed { c.sortEntries() + // Rebuild maps after pruning so removed entries are no longer reachable. + c.rebuildMaps() } return changed } +// ensureAlias returns the alias for uuid, creating one if necessary. It uses +// the byUUID map for O(1) lookup instead of a linear slice scan. func (c *taskAliasCache) ensureAlias(uuid string, now time.Time) (string, bool) { - if entry, ok := c.findEntry(func(entry taskAliasCacheEntry) bool { return entry.UUID == uuid }); ok { + if entry, ok := c.byUUID[uuid]; ok { if entry.LastAccessedAt.Equal(now) { return entry.Alias, false } @@ -173,20 +202,17 @@ func (c *taskAliasCache) ensureAlias(uuid string, now time.Time) (string, bool) LastAccessedAt: now, }) c.sortEntries() + // Rebuild maps after append because sortEntries may reorder the slice and + // the previous pointer stored in byUUID for other entries may now be stale. + c.rebuildMaps() return alias, true } -func (c *taskAliasCache) findEntry(match func(taskAliasCacheEntry) bool) (*taskAliasCacheEntry, bool) { - for i := range c.Entries { - if match(c.Entries[i]) { - return &c.Entries[i], true - } - } - return nil, false -} - +// lookupUUIDByAlias returns the UUID for the given alias using an O(1) map +// lookup. Returns (uuid, found, changed) where changed is true when +// LastAccessedAt was updated. func (c *taskAliasCache) lookupUUIDByAlias(alias string, now time.Time) (string, bool, bool) { - entry, ok := c.findEntry(func(entry taskAliasCacheEntry) bool { return entry.Alias == alias }) + entry, ok := c.byAlias[alias] if !ok { return "", false, false } @@ -195,8 +221,11 @@ func (c *taskAliasCache) lookupUUIDByAlias(alias string, now time.Time) (string, return entry.UUID, true, changed } +// lookupAliasByUUID returns the alias for the given UUID using an O(1) map +// lookup. Returns (alias, found, changed) where changed is true when +// LastAccessedAt was updated. func (c *taskAliasCache) lookupAliasByUUID(uuid string, now time.Time) (string, bool, bool) { - entry, ok := c.findEntry(func(entry taskAliasCacheEntry) bool { return entry.UUID == uuid }) + entry, ok := c.byUUID[uuid] if !ok { return "", false, false } |
