mirror of
https://github.com/samsonjs/bin.git
synced 2026-06-24 04:49:05 +00:00
Use physical footprint instead of RSS in mem-report
Summing ps RSS across processes overcounts shared memory: shared read-only pages like the dyld shared cache and frameworks get charged to every process that maps them. With 300+ iOS simulator daemons all sharing those pages, the Simulator group reported ~34 GB when its real cost is ~5 GB. Switch the per-process metric to physical footprint (top's MEM column, what Activity Monitor reports), joined with ps on PID for full command names since top truncates them. Shared pages are now charged once and the grouped sums stay under the system app-memory total.
This commit is contained in:
parent
ed252fb3cd
commit
fdb8415b21
1 changed files with 49 additions and 17 deletions
66
mem-report
66
mem-report
|
|
@ -59,18 +59,50 @@ system_summary = {
|
|||
free_memory: to_mb.call((vm[:free] || 0) + (vm[:speculative] || 0)).round(0),
|
||||
}
|
||||
|
||||
# --- Per-process memory (via ps) ---
|
||||
# --- Per-process memory (physical footprint via top, joined with ps) ---
|
||||
#
|
||||
# We use physical footprint (top's MEM column) rather than ps RSS. RSS counts
|
||||
# every resident page, including shared read-only pages like the dyld shared
|
||||
# cache and frameworks — so summing it across many processes that share those
|
||||
# pages (e.g. 300+ iOS simulator daemons) overcounts wildly. Footprint charges
|
||||
# shared pages once and matches what Activity Monitor reports. top truncates
|
||||
# command names though, so we pull full paths from ps and join on PID.
|
||||
|
||||
ps_output = run("ps -axo pid=,user=,rss=,comm=")
|
||||
processes = ps_output.each_line.filter_map do |line|
|
||||
parts = line.strip.split(/\s+/, 4)
|
||||
next if parts.size < 4
|
||||
def parse_mem_mb(str)
|
||||
m = str.match(/\A([\d.]+)([KMGB]?)/)
|
||||
return nil unless m
|
||||
|
||||
pid, user, rss_kb, comm = parts
|
||||
rss_mb = rss_kb.to_f / 1024.0
|
||||
next if rss_mb < 0.1
|
||||
val = m[1].to_f
|
||||
case m[2]
|
||||
when "G" then val * 1024
|
||||
when "M" then val
|
||||
when "K" then val / 1024.0
|
||||
else val / 1024.0 / 1024.0 # bytes
|
||||
end
|
||||
end
|
||||
|
||||
{ pid: pid.to_i, user:, rss_mb:, command: comm }
|
||||
footprints = {}
|
||||
run("top -l 1 -stats pid,mem").each_line do |line|
|
||||
parts = line.strip.split(/\s+/)
|
||||
next unless parts.size == 2 && parts[0].match?(/\A\d+\z/)
|
||||
|
||||
mb = parse_mem_mb(parts[1])
|
||||
footprints[parts[0].to_i] = mb if mb
|
||||
end
|
||||
|
||||
commands = {}
|
||||
run("ps -axo pid=,comm=").each_line do |line|
|
||||
pid, comm = line.strip.split(/\s+/, 2)
|
||||
commands[pid.to_i] = comm if pid && comm
|
||||
end
|
||||
|
||||
processes = footprints.filter_map do |pid, mem_mb|
|
||||
next if mem_mb < 0.1
|
||||
|
||||
command = commands[pid]
|
||||
next unless command
|
||||
|
||||
{ pid:, mem_mb:, command: }
|
||||
end
|
||||
|
||||
# --- Grouped memory ---
|
||||
|
|
@ -100,14 +132,14 @@ def group_name(command)
|
|||
File.basename(command).sub(/\.\w+$/, "")
|
||||
end
|
||||
|
||||
groups = Hash.new { |h, k| h[k] = { rss_mb: 0.0, count: 0 } }
|
||||
groups = Hash.new { |h, k| h[k] = { mem_mb: 0.0, count: 0 } }
|
||||
processes.each do |proc|
|
||||
name = group_name(proc[:command])
|
||||
groups[name][:rss_mb] += proc[:rss_mb]
|
||||
groups[name][:mem_mb] += proc[:mem_mb]
|
||||
groups[name][:count] += 1
|
||||
end
|
||||
|
||||
top_groups = groups.sort_by { -it[1][:rss_mb] }.first(options[:group_top])
|
||||
top_groups = groups.sort_by { -it[1][:mem_mb] }.first(options[:group_top])
|
||||
|
||||
# --- Output ---
|
||||
|
||||
|
|
@ -115,9 +147,9 @@ if options[:json]
|
|||
data = {
|
||||
system: system_summary.transform_values(&:to_i),
|
||||
top_groups: top_groups.map do |name, info|
|
||||
{ group: name, rss_mb: info[:rss_mb].round(1), process_count: info[:count] }
|
||||
{ group: name, footprint_mb: info[:mem_mb].round(1), process_count: info[:count] }
|
||||
end,
|
||||
note: "RSS values and group sums can overcount shared memory (shared libraries, frameworks, mmap regions).",
|
||||
note: "Physical footprint (matches Activity Monitor); shared pages are charged once.",
|
||||
}
|
||||
puts JSON.pretty_generate(data)
|
||||
exit
|
||||
|
|
@ -139,9 +171,9 @@ puts
|
|||
|
||||
puts "#{bold}=== Top #{options[:group_top]} Grouped Programs ==#{reset}"
|
||||
puts
|
||||
puts " #{dim}%-28s %10s %8s#{reset}" % ["GROUP", "RSS", "PROCS"]
|
||||
puts " #{dim}%-28s %10s %8s#{reset}" % ["GROUP", "MEM", "PROCS"]
|
||||
top_groups.each do |name, info|
|
||||
puts " %-28s %10s %8d" % [name, human_bytes(info[:rss_mb]), info[:count]]
|
||||
puts " %-28s %10s %8d" % [name, human_bytes(info[:mem_mb]), info[:count]]
|
||||
end
|
||||
puts
|
||||
puts "#{dim}Note: RSS and group sums can overcount shared memory (shared libs, frameworks, mmap).#{reset}"
|
||||
puts "#{dim}Note: physical footprint (matches Activity Monitor); shared pages charged once.#{reset}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue