• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

smarr / ReBench / 12978886586

26 Jan 2025 10:23PM UTC coverage: 85.2% (+0.7%) from 84.488%
12978886586

Pull #281

github

smarr
Enable denoise.py to work as script to avoid need for PYTHONPATH to be set in environment

- make denoise.py executable for all
- denoise.py does not support —version any longer, but that seems like an ok tradeoff

Signed-off-by: Stefan Marr <git@stefan-marr.de>
Pull Request #281: Enable denoise.py to work as script to avoid need for PYTHONPATH in env

10 of 21 new or added lines in 3 files covered. (47.62%)

13 existing lines in 1 file now uncovered.

5630 of 6608 relevant lines covered (85.2%)

0.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.2
/rebench/denoise.py
1
#!/usr/bin/env python3
2
import json
1✔
3
import os
1✔
4
import sys
1✔
5

6
from argparse import ArgumentParser
1✔
7
from math import log, floor
1✔
8
from multiprocessing import Pool
1✔
9
from subprocess import check_output, CalledProcessError, DEVNULL, STDOUT
1✔
10

11
denoise_py = os.path.abspath(__file__)
1✔
12

13
if __name__ == "__main__":
1✔
14
    # ensure that the rebench module is available
NEW
15
    rebench_module = os.path.dirname(denoise_py)
×
NEW
16
    sys.path.append(os.path.dirname(rebench_module))
×
17

18
    # pylint: disable-next=import-error
NEW
19
    from output import output_as_str, UIError  # type: ignore
×
20

21
    # pylint: disable-next=import-error
NEW
22
    from subprocess_kill import kill_process  # type: ignore
×
23
else:
24
    from .output import output_as_str, UIError
1✔
25
    from .subprocess_kill import kill_process
1✔
26

27

28
class CommandsPaths:
1✔
29
    """Hold the path information for commands."""
30

31
    def __init__(self):
1✔
32
        self._cset_path = None
1✔
33
        self._denoise_path = None
1✔
34
        self._which_path = None
1✔
35

36
    def get_which(self):
1✔
37
        if not self._which_path:
1✔
38
            if os.path.isfile("/usr/bin/which"):
1✔
39
                self._which_path = "/usr/bin/which"
1✔
40
            else:
41
                raise UIError(
×
42
                    "The basic `which` command was not found."
43
                    " In many systems it is available at /usr/bin/which."
44
                    " If it is elsewhere rebench-denoise will need to be"
45
                    " adapted to support a different location.\n",
46
                    None,
47
                )
48

49
        return self._which_path
1✔
50

51
    def _absolute_path_for_command(self, command, arguments_for_successful_exe):
1✔
52
        """
53
        Find and return the canonical absolute path to make sudo happy.
54
        If the command is not found or does not execute successfully, return None.
55
        """
56
        try:
1✔
57
            selected_cmd = output_as_str(
1✔
58
                check_output([self.get_which(), command], shell=False, stderr=DEVNULL)
59
            ).strip()
UNCOV
60
            result_cmd = os.path.realpath(selected_cmd)
×
61
        except CalledProcessError:
1✔
62
            result_cmd = command
1✔
63

64
        try:
1✔
65
            check_output(
1✔
66
                [result_cmd] + arguments_for_successful_exe, shell=False, stderr=DEVNULL
67
            )
UNCOV
68
            return result_cmd
×
69
        except (CalledProcessError, FileNotFoundError):
1✔
70
            return False
1✔
71

72
    def has_cset(self):
1✔
73
        if self._cset_path is None:
1✔
74
            self._cset_path = self._absolute_path_for_command("cset", ["--help"])
1✔
75

76
        return self._cset_path is not None and self._cset_path is not False
1✔
77

78
    def get_cset(self):
1✔
79
        return self._cset_path
×
80

81
    def set_cset(self, cset_path):
1✔
82
        self._cset_path = cset_path
×
83

84
    def has_denoise(self):
1✔
85
        if self._denoise_path is None:
1✔
86
            self._denoise_path = denoise_py
1✔
87

88
        return self._denoise_path is not None and self._denoise_path is not False
1✔
89

90
    def get_denoise(self):
1✔
91
        if not self.has_denoise():
1✔
92
            raise UIError(
×
93
                f"{denoise_py} not found. "
94
                "Could it be that the user has no access to the file? "
95
                "To use ReBench without denoise, use the --no-denoise option.\n",
96
                None,
97
            )
98

99
        return self._denoise_path
1✔
100

