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.
|
// LiveConvoyFetcher fetches convoy data from beads.
|
||||||
type LiveConvoyFetcher struct {
|
type LiveConvoyFetcher struct {
|
||||||
|
townRoot string
|
||||||
townBeads string
|
townBeads string
|
||||||
|
routes map[string]string // prefix -> rig path (e.g., "sc-" -> "scout")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLiveConvoyFetcher creates a fetcher for the current workspace.
|
// 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 nil, fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LiveConvoyFetcher{
|
fetcher := &LiveConvoyFetcher{
|
||||||
|
townRoot: townRoot,
|
||||||
townBeads: filepath.Join(townRoot, ".beads"),
|
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.
|
// FetchConvoys fetches all open convoys with their activity data.
|
||||||
func (f *LiveConvoyFetcher) FetchConvoys() ([]ConvoyRow, error) {
|
func (f *LiveConvoyFetcher) FetchConvoys() ([]ConvoyRow, error) {
|
||||||
@@ -37,6 +88,8 @@ func (f *LiveConvoyFetcher) FetchConvoys() ([]ConvoyRow, error) {
|
|||||||
listArgs := []string{"list", "--type=convoy", "--status=open", "--json"}
|
listArgs := []string{"list", "--type=convoy", "--status=open", "--json"}
|
||||||
listCmd := exec.Command("bd", listArgs...)
|
listCmd := exec.Command("bd", listArgs...)
|
||||||
listCmd.Dir = f.townBeads
|
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
|
var stdout bytes.Buffer
|
||||||
listCmd.Stdout = &stdout
|
listCmd.Stdout = &stdout
|
||||||
@@ -228,49 +281,63 @@ type issueDetail struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getIssueDetailsBatch fetches details for multiple issues.
|
// 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 {
|
func (f *LiveConvoyFetcher) getIssueDetailsBatch(issueIDs []string) map[string]*issueDetail {
|
||||||
result := make(map[string]*issueDetail)
|
result := make(map[string]*issueDetail)
|
||||||
if len(issueIDs) == 0 {
|
if len(issueIDs) == 0 {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
args := append([]string{"show"}, issueIDs...)
|
// Group issues by their beads directory
|
||||||
args = append(args, "--json")
|
byDir := make(map[string][]string)
|
||||||
|
for _, id := range issueIDs {
|
||||||
// #nosec G204 -- bd is a trusted internal tool, args are issue IDs
|
dir := f.getBeadsDir(id)
|
||||||
showCmd := exec.Command("bd", args...)
|
byDir[dir] = append(byDir[dir], id)
|
||||||
var stdout bytes.Buffer
|
|
||||||
showCmd.Stdout = &stdout
|
|
||||||
|
|
||||||
if err := showCmd.Run(); err != nil {
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var issues []struct {
|
// Fetch from each directory
|
||||||
ID string `json:"id"`
|
for dir, ids := range byDir {
|
||||||
Title string `json:"title"`
|
args := append([]string{"show"}, ids...)
|
||||||
Status string `json:"status"`
|
args = append(args, "--json")
|
||||||
Assignee string `json:"assignee"`
|
|
||||||
UpdatedAt string `json:"updated_at"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, issue := range issues {
|
// #nosec G204 -- bd is a trusted internal tool, args are issue IDs
|
||||||
detail := &issueDetail{
|
showCmd := exec.Command("bd", args...)
|
||||||
ID: issue.ID,
|
showCmd.Dir = dir
|
||||||
Title: issue.Title,
|
// Bypass daemon for faster execution (daemon adds ~5s latency)
|
||||||
Status: issue.Status,
|
showCmd.Env = append(showCmd.Environ(), "BEADS_NO_DAEMON=1")
|
||||||
Assignee: issue.Assignee,
|
var stdout bytes.Buffer
|
||||||
|
showCmd.Stdout = &stdout
|
||||||
|
|
||||||
|
if err := showCmd.Run(); err != nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
// Parse updated_at timestamp
|
|
||||||
if issue.UpdatedAt != "" {
|
var issues []struct {
|
||||||
if t, err := time.Parse(time.RFC3339, issue.UpdatedAt); err == nil {
|
ID string `json:"id"`
|
||||||
detail.UpdatedAt = t
|
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
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user