Skip to content

Commit

Permalink
fix: improve stats display performance and formatting
Browse files Browse the repository at this point in the history
This commit improves the stats command in several ways:

1. Performance Improvements:
   - Pre-calculate display statistics during cache refresh
   - Store formatted stats in cache to avoid recalculation
   - Remove unnecessary parallel processing for small datasets
   - Optimize data structures with pre-allocated maps

2. Display Formatting:
   - Fix table header centering to match table width exactly
   - Restore table styling from user configuration
   - Add proper borders and separators based on config
   - Fix language statistics formatting

3. Code Organization:
   - Move display logic to dedicated structs
   - Add proper type registration for gob encoding
   - Improve error handling in cache operations

The stats command now runs in sub-second time and maintains
consistent formatting across different terminal sizes.
  • Loading branch information
AccursedGalaxy committed Dec 30, 2024
1 parent 2b7e3fc commit d979cba
Show file tree
Hide file tree
Showing 5 changed files with 1,091 additions and 213 deletions.
301 changes: 152 additions & 149 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cache

import (
"encoding/json"
"fmt"
"log"
"os"
Expand All @@ -14,225 +13,229 @@ import (
"github.com/AccursedGalaxy/streakode/scan"
)

// in-memory cache to hold repo metadata during runtime
// Global cache manager instance
var (
Cache map[string]scan.RepoMetadata
metadata CacheMetadata
mutex sync.RWMutex
manager *CacheManager
mutex sync.RWMutex
)

type CacheMetadata struct {
LastRefresh time.Time
Version string // For Future Version Tracking
}

// InitCache - Initializes the in memory cache
// InitCache - Initializes the cache manager
func InitCache() {
Cache = make(map[string]scan.RepoMetadata)
}
mutex.Lock()
defer mutex.Unlock()

// check if refresh is needed
func ShouldAutoRefresh(refreshInterval time.Duration) bool {
mutex.RLock()
defer mutex.RUnlock()
if manager != nil {
return
}

if metadata.LastRefresh.IsZero() {
return true
manager = NewCacheManager(getCacheFilePath())
if err := manager.Load(); err != nil {
log.Printf("Error loading cache: %v\n", err)
}
return time.Since(metadata.LastRefresh) > refreshInterval
}

// LoadCache - loads repository metadata from a JSON cache file into memory
// LoadCache - loads repository metadata from cache file
func LoadCache(filePath string) error {
mutex.Lock()
defer mutex.Unlock()

file, err := os.Open(filePath)
if os.IsNotExist(err) {
InitCache()
return nil
}
if err != nil {
return fmt.Errorf("error opening cache file: %v", err)
if manager == nil {
manager = NewCacheManager(filePath)
}
defer file.Close()

decoder := json.NewDecoder(file)
if err := decoder.Decode(&Cache); err != nil {
InitCache()
return nil
}
return manager.Load()
}

// Load metadata from a separate file
metadataPath := filePath + ".meta"
metaFile, err := os.Open(metadataPath)
if os.IsNotExist(err) {
metadata = CacheMetadata{LastRefresh: time.Time{}}
return nil
}
if err != nil {
return fmt.Errorf("error opening metadata file: %v", err)
}
defer metaFile.Close()
// SaveCache - saves the cache to disk
func SaveCache(filePath string) error {
mutex.Lock()
defer mutex.Unlock()

decoder = json.NewDecoder(metaFile)
if err := decoder.Decode(&metadata); err != nil {
metadata = CacheMetadata{LastRefresh: time.Time{}}
return nil
if manager == nil {
return fmt.Errorf("cache manager not initialized")
}

return nil
return manager.Save()
}

// SaveCache - saves the in-memory cache to a JSON file
func SaveCache(filePath string) error {
// RefreshCache - updates the cache with fresh data
func RefreshCache(dirs []string, author string, cacheFilePath string, excludedPatterns []string, excludedPaths []string) error {
mutex.Lock()
defer mutex.Unlock()

// Create directory if it doesn't exist
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating directory: %v", err)
if manager == nil {
manager = NewCacheManager(cacheFilePath)
}

file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("error creating cache file: %v", err)
}
defer file.Close()
// Create exclusion function
shouldExclude := func(path string) bool {
// Check full path exclusions
for _, excludedPath := range excludedPaths {
if strings.HasPrefix(path, excludedPath) {
return true
}
}

encoder := json.NewEncoder(file)
if err := encoder.Encode(Cache); err != nil {
return fmt.Errorf("error encoding cache: %v", err)
// Check pattern-based exclusions
for _, pattern := range excludedPatterns {
if strings.Contains(path, pattern) {
return true
}
}
return false
}

// Save metadata to a separate file
metadataPath := filePath + ".meta"
metaFile, err := os.Create(metadataPath)
// Scan directories for repositories
repos, err := scan.ScanDirectories(dirs, author, shouldExclude)
if err != nil {
return fmt.Errorf("error creating metadata file: %v", err)
return fmt.Errorf("error scanning directories: %v", err)
}
defer metaFile.Close()

metadata.LastRefresh = time.Now()
encoder = json.NewEncoder(metaFile)
if err := encoder.Encode(metadata); err != nil {
return fmt.Errorf("error encoding metadata: %v", err)
// Convert repos slice to map
reposMap := make(map[string]scan.RepoMetadata)
for _, repo := range repos {
reposMap[repo.Path] = repo
}

return nil
// Update cache with new data using the manager's method
manager.updateCacheData(reposMap)

return manager.Save()
}

// AsyncRefreshCache performs a non-blocking cache refresh
func AsyncRefreshCache(dirs []string, author string, cacheFilePath string, excludedPatterns []string, excludedPaths []string) {
go func() {
if err := RefreshCache(dirs, author, cacheFilePath, excludedPatterns, excludedPaths); err != nil {
log.Printf("Background cache refresh failed: %v", err)
}
}()
}

// Add new method to check if cache needs refresh
func NeedsRefresh(path string, lastCommit time.Time) bool {
if cached, exists := Cache[path]; exists {
// Only refresh if new commits exist
return lastCommit.After(cached.LastCommit)
// QuickNeedsRefresh performs a fast check if refresh is needed
func QuickNeedsRefresh(refreshInterval time.Duration) bool {
mutex.RLock()
defer mutex.RUnlock()

if manager == nil || manager.cache == nil {
return true
}
return true

return time.Since(manager.cache.LastSync) > refreshInterval
}

// Clean Cache
// CleanCache removes the cache file and resets the in-memory cache
func CleanCache(cacheFilePath string) error {
//Reset in-memory cache
Cache = make(map[string]scan.RepoMetadata)
mutex.Lock()
defer mutex.Unlock()

if manager != nil {
manager.cache = newCommitCache()
}

// Remove cache file if present
if err := os.Remove(cacheFilePath); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("something went wrong removing the cache file: %v", err)
return fmt.Errorf("error removing cache file: %v", err)
}
}

// Remove metadata file if present
metaFile := cacheFilePath + ".meta"
if err := os.Remove(metaFile); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("error removing metadata file: %v", err)
}
}

return nil
}

// Modified RefreshCache to support exclusions
func RefreshCache(dirs []string, author string, cacheFilePath string, excludedPatterns []string, excludedPaths []string) error {
// Clean cache and handle potential errors
if err := CleanCache(cacheFilePath); err != nil {
return fmt.Errorf("failed to clean cache: %v", err)
// Helper function to get cache file path
func getCacheFilePath() string {
home, err := os.UserHomeDir()
if err != nil {
log.Fatal(err)
}

// Create a function to check if a path should be excluded
shouldExclude := func(path string) bool {
// Check full path exclusions
for _, excludedPath := range excludedPaths {
if strings.HasPrefix(path, excludedPath) {
return true
}
}

// Check pattern-based exclusions
for _, pattern := range excludedPatterns {
if strings.Contains(path, pattern) {
return true
}
}
return false
if config.AppState.ActiveProfile == "" {
return filepath.Join(home, ".streakode.cache")
}
return filepath.Join(home, fmt.Sprintf(".streakode_%s.cache", config.AppState.ActiveProfile))
}

// Filter out excluded directories before scanning
var filteredDirs []string
for _, dir := range dirs {
if !shouldExclude(dir) {
filteredDirs = append(filteredDirs, dir)
}
}
// Cache is now a proxy to the manager's cache
var Cache = &cacheProxy{}

repos, err := scan.ScanDirectories(filteredDirs, author, shouldExclude)
if err != nil {
log.Printf("Error scanning directories: %v", err)
return err
}
type cacheProxy struct{}

// Only update changed repositories
for _, repo := range repos {
if NeedsRefresh(repo.Path, repo.LastCommit) {
Cache[repo.Path] = repo
}
func (cp *cacheProxy) Get(key string) (scan.RepoMetadata, bool) {
mutex.RLock()
defer mutex.RUnlock()

if manager == nil || manager.cache == nil {
return scan.RepoMetadata{}, false
}

// Validate all repo data before saving
if config.AppConfig.Debug {
fmt.Println("Debug: Validating repository data...")
repo, exists := manager.cache.Repositories[key]
return repo, exists
}

func (cp *cacheProxy) GetDisplayStats() *DisplayStats {
mutex.RLock()
defer mutex.RUnlock()

if manager == nil || manager.cache == nil {
return nil
}

for i, repo := range repos {
result := repo.ValidateData()
if !result.Valid {
fmt.Printf("Warning: Data validation issues found in repo %d:\n", i)
for _, issue := range result.Issues {
fmt.Printf(" - %s\n", issue)
}
}
return &manager.cache.DisplayStats
}

func (cp *cacheProxy) Set(key string, value scan.RepoMetadata) {
mutex.Lock()
defer mutex.Unlock()

if manager == nil || manager.cache == nil {
return
}

return SaveCache(cacheFilePath)
manager.cache.Repositories[key] = value
}

// AsyncRefreshCache performs a non-blocking cache refresh
func AsyncRefreshCache(dirs []string, author string, cacheFilePath string, excludedPatterns []string, excludedPaths []string) {
go func() {
if err := RefreshCache(dirs, author, cacheFilePath, excludedPatterns, excludedPaths); err != nil {
log.Printf("Background cache refresh failed: %v", err)
}
}()
func (cp *cacheProxy) Delete(key string) {
mutex.Lock()
defer mutex.Unlock()

if manager == nil || manager.cache == nil {
return
}

delete(manager.cache.Repositories, key)
}

// QuickNeedsRefresh performs a fast check if refresh is needed without scanning repositories
func QuickNeedsRefresh(refreshInterval time.Duration) bool {
func (cp *cacheProxy) Range(f func(key string, value scan.RepoMetadata) bool) {
mutex.RLock()
defer mutex.RUnlock()

if metadata.LastRefresh.IsZero() {
return true
if manager == nil || manager.cache == nil {
return
}

// Check if cache file exists and its modification time
if time.Since(metadata.LastRefresh) > refreshInterval {
return true
for k, v := range manager.cache.Repositories {
if !f(k, v) {
break
}
}
}

func (cp *cacheProxy) Len() int {
mutex.RLock()
defer mutex.RUnlock()

if manager == nil || manager.cache == nil {
return 0
}

return false
return len(manager.cache.Repositories)
}
Loading

0 comments on commit d979cba

Please sign in to comment.