Skip to content

[Linux] process_iter(attrs=...) spends ~75% of its time building Python objects, not doing I/O #2956

Description

@Dipet

Summary

  • OS: Linux (Ubuntu 22.04, x86_64, 128 cores, ~15 600 processes)
  • Type: performance

Description

On a busy machine process_iter(attrs=[...]) is slow, and I wanted to find out where the time actually goes before proposing anything. The answer surprised me, so I am opening this before writing a real patch.

Setup: psutil master, Python 3.12, ~15 600 processes, 12 attributes (the set Glances asks for):
cpu_percent, cpu_times, memory_percent, name, status, num_threads, io_counters, memory_info, nice, pid, gids, cpu_num.

Where the time goes

In steady state psutil opens 4 files per process — stat, status, statm, io — and all four are genuinely needed for those attributes. There is no redundant read to remove. (On the first process_iter() call stat is read twice per process, because Process.__init__ calls create_time() outside the oneshot() window, but that is a one-off.)

So I measured the ceiling of "make I/O free": I preloaded every one of those files into a dict and monkeypatched open_binary/bcat to serve from memory, so the measured run does literally zero file I/O.

psutil as-is                       min=3036.6ms   1.00x
psutil with ALL file I/O removed   min=2269.6ms   1.34x

Even with I/O costing nothing, it is only 1.34x. Roughly three quarters of the time is the per-process Python machinery: creating a Process, as_dict(), the wrap_exceptions / memoize_when_activated decorator chain, and the namedtuple constructors.

That rules out the obvious fix (reading the files more cheaply, in C or otherwise) — it cannot pay off. The only way to a large win is to not build the per-process Python objects at all.

Proof of concept

I put a draft together to check that the ceiling is real:
https://github.com/Dipet/psutil/tree/perf/batch-process-scan-draft

It adds psutil/arch/linux/proc_scan.c, which reads stat/status/statm/io for every pid in one call with the GIL released, and a batch_scan() in _pslinux.py that process_iter() uses to prefill Process._prefetch. It falls back to the existing per-process path whenever the requested attrs are not fully covered, or for a process that appeared after the scan. Because the scan holds no GIL, it splits across a few threads (min(4, num_pids // 4000)).

process_iter(12 attrs), 15 623 procs, 4 interleaved pairs
  per-process path  median=3407.0ms
  batch path        median=1198.6ms
  speedup: 2.84x

Of the remaining 1.2s, 59% is the C scan and 41% is still Python. Two obvious leftovers: posix.listdir costs 0.19s because pids() walks /proc again after the scan already enumerated it, and 0.17s goes into namedtuple constructors.

I compared the resulting dicts against the per-process path for every process: the key sets are identical and the only differing values are ones that genuinely move between two samples (cpu_times off by one tick, cpu_num, kworkers renaming themselves). As a control, two consecutive process_iter() runs of unmodified psutil disagree with each other on those same fields at the same rate or higher.

The part I cannot decide myself

241 tests pass, but 8 in TestProcessIter fail, and they are not sloppiness on my side — they are a genuine collision:

test_attrs, test_emulate_access_denied, test_emulate_nsp,
test_prefetch_ad_value, test_prefetch_ad_value_is_not_none,
test_prefetch_derived_methods, test_prefetch_derived_username,
test_prefetch_memory_percent

They mock psutil._psplatform.Process.<method> to raise AccessDenied and assert that process_iter() substitutes ad_value. A batch path does not call those methods, so the mocks cannot take effect. Making them pass would mean routing through the platform methods again, which is exactly the cost being removed.

There is also a real gap underneath the test artifact: as_dict(ad_value=...) substitutes on AccessDenied, and the batch has no such layer. Today it returns None for an unreadable /proc/<pid>/io, which coincides with the default ad_value but is wrong for any other value. That part I can fix — the scan already distinguishes "could not read" from "read".

So before I invest in a mergeable patch: is a batch path that bypasses _psplatform.Process something you would consider at all, given it needs TestProcessIter to be reworked? If not, I would rather know now. If yes, I am happy to do the work — including whatever shape you prefer for the API (internal-only, opt-in, or transparent as in the draft).

Happy to provide the profiles or rerun anything on this machine.

Metadata

Metadata

Assignees

No one assigned

    Labels

    linuxplatform : Linux specificperformanceanything that speeds up execution of any component (core, tests, ci scripts, build, ...)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions