vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Drive the home rail from the events table
20442b5948ea72c5e84b6cf2ff922f3cc987ac13 · Keenan Nemetz · 9d ago · parent bca9ec24
Drive the home rail from the events table The landing page still built every visible repository's whole feed in memory to show the newest ten events across the instance. It now asks the events table for one bounded page, with the repository names attached, so the rail costs a page instead of the history. The cards are separated from the feed: - counts come from the index (issue and change totals); - the newest event time per card is one grouped query (`LastEventTimes`) instead of deriving it from the feed that was built anyway; - the release chip comes from the newest release event (`LastReleases`) instead of listing tags from git per card; - the head commit and language still come from git, which is the remaining per-repository work on the page. `visibleRepos` now builds handles with `Meta` rather than `Open`, which removes a `symbolic-ref` per repository from the landing page and the instance activity page. The all-git card builder stays as the fallback for a Web without an index. Load harness at concurrency 4 with three repositories: home goes from 178 to 243 req/s, p50 from 21 ms to 16 ms.
forge/index/events.go +63
| @@ -18,6 +18,7 @@ import ( | |||
| 18 | 18 | "database/sql" | |
| 19 | 19 | "fmt" | |
| 20 | 20 | "strings" | |
| 21 | "time" | ||
| 21 | 22 | ||
| 22 | 23 | "github.com/rodent-software/vole/core/git" | |
| 23 | 24 | "github.com/rodent-software/vole/forge/repo" | |
| @@ -367,3 +368,65 @@ func questionMarks(n int) string { | |||
| 367 | 368 | } | |
| 368 | 369 | return strings.TrimSuffix(strings.Repeat("?,", n), ",") | |
| 369 | 370 | } | |
| 371 | |||
| 372 | // LastEventTimes returns the newest event time per repository. It is the card's | ||
| 373 | // "updated" time, read for a whole page of cards in one grouped query rather | ||
| 374 | // than one query per card. | ||
| 375 | func (s *Store) LastEventTimes(repoIDs []string) (map[string]time.Time, error) { | ||
| 376 | if len(repoIDs) == 0 { | ||
| 377 | return nil, nil | ||
| 378 | } | ||
| 379 | args := make([]any, 0, len(repoIDs)) | ||
| 380 | for _, id := range repoIDs { | ||
| 381 | args = append(args, id) | ||
| 382 | } | ||
| 383 | rows, err := s.db.Query( | ||
| 384 | `SELECT repo_id, MAX(at) FROM events WHERE repo_id IN (`+questionMarks(len(repoIDs))+`) | ||
| 385 | GROUP BY repo_id`, args...) | ||
| 386 | if err != nil { | ||
| 387 | return nil, err | ||
| 388 | } | ||
| 389 | defer rows.Close() | ||
| 390 | out := map[string]time.Time{} | ||
| 391 | for rows.Next() { | ||
| 392 | var id string | ||
| 393 | var at int64 | ||
| 394 | if err := rows.Scan(&id, &at); err != nil { | ||
| 395 | return nil, err | ||
| 396 | } | ||
| 397 | out[id] = time.Unix(at, 0).UTC() | ||
| 398 | } | ||
| 399 | return out, rows.Err() | ||
| 400 | } | ||
| 401 | |||
| 402 | // LastReleases returns the newest release event's tag name per repository, for a | ||
| 403 | // card's release chip. It reads the same rows the feed does rather than listing | ||
| 404 | // tags from git again. | ||
| 405 | func (s *Store) LastReleases(repoIDs []string) (map[string]string, error) { | ||
| 406 | if len(repoIDs) == 0 { | ||
| 407 | return nil, nil | ||
| 408 | } | ||
| 409 | args := make([]any, 0, len(repoIDs)) | ||
| 410 | for _, id := range repoIDs { | ||
| 411 | args = append(args, id) | ||
| 412 | } | ||
| 413 | rows, err := s.db.Query( | ||
| 414 | `SELECT repo_id, title FROM events | ||
| 415 | WHERE kind='release' AND repo_id IN (`+questionMarks(len(repoIDs))+`) | ||
| 416 | ORDER BY at DESC, id DESC`, args...) | ||
| 417 | if err != nil { | ||
| 418 | return nil, err | ||
| 419 | } | ||
| 420 | defer rows.Close() | ||
| 421 | out := map[string]string{} | ||
| 422 | for rows.Next() { | ||
| 423 | var repoID, name string | ||
| 424 | if err := rows.Scan(&repoID, &name); err != nil { | ||
| 425 | return nil, err | ||
| 426 | } | ||
| 427 | if _, ok := out[repoID]; !ok { | ||
| 428 | out[repoID] = name | ||
| 429 | } | ||
| 430 | } | ||
| 431 | return out, rows.Err() | ||
| 432 | } | ||