Fix dashboard slowness by routing bd show to correct rig beads
The dashboard was taking 120+ seconds to respond due to two issues: 1. bd show commands for cross-rig issues ran from wrong beads directory, causing slow routing resolution (5.2s instead of 0.09s per lookup) 2. The bd daemon adds ~5s latency per command Changes: - Add routes map to LiveConvoyFetcher, loaded from routes.jsonl - Add getBeadsDir() to determine correct beads directory for issue prefix - Update getIssueDetailsBatch() to group issues by rig and run bd from the correct beads directory - Add BEADS_NO_DAEMON=1 to bypass daemon for faster execution Performance improvement: - Before: 126+ seconds (timeout) - After: ~5 seconds Closes: hq-tyfkm
This commit is contained in:
@@ -15,7 +15,9 @@ import (
|
||||
|
||||
// LiveConvoyFetcher fetches convoy data from beads.
|
||||
type LiveConvoyFetcher struct {
|
||||
townRoot string
|
||||
townBeads string
|
||||
routes map[string]string // prefix -> rig path (e.g., "sc-" -> "scout")
|
||||
}
|
||||
|
||||
// NewLiveConvoyFetcher creates a fetcher for the current workspace.
|
||||
@@ -25,11 +27,60 @@ func NewLiveConvoyFetcher() (*LiveConvoyFetcher, error) {
|
||||
return nil, fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||
}
|
||||
|
||||
return &LiveConvoyFetcher{
|
||||
fetcher := &LiveConvoyFetcher{
|
||||
townRoot: townRoot,
|
||||
townBeads: filepath.Join(townRoot, ".beads"),
|
||||
}, nil
|
||||
routes: make(map[string]string),
|
||||
}
|
||||
|
||||
// Load routes from routes.jsonl
|
||||
fetcher.loadRoutes()
|
||||
|
||||
return fetcher, nil
|
||||
}
|
||||
|
||||
// loadRoutes loads prefix routing from routes.jsonl.
|
||||
func (f *LiveConvoyFetcher) loadRoutes() {
|
||||
routesFile := filepath.Join(f.townBeads, "routes.jsonl")
|
||||
data, err := exec.Command("cat", routesFile).Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var route struct {
|
||||
Prefix string `json:"prefix"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(line), &route); err == nil && route.Prefix != "" {
|
||||
f.routes[route.Prefix] = route.Path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getBeadsDir returns the appropriate beads directory for an issue ID.
|
||||
// Routes issues to the correct rig's beads based on prefix.
|
||||
func (f *LiveConvoyFetcher) getBeadsDir(issueID string) string {
|
||||
// Find longest matching prefix
|
||||
var bestMatch string
|
||||
var bestPath string
|
||||
for prefix, path := range f.routes {
|
||||
if strings.HasPrefix(issueID, prefix) && len(prefix) > len(bestMatch) {
|
||||
bestMatch = prefix
|
||||
bestPath = path
|
||||
}
|
||||
}
|
||||
|
||||
if bestPath == "" || bestPath == "." {
|
||||
return f.townBeads
|
||||
}
|
||||
|
||||
return filepath.Join(f.townRoot, bestPath, ".beads")
|
||||
}
|
||||
|
||||
// FetchConvoys fetches all open convoys with their activity data.
|
||||
func (f *LiveConvoyFetcher) FetchConvoys() ([]ConvoyRow, error) {
|
||||
@@ -37,6 +88,8 @@ func (f *LiveConvoyFetcher) FetchConvoys() ([]ConvoyRow, error) {
|
||||
listArgs := []string{"list", "--type=convoy", "--status=open", "--json"}
|
||||
listCmd := exec.Command("bd", listArgs...)
|
||||
listCmd.Dir = f.townBeads
|
||||
// Bypass daemon for faster execution (daemon adds ~5s latency)
|
||||
listCmd.Env = append(listCmd.Environ(), "BEADS_NO_DAEMON=1")
|
||||
|
||||
var stdout bytes.Buffer
|
||||
listCmd.Stdout = &stdout
|
||||
@@ -228,49 +281,63 @@ type issueDetail struct {
|
||||
}
|
||||
|
||||
// getIssueDetailsBatch fetches details for multiple issues.
|
||||
// Groups issues by their rig prefix and fetches from the correct beads directory.
|
||||
func (f *LiveConvoyFetcher) getIssueDetailsBatch(issueIDs []string) map[string]*issueDetail {
|
||||
result := make(map[string]*issueDetail)
|
||||
if len(issueIDs) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
args := append([]string{"show"}, issueIDs...)
|
||||
args = append(args, "--json")
|
||||
|
||||
// #nosec G204 -- bd is a trusted internal tool, args are issue IDs
|
||||
showCmd := exec.Command("bd", args...)
|
||||
var stdout bytes.Buffer
|
||||
showCmd.Stdout = &stdout
|
||||
|
||||
if err := showCmd.Run(); err != nil {
|
||||
return result
|
||||
// Group issues by their beads directory
|
||||
byDir := make(map[string][]string)
|
||||
for _, id := range issueIDs {
|
||||
dir := f.getBeadsDir(id)
|
||||
byDir[dir] = append(byDir[dir], id)
|
||||
}
|
||||
|
||||
var issues []struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Assignee string `json:"assignee"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil {
|
||||
return result
|
||||
}
|
||||
// Fetch from each directory
|
||||
for dir, ids := range byDir {
|
||||
args := append([]string{"show"}, ids...)
|
||||
args = append(args, "--json")
|
||||
|
||||
for _, issue := range issues {
|
||||
detail := &issueDetail{
|
||||
ID: issue.ID,
|
||||
Title: issue.Title,
|
||||
Status: issue.Status,
|
||||
Assignee: issue.Assignee,
|
||||
// #nosec G204 -- bd is a trusted internal tool, args are issue IDs
|
||||
showCmd := exec.Command("bd", args...)
|
||||
showCmd.Dir = dir
|
||||
// Bypass daemon for faster execution (daemon adds ~5s latency)
|
||||
showCmd.Env = append(showCmd.Environ(), "BEADS_NO_DAEMON=1")
|
||||
var stdout bytes.Buffer
|
||||
showCmd.Stdout = &stdout
|
||||
|
||||
if err := showCmd.Run(); err != nil {
|
||||
continue
|
||||
}
|
||||
// Parse updated_at timestamp
|
||||
if issue.UpdatedAt != "" {
|
||||
if t, err := time.Parse(time.RFC3339, issue.UpdatedAt); err == nil {
|
||||
detail.UpdatedAt = t
|
||||
|
||||
var issues []struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Assignee string `json:"assignee"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
detail := &issueDetail{
|
||||
ID: issue.ID,
|
||||
Title: issue.Title,
|
||||
Status: issue.Status,
|
||||
Assignee: issue.Assignee,
|
||||
}
|
||||
// Parse updated_at timestamp
|
||||
if issue.UpdatedAt != "" {
|
||||
if t, err := time.Parse(time.RFC3339, issue.UpdatedAt); err == nil {
|
||||
detail.UpdatedAt = t
|
||||
}
|
||||
}
|
||||
result[issue.ID] = detail
|
||||
}
|
||||
result[issue.ID] = detail
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user