Skip to content

perf(process): build snapshot map once on Windows to fix O(N²) process listing#2062

Open
HarshalPatel1972 wants to merge 3 commits into
shirou:masterfrom
HarshalPatel1972:fix/windows-process-snap-perf
Open

perf(process): build snapshot map once on Windows to fix O(N²) process listing#2062
HarshalPatel1972 wants to merge 3 commits into
shirou:masterfrom
HarshalPatel1972:fix/windows-process-snap-perf

Conversation

@HarshalPatel1972
Copy link
Copy Markdown

@HarshalPatel1972 HarshalPatel1972 commented Mar 22, 2026

Fixes #818

Problem

On Windows, ProcessesWithContext calls getFromSnapProcess once per process to retrieve name, PPID, and thread count. getFromSnapProcess internally iterates through the entire CreateToolhelp32Snapshot result each time — making the overall complexity O(N²). On a machine with ~320 processes this results in ~13 seconds and 30–40% CPU usage, vs ~100ms on Linux.

Fix

Added buildSnapProcessMap() which calls CreateToolhelp32Snapshot exactly once and builds a map[uint32]windows.ProcessEntry32 keyed by PID. ProcessesWithContext now calls this once at startup and does O(1) per-process lookups for name, PPID, and thread count.

getFromSnapProcess is left intact for single-process query paths — no API changes.

Benchmark

Before After
320 processes — Name + Ppid + NumThreads ~13,130ms ~68ms
Improvement ~99.5% faster

Tested on Windows 11 with ~320 running processes.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes Windows process enumeration in gopsutil/process by avoiding repeated Toolhelp snapshot scans when collecting per-process metadata (name, PPID, thread count), addressing the O(N²) behavior reported in #818.

Changes:

  • Adds snapshot pre-scan (buildSnapProcessMap) to build a PID→ProcessEntry32 map in one pass.
  • Populates per-Process caches (name, parent/PPID, numThreads) during ProcessesWithContext.
  • Adds simple caching fast-paths in NameWithContext and NumThreadsWithContext.
Comments suppressed due to low confidence (1)

process/process_windows.go:339

  • NameWithContext now checks p.name first, but when p.name is empty it computes the name from ExeWithContext and returns it without storing it back into p.name. That means processes created via NewProcess* still pay the full cost on every Name* call, and the new cache is only effective when ProcessesWithContext pre-populates the field. Consider assigning the computed base name to p.name before returning (and optionally caching the special-case PID 0/4 names too).
	if p.name != "" {
		return p.name, nil
	}
	if p.Pid == 0 {
		return "System Idle Process", nil
	}
	if p.Pid == 4 {
		return "System", nil
	}

	exe, err := p.ExeWithContext(ctx)
	if err != nil {
		return "", fmt.Errorf("could not get Name: %w", err)
	}

	return filepath.Base(exe), nil

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +925 to +930
for {
snapMap[pe32.ProcessID] = pe32
if err = windows.Process32Next(snap, &pe32); err != nil {
break
}
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildSnapProcessMap breaks out of the loop on any Process32Next error and returns the map, which can silently return an incomplete snapshot if Process32Next fails for reasons other than "no more files". Please explicitly treat windows.ERROR_NO_MORE_FILES (or the appropriate sentinel) as the normal termination condition, and return the error for any other failure.

Copilot uses AI. Check for mistakes.
Comment thread process/process_windows.go Outdated

snapMap, err := buildSnapProcessMap()
if err != nil {
return nil, err
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On error from buildSnapProcessMap, ProcessesWithContext returns a nil slice (return nil, err), while other error paths in this function (and other OS implementations) return the already-initialized out slice. For API consistency and to avoid surprising nil slices for callers that don’t nil-check, consider returning out here as well (and optionally wrapping the error with context like the earlier PidsWithContext failure).

Suggested change
return nil, err
return out, fmt.Errorf("could not build process snapshot %w", err)

Copilot uses AI. Check for mistakes.
@HarshalPatel1972 HarshalPatel1972 changed the title perf(process): build snapshot map once on Windows to fix O(N²) process listing (#818) perf(process): build snapshot map once on Windows to fix O(N²) process listing Mar 22, 2026
@HarshalPatel1972
Copy link
Copy Markdown
Author

@copilot AI I have addressed all 3 pieces of review feedback:

  1. \�uildSnapProcessMap\ now explicitly checks for \windows.ERROR_NO_MORE_FILES\ (via \�rrors.Is) and bubbles up other errors to avoid silent partial maps.
  2. \ProcessesWithContext\ error path now returns the \out\ slice and wraps the error for consistency.
  3. \NameWithContext\ now properly writes back the computed name to \p.name\ so cached checks can succeed for dynamically opened processes.

Also ran \golangci-lint\ to resolve the subsequent warnings.

Copy link
Copy Markdown
Owner

@shirou shirou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix for a real performance problem. A few items to address:

  1. In NameWithContext, the p.name != "" early return should be placed after the p.Pid == 0 check, not before — otherwise PID 0 may return the snapshot's ExeFile name instead of the hardcoded "System Idle Process".
  2. The numThreads != 0 cache guard is incorrect — 0 is a valid thread count. Either remove the early return (the snapshot-populated value will flow through the existing code), or use a separate bool to track whether it's been cached.
  3. Minor: Consider adding a brief comment in ProcessesWithContext noting that the snapshot and PID enumeration are separate calls
    and may not be perfectly consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Combine Process .Ppid(), .Name(), NumThreads() for Windows

3 participants