101

102
paths = CommandsPaths()
1✔
103

104

105
def _can_set_niceness():
1✔
106
    """
107
    Check whether we can ask the operating system to influence the priority of
108
    our benchmarks.
109
    """
110
    try:
×
111
        output = check_output(["nice", "-n-20", "echo", "test"], stderr=STDOUT)
×
112
        output = output_as_str(output)
×
113
    except OSError:
×
114
        return False
×
115

116
    if "cannot set niceness" in output or "Permission denied" in output:
×
117
        return False
×
118
    else:
119
        return True
×
120

121

122
def _shield_lower_bound(num_cores):
1✔
123
    return int(floor(log(num_cores)))
×
124

125

126
def _shield_upper_bound(num_cores):
1✔
127
    return num_cores - 1
×
128

129

130
def _activate_shielding(num_cores):
1✔
131
    min_cores = _shield_lower_bound(num_cores)
×
132
    max_cores = _shield_upper_bound(num_cores)
×
133
    core_spec = "%d-%d" % (min_cores, max_cores)
×
134

135
    if not paths.has_cset():
×
136
        return False
×
137

138
    try:
×
139
        output = check_output(
×
140
            [paths.get_cset(), "shield", "-c", core_spec, "-k", "on"], stderr=STDOUT
141
        )
142
        output = output_as_str(output)
×
143
    except OSError:
×
144
        return False
×
145

146
    if "Permission denied" in output:
×
147
        return False
×
148

149
    if "kthread shield activated" in output:
×
150
        return core_spec
×
151

152
    return False
×
153

154

155
def _reset_shielding():
1✔
156
    try:
×
157
        output = check_output([paths.get_cset(), "shield", "-r"], stderr=STDOUT)
×
158
        output = output_as_str(output)
×
159
        return "cset: done" in output
×
160
    except OSError:
×
161
        return False
×
162
    except CalledProcessError:
×
163
        return False
×
164

165

166
# For intel_pstate systems, there's only powersave and performance
167
SCALING_GOVERNOR_POWERSAVE = "powersave"
1✔
168
SCALING_GOVERNOR_PERFORMANCE = "performance"
1✔
169

170

171
def _set_scaling_governor(governor, num_cores):
1✔
172
    assert governor in (SCALING_GOVERNOR_POWERSAVE, SCALING_GOVERNOR_PERFORMANCE), (
×
173
        "The scaling governor is expected to be performance or powersave, but was "
174
        + governor
175
    )
176

177
    try:
×
178
        for cpu_i in range(num_cores):
×
179
            filename = (
×
180
                "/sys/devices/system/cpu/cpu" + str(cpu_i) + "/cpufreq/scaling_governor"
181
            )
182
            with open(filename, "w", encoding="utf-8") as gov_file:
×
183
                gov_file.write(governor + "\n")
×
184
    except IOError:
×
185
        return "failed"
×
186

187
    return governor
×
188

189

190
def _set_no_turbo(with_no_turbo):
1✔
191
    if with_no_turbo:
×
192
        value = "1"
×
193
    else:
194
        value = "0"
×
195

196
    try:
×
197
        with open(
×
198
            "/sys/devices/system/cpu/intel_pstate/no_turbo", "w", encoding="utf-8"
199
        ) as nt_file:
200
            nt_file.write(value + "\n")
×
201
    except IOError:
×
202
        return "failed"
×
203
    return with_no_turbo
×
204

205

206
def _configure_perf_sampling(for_profiling):
1✔
207
    try:
×
208
        with open(
×
209
            "/proc/sys/kernel/perf_cpu_time_max_percent", "w", encoding="utf-8"
210
        ) as perc_file:
211
            if for_profiling:
×
212
                perc_file.write("0\n")
×
213
            else:
214
                perc_file.write("1\n")
×
215

216
        with open(
×
217
            "/proc/sys/kernel/perf_event_max_sample_rate", "w", encoding="utf-8"
218
        ) as sample_file:
219
            # for profiling we just disabled it above, and then don't need to set it
220
            if not for_profiling:
×
221
                sample_file.write("1\n")
×
222

223
        if for_profiling:
×
224
            with open(
×
225
                "/proc/sys/kernel/perf_event_paranoid", "w", encoding="utf-8"
226
            ) as perf_file:
227
                perf_file.write("-1\n")
×
228
    except IOError:
×
229
        return "failed"
×
230

231
    if for_profiling:
×
232
        return 0
×
233
    else:
234
        return 1
×
235

236

237
def _restore_perf_sampling():
1✔
238
    try:
×
239
        with open(
×
240
            "/proc/sys/kernel/perf_cpu_time_max_percent", "w", encoding="utf-8"
241
        ) as perc_file:
242
            perc_file.write("25\n")
×
243

244
        with open(
×
245
            "/proc/sys/kernel/perf_event_max_sample_rate", "w", encoding="utf-8"
246
        ) as sample_file:
247
            sample_file.write("50000\n")
×
248

249
        with open(
×
250
            "/proc/sys/kernel/perf_event_paranoid", "w", encoding="utf-8"
251
        ) as perf_file:
252
            perf_file.write("3\n")
×
253
    except IOError:
×
254
        return "failed"
×
255
    return "restored"
×
256

257

258
def _minimize_noise(num_cores, use_nice, use_shielding, for_profiling):
1✔
259
    governor = _set_scaling_governor(SCALING_GOVERNOR_PERFORMANCE, num_cores)
×
260
    no_turbo = _set_no_turbo(True)
×
261
    perf = _configure_perf_sampling(for_profiling)
×
262

263
    can_nice = _can_set_niceness() if use_nice else False
×
264
    shielding = _activate_shielding(num_cores) if use_shielding else False
×
265

266
    return {
×
267
        "scaling_governor": governor,
268
        "no_turbo": no_turbo,
269
        "perf_event_max_sample_rate": perf,
270
        "can_set_nice": can_nice,
271
        "shielding": shielding,
272
    }
273

274

275
def _restore_standard_settings(num_cores, use_shielding):
1✔
276
    governor = _set_scaling_governor(SCALING_GOVERNOR_POWERSAVE, num_cores)
×
277
    no_turbo = _set_no_turbo(False)
×
278
    perf = _restore_perf_sampling()
×
279
    shielding = _reset_shielding() if use_shielding else False
×
280

281
    return {
×
282
        "scaling_governor": governor,
283
        "no_turbo": no_turbo,
284
        "perf_event_max_sample_rate": perf,
285
        "shielding": shielding,
286
    }
287

288

289
def _exec(num_cores, use_nice, use_shielding, args):
1✔
290
    cmdline = []
×
291
    if use_shielding and paths.has_cset():
×
292
        cmdline += [paths.get_cset(), "shield", "--exec", "--"]
×
293
    if use_nice:
×
294
        cmdline += ["nice", "-n-20"]
×
295
    cmdline += args
×
296

297
    # the first element of cmdline is ignored as argument, since it's the file argument, too
298
    cmd = cmdline[0]
×
299

300
    # communicate the used core spec to executed command as part of its environment
301
    env = os.environ.copy()
×
302
    if use_shielding and paths.has_cset():
×
303
        min_cores = _shield_lower_bound(num_cores)
×
304
        max_cores = _shield_upper_bound(num_cores)
×
305
        core_spec = "%d-%d" % (min_cores, max_cores)
×
306
        env["REBENCH_DENOISE_CORE_SET"] = core_spec
×
307

308
    os.execvpe(cmd, cmdline, env)
×
309

310

311
def _kill(proc_id):
1✔
312
    kill_process(int(proc_id), True, None, None)
×
313

314

315
def _calculate(core_id):
1✔
316
    print("Started calculating: %d" % core_id)
×
317
    try:
×
318
        val = 0
×
319
        for _ in range(1, 1000):
×
320
            for i in range(1, 1000000):
×
321
                val *= i * i / i + i - i
×
322
    except KeyboardInterrupt:
×
323
        pass
×
324
    print("Finished calculating: %d" % core_id)
×
325

326

327
def _test(num_cores):
1✔
328
    lower = _shield_lower_bound(num_cores)
×
329
    upper = _shield_upper_bound(num_cores)
×
330
    core_cnt = upper - lower + 1
×
331
    pool = Pool(core_cnt)  # pylint: disable=consider-using-with
×
332

333
    print("Test on %d cores" % core_cnt)
×
334

335
    core_spec = "%d-%d" % (lower, upper)
×
336
    env_spec = os.environ.get("REBENCH_DENOISE_CORE_SET", None)
×
337
    if core_spec != env_spec:
×
338
        print("Core Spec set by denoise was: ", env_spec)
×
339
        print("Locally determined one was: ", core_spec)
×
340
        print("The specs did not match!")
×
341

342
    try:
×
343
        pool.map(_calculate, range(0, core_cnt))
×
344
    except KeyboardInterrupt:
×
345
        pass
×
346

347
    print("exit main")
×
348
    pool.terminate()
×
349
    print("Done testing on %d cores" % core_cnt)
×
350

351

352
def _shell_options():
1✔
UNCOV
353
    parser = ArgumentParser()
×
UNCOV
354
    parser.add_argument(
×
355
        "--json",
356
        action="store_true",
357
        default=False,
358
        help="Output results as JSON for processing",
359
    )
UNCOV
360
    parser.add_argument(
×
361
        "--without-nice",
362
        action="store_false",
363
        default=True,
364
        dest="use_nice",
365
        help="Don't try setting process niceness",
366
    )
UNCOV
367
    parser.add_argument(
×
368
        "--without-shielding",
369
        action="store_false",
370
        default=True,
371
        dest="use_shielding",
372
        help="Don't try shielding cores",
373
    )
UNCOV
374
    parser.add_argument(
×
375
        "--for-profiling",
376
        action="store_true",
377
        default=False,
378
        dest="for_profiling",
379
        help="Don't restrict CPU usage by profiler",
380
    )
UNCOV
381
    parser.add_argument("--cset-path", help="Absolute path to cset", default=None)
×
UNCOV
382
    parser.add_argument(
×
383
        "--num-cores", help="Number of cores. Is required.", default=None
384
    )
UNCOV
385
    parser.add_argument(
×
386
        "command",
387
        help=(
388
            "`minimize`|`restore`|`exec -- `|`kill pid`|`test`: "
389
            "`minimize` sets system to reduce noise. "
390
            "`restore` sets system to the assumed original settings. "
391
            "`exec -- ` executes the given arguments. "
392
            "`kill pid` send kill signal to the process with given id "
393
            "and all child processes. "
394
            "`test` executes a computation for 20 seconds in parallel. "
395
            "it is only useful to test rebench-denoise itself."
396
        ),
397
        default=None,
398
    )
UNCOV
399
    return parser
×
400

401

402
EXIT_CODE_SUCCESS = 0
1✔
403
EXIT_CODE_CHANGING_SETTINGS_FAILED = 1
1✔
404
EXIT_CODE_NUM_CORES_UNSET = 2
1✔
405
EXIT_CODE_NO_COMMAND_SELECTED = 3
1✔
406

407

408
def main_func():
1✔
UNCOV
409
    arg_parser = _shell_options()
×
UNCOV
410
    args, remaining_args = arg_parser.parse_known_args()
×
411

412
    paths.set_cset(args.cset_path)
×
413

414
    num_cores = int(args.num_cores) if args.num_cores else None
×
415
    result = {}
×
416

417
    if args.command == "minimize" and num_cores is not None:
×
418
        result = _minimize_noise(
×
419
            num_cores, args.use_nice, args.use_shielding, args.for_profiling
420
        )
421
    elif args.command == "restore" and num_cores is not None:
×
422
        result = _restore_standard_settings(num_cores, args.use_shielding)
×
423
    elif args.command == "exec":
×
424
        _exec(num_cores, args.use_nice, args.use_shielding, remaining_args)
×
425
    elif args.command == "kill":
×
426
        _kill(remaining_args[0])
×
427
    elif args.command == "test" and num_cores is not None:
×
428
        _test(num_cores)
×
429
    else:
430
        arg_parser.print_help()
×
431
        if num_cores is None:
×
432
            print("The --num-cores must be provided.")
×
433
            return EXIT_CODE_NUM_CORES_UNSET
×
434
        return EXIT_CODE_NO_COMMAND_SELECTED
×
435

436
    if args.json:
×
437
        print(json.dumps(result))
×
438
    else:
439
        print(
×
440
            "Setting scaling_governor:           ", result.get("scaling_governor", None)
441
        )
442
        print("Setting no_turbo:                   ", result.get("no_turbo", False))
×
443
        print(
×
444
            "Setting perf_event_max_sample_rate: ",
445
            result.get("perf_event_max_sample_rate", None),
446
        )
447
        print("")
×
448
        print("Enabled core shielding:             ", result.get("shielding", False))
×
449
        print("")
×
450
        print("Can set niceness:                   ", result.get("can_set_nice", False))
×
451

452
    if "failed" in result.values():
×
453
        return EXIT_CODE_CHANGING_SETTINGS_FAILED
×
454
    else:
455
        return EXIT_CODE_SUCCESS
×
456

457

458
if __name__ == "__main__":
1✔
459
    sys.exit(main_func())
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc