Skip to content

Commit 6de6caa

Browse files
committed
feat: optimise database and query
1 parent ce72d36 commit 6de6caa

File tree

7 files changed

+334
-98
lines changed

7 files changed

+334
-98
lines changed

core/stats/achievements.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ var AchievementConfigs = []AchievementConfig{
5151
}
5252

5353
// CalculateAchievements determines which achievements are unlocked based on stats and activities
54-
func CalculateAchievements(periodStats PeriodStats, activities []Activity, streakInfo StreakInfo) []Achievement {
54+
func CalculateAchievements(periodStats PeriodStats, streakInfo StreakInfo) []Achievement {
5555
var achievements []Achievement
5656

5757
// Count unique languages (only code languages)
@@ -62,10 +62,6 @@ func CalculateAchievements(periodStats PeriodStats, activities []Activity, strea
6262
}
6363
}
6464

65-
// Group activities into sessions for session achievements
66-
sm := NewSessionManager(15*60, 1*60) // 15 min timeout, 1 min minimum
67-
sessions := sm.GroupActivitiesIntoSessions(activities)
68-
6965
for _, cfg := range AchievementConfigs {
7066
unlocked := false
7167

@@ -91,7 +87,7 @@ func CalculateAchievements(periodStats PeriodStats, activities []Activity, strea
9187
}
9288

9389
case "session":
94-
for _, s := range sessions {
90+
for _, s := range periodStats.Sessions {
9591
if s.Duration >= float64(cfg.MinSession) {
9692
unlocked = true
9793
break

core/stats/bridge.go

Lines changed: 104 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package stats
33

44
import (
55
"database/sql"
6+
"fmt"
67
"time"
78
)
89

@@ -20,6 +21,15 @@ type APIStats struct {
2021
DailyActivity map[string]DailyStat `json:"daily_activity"`
2122
WeeklyHeatmap []HeatmapDay `json:"weekly_heatmap"`
2223
GeneratedAt time.Time `json:"generated_at"`
24+
Meta APIMeta `json:"_meta"` // NEW: Performance metadata
25+
}
26+
27+
// APIMeta contains performance and diagnostic information
28+
type APIMeta struct {
29+
LoadedActivities int `json:"loaded_activities"`
30+
TotalActivities int `json:"total_activities"`
31+
QueryTimeMs float64 `json:"query_time_ms"`
32+
DataWindow string `json:"data_window"` // e.g., "last_365_days"
2333
}
2434

2535
// APIPeriodStats is simplified version of PeriodStats for API responses
@@ -90,22 +100,50 @@ type APISession struct {
90100
EndTime time.Time `json:"end_time"`
91101
Duration float64 `json:"duration"`
92102
Projects []string `json:"projects"`
93-
Languages []string `json:"languages,omitempty"` // Top languages used in session
103+
Languages []string `json:"languages,omitempty"`
94104
IsActive bool `json:"is_active"`
95-
BreakAfter float64 `json:"break_after,omitempty"` // Duration until next session
105+
BreakAfter float64 `json:"break_after,omitempty"`
96106
}
97107

98108
type APIComparisonResult struct {
99109
Trend string `json:"trend"`
100110
}
101111

102-
// GetAPIStats returns simplified stats optimized for API consumers
112+
const LOOKBACK_DAYS = 180
113+
114+
// GetAPIStats with smart data loading
103115
func GetAPIStats(db *sql.DB) (*APIStats, error) {
104-
activities, err := loadActivitiesFromDB(db)
116+
return GetAPIStatsWithOptions(db, APIStatsOptions{
117+
LoadRecentDays: LOOKBACK_DAYS, // Default: only load last x days
118+
})
119+
}
120+
121+
// APIStatsOptions controls how much data to load
122+
type APIStatsOptions struct {
123+
LoadRecentDays int // Load only last N days (0 = all time)
124+
IncludeAllTime bool // Include all-time stats
125+
}
126+
127+
// GetAPIStatsWithOptions allows custom data loading
128+
func GetAPIStatsWithOptions(db *sql.DB, opts APIStatsOptions) (*APIStats, error) {
129+
startTime := time.Now()
130+
131+
if opts.LoadRecentDays == 0 {
132+
opts.LoadRecentDays = LOOKBACK_DAYS
133+
}
134+
135+
now := time.Now()
136+
cutoffDate := now.AddDate(0, 0, -opts.LoadRecentDays)
137+
138+
// Only load recent activities
139+
activities, err := loadActivitiesSince(db, cutoffDate)
105140
if err != nil {
106141
return nil, err
107142
}
108143

144+
// Get total count for metadata
145+
totalCount, _ := getTotalActivityCount(db)
146+
109147
calc := NewCalculator(time.Local)
110148
fullStats, err := calc.Calculate(activities)
111149
if err != nil {
@@ -114,9 +152,71 @@ func GetAPIStats(db *sql.DB) (*APIStats, error) {
114152

115153
apiStats := convertToAPIStats(fullStats)
116154

155+
// Add performance metadata
156+
queryTime := time.Since(startTime).Seconds() * 1000 // Convert to ms
157+
apiStats.Meta = APIMeta{
158+
LoadedActivities: len(activities),
159+
TotalActivities: totalCount,
160+
QueryTimeMs: queryTime,
161+
DataWindow: fmt.Sprintf("last_%d_days", opts.LoadRecentDays),
162+
}
163+
117164
return apiStats, nil
118165
}
119166

167+
// Load only activities since a specific date
168+
func loadActivitiesSince(db *sql.DB, since time.Time) ([]Activity, error) {
169+
query := `
170+
SELECT id, timestamp, lines, language, project,
171+
editor, file, COALESCE(branch, ''), is_write
172+
FROM activities
173+
WHERE timestamp >= ?
174+
ORDER BY timestamp ASC
175+
`
176+
177+
rows, err := db.Query(query, since.Unix())
178+
if err != nil {
179+
return nil, err
180+
}
181+
defer rows.Close()
182+
183+
activities := []Activity{}
184+
for rows.Next() {
185+
var a Activity
186+
var timestampUnix int64
187+
var isWriteInt int
188+
189+
err := rows.Scan(
190+
&a.ID,
191+
&timestampUnix,
192+
&a.Lines,
193+
&a.Language,
194+
&a.Project,
195+
&a.Editor,
196+
&a.File,
197+
&a.Branch,
198+
&isWriteInt,
199+
)
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
a.Timestamp = time.Unix(timestampUnix, 0)
205+
a.IsWrite = isWriteInt == 1
206+
207+
activities = append(activities, a)
208+
}
209+
210+
return activities, rows.Err()
211+
}
212+
213+
// Get total activity count (for metadata)
214+
func getTotalActivityCount(db *sql.DB) (int, error) {
215+
var count int
216+
err := db.QueryRow("SELECT COUNT(*) FROM activities").Scan(&count)
217+
return count, err
218+
}
219+
120220
// convertToAPIStats converts full Stats to simplified APIStats
121221
func convertToAPIStats(s *Stats) *APIStats {
122222
return &APIStats{
@@ -263,49 +363,3 @@ func convertComparisonResultToAPI(comp ComparisonResult) APIComparisonResult {
263363
Trend: comp.Trend,
264364
}
265365
}
266-
267-
// loadActivitiesFromDB loads all activities from the database
268-
func loadActivitiesFromDB(db *sql.DB) ([]Activity, error) {
269-
query := `
270-
SELECT id, timestamp, lines, language, project,
271-
editor, file, COALESCE(branch, ''), is_write
272-
FROM activities
273-
ORDER BY timestamp ASC
274-
`
275-
276-
rows, err := db.Query(query)
277-
if err != nil {
278-
return nil, err
279-
}
280-
defer rows.Close()
281-
282-
activities := []Activity{}
283-
for rows.Next() {
284-
var a Activity
285-
var timestampUnix int64
286-
var isWriteInt int
287-
288-
err := rows.Scan(
289-
&a.ID,
290-
&timestampUnix,
291-
&a.Lines,
292-
&a.Language,
293-
&a.Project,
294-
&a.Editor,
295-
&a.File,
296-
&a.Branch,
297-
&isWriteInt,
298-
)
299-
if err != nil {
300-
return nil, err
301-
}
302-
303-
// Convert Unix timestamp to time.Time
304-
a.Timestamp = time.Unix(timestampUnix, 0)
305-
a.IsWrite = isWriteInt == 1
306-
307-
activities = append(activities, a)
308-
}
309-
310-
return activities, rows.Err()
311-
}

core/stats/calculator.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,7 @@ func (c *Calculator) calculatePeriod(
166166
day.Time += a.Duration
167167
day.Lines += a.Lines
168168

169-
// Add languages & projects to sets
170-
if a.Language != "" {
169+
if IsValidLanguage(a.Language) {
171170
langSets[date].Add(a.Language)
172171
}
173172
if a.Project != "" {
@@ -198,18 +197,23 @@ func (c *Calculator) calculateLanguageStats(
198197
periodActivities []Activity,
199198
allActivities []Activity,
200199
) []LanguageStats {
201-
// Aggregate period data
202200
periodData := make(map[string]*LanguageStats)
203201
total := 0.0
204202

205203
for _, a := range periodActivities {
206-
if periodData[a.Language] == nil {
207-
periodData[a.Language] = &LanguageStats{Name: a.Language}
204+
if !IsValidLanguage(a.Language) {
205+
continue
208206
}
209-
periodData[a.Language].Time += a.Duration
210-
periodData[a.Language].Lines += a.Lines
211-
periodData[a.Language].Files++
207+
208+
lang := NormalizeLanguage(a.Language)
209+
if periodData[lang] == nil {
210+
periodData[lang] = &LanguageStats{Name: lang}
211+
}
212+
periodData[lang].Time += a.Duration
213+
periodData[lang].Lines += a.Lines
214+
periodData[lang].Files++
212215
total += a.Duration
216+
213217
}
214218

215219
// Calculate lifetime hours for proficiency

core/stats/languages.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,24 @@ func IsCodeLanguage(lang string) bool {
211211
kind, ok := LanguageClassification[lower]
212212
return ok && kind == "code"
213213
}
214+
215+
var invalidLanguages = map[string]struct{}{
216+
"": {},
217+
"n/a": {},
218+
"na": {},
219+
"unknown": {},
220+
"undefined": {},
221+
"null": {},
222+
"none": {},
223+
"None": {},
224+
}
225+
226+
func IsValidLanguage(lang string) bool {
227+
lang = strings.TrimSpace(strings.ToLower(lang))
228+
_, invalid := invalidLanguages[lang]
229+
return !invalid
230+
}
231+
232+
func NormalizeLanguage(lang string) string {
233+
return strings.TrimSpace(strings.ToLower(lang))
234+
}

core/stats/session.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ func (sm *SessionManager) GroupActivitiesIntoSessions(activities []Activity) []S
5959
projects = util.NewStringSet()
6060
languages = util.NewStringSet()
6161
projects.Add(a.Project)
62-
languages.Add(a.Language)
62+
if IsValidLanguage(a.Language) {
63+
languages.Add(NormalizeLanguage(a.Language))
64+
}
6365

6466
current = &Session{
6567
ID: a.ID,
@@ -109,7 +111,9 @@ func (sm *SessionManager) GroupActivitiesIntoSessions(activities []Activity) []S
109111
current.Activities = append(current.Activities, a)
110112
current.EndTime = a.Timestamp
111113
projects.Add(a.Project)
112-
languages.Add(a.Language)
114+
if IsValidLanguage(a.Language) {
115+
languages.Add(NormalizeLanguage(a.Language))
116+
}
113117
}
114118

115119
// Finalize last session

0 commit comments

Comments
 (0)