Skip to content

Commit 58741ad

Browse files
authored
perf: List off-lock IO + DeleteAll shared /proc scan (#60)
- Backend.List captures VMRecord snapshots under the DB lock then runs ToVM (which reads pidfile + stat vsock socket) outside the lock. Concurrent writers no longer queue behind status polls. - utils.ScanProcsByBinary walks /proc once and returns a cache; the per-call FindVMMByCmdline becomes a thin wrapper. DeleteAll calls ScanProcsByBinary up-front and reuses the scan via ProcScan.Find for each VM — N walks of /proc collapse to 1. - Non-linux stubs added for ProcScan / ScanProcsByBinary so darwin build stays green.
1 parent 01bc7e7 commit 58741ad

4 files changed

Lines changed: 71 additions & 19 deletions

File tree

hypervisor/inspect.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,28 @@ func (b *Backend) Inspect(ctx context.Context, ref string) (*types.VM, error) {
2121
})
2222
}
2323

24+
// List snapshots all records under the DB lock then runs ToVM (which does file IO) outside the lock so concurrent writers don't queue behind status polls. Mutable map fields are cloned inside the lock to avoid a concurrent-read race with RecordSnapshot etc.
2425
func (b *Backend) List(ctx context.Context) ([]*types.VM, error) {
25-
var result []*types.VM
26-
return result, b.DB.With(ctx, func(idx *VMIndex) error {
27-
result = utils.MapValues(idx.VMs, b.ToVM)
26+
var recs []*VMRecord
27+
if err := b.DB.With(ctx, func(idx *VMIndex) error {
28+
recs = make([]*VMRecord, 0, len(idx.VMs))
29+
for _, r := range idx.VMs {
30+
if r == nil {
31+
continue
32+
}
33+
cp := *r
34+
cp.SnapshotIDs = maps.Clone(r.SnapshotIDs)
35+
recs = append(recs, &cp)
36+
}
2837
return nil
29-
})
38+
}); err != nil {
39+
return nil, err
40+
}
41+
result := make([]*types.VM, len(recs))
42+
for i, r := range recs {
43+
result[i] = b.ToVM(r)
44+
}
45+
return result, nil
3046
}
3147

3248
func (b *Backend) ToVM(rec *VMRecord) *types.VM {

hypervisor/stop.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ func (b *Backend) DeleteAll(ctx context.Context, refs []string, force bool, stop
6565
if err != nil {
6666
return nil, err
6767
}
68+
// One /proc scan up-front; per-VM orphan check filters this cache instead of re-walking /proc N times.
69+
procScan, scanErr := utils.ScanProcsByBinary(b.Conf.BinaryName())
70+
if scanErr != nil {
71+
return nil, fmt.Errorf("refuse delete: /proc scan errored: %w (resolve the host issue and retry)", scanErr)
72+
}
6873
return b.ForEachVM(ctx, ids, "Delete", func(ctx context.Context, id string) error {
6974
rec, loadErr := b.LoadRecord(ctx, id)
7075
if loadErr != nil {
@@ -91,12 +96,7 @@ func (b *Backend) DeleteAll(ctx context.Context, refs []string, force bool, stop
9196
}
9297
return fmt.Errorf("refuse delete: api socket %s still responsive (suspected orphan vmm; kill the vmm process then retry)", sockPath)
9398
}
94-
// Catches workers/siblings the pidfile-based stop didn't see; fail-closed on scan error so we never wipe rundir while VMM state is unknown.
95-
scanned, scanErr := utils.FindVMMByCmdline(b.Conf.BinaryName(), sockPath)
96-
if scanErr != nil {
97-
return fmt.Errorf("refuse delete: VM %s /proc scan errored: %w (resolve the host issue and retry)", id, scanErr)
98-
}
99-
for _, pid := range scanned {
99+
for _, pid := range procScan.Find(sockPath) {
100100
if termErr := utils.TerminateProcess(ctx, pid, b.Conf.BinaryName(), sockPath, b.Conf.TerminateGracePeriod()); termErr != nil {
101101
return fmt.Errorf("terminate orphan VMM pid=%d for VM %s: %w", pid, id, termErr)
102102
}

utils/process_linux.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,69 @@ import (
1313
"strings"
1414
)
1515

16-
// FindVMMByCmdline returns pids whose argv[0] basename matches binaryName and args contain expectArg, sorted numerically; fails closed on non-ENOENT cmdline read errors.
17-
func FindVMMByCmdline(binaryName, expectArg string) ([]int, error) {
16+
// ProcScan caches /proc cmdlines for one binaryName. Batch callers scan once then Find per id, replacing N /proc walks with one.
17+
type ProcScan []procEntry
18+
19+
type procEntry struct {
20+
pid int
21+
cmdline string
22+
}
23+
24+
// ScanProcsByBinary walks /proc once, capturing argv[0]-basename matches. ENOENT (process exited mid-scan) is skipped; other read errors fail closed.
25+
func ScanProcsByBinary(binaryName string) (ProcScan, error) {
1826
entries, err := os.ReadDir("/proc")
1927
if err != nil {
2028
return nil, err
2129
}
22-
var pids []int
30+
var scan ProcScan
2331
var firstErr error
2432
for _, e := range entries {
2533
pid, atoiErr := strconv.Atoi(e.Name())
2634
if atoiErr != nil || pid <= 0 {
2735
continue
2836
}
29-
matched, readErr := verifyProcessCmdline(pid, binaryName, expectArg)
37+
data, readErr := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
3038
if readErr != nil {
31-
// ENOENT = process exited mid-scan, safe to skip; everything else means we can't tell, so callers must fail closed.
3239
if !errors.Is(readErr, fs.ErrNotExist) && firstErr == nil {
3340
firstErr = fmt.Errorf("read /proc/%d/cmdline: %w", pid, readErr)
3441
}
3542
continue
3643
}
37-
if matched {
38-
pids = append(pids, pid)
44+
argv0, _, _ := strings.Cut(string(data), "\x00")
45+
if filepath.Base(argv0) != binaryName {
46+
continue
3947
}
48+
scan = append(scan, procEntry{pid: pid, cmdline: string(data)})
4049
}
4150
if firstErr != nil {
4251
return nil, firstErr
4352
}
53+
return scan, nil
54+
}
55+
56+
// Find returns the cached pids whose cmdline contains expectArg, sorted numerically; empty expectArg matches all.
57+
func (s ProcScan) Find(expectArg string) []int {
58+
var pids []int
59+
for _, e := range s {
60+
_, rest, _ := strings.Cut(e.cmdline, "\x00")
61+
if expectArg == "" || strings.Contains(rest, expectArg) {
62+
pids = append(pids, e.pid)
63+
}
64+
}
4465
slices.Sort(pids)
45-
return pids, nil
66+
return pids
67+
}
68+
69+
// FindVMMByCmdline is the one-shot equivalent of ScanProcsByBinary().Find(); batch callers should use ScanProcsByBinary directly to share one /proc walk.
70+
func FindVMMByCmdline(binaryName, expectArg string) ([]int, error) {
71+
scan, err := ScanProcsByBinary(binaryName)
72+
if err != nil {
73+
return nil, err
74+
}
75+
return scan.Find(expectArg), nil
4676
}
4777

48-
// Match argv[0] basename strictly + expectArg substring on the rest so "bash -c 'cloud-hypervisor ...'" can't impersonate the VMM; error surfaces cmdline-read failures so callers distinguish transient ENOENT from real issues.
78+
// Match argv[0] basename strictly + expectArg substring so "bash -c 'cloud-hypervisor ...'" can't impersonate the VMM.
4979
func verifyProcessCmdline(pid int, binaryName, expectArg string) (bool, error) {
5080
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
5181
if err != nil {

utils/process_other.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import "errors"
66

77
var errVerifyUnsupported = errors.New("verifyProcessCmdline: unsupported on this OS")
88

9+
type ProcScan struct{}
10+
11+
func ScanProcsByBinary(_ string) (ProcScan, error) { return ProcScan{}, nil }
12+
13+
func (ProcScan) Find(_ string) []int { return nil }
14+
915
func FindVMMByCmdline(_, _ string) ([]int, error) {
1016
return nil, nil
1117
}

0 commit comments

Comments
 (0)