Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix num cpu #1518

Closed
wants to merge 5 commits into from
Closed

Fix num cpu #1518

wants to merge 5 commits into from

Conversation

jdamato-fsly
Copy link
Contributor

runtime.NumCPU() returns the number of CPUs that the process can run
on. This number does not necessarily correlate to CPU ids if the
affinity mask of the process is set.

This change maintains the current behavior as default, but also allows
the user to specify a range of CPUids to use instead.

The CPU id is stored as the value of a map keyed on the profiler
object's address.

Copy link
Member

@SuperQ SuperQ left a comment

Choose a reason for hiding this comment

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

Interesting, thanks, it seems mostly reasonable.

Please sign off your commits with git commit -s --amend. Also, it would be good to add an [ENHANCEMENT] entry to the changelog.

collector/perf_linux.go Outdated Show resolved Hide resolved
`runtime.NumCPU()` returns the number of CPUs that the process can run
on. This number does not necessarily correlate to CPU ids if the
affinity mask of the process is set.

This change maintains the current behavior as default, but also allows
the user to specify a range of CPUids to use instead.

The CPU id is stored as the value of a map keyed on the profiler
object's address.

Signed-off-by: Joe Damato <[email protected]>
@jdamato-fsly
Copy link
Contributor Author

Thanks for the review @SuperQ. Made some changes as you've suggested. Let me know how this looks.

@hodgesds
Copy link
Contributor

This is a nice config option!

collector/perf_linux.go Outdated Show resolved Hide resolved
@jdamato-fsly
Copy link
Contributor Author

I think this addresses your feedback @discordianfish and @SuperQ.

Was wondering if there might be anyone who would be interested in testing this just to double check that this is working for them? cc @hodgesds

Signed-off-by: Joe Damato <[email protected]>
for cpu, profiler := range c.perfSwProfilers {
cpuStr := fmt.Sprintf("%d", cpu)
for _, profiler := range c.perfSwProfilers {
cpuid := c.swProfilerCpuMap[&profiler]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, I'm new to go and this appears to be a bug.

&profiler here refers to the address of the profiler object itself and not the address of the perf.SoftwareProfiler that was inserted into c.swProfilerCpuMap.

This leads to the incorrect CPU id being retrieved. I'm not really sure how to deal with this as I know basically 0 golang (sorry). Would love any suggestions you folks have on the best way to fix this.

@jdamato-fsly
Copy link
Contributor Author

I don't really know anything about golang but this commit I just pushed: 573cf02 I think fixes the address of the object issue I mentioned here: #1518 (comment)

perfSwProfilers map[int]perf.SoftwareProfiler
perfCacheProfilers map[int]perf.CacheProfiler
desc map[string]*prometheus.Desc
hwProfilerCpuMap map[*perf.HardwareProfiler]int
Copy link
Contributor

Choose a reason for hiding this comment

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

I wouldn't expect to keep a map of pointers to interfaces. For example NewSoftwareProfiler returns an interface that can be used directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yea but I think you need them otherwise you can't generate a map of pointers to CPU ids.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

confirmed that this change (pointers to interfaces) works correctly, as the address pointed to can be used as a key for the CPU ID map. the code without this change does not work as mentioned here: #1518 (comment)

I think keeping this is correct -- I am using this in a lab setting and am getting the expected results now.

README.md Outdated Show resolved Hide resolved
@hodgesds
Copy link
Contributor

It might be nice to make the flag match the -c option of taskset.

@jdamato-fsly
Copy link
Contributor Author

It might be nice to make the flag match the -c option of taskset.

That sounds like a lot of work. Happy to turn that over to anyone else who'd like to jump in on this, though.

@hodgesds
Copy link
Contributor

hodgesds commented Oct 17, 2019

It might be nice to make the flag match the -c option of taskset.

That sounds like a lot of work. Happy to turn that over to anyone else who'd like to jump in on this, though.

I think this roughly does what you want, let me know your thoughts.

@jdamato-fsly
Copy link
Contributor Author

I think this roughly does what you want, let me know your thoughts.

I left a few comments on the code over there. As mentioned above, I'm not a golang programmer so I'm probably not a good person to ask for a code review 😅

FWIW I am using the code I wrote in this branch in a lab setting and it is working as expected (as mentioned here: #1518 (comment)).

In my use case: I run node_exporter on a small set of CPUs and want to collect perf stats from all CPUs; so having more advanced CPU set options isn't something that would apply directly to my use case.

It's worth noting that any use of runtime.numCPU where the affinity list is not set to all CPUs will be incorrect -- the stats returned will be mislabeled with the wrong CPU ID. It may be worth noting that in the docs.

@hodgesds
Copy link
Contributor

I see the bug now, here's a small example that shows it's because a interface isn't assignable, the index expression isn't valid by taking the address of the interface it works. I think this is fine then, would you mind porting over the flag parsing code so that strides work properly as well?

@jdamato-fsly
Copy link
Contributor Author

I see the bug now, here's a small example that shows it's because a interface isn't assignable, the index expression isn't valid by taking the address of the interface it works. I think this is fine then, would you mind porting over the flag parsing code so that strides work properly as well?

Thanks for the detailed explanation. Sure, I can copy over your flag parsing code shortly.

@jdamato-fsly
Copy link
Contributor Author

FWIW, worth noting that using the code I wrote in this branch the HardwareProfiler which gathers unix.PERF_COUNT_HW_CACHE_REFERENCES and unix.PERF_COUNT_HW_CACHE_MISSES appears to be returning incorrect results. I am not sure if this is a bug in node_exporter or perf-utils, though.

@hodgesds
Copy link
Contributor

FWIW, worth noting that using the code I wrote in this branch the HardwareProfiler which gathers unix.PERF_COUNT_HW_CACHE_REFERENCES and unix.PERF_COUNT_HW_CACHE_MISSES appears to be returning incorrect results. I am not sure if this is a bug in node_exporter or perf-utils, though.

What are you observing? PERF_COUNT_HW_CACHE_REFERENCES is very hardware dependent. From the perf_event_open man page:

PERF_COUNT_HW_CACHE_REFERENCES
Cache accesses. Usually this indicates Last Level
Cache accesses but this may vary depending on your
CPU. This may include prefetches and coherency
messages; again this depends on the design of your
CPU.

Note that configuration for those events also includes some defaults that are pretty opinionated. For more configuration and moved much of that functionality into perf_exporter.

@jdamato-fsly
Copy link
Contributor Author

jdamato-fsly commented Oct 18, 2019

What are you observing? PERF_COUNT_HW_CACHE_REFERENCES is very hardware dependent. From the perf_event_open man page:

On kernel 4.19 with a sandybridge CPU issuing curl against node_exporter's HTTP server shows that some set of CPUs cache refs values never change (which is impossible on a system with this much load on all CPUs) -- HOWEVER -- when I use perf stat to obtain the same metric via the command line, I can clearly see the stat values changing on each run of perf stat.

For example, on my system node_exporter returns values which are unchanged for CPU 23 on every single curl request against node_exporter's HTTP server that I issued over a period of several minutes.

However, running perf stat -e cache-references,cache-misses -C 23 even for very sort periods of time shows that both values change very quickly (as expected).

This leads me to believe that either node_exporter or perf-utils are returning stale values for some currently unknown reason for certain CPUs, as the command line perf stat -e cache-references,cache-misses -C $CPU returns accurate values.

I'm not sure if this bug is related to my code or something else.

EDIT : I should note that some CPUs seem to have correct values, but others have values which never seem to change from node_exporter.

@hodgesds
Copy link
Contributor

I've tested your branch on two kernels (4.14.78 and 5.2.5) on a E3-1505M v5 and Ryzen 2600 processors and both seem to be working as expected. Do you have access to any other architectures for testing?

@jdamato-fsly
Copy link
Contributor Author

It appears to work on kernel 4.9 on this machine, but not on kernel 4.19. Could be some weird kernel regression. Any chance you could test on 4.19?

@hodgesds
Copy link
Contributor

I run custom kernels for everything so my config will likely be different. Can you post the output of:

cat /boot/config-$(uname -r) | grep -i perf
# or 
zcat /proc/config.gz | grep -i perf

@jdamato-fsly
Copy link
Contributor Author

cat /boot/config-$(uname -r) | grep -i perf
CONFIG_CGROUP_PERF=y
CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE=y
CONFIG_HAVE_PERF_EVENTS=y
# Kernel Performance Events And Counters
CONFIG_PERF_EVENTS=y
# CONFIG_DEBUG_PERF_USE_VMALLOC is not set
# Performance monitoring
CONFIG_PERF_EVENTS_INTEL_UNCORE=m
CONFIG_PERF_EVENTS_INTEL_RAPL=m
CONFIG_PERF_EVENTS_INTEL_CSTATE=m
# CONFIG_PERF_EVENTS_AMD_POWER is not set
CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCE=y
CONFIG_CPU_FREQ_GOV_PERFORMANCE=y
CONFIG_PCIEASPM_PERFORMANCE=y
CONFIG_HAVE_PERF_EVENTS_NMI=y
CONFIG_HAVE_HARDLOCKUP_DETECTOR_PERF=y
CONFIG_HAVE_PERF_REGS=y
CONFIG_HAVE_PERF_USER_STACK_DUMP=y
# Performance monitor support
CONFIG_RCU_PERF_TEST=m

@hodgesds
Copy link
Contributor

I've tested this on a few machines with a similar config and it seems to be working as expected. I think this is good to go, however you may want to do some more digging (strace the perf_event_open calls to make sure they look sane).

kingpin "gopkg.in/alecthomas/kingpin.v2"
"runtime"
"strconv"
"strings"
Copy link
Contributor

Choose a reason for hiding this comment

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

Normally we group all the built-in imports at the top, can you re-format these using goimports?

go get golang.org/x/tools/cmd/goimports
goimports -w ./collector/perf_linux.go


ncpus, err = strconv.Atoi(cpuRange[1])
if err != nil {
ncpus = runtime.NumCPU() - 1
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should fallback to the default when a user provided a non-integer. This should fail instead.

cacheProfilerCpuMap: map[*perf.CacheProfiler]int{},
}

start := 0
Copy link
Member

Choose a reason for hiding this comment

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

Use a var block here:

var (
  start = 0
  ncpus = 0
  ..
)

// Use -1 to profile all processes on the CPU, see:
// man perf_event_open
collector.perfHwProfilers[i] = perf.NewHardwareProfiler(-1, i)
if err := collector.perfHwProfilers[i].Start(); err != nil {
p := perf.NewHardwareProfiler(-1, i)
Copy link
Member

Choose a reason for hiding this comment

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

Let's rename the p's here:

  • ph = hardware profiler
  • ps = software profiler
  • pc = cache profiler

return collector, err
} else {
Copy link
Member

Choose a reason for hiding this comment

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

No need for the else here, if err != nil it returns anyway.

Copy link
Member

Choose a reason for hiding this comment

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

(same below)

@hodgesds hodgesds mentioned this pull request Nov 30, 2019
@SuperQ
Copy link
Member

SuperQ commented Feb 18, 2020

Should we close this in favor of #1561?

@SuperQ
Copy link
Member

SuperQ commented Feb 20, 2020

Superseded by #1561.

@SuperQ SuperQ closed this Feb 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants