Skip to content

Commit 1e3e442

Browse files
committed
finally fix the excessive menu rebuilds
1 parent bbfb3a7 commit 1e3e442

File tree

3 files changed

+131
-84
lines changed

3 files changed

+131
-84
lines changed

cache.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type cacheEntry struct {
2424
}
2525

2626
// turnData fetches Turn API data with caching.
27-
func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (*turn.CheckResponse, error) {
27+
func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (*turn.CheckResponse, bool, error) {
2828
// Create cache key from URL and updated timestamp
2929
key := fmt.Sprintf("%s-%s", url, updatedAt.Format(time.RFC3339))
3030
hash := sha256.Sum256([]byte(key))
@@ -43,7 +43,7 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
4343
}
4444
} else if time.Since(entry.CachedAt) < cacheTTL && entry.UpdatedAt.Equal(updatedAt) {
4545
// Check if cache is still valid (2 hour TTL)
46-
return entry.Data, nil
46+
return entry.Data, true, nil
4747
}
4848
}
4949
}
@@ -80,7 +80,7 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
8080
)
8181
if err != nil {
8282
log.Printf("Turn API error after %d retries (will use PR without metadata): %v", maxRetries, err)
83-
return nil, err
83+
return nil, false, err
8484
}
8585

8686
// Save to cache (don't fail if caching fails) - skip if --no-cache is set
@@ -102,7 +102,7 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
102102
}
103103
}
104104

105-
return data, nil
105+
return data, false, nil
106106
}
107107

108108
// cleanupOldCache removes cache files older than 5 days.

github.go

Lines changed: 94 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,11 @@ func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incomin
230230
}
231231

232232
log.Printf("[GITHUB] Found %d incoming, %d outgoing PRs from GitHub", len(incoming), len(outgoing))
233-
for _, pr := range incoming {
234-
log.Printf("[GITHUB] Incoming PR: %s", pr.URL)
233+
for i := range incoming {
234+
log.Printf("[GITHUB] Incoming PR: %s", incoming[i].URL)
235235
}
236-
for _, pr := range outgoing {
237-
log.Printf("[GITHUB] Outgoing PR: %s", pr.URL)
236+
for i := range outgoing {
237+
log.Printf("[GITHUB] Outgoing PR: %s", outgoing[i].URL)
238238
}
239239

240240
// Fetch Turn API data
@@ -255,41 +255,63 @@ func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incomin
255255
}
256256

257257
// updatePRData updates PR data with Turn API results.
258-
func (app *App) updatePRData(url string, needsReview bool, isOwner bool, actionReason string) *PR {
258+
func (app *App) updatePRData(url string, needsReview bool, isOwner bool, actionReason string) (*PR, bool) {
259259
app.mu.Lock()
260260
defer app.mu.Unlock()
261261

262262
if isOwner {
263263
// Update outgoing PRs
264264
for i := range app.outgoing {
265-
if app.outgoing[i].URL == url {
266-
app.outgoing[i].NeedsReview = needsReview
267-
app.outgoing[i].IsBlocked = needsReview
268-
app.outgoing[i].ActionReason = actionReason
269-
return &app.outgoing[i]
265+
if app.outgoing[i].URL != url {
266+
continue
270267
}
268+
// Check if Turn data was already applied for this UpdatedAt
269+
now := time.Now()
270+
if app.outgoing[i].TurnDataAppliedAt.After(app.outgoing[i].UpdatedAt) {
271+
// Turn data already applied for this PR version, no change
272+
return &app.outgoing[i], false
273+
}
274+
changed := app.outgoing[i].NeedsReview != needsReview ||
275+
app.outgoing[i].IsBlocked != needsReview ||
276+
app.outgoing[i].ActionReason != actionReason
277+
app.outgoing[i].NeedsReview = needsReview
278+
app.outgoing[i].IsBlocked = needsReview
279+
app.outgoing[i].ActionReason = actionReason
280+
app.outgoing[i].TurnDataAppliedAt = now
281+
return &app.outgoing[i], changed
271282
}
272283
} else {
273284
// Update incoming PRs
274285
for i := range app.incoming {
275-
if app.incoming[i].URL == url {
276-
app.incoming[i].NeedsReview = needsReview
277-
app.incoming[i].ActionReason = actionReason
278-
return &app.incoming[i]
286+
if app.incoming[i].URL != url {
287+
continue
288+
}
289+
// Check if Turn data was already applied for this UpdatedAt
290+
now := time.Now()
291+
if app.incoming[i].TurnDataAppliedAt.After(app.incoming[i].UpdatedAt) {
292+
// Turn data already applied for this PR version, no change
293+
return &app.incoming[i], false
279294
}
295+
changed := app.incoming[i].NeedsReview != needsReview ||
296+
app.incoming[i].ActionReason != actionReason
297+
app.incoming[i].NeedsReview = needsReview
298+
app.incoming[i].ActionReason = actionReason
299+
app.incoming[i].TurnDataAppliedAt = now
300+
return &app.incoming[i], changed
280301
}
281302
}
282-
return nil
303+
return nil, false
283304
}
284305

285306
// fetchTurnDataSync fetches Turn API data synchronously and updates PRs directly.
286307
func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, user string, incoming *[]PR, outgoing *[]PR) {
287308
turnStart := time.Now()
288309
type prResult struct {
289-
err error
290-
turnData *turn.CheckResponse
291-
url string
292-
isOwner bool
310+
err error
311+
turnData *turn.CheckResponse
312+
url string
313+
isOwner bool
314+
wasFromCache bool
293315
}
294316

295317
// Create a channel for results
@@ -312,13 +334,14 @@ func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, u
312334
updatedAt := issue.GetUpdatedAt().Time
313335

314336
// Call turnData - it now has proper exponential backoff with jitter
315-
turnData, err := app.turnData(ctx, url, updatedAt)
337+
turnData, wasFromCache, err := app.turnData(ctx, url, updatedAt)
316338

317339
results <- prResult{
318-
url: issue.GetHTMLURL(),
319-
turnData: turnData,
320-
err: err,
321-
isOwner: issue.GetUser().GetLogin() == user,
340+
url: issue.GetHTMLURL(),
341+
turnData: turnData,
342+
err: err,
343+
isOwner: issue.GetUser().GetLogin() == user,
344+
wasFromCache: wasFromCache,
322345
}
323346
}(issue)
324347
}
@@ -381,10 +404,11 @@ func (app *App) fetchTurnDataAsync(ctx context.Context, issues []*github.Issue,
381404

382405
turnStart := time.Now()
383406
type prResult struct {
384-
err error
385-
turnData *turn.CheckResponse
386-
url string
387-
isOwner bool
407+
err error
408+
turnData *turn.CheckResponse
409+
url string
410+
isOwner bool
411+
wasFromCache bool
388412
}
389413

390414
// Create a channel for results
@@ -407,13 +431,14 @@ func (app *App) fetchTurnDataAsync(ctx context.Context, issues []*github.Issue,
407431
updatedAt := issue.GetUpdatedAt().Time
408432

409433
// Call turnData - it now has proper exponential backoff with jitter
410-
turnData, err := app.turnData(ctx, url, updatedAt)
434+
turnData, wasFromCache, err := app.turnData(ctx, url, updatedAt)
411435

412436
results <- prResult{
413-
url: issue.GetHTMLURL(),
414-
turnData: turnData,
415-
err: err,
416-
isOwner: issue.GetUser().GetLogin() == user,
437+
url: issue.GetHTMLURL(),
438+
turnData: turnData,
439+
err: err,
440+
isOwner: issue.GetUser().GetLogin() == user,
441+
wasFromCache: wasFromCache,
417442
}
418443
}(issue)
419444
}
@@ -442,7 +467,8 @@ func (app *App) fetchTurnDataAsync(ctx context.Context, issues []*github.Issue,
442467
needsReview = true
443468
actionReason = action.Reason
444469
log.Printf("[TURN] UnblockAction for %s: Reason=%q, Kind=%q", result.url, action.Reason, action.Kind)
445-
} else {
470+
} else if !result.wasFromCache {
471+
// Only log "no action" for fresh API results, not cached ones
446472
log.Printf("[TURN] No UnblockAction found for user %s on %s", user, result.url)
447473
}
448474

@@ -452,14 +478,22 @@ func (app *App) fetchTurnDataAsync(ctx context.Context, issues []*github.Issue,
452478
NeedsReview: needsReview,
453479
IsOwner: result.isOwner,
454480
ActionReason: actionReason,
481+
WasFromCache: result.wasFromCache,
455482
}
456483

457484
app.mu.Lock()
458485
app.pendingTurnResults = append(app.pendingTurnResults, turnResult)
459486
app.mu.Unlock()
460487

461488
updatesApplied++
462-
log.Printf("[TURN] Turn data received for %s (needsReview=%v, actionReason=%q) - buffered", result.url, needsReview, actionReason)
489+
// Reduce verbosity - only log if not from cache or if blocked
490+
if !result.wasFromCache || needsReview {
491+
cacheStatus := "fresh"
492+
if result.wasFromCache {
493+
cacheStatus = "cached"
494+
}
495+
log.Printf("[TURN] %s data for %s (needsReview=%v, actionReason=%q)", cacheStatus, result.url, needsReview, actionReason)
496+
}
463497
} else if result.err != nil {
464498
turnFailures++
465499
}
@@ -475,15 +509,36 @@ func (app *App) fetchTurnDataAsync(ctx context.Context, issues []*github.Issue,
475509
app.loadingTurnData = false
476510
app.mu.Unlock()
477511

478-
log.Printf("[TURN] Applying %d buffered Turn results", len(pendingResults))
512+
// Check if any results came from fresh API calls (not cache)
513+
var cacheHits, freshResults int
479514
for _, result := range pendingResults {
480-
app.updatePRData(result.URL, result.NeedsReview, result.IsOwner, result.ActionReason)
515+
if result.WasFromCache {
516+
cacheHits++
517+
} else {
518+
freshResults++
519+
}
520+
}
521+
522+
log.Printf("[TURN] Applying %d buffered Turn results (%d from cache, %d fresh)", len(pendingResults), cacheHits, freshResults)
523+
524+
// Track how many PRs actually changed
525+
var actualChanges int
526+
for _, result := range pendingResults {
527+
_, changed := app.updatePRData(result.URL, result.NeedsReview, result.IsOwner, result.ActionReason)
528+
if changed {
529+
actualChanges++
530+
}
481531
}
482532

483533
// Update tray title and menu with final Turn data if menu is already initialized
484534
app.setTrayTitle()
485535
if app.menuInitialized {
486-
log.Print("[TURN] Turn data loaded, checking if menu needs update")
487-
app.updateMenuIfChanged(ctx)
536+
// Only trigger menu update if PR data actually changed
537+
if actualChanges > 0 {
538+
log.Printf("[TURN] Turn data applied - %d PRs actually changed, checking if menu needs update", actualChanges)
539+
app.updateMenuIfChanged(ctx)
540+
} else {
541+
log.Print("[TURN] Turn data applied - no PR changes detected (cached data unchanged), skipping menu update")
542+
}
488543
}
489544
}

main.go

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,15 @@ const (
5151

5252
// PR represents a pull request with metadata.
5353
type PR struct {
54-
UpdatedAt time.Time
55-
Title string
56-
URL string
57-
Repository string
58-
ActionReason string // Action reason from Turn API when blocked
59-
Number int
60-
IsBlocked bool
61-
NeedsReview bool
54+
UpdatedAt time.Time
55+
TurnDataAppliedAt time.Time
56+
Title string
57+
URL string
58+
Repository string
59+
ActionReason string
60+
Number int
61+
IsBlocked bool
62+
NeedsReview bool
6263
}
6364

6465
// MenuState represents the current state of menu items for comparison.
@@ -85,6 +86,7 @@ type TurnResult struct {
8586
ActionReason string
8687
NeedsReview bool
8788
IsOwner bool
89+
WasFromCache bool // Track if this result came from cache
8890
}
8991

9092
// App holds the application state.
@@ -427,15 +429,15 @@ func (app *App) buildCurrentMenuState() *MenuState {
427429
now := time.Now()
428430
staleThreshold := now.Add(-stalePRThreshold)
429431

430-
for _, pr := range app.incoming {
431-
if !app.hideStaleIncoming || pr.UpdatedAt.After(staleThreshold) {
432-
filteredIncoming = append(filteredIncoming, pr)
432+
for i := range app.incoming {
433+
if !app.hideStaleIncoming || app.incoming[i].UpdatedAt.After(staleThreshold) {
434+
filteredIncoming = append(filteredIncoming, app.incoming[i])
433435
}
434436
}
435437

436-
for _, pr := range app.outgoing {
437-
if !app.hideStaleIncoming || pr.UpdatedAt.After(staleThreshold) {
438-
filteredOutgoing = append(filteredOutgoing, pr)
438+
for i := range app.outgoing {
439+
if !app.hideStaleIncoming || app.outgoing[i].UpdatedAt.After(staleThreshold) {
440+
filteredOutgoing = append(filteredOutgoing, app.outgoing[i])
439441
}
440442
}
441443

@@ -445,28 +447,28 @@ func (app *App) buildCurrentMenuState() *MenuState {
445447

446448
// Build menu item states
447449
incomingItems := make([]MenuItemState, len(incomingSorted))
448-
for i, pr := range incomingSorted {
450+
for i := range incomingSorted {
449451
incomingItems[i] = MenuItemState{
450-
URL: pr.URL,
451-
Title: pr.Title,
452-
Repository: pr.Repository,
453-
Number: pr.Number,
454-
NeedsReview: pr.NeedsReview,
452+
URL: incomingSorted[i].URL,
453+
Title: incomingSorted[i].Title,
454+
Repository: incomingSorted[i].Repository,
455+
Number: incomingSorted[i].Number,
456+
NeedsReview: incomingSorted[i].NeedsReview,
455457
IsBlocked: false, // incoming PRs don't use IsBlocked
456-
ActionReason: pr.ActionReason,
458+
ActionReason: incomingSorted[i].ActionReason,
457459
}
458460
}
459461

460462
outgoingItems := make([]MenuItemState, len(outgoingSorted))
461-
for i, pr := range outgoingSorted {
463+
for i := range outgoingSorted {
462464
outgoingItems[i] = MenuItemState{
463-
URL: pr.URL,
464-
Title: pr.Title,
465-
Repository: pr.Repository,
466-
Number: pr.Number,
467-
NeedsReview: pr.NeedsReview,
468-
IsBlocked: pr.IsBlocked,
469-
ActionReason: pr.ActionReason,
465+
URL: outgoingSorted[i].URL,
466+
Title: outgoingSorted[i].Title,
467+
Repository: outgoingSorted[i].Repository,
468+
Number: outgoingSorted[i].Number,
469+
NeedsReview: outgoingSorted[i].NeedsReview,
470+
IsBlocked: outgoingSorted[i].IsBlocked,
471+
ActionReason: outgoingSorted[i].ActionReason,
470472
}
471473
}
472474

@@ -482,18 +484,8 @@ func (app *App) updateMenuIfChanged(ctx context.Context) {
482484
app.mu.RLock()
483485
// Skip menu updates while Turn data is still loading to avoid excessive rebuilds
484486
if app.loadingTurnData {
485-
// But still save the current state for future comparisons
486-
currentMenuState := app.buildCurrentMenuState()
487-
if app.lastMenuState == nil {
488-
log.Print("[MENU] Skipping menu update - Turn data still loading, but saving state for future comparison")
489-
app.mu.RUnlock()
490-
app.mu.Lock()
491-
app.lastMenuState = currentMenuState
492-
app.mu.Unlock()
493-
} else {
494-
app.mu.RUnlock()
495-
log.Print("[MENU] Skipping menu update - Turn data still loading")
496-
}
487+
app.mu.RUnlock()
488+
log.Print("[MENU] Skipping menu update - Turn data still loading")
497489
return
498490
}
499491
log.Print("[MENU] *** updateMenuIfChanged called - calculating diff ***")

0 commit comments

Comments
 (0)