perf(tmux): batch session queries in gt down (#477)

* perf(tmux): batch session queries in gt down to reduce N+1 subprocess calls

Add SessionSet type to tmux package for O(1) session existence checks.
Instead of calling HasSession() (which spawns a subprocess) for each
rig/session during shutdown, now calls ListSessions() once and uses
in-memory map lookups.

Changes:
- internal/tmux/tmux.go: Add SessionSet type with GetSessionSet() and Has()
- internal/cmd/down.go: Use SessionSet for dry-run checks and session stops
- internal/session/town.go: Add StopTownSessionWithCache() variant
- internal/tmux/tmux_test.go: Add test for SessionSet

With 5 rigs, this reduces subprocess calls from ~15 to 1 during shutdown
preview, saving 60-150ms of execution time.

Closes: gt-xh2bh

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* perf(tmux): optimize SessionSet to avoid intermediate slice allocation

- Build map directly from tmux output instead of calling ListSessions()
- Use strings.IndexByte for efficient newline parsing
- Pre-size map using newline count to avoid rehashing
- Simplify nil checks in Has() and Names()

* fix(sling): restore bd cook directory context for formula-on-bead mode

The bd cook command needs to run from the target rig's directory to
access the correct formula database. This was accidentally removed
in a previous commit, causing TestSlingFormulaOnBeadRoutesBDCommandsToTargetRig
to fail.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Keith Wyatt
2026-01-13 22:07:05 -08:00
committed by GitHub
parent 5d96243414
commit 08755f62cd
5 changed files with 162 additions and 7 deletions

View File

@@ -105,6 +105,9 @@ func runDown(cmd *cobra.Command, args []string) error {
rigs := discoverRigs(townRoot)
// Pre-fetch all sessions once for O(1) lookups (avoids N+1 subprocess calls)
sessionSet, _ := t.GetSessionSet() // Ignore error - empty set is safe fallback
// Phase 0.5: Stop polecats if --polecats
if downPolecats {
if downDryRun {
@@ -161,12 +164,12 @@ func runDown(cmd *cobra.Command, args []string) error {
for _, rigName := range rigs {
sessionName := fmt.Sprintf("gt-%s-refinery", rigName)
if downDryRun {
if running, _ := t.HasSession(sessionName); running {
if sessionSet.Has(sessionName) {
printDownStatus(fmt.Sprintf("Refinery (%s)", rigName), true, "would stop")
}
continue
}
wasRunning, err := stopSession(t, sessionName)
wasRunning, err := stopSessionWithCache(t, sessionName, sessionSet)
if err != nil {
printDownStatus(fmt.Sprintf("Refinery (%s)", rigName), false, err.Error())
allOK = false
@@ -181,12 +184,12 @@ func runDown(cmd *cobra.Command, args []string) error {
for _, rigName := range rigs {
sessionName := fmt.Sprintf("gt-%s-witness", rigName)
if downDryRun {
if running, _ := t.HasSession(sessionName); running {
if sessionSet.Has(sessionName) {
printDownStatus(fmt.Sprintf("Witness (%s)", rigName), true, "would stop")
}
continue
}
wasRunning, err := stopSession(t, sessionName)
wasRunning, err := stopSessionWithCache(t, sessionName, sessionSet)
if err != nil {
printDownStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
allOK = false
@@ -200,12 +203,12 @@ func runDown(cmd *cobra.Command, args []string) error {
// Phase 3: Stop town-level sessions (Mayor, Boot, Deacon)
for _, ts := range session.TownSessions() {
if downDryRun {
if running, _ := t.HasSession(ts.SessionID); running {
if sessionSet.Has(ts.SessionID) {
printDownStatus(ts.Name, true, "would stop")
}
continue
}
stopped, err := session.StopTownSession(t, ts, downForce)
stopped, err := session.StopTownSessionWithCache(t, ts, downForce, sessionSet)
if err != nil {
printDownStatus(ts.Name, false, err.Error())
allOK = false
@@ -390,6 +393,23 @@ func stopSession(t *tmux.Tmux, sessionName string) (bool, error) {
return true, t.KillSessionWithProcesses(sessionName)
}
// stopSessionWithCache is like stopSession but uses a pre-fetched SessionSet
// for O(1) existence check instead of spawning a subprocess.
func stopSessionWithCache(t *tmux.Tmux, sessionName string, cache *tmux.SessionSet) (bool, error) {
if !cache.Has(sessionName) {
return false, nil // Already stopped
}
// Try graceful shutdown first (Ctrl-C, best-effort interrupt)
if !downForce {
_ = t.SendKeysRaw(sessionName, "C-c")
time.Sleep(100 * time.Millisecond)
}
// Kill the session (with explicit process termination to prevent orphans)
return true, t.KillSessionWithProcesses(sessionName)
}
// acquireShutdownLock prevents concurrent shutdowns.
// Returns the lock (caller must defer Unlock()) or error if lock held.
func acquireShutdownLock(townRoot string) (*flock.Flock, error) {

View File

@@ -380,8 +380,9 @@ func runSling(cmd *cobra.Command, args []string) error {
formulaWorkDir := beads.ResolveHookDir(townRoot, beadID, hookWorkDir)
// Step 1: Cook the formula (ensures proto exists)
// Cook doesn't need database context - runs from cwd like gt formula show
// Cook runs from rig directory to access the correct formula database
cookCmd := exec.Command("bd", "--no-daemon", "cook", formulaName)
cookCmd.Dir = formulaWorkDir
cookCmd.Stderr = os.Stderr
if err := cookCmd.Run(); err != nil {
return fmt.Errorf("cooking formula %s: %w", formulaName, err)