perfkitbenchmarker/linux_benchmarks/stress_ng_benchmark.py (444 lines of code) (raw):
# Copyright 2019 PerfKitBenchmarker Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Runs stress-ng.
From the stress-ng ubuntu documentation:
stress-ng will stress test a computer system in various selectable ways.
It was designed to exercise various physical subsystems of a computer as
well as the various operating system kernel interfaces. stress-ng also has
a wide range of CPU specific stress tests that exercise floating point,
integer, bit manipulation and control flow.
stress-ng manpage:
http://manpages.ubuntu.com/manpages/xenial/man1/stress-ng.1.html
"""
import logging
from absl import flags
import numpy
from perfkitbenchmarker import configs
from perfkitbenchmarker import sample
FLAGS = flags.FLAGS
BENCHMARK_NAME = 'stress_ng'
BENCHMARK_CONFIG = """
stress_ng:
description: Runs stress-ng
vm_groups:
default:
vm_spec: *default_dual_core
disk_spec: *default_50_gb
"""
VALID_CPU_METHODS = frozenset({
'all',
'ackermann',
'bitops',
'callfunc',
'cdouble',
'cfloat',
'clongdouble',
'correlate',
'crc16',
'decimal32',
'decimal64',
'decimal128',
'dither',
'djb2a',
'double',
'euler',
'explog',
'fft',
'fibonacci',
'float',
'fnv1a',
'gamma',
'gcd',
'gray',
'hamming',
'hanoi',
'hyperbolic',
'idct',
'int128',
'int64',
'int32',
'int16',
'int8',
'int128float',
'int128double',
'int128longdouble',
'int128decimal32',
'int128decimal64',
'int128decimal128',
'int64float',
'int64double',
'int64longdouble',
'int32float',
'int32double',
'int32longdouble',
'jenkin',
'jmp',
'ln2',
'longdouble',
'loop',
'matrixprod',
'nsqrt',
'omega',
'parity',
'phi',
'pi',
'pjw',
'prime',
'psi',
'queens',
'rand',
'rand48',
'rgb',
'sdbm',
'sieve',
'sqrt',
'trig',
'union',
'zeta',
})
VALID_STRESSORS = frozenset({
'affinity',
'af-alg',
'aio',
'aio-linux',
'apparmor',
'bigheap',
'brk',
'bsearch',
'cache',
'chdir',
'chmod',
'clock',
'clone',
'context',
'cpu',
'cpu-online',
'crypt',
'daemon',
'dentry',
'dir',
'dup',
'epoll',
'eventfd',
'exec',
'fallocate',
'fault',
'fcntl',
'fiemap',
'fifo',
'filename',
'flock',
'fork',
'fp-error',
'fstat',
'futex',
'get',
'getrandom',
'getdent',
'handle',
'hdd',
'heapsort',
'hsearch',
'icache',
'iosync',
'inotify',
'itimer',
'kcmp',
'key',
'kill',
'klog',
'lease',
'link',
'lockbus',
'lockf',
'longjmp',
'lsearch',
'malloc',
'matrix',
'membarrier',
'memcpy',
'memfd',
'mergesort',
'mincore',
'mknod',
'mlock',
'mmap',
'mmapfork',
'mmapmany',
'mremap',
'msg',
'mq',
'nice',
'null',
'numa',
'oom-pipe',
'open',
'personality',
'pipe',
'poll',
'procfs',
'pthread',
'ptrace',
'qsort',
'quota',
'rdrand',
'readahead',
'remap-file-pages',
'rename',
'rlimit',
'seccomp',
'seek',
'sem-posix',
'sem-sysv',
'shm-posix',
'shm-sysv',
'sendfile',
'sigfd',
'sigfpe',
'sigpending',
'sigq',
'sigsegv',
'sigsuspend',
'sleep',
'socket',
'socket-fd',
'socket-pair',
'spawn',
'splice',
'stack',
'str',
'stream',
'switch',
'symlink',
'sync-file',
'sysinfo',
'sysfs',
'tee',
'timer',
'timerfd',
'tsc',
'tsearch',
'udp',
'udp-flood',
'unshare',
'urandom',
'userfaultfd',
'utime',
'vecmath',
'vfork',
'vm',
'vm-rw',
'vm-splice',
'wait',
'wcs',
'xattr',
'yield',
'zero',
'zlib',
'zombie',
})
CPU_SUITE = frozenset({
'af-alg',
'bsearch',
'context',
'cpu',
'cpu-online',
'crypt',
'fp-error',
'getrandom',
'heapsort',
'hsearch',
'longjmp',
'lsearch',
'matrix',
'mergesort',
'numa',
'qsort',
'rdrand',
'str',
'stream',
'tsc',
'tsearch',
'vecmath',
'wcs',
'zlib',
})
CPU_CACHE_SUITE = frozenset({
'bsearch',
'cache',
'heapsort',
'hsearch',
'icache',
'lockbus',
'lsearch',
'malloc',
'matrix',
'membarrier',
'memcpy',
'mergesort',
'qsort',
'str',
'stream',
'tsearch',
'vecmath',
'wcs',
'zlib',
})
MEMORY_SUITE = frozenset({
'bsearch',
'context',
'heapsort',
'hsearch',
'lockbus',
'lsearch',
'malloc',
'matrix',
'membarrier',
'memcpy',
'memfd',
'mergesort',
'mincore',
'null',
'numa',
'oom-pipe',
'pipe',
'qsort',
'stack',
'str',
'stream',
'tsearch',
'vm',
'vm-rw',
'wcs',
'zero',
'zlib',
})
# Run the stressors that are each part of all of the compute related stress-ng
# classes: cpu, cpu-cache, and memory.
DEFAULT_STRESSORS = sorted(
CPU_SUITE.intersection(CPU_CACHE_SUITE).intersection(MEMORY_SUITE)
)
flags.DEFINE_integer(
'stress_ng_duration', 10, 'Number of seconds to run the test.'
)
flags.DEFINE_boolean(
'stress_ng_calc_geomean', True, 'Whether to calculate geomean or not.'
)
flags.DEFINE_list(
'stress_ng_custom_stressors',
DEFAULT_STRESSORS,
'List of stressors to run against. Default combines cpu,'
'cpu-cache, and memory suites',
)
flags.DEFINE_list(
'stress_ng_cpu_methods',
[],
'List of cpu methods to run with. By default none are ran.',
)
ALL_WORKLOADS = ['small', 'medium', 'large']
flags.DEFINE_list(
'stress_ng_thread_workloads',
['large'],
'List of threads sizes to run against. Options are'
'small (1 thread total), medium (1 thread per 2 cpus), and '
'large (1 thread per cpu).',
)
flags.register_validator(
'stress_ng_thread_workloads',
lambda workloads: workloads and set(workloads).issubset(ALL_WORKLOADS),
)
def _GeoMeanOverflow(iterable):
"""Returns the geometric mean.
See https://en.wikipedia.org/wiki/Geometric_mean#Relationship_with_logarithms
Args:
iterable: a list of positive floats to take the geometric mean of.
Returns: The geometric mean of the list.
"""
a = numpy.log(iterable)
return numpy.exp(a.sum() / len(a))
def StressngCustomStressorsValidator(stressors):
"""Returns whether or not the list of custom stressors is valid."""
return VALID_STRESSORS.issuperset(set(stressors))
def StressngCpuMethodsValidator(cpu_methods):
"""Returns whether or not the list of cpu methods is valid."""
return 'all_cpu_methods' in cpu_methods or VALID_CPU_METHODS.issuperset(
set(cpu_methods)
)
flags.register_validator(
'stress_ng_custom_stressors', StressngCustomStressorsValidator
)
flags.register_validator('stress_ng_cpu_methods', StressngCpuMethodsValidator)
def GetConfig(user_config):
return configs.LoadConfig(BENCHMARK_CONFIG, user_config, BENCHMARK_NAME)
def Prepare(benchmark_spec):
"""Installs stress-ng on the target vm.
Args:
benchmark_spec: The benchmark specification. Contains all data that is
required to run the benchmark.
"""
vm = benchmark_spec.vms[0]
vm.InstallPackages('stress-ng')
def _ParseStressngResult(
metadata, output, cpu_method=None
) -> sample.Sample | None:
"""Returns stress-ng data as a sample.
Sample output eg:
stress-ng: info: [2566] dispatching hogs: 2 context
stress-ng: info: [2566] successful run completed in 5.00s
stress-ng: info: [2566] stressor bogo ops real time usr time sys
time bogo ops/s bogo ops/s
stress-ng: info: [2566] (secs) (secs) (secs)
(real time) (usr+sys time)
stress-ng: info: [2566] context 22429 5.00 5.49
4.48 4485.82 2249.65
Args:
metadata: metadata of the sample.
output: the output of the stress-ng benchmark.
cpu_method: an optional flag for the cpu method for the cpu stressor.
"""
output_list = output.splitlines()
output_matrix = [i.split() for i in output_list]
if len(output_matrix) < 5:
logging.error('output is missing')
return None
while output_matrix[-1][-1] == 'stressor)':
output_matrix.pop()
assert output_matrix[-3][-4] == 'bogo' and output_matrix[-3][-3] == 'ops/s'
assert output_matrix[-2][-4] == '(real' and output_matrix[-2][-3] == 'time)'
line = output_matrix[-1]
name = line[3]
value = float(line[-2]) # parse bogo ops/s (real time)
if name == 'cpu' and cpu_method:
return sample.Sample(
metric=cpu_method,
value=value,
unit='bogus_ops_sec', # bogus operations per second
metadata=metadata,
)
return sample.Sample(
metric=name,
value=value,
unit='bogus_ops_sec', # bogus operations per second
metadata=metadata,
)
def _RunWorkload(vm, num_threads):
"""Runs stress-ng on the target vm.
Args:
vm: The target vm to run on.
num_threads: Number of instances of stressors to launch.
Returns:
A list of sample.Sample objects.
"""
metadata = {
'duration_sec': FLAGS.stress_ng_duration,
'threads': num_threads,
}
samples = []
values_to_geomean_list = []
stressors = FLAGS.stress_ng_custom_stressors
for stressor in stressors:
cmd = (
'stress-ng --{stressor} {numthreads} --metrics-brief '
'-t {duration}'.format(
stressor=stressor,
numthreads=num_threads,
duration=FLAGS.stress_ng_duration,
)
)
stdout, stderr = vm.RemoteCommand(cmd)
stdout = stderr
stressng_sample = _ParseStressngResult(metadata, stdout)
if stressng_sample:
samples.append(stressng_sample)
values_to_geomean_list.append(stressng_sample.value)
cpu_methods = (
VALID_CPU_METHODS
if 'all_cpu_methods' in FLAGS.stress_ng_cpu_methods
else FLAGS.stress_ng_cpu_methods
)
for cpu_method in cpu_methods:
cmd = (
'stress-ng --cpu {numthreads} --metrics-brief '
'-t {duration} --cpu-method {cpu_method}'.format(
numthreads=num_threads,
duration=FLAGS.stress_ng_duration,
cpu_method=cpu_method,
)
)
stdout, _ = vm.RemoteCommand(cmd)
stressng_sample = _ParseStressngResult(metadata, stdout, cpu_method)
if stressng_sample:
samples.append(stressng_sample)
values_to_geomean_list.append(stressng_sample.value)
if FLAGS.stress_ng_calc_geomean:
geomean_metadata = metadata.copy()
geomean_metadata['stressors'] = stressors
# True only if each stressor provided a value
geomean_metadata['valid_run'] = len(values_to_geomean_list) == len(
stressors
) + len(cpu_methods)
geomean_sample = sample.Sample(
metric='STRESS_NG_GEOMEAN',
value=_GeoMeanOverflow(values_to_geomean_list),
unit='bogus_ops_sec',
metadata=geomean_metadata,
)
samples.append(geomean_sample)
return samples
def Run(benchmark_spec):
"""Runs stress-ng on the target vm.
Args:
benchmark_spec: The benchmark specification. Contains all data that is
required to run the benchmark.
Returns:
A list of sample.Sample objects.
"""
vm = benchmark_spec.vms[0]
samples = []
for workload in FLAGS.stress_ng_thread_workloads:
if workload == 'small':
samples.extend(_RunWorkload(vm, 1))
elif workload == 'medium':
samples.extend(_RunWorkload(vm, vm.NumCpusForBenchmark() / 2))
elif workload == 'large':
samples.extend(_RunWorkload(vm, vm.NumCpusForBenchmark()))
return samples
def Cleanup(benchmark_spec):
"""Cleans up stress-ng from the target vm.
Args:
benchmark_spec: The benchmark specification. Contains all data that is
required to run the benchmark.
"""
vm = benchmark_spec.vms[0]
vm.Uninstall('stress-ng')