From 4579bd58062333847b8ceccdd5a09d7cca0f2d59 Mon Sep 17 00:00:00 2001 From: rishika-y-gaur Date: Mon, 21 Sep 2026 16:56:13 +0530 Subject: [PATCH 1/5] fix: npu default value --- benchmark-scripts/consolidate_multiple_run_of_metrics.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/benchmark-scripts/consolidate_multiple_run_of_metrics.py b/benchmark-scripts/consolidate_multiple_run_of_metrics.py index fbfad0e..c66d0d8 100644 --- a/benchmark-scripts/consolidate_multiple_run_of_metrics.py +++ b/benchmark-scripts/consolidate_multiple_run_of_metrics.py @@ -869,6 +869,7 @@ def add_parser(): root_directory = args['root_directory'][0] output = args['output'][0] + wsl2 = os.environ.get('WSL2', '').lower() == 'true' n = 0 df = pd.DataFrame() @@ -876,6 +877,8 @@ def add_parser(): all_channel_medians = [] # Collect all channel medians from all files for kpiExtractor in KPIExtractor_OPTION: + if wsl2 and kpiExtractor == 'npu_usage.csv': + continue fileFound = False for dirpath, dirname, filename in os.walk(root_directory): for file in filename: @@ -900,6 +903,9 @@ def add_parser(): full_kpi_dict["Overall Latency (ms)"] = round(overall_median, 3) + if wsl2: + full_kpi_dict[AVG_NPU_USAGE_CONSTANT] = '0.00' + # Write out summary csv file from dictionary with open(output, 'w') as csv_file: writer = csv.writer(csv_file) From 4e7be4f1c74c0d39422bf561c90da038eb28735f Mon Sep 17 00:00:00 2001 From: rishika-y-gaur Date: Mon, 21 Sep 2026 23:04:50 +0530 Subject: [PATCH 2/5] fix: cleaned code --- benchmark-scripts/benchmark.py | 7 + benchmark-scripts/benchmark_test.py | 124 ++++++ .../consolidate_multiple_run_of_metrics.py | 27 +- benchmark-scripts/windows_metrics.py | 356 ++++++++++++++++++ 4 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 benchmark-scripts/windows_metrics.py diff --git a/benchmark-scripts/benchmark.py b/benchmark-scripts/benchmark.py index 52568cb..f2542b9 100644 --- a/benchmark-scripts/benchmark.py +++ b/benchmark-scripts/benchmark.py @@ -14,6 +14,7 @@ import json import stream_density from device_validation import validate_target_device, resolve_target_device_default +from windows_metrics import collector as windows_collector def parse_args(print=False): @@ -131,6 +132,8 @@ def docker_compose_containers(command, compose_files=[], compose_pre_args="", returncode: Popen return code ''' try: + if env_vars.get('WSL2', '').lower() == 'true' and command in ('down', 'stop'): + windows_collector.stop() files = " -f ".join(compose_files) compose_string = ("docker compose %s -f %s %s %s" % (compose_pre_args, files, command, @@ -143,6 +146,8 @@ def docker_compose_containers(command, compose_files=[], compose_pre_args="", env=env_vars) # nosec B404, B603 stdout, stderr = p.communicate() + if env_vars.get('WSL2', '').lower() == 'true' and command == 'up' and p.returncode == 0: + windows_collector.start(env_vars) if p.returncode and stderr: print("Error bringing %s the compose files: %s" % (command, stderr)) @@ -199,6 +204,8 @@ def main(): env_vars = os.environ.copy() env_vars["log_dir"] = results_dir env_vars["RESULTS_DIR"] = results_dir + if env_vars.get('WSL2', '').lower() == 'true': + env_vars["INIT_DURATION"] = str(my_args.init_duration) env_vars["DEVICE"] = my_args.target_device retail_use_case_root = os.path.abspath(my_args.retail_use_case_root) env_vars["RETAIL_USE_CASE_ROOT"] = retail_use_case_root diff --git a/benchmark-scripts/benchmark_test.py b/benchmark-scripts/benchmark_test.py index b05f63d..9872586 100644 --- a/benchmark-scripts/benchmark_test.py +++ b/benchmark-scripts/benchmark_test.py @@ -9,6 +9,130 @@ import unittest import benchmark import os +import json +import tempfile +from pathlib import Path +from types import SimpleNamespace +import windows_metrics + + +class WindowsMetricsTesting(unittest.TestCase): + def test_gpu_power_rejects_cpu_and_preserves_missing_values(self): + reader = windows_metrics.GpuPowerReader.__new__(windows_metrics.GpuPowerReader) + sensor = SimpleNamespace(SensorType='Power', Value=None, + Name='GPU Power', Identifier='/gpu/power/0') + cpu_sensor = SimpleNamespace(SensorType='Power', Value=100, + Name='CPU Package', Identifier='/cpu/power/0') + reader.computer = SimpleNamespace(Hardware=[ + SimpleNamespace(HardwareType='Cpu', Sensors=[cpu_sensor]), + SimpleNamespace(HardwareType='GpuIntel', Name='GPU', Identifier='/gpu', + Sensors=[sensor], Update=lambda: None)]) + self.assertIsNone(reader.read('/cpu/power/0')) + self.assertIsNone(reader.read('/gpu/power/0')) + sensor.Value = 0 + self.assertEqual(reader.read('/gpu/power/0'), 0) + sensor.Value = 12.5 + self.assertEqual(reader.read('/gpu/power/0'), 12.5) + sensor.Value = float('nan') + self.assertIsNone(reader.read('/gpu/power/0')) + + def test_gpu_power_without_activity_counters(self): + stop = mock.MagicMock() + stop.wait.side_effect = [False, False, True] + with tempfile.TemporaryDirectory() as directory, \ + mock.patch.object(windows_metrics, 'GpuPowerReader') as provider, \ + mock.patch.dict('sys.modules', {'win32pdh': None}), \ + mock.patch.object(windows_metrics.shutil, 'which', return_value=None): + provider.return_value.read.return_value = 12.5 + windows_metrics.collect(directory, stop, lhm_dll='library.dll', + power_sensor='/gpu/power/0', + power_adapter='luid_0x0_0x1_phys_0') + metrics = json.loads((Path(directory) / 'windows_metrics.json').read_text())['metrics'] + self.assertEqual(metrics['GPU_1 GPU Power (W)'], 12.5) + self.assertEqual(metrics['S0 Power Draw W'], 'NA') + provider.return_value.close.assert_called_once() + + def test_gpu_engines(self): + counters = { + f'pid_{process}_luid_0x0_0x1_phys_0_eng_{engine}_engtype_{kind}': value + for process, engine, kind, value in ( + (1, 0, '3D', 20), (2, 0, '3D', 30), (1, 1, '3D', 40), + (1, 2, 'Compute_0', 0), (1, 3, 'Copy', float('nan'))) + } + result = windows_metrics.gpu_sample(counters) + self.assertEqual(result, { + ('luid_0x0_0x1_phys_0', 'Render/3D[RCS] Utilization %'): 50, + ('luid_0x0_0x1_phys_0', 'Compute[CCS] Utilization %'): 0}) + + def test_pcm_units(self): + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / 'windows_pcm_raw.log' + source.write_text( + 'System,System,Socket 0,Socket 0,Proc Energy (Joules)\n' + 'Date,Time,READ,WRITE,SKT0\n' + '2026-09-21,10:00:00,1,2,20\n' + '2026-09-21,10:00:02,2,3,40\n') + self.assertEqual(windows_metrics.read_pcm(source), { + 'S0 Memory Bandwidth Usage MB/s': 5000, + 'S0 Power Draw W': 20}) + + def test_native_noop_and_wsl_launch_failure(self): + collector = windows_metrics.WindowsMetricsCollector() + with mock.patch.object(windows_metrics.subprocess, 'Popen') as launch: + collector.start({'WSL2': 'false'}) + launch.assert_not_called() + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / 'windows_metrics.json' + output.write_text('{"metrics":{"stale":100}}') + with mock.patch.object(windows_metrics.subprocess, 'check_output', + side_effect=FileNotFoundError('wslpath')): + collector.start({'WSL2': 'true', 'RESULTS_DIR': directory}) + self.assertIsNone(collector.process) + self.assertEqual(json.loads(output.read_text())['metrics'], windows_metrics.blank_metrics()) + + def test_worker_missing_pcm_and_invalid_gpu_sample(self): + pdh = mock.MagicMock(PDH_FMT_DOUBLE=512) + pdh.GetCounterInfo.return_value = (None,) * 6 + ('localized wildcard',) + pdh.ExpandCounterPath.return_value = [ + r'\GPU Engine(pid_1_luid_0x0_0x1_phys_0_eng_0_engtype_3D)\Utilization Percentage'] + pdh.GetFormattedCounterValue.side_effect = [RuntimeError('invalid'), (0, 25)] + stop = mock.MagicMock() + stop.wait.side_effect = [False, False, False, False, True] + with tempfile.TemporaryDirectory() as directory, \ + mock.patch.dict('sys.modules', {'win32pdh': pdh}), \ + mock.patch.object(windows_metrics.shutil, 'which', return_value=None): + windows_metrics.collect(directory, stop) + metrics = json.loads((Path(directory) / 'windows_metrics.json').read_text())['metrics'] + self.assertEqual(metrics['GPU_1 Render/3D[RCS] Utilization %'], 25) + self.assertEqual(metrics['GPU_1 GPU Power (W)'], 'NA') + self.assertEqual(metrics['S0 Power Draw W'], 'NA') + self.assertEqual(pdh.GetFormattedCounterValue.call_count, 2) + + def test_compose_lifecycle(self): + with mock.patch.object(benchmark, 'windows_collector') as collector, \ + mock.patch.object(benchmark.subprocess, 'Popen') as launch: + launch.return_value.communicate.return_value = (b'ok', b'') + launch.return_value.returncode = 0 + env = {'WSL2': 'true', 'RESULTS_DIR': '/benchmark'} + benchmark.docker_compose_containers('up', env_vars=env) + collector.start.assert_called_once_with(env) + benchmark.docker_compose_containers('down', env_vars=env) + collector.stop.assert_called_once() + collector.reset_mock() + launch.return_value.returncode = 1 + benchmark.docker_compose_containers('up', env_vars=env) + collector.start.assert_not_called() + + def test_native_compose_never_calls_collector(self): + with mock.patch.object(benchmark, 'windows_collector') as collector, \ + mock.patch.object(benchmark.subprocess, 'Popen') as launch: + launch.return_value.communicate.return_value = (b'ok', b'') + launch.return_value.returncode = 0 + for env in ({}, {'WSL2': 'false'}, {'WSL': 'true'}): + benchmark.docker_compose_containers('up', env_vars=env) + benchmark.docker_compose_containers('down', env_vars=env) + collector.start.assert_not_called() + collector.stop.assert_not_called() class Testing(unittest.TestCase): diff --git a/benchmark-scripts/consolidate_multiple_run_of_metrics.py b/benchmark-scripts/consolidate_multiple_run_of_metrics.py index c66d0d8..2969d0c 100644 --- a/benchmark-scripts/consolidate_multiple_run_of_metrics.py +++ b/benchmark-scripts/consolidate_multiple_run_of_metrics.py @@ -877,7 +877,9 @@ def add_parser(): all_channel_medians = [] # Collect all channel medians from all files for kpiExtractor in KPIExtractor_OPTION: - if wsl2 and kpiExtractor == 'npu_usage.csv': + if wsl2 and KPIExtractor_OPTION[kpiExtractor] in ( + NPUUsageExtractor, PCMExtractor, MemBandwidthExtractor, + PowerUsageExtractor, QMASSAGPUUsageExtractor, XPUMUsageExtractor): continue fileFound = False for dirpath, dirname, filename in os.walk(root_directory): @@ -904,7 +906,30 @@ def add_parser(): full_kpi_dict["Overall Latency (ms)"] = round(overall_median, 3) if wsl2: + from windows_metrics import blank_metrics + full_kpi_dict.update(blank_metrics()) full_kpi_dict[AVG_NPU_USAGE_CONSTANT] = '0.00' + windows_metrics_path = pathlib.Path(root_directory) / 'windows_metrics.json' + if windows_metrics_path.is_file(): + try: + with windows_metrics_path.open(encoding='utf-8-sig') as windows_file: + windows_metrics = json.load(windows_file)['metrics'] + if not isinstance(windows_metrics, dict): + raise ValueError('metrics must be an object') + allowed_metric = re.compile( + r'(?:GPU_\d+ (?:Compute\[CCS\] Utilization %|' + r'Render/3D\[RCS\] Utilization %|Video\[VCS\] Utilization %|' + r'VideoEnhance\[VECS\] Utilization %|Blitter Copy Engine %|' + r'GPU Power \(W\))|S\d+ (?:Power Draw W|Memory Bandwidth Usage MB/s))') + for key, value in windows_metrics.items(): + if not allowed_metric.fullmatch(key): + continue + if value == 'NA': + full_kpi_dict[key] = value + elif type(value) in (int, float) and np.isfinite(value) and value >= 0: + full_kpi_dict[key] = round(value, 2) + except (OSError, ValueError, KeyError, TypeError) as error: + print(f'WARN: Windows hardware metrics unavailable: {error}') # Write out summary csv file from dictionary with open(output, 'w') as csv_file: diff --git a/benchmark-scripts/windows_metrics.py b/benchmark-scripts/windows_metrics.py new file mode 100644 index 0000000..9ab03f1 --- /dev/null +++ b/benchmark-scripts/windows_metrics.py @@ -0,0 +1,356 @@ +"""Automatic Windows-host telemetry for WSL benchmarks.""" + +import atexit +import argparse +import csv +import datetime +import json +import math +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import threading +from collections import defaultdict + + +GPU_FIELDS = ( + 'Compute[CCS] Utilization %', 'Render/3D[RCS] Utilization %', + 'Video[VCS] Utilization %', 'VideoEnhance[VECS] Utilization %', + 'Blitter Copy Engine %', 'GPU Power (W)', +) + + +def blank_metrics(): + metrics = {f'GPU_1 {field}': 'NA' for field in GPU_FIELDS} + metrics.update({'S0 Power Draw W': 'NA', 'S0 Memory Bandwidth Usage MB/s': 'NA'}) + return metrics + + +class GpuPowerReader: + def __init__(self, dll): + import clr + library = Path(dll).resolve(strict=True) + sys.path.append(str(library.parent)) + clr.AddReference(str(library)) + from LibreHardwareMonitor.Hardware import Computer + self.computer = Computer() + self.computer.IsGpuEnabled = True + try: + self.computer.Open() + except Exception: + self.computer.Close() + raise + + def sensors(self): + result = [] + for hardware in self.computer.Hardware: + if not str(hardware.HardwareType).startswith('Gpu'): + continue + hardware.Update() + for sensor in hardware.Sensors: + if str(sensor.SensorType) != 'Power': + continue + raw = sensor.Value + value = float(raw) if raw is not None else None + if value is not None and (not math.isfinite(value) or value < 0): + value = None + result.append({'hardware': str(hardware.Name), + 'hardware_id': str(hardware.Identifier), + 'sensor': str(sensor.Name), + 'sensor_id': str(sensor.Identifier), 'watts': value}) + return result + + def read(self, sensor_id): + return next((sensor['watts'] for sensor in self.sensors() + if sensor['sensor_id'] == sensor_id), None) + + def close(self): + self.computer.Close() + + +def gpu_sample(instances): + engines = defaultdict(float) + for name, value in instances.items(): + match = re.search(r'(luid_.+?_phys_\d+)_eng_(\d+)_engtype_(.+)', name) + if not match or not math.isfinite(value) or value < 0: + continue + adapter, engine, kind = match.groups() + kind = kind.lower() + if kind.startswith('compute'): + field = GPU_FIELDS[0] + else: + field = {'3d': GPU_FIELDS[1], 'videodecode': GPU_FIELDS[2], + 'videoencode': GPU_FIELDS[2], 'videoprocessing': GPU_FIELDS[3], + 'copy': GPU_FIELDS[4]}.get(kind) + if field: + engines[adapter, field, engine] += value + result = {} + for (adapter, field, engine), value in engines.items(): + result[adapter, field] = max(result.get((adapter, field), 0), min(value, 100)) + return result + + +def read_pcm(path): + with Path(path).open(encoding='utf-8-sig', newline='') as source: + rows = csv.reader(source) + groups = next(rows, []) + headers = next(rows, []) + if headers[:2] != ['Date', 'Time']: + return {} + values = defaultdict(list) + previous = None + for row in rows: + if len(row) < len(headers): + continue + try: + timestamp = datetime.datetime.fromisoformat(f'{row[0]}T{row[1]}') + except ValueError: + continue + interval = (timestamp - previous).total_seconds() if previous else 0 + previous = timestamp + if interval <= 0: + continue + for index, (group, header) in enumerate(zip(groups, headers)): + try: + if header == 'READ' and group.startswith('Socket '): + socket = int(group.split()[-1]) + write_index = next(position for position, pair in enumerate(zip(groups, headers)) + if pair == (group, 'WRITE')) + read, write = float(row[index]), float(row[write_index]) + if min(read, write) < 0: + continue + value = (read + write) * 1000 + key = f'S{socket} Memory Bandwidth Usage MB/s' + elif group == 'Proc Energy (Joules)' and re.fullmatch(r'SKT\d+', header): + key = f'S{int(header[3:])} Power Draw W' + value = float(row[index]) / interval + else: + continue + if math.isfinite(value) and value >= 0: + values[key].append((value, interval)) + except (ValueError, StopIteration, IndexError): + continue + return {key: round(sum(value * duration for value, duration in samples) / + sum(duration for value, duration in samples), 2) + for key, samples in values.items()} + + +def collect(directory, stop, init_duration=0, pcm_exe=None, + lhm_dll=None, power_sensor=None, power_adapter=None): + directory = Path(directory) + metrics = blank_metrics() + adapters, samples = {}, defaultdict(list) + query, pcm, pcm_log = None, None, None + power_reader = None + try: + if stop.wait(init_duration): + return + if lhm_dll: + try: + if not power_sensor or not re.fullmatch(r'luid_.+_phys_\d+', power_adapter or ''): + raise ValueError('GPU power requires WINDOWS_GPU_POWER_SENSOR and WINDOWS_GPU_POWER_ADAPTER') + power_reader = GpuPowerReader(lhm_dll) + power_reader.sensors() + adapters[power_adapter] = 1 + except Exception as error: + print(f'WARN: GPU power unavailable: {error}', flush=True) + if power_reader is not None: + power_reader.close() + power_reader = None + try: + import win32pdh + query = win32pdh.OpenQuery() + counter = win32pdh.AddEnglishCounter( + query, r'\GPU Engine(*)\Utilization Percentage') + counter_path = win32pdh.GetCounterInfo(counter, False)[6] + win32pdh.RemoveCounter(counter) + counters = {} + except Exception as error: + print(f'WARN: GPU counters unavailable: {error}', flush=True) + if query is not None: + win32pdh.CloseQuery(query) + query = None + try: + executable = pcm_exe or shutil.which('pcm.exe') + if not executable: + raise FileNotFoundError('pcm.exe not found; set WINDOWS_PCM_EXE to its Windows path') + pcm_log = (directory / 'windows_pcm.log').open('w') + pcm = subprocess.Popen( + [executable, '1', f'-csv={directory / "windows_pcm_raw.log"}', '-nc'], + stdin=subprocess.DEVNULL, stdout=pcm_log, stderr=subprocess.STDOUT) + except OSError as error: + print(f'WARN: Windows PCM unavailable: {error}', flush=True) + while not stop.wait(1): + if power_reader is not None: + try: + watts = power_reader.read(power_sensor) + if watts is not None: + samples[f'GPU_{adapters[power_adapter]} GPU Power (W)'].append(watts) + except Exception as error: + print(f'WARN: GPU power sample unavailable: {error}', flush=True) + if query is None: + continue + try: + paths = set(win32pdh.ExpandCounterPath(counter_path)) + ready = set(counters) & paths + for path in set(counters) - paths: + win32pdh.RemoveCounter(counters.pop(path)) + for path in paths - set(counters): + counters[path] = win32pdh.AddCounter(query, path) + win32pdh.CollectQueryData(query) + instances = {} + for path in sorted(ready): + try: + instances[path.split('(', 1)[1].rsplit(')', 1)[0]] = ( + win32pdh.GetFormattedCounterValue(counters[path], win32pdh.PDH_FMT_DOUBLE)[1]) + except Exception: + continue + current = gpu_sample(instances) + for (adapter, field), value in current.items(): + if adapter not in adapters: + adapters[adapter] = len(adapters) + 1 + samples[f'GPU_{adapters[adapter]} {field}'].append(value) + except Exception as error: + print(f'WARN: GPU sample unavailable: {error}', flush=True) + finally: + if power_reader is not None: + try: + power_reader.close() + except Exception as error: + print(f'WARN: GPU power cleanup: {error}', flush=True) + if query is not None: + try: + win32pdh.CloseQuery(query) + except Exception as error: + print(f'WARN: GPU counter cleanup: {error}', flush=True) + if pcm is not None: + try: + if pcm.poll() is None: + pcm.terminate() + pcm.wait(timeout=5) + except subprocess.TimeoutExpired: + pcm.kill() + pcm.wait(timeout=2) + except OSError as error: + print(f'WARN: PCM cleanup: {error}', flush=True) + if pcm_log is not None: + pcm_log.close() + for number in adapters.values(): + metrics.update({f'GPU_{number} {field}': 'NA' for field in GPU_FIELDS}) + metrics.update({key: round(sum(values) / len(values), 2) for key, values in samples.items()}) + try: + metrics.update(read_pcm(directory / 'windows_pcm_raw.log')) + except (OSError, csv.Error) as error: + print(f'WARN: PCM measurements unavailable: {error}', flush=True) + output = {'metrics': metrics, 'adapters': adapters, + 'gpu_source': 'Windows GPU Engine counters (host-wide)', + 'gpu_aggregation': 'sum processes per engine, then busiest engine per category', + 'gpu_power_source': ({'provider': 'LibreHardwareMonitor', + 'sensor_id': power_sensor, 'adapter': power_adapter} + if power_reader is not None else None), + 'pcm_source': 'native Windows PCM; package energy / sample interval; READ+WRITE GB/s * 1000'} + temporary = directory / 'windows_metrics.json.tmp' + temporary.write_text(json.dumps(output, indent=2), encoding='utf-8') + temporary.replace(directory / 'windows_metrics.json') + + +class WindowsMetricsCollector: + def __init__(self): + self.process = None + atexit.register(self.stop) + + def start(self, env_vars): + if env_vars.get('WSL2', '').lower() != 'true': + return + self.stop() + try: + directory = Path(env_vars['RESULTS_DIR']).resolve() + directory.mkdir(parents=True, exist_ok=True) + output = directory / 'windows_metrics.json' + output.write_text(json.dumps({'metrics': blank_metrics()}), encoding='utf-8') + for name in ('windows_pcm_raw.log', 'windows_pcm.log', 'windows_metrics.json.tmp'): + (directory / name).unlink(missing_ok=True) + paths = [] + for path in (Path(__file__).resolve(), directory): + paths.append(subprocess.check_output( + ['wslpath', '-w', str(path)], text=True, timeout=5).strip()) + command = [env_vars.get('WINDOWS_PYTHON', 'python.exe'), '-u', + paths[0], '--output-dir', paths[1]] + if env_vars.get('WINDOWS_PCM_EXE'): + command.extend(['--pcm-exe', env_vars['WINDOWS_PCM_EXE']]) + for variable, option in ( + ('WINDOWS_LHM_DLL', '--lhm-dll'), + ('WINDOWS_GPU_POWER_SENSOR', '--gpu-power-sensor'), + ('WINDOWS_GPU_POWER_ADAPTER', '--gpu-power-adapter')): + if env_vars.get(variable): + command.extend([option, env_vars[variable]]) + command.extend(['--init-duration', env_vars.get('INIT_DURATION', '0')]) + with (directory / 'windows_metrics.log').open('w') as log: + self.process = subprocess.Popen( + command, stdin=subprocess.PIPE, stdout=log, + stderr=subprocess.STDOUT, env=env_vars) + print(f'Windows hardware collector started: {directory}') + except (OSError, subprocess.SubprocessError, KeyError, ValueError) as error: + print(f'WARN: Windows hardware collection unavailable: {error}') + + def stop(self): + process, self.process = self.process, None + if process is None: + return + try: + process.stdin.close() + process.wait(timeout=15) + if process.returncode: + print('WARN: Windows hardware collector failed; see windows_metrics.log') + except (OSError, subprocess.SubprocessError) as error: + print(f'WARN: Windows hardware collector shutdown: {error}') + try: + process.kill() + process.wait(timeout=5) + except (OSError, subprocess.SubprocessError): + pass + + +collector = WindowsMetricsCollector() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output-dir', required=True) + parser.add_argument('--init-duration', type=float, default=0) + parser.add_argument('--pcm-exe') + parser.add_argument('--lhm-dll') + parser.add_argument('--gpu-power-sensor') + parser.add_argument('--gpu-power-adapter') + parser.add_argument('--list-gpu-power-sensors', action='store_true') + args = parser.parse_args() + if os.name != 'nt': + parser.error('This worker must run under Windows Python, launched by the WSL benchmark') + if args.list_gpu_power_sensors: + if not args.lhm_dll: + parser.error('--list-gpu-power-sensors requires --lhm-dll') + reader = GpuPowerReader(args.lhm_dll) + try: + reader.sensors() + threading.Event().wait(1) + sensors = reader.sensors() + directory = Path(args.output_dir) + directory.mkdir(parents=True, exist_ok=True) + destination = directory / 'windows_gpu_power_sensors.json' + destination.write_text(json.dumps(sensors, indent=2), encoding='utf-8') + print(json.dumps(sensors, indent=2)) + finally: + reader.close() + sys.exit(0) + stopped = threading.Event() + + def wait_for_parent(): + sys.stdin.buffer.read() + stopped.set() + + threading.Thread(target=wait_for_parent, daemon=True).start() + collect(args.output_dir, stopped, max(0, args.init_duration), args.pcm_exe, + args.lhm_dll, args.gpu_power_sensor, args.gpu_power_adapter) \ No newline at end of file From bd1704cc47f0fe7d932e5b0b7b9f28d09755d14b Mon Sep 17 00:00:00 2001 From: rishika-y-gaur Date: Mon, 21 Sep 2026 23:50:32 +0530 Subject: [PATCH 3/5] fix: added variables --- benchmark-scripts/benchmark_test.py | 26 +++++++++++++ benchmark-scripts/windows_metrics.py | 57 +++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/benchmark-scripts/benchmark_test.py b/benchmark-scripts/benchmark_test.py index 9872586..113281e 100644 --- a/benchmark-scripts/benchmark_test.py +++ b/benchmark-scripts/benchmark_test.py @@ -17,6 +17,32 @@ class WindowsMetricsTesting(unittest.TestCase): + def test_python_discovery_launcher_fallback(self): + windows_path = r'C:\Users\intel\AppData\Local\Programs\Python\Python311\python.exe' + linux_path = '/mnt/c/Users/intel/AppData/Local/Programs/Python/Python311/python.exe' + with mock.patch.object(windows_metrics.subprocess, 'check_output', side_effect=[ + subprocess.CalledProcessError(9009, 'python.exe'), + json.dumps(windows_path), linux_path, json.dumps(windows_path)]) as probe: + self.assertEqual(windows_metrics.resolve_windows_python({}), linux_path) + self.assertEqual(probe.call_args_list[1].args[0][:2], ['py.exe', '-3.11']) + self.assertEqual(probe.call_args_list[-1].args[0][0], linux_path) + + def test_python_discovery_explicit_path_with_spaces(self): + executable = '/mnt/c/Program Files/Python311/python.exe' + with mock.patch.object(windows_metrics.subprocess, 'check_output', + return_value=json.dumps(executable)) as probe: + self.assertEqual(windows_metrics.resolve_windows_python( + {'WINDOWS_PYTHON': executable}), executable) + self.assertEqual(probe.call_args_list[0].args[0][0], executable) + + def test_python_discovery_failure_and_override(self): + for env, count in (({}, 3), ({'WINDOWS_PYTHON': '/missing/python.exe'}, 1)): + with mock.patch.object(windows_metrics.subprocess, 'check_output', + side_effect=FileNotFoundError()) as probe: + with self.assertRaisesRegex(ValueError, 'No working Windows Python'): + windows_metrics.resolve_windows_python(env) + self.assertEqual(probe.call_count, count) + def test_gpu_power_rejects_cpu_and_preserves_missing_values(self): reader = windows_metrics.GpuPowerReader.__new__(windows_metrics.GpuPowerReader) sensor = SimpleNamespace(SensorType='Power', Value=None, diff --git a/benchmark-scripts/windows_metrics.py b/benchmark-scripts/windows_metrics.py index 9ab03f1..a7c2fbd 100644 --- a/benchmark-scripts/windows_metrics.py +++ b/benchmark-scripts/windows_metrics.py @@ -257,6 +257,42 @@ def collect(directory, stop, init_duration=0, pcm_exe=None, temporary.replace(directory / 'windows_metrics.json') +def resolve_windows_python(env_vars): + override = env_vars.get('WINDOWS_PYTHON', '').strip() + probe = 'import json, os, sys; assert os.name == "nt"; print(json.dumps(sys.executable))' + + def linux_path(path): + if re.match(r'^[A-Za-z]:[\\/]', path) or path.startswith('\\\\'): + return subprocess.check_output( + ['wslpath', '-u', path], env=env_vars, text=True, + stderr=subprocess.PIPE, timeout=10).strip() + return path + + candidates = [[override]] if override else [ + ['python.exe'], ['py.exe', '-3.11'], ['py.exe', '-3']] + for candidate in candidates: + try: + command = [linux_path(candidate[0]), *candidate[1:]] + result = subprocess.check_output( + [*command, '-c', probe], env=env_vars, text=True, + stderr=subprocess.PIPE, timeout=15) + executable = json.loads(result.strip()) + if not isinstance(executable, str) or not executable: + raise ValueError('Windows Python returned no executable path') + executable = linux_path(executable) + subprocess.check_output( + [executable, '-c', probe], env=env_vars, text=True, + stderr=subprocess.PIPE, timeout=15) + return executable + except (OSError, subprocess.SubprocessError, ValueError): + continue + raise ValueError( + 'No working Windows Python found. Install Windows Python with the py.exe launcher, ' + 'or set WINDOWS_PYTHON to its executable path (for example ' + '/mnt/c/Users/intel/AppData/Local/Programs/Python/Python311/python.exe). ' + 'The Microsoft Store placeholder is not a Python installation.') + + class WindowsMetricsCollector: def __init__(self): self.process = None @@ -277,7 +313,8 @@ def start(self, env_vars): for path in (Path(__file__).resolve(), directory): paths.append(subprocess.check_output( ['wslpath', '-w', str(path)], text=True, timeout=5).strip()) - command = [env_vars.get('WINDOWS_PYTHON', 'python.exe'), '-u', + executable = resolve_windows_python(env_vars) + command = [executable, '-u', paths[0], '--output-dir', paths[1]] if env_vars.get('WINDOWS_PCM_EXE'): command.extend(['--pcm-exe', env_vars['WINDOWS_PCM_EXE']]) @@ -319,7 +356,10 @@ def stop(self): if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--output-dir', required=True) + setup = parser.add_mutually_exclusive_group() + setup.add_argument('--resolve-python', action='store_true') + setup.add_argument('--install-dependencies', action='store_true') + parser.add_argument('--output-dir') parser.add_argument('--init-duration', type=float, default=0) parser.add_argument('--pcm-exe') parser.add_argument('--lhm-dll') @@ -327,6 +367,19 @@ def stop(self): parser.add_argument('--gpu-power-adapter') parser.add_argument('--list-gpu-power-sensors', action='store_true') args = parser.parse_args() + if args.resolve_python or args.install_dependencies: + if os.name == 'nt' or os.environ.get('WSL2', '').lower() != 'true': + parser.error('Windows Python discovery must be invoked from WSL with WSL2=true') + try: + executable = resolve_windows_python(os.environ.copy()) + print(executable, flush=True) + if args.install_dependencies: + subprocess.run([executable, '-m', 'pip', 'install', 'psutil', 'pywin32'], check=True) + except (OSError, subprocess.SubprocessError, ValueError) as error: + parser.exit(1, f'ERROR: {error}\n') + sys.exit(0) + if not args.output_dir: + parser.error('--output-dir is required for collection or sensor discovery') if os.name != 'nt': parser.error('This worker must run under Windows Python, launched by the WSL benchmark') if args.list_gpu_power_sensors: From b3692c2cddf30b394bd3781dacfe210c8ab2f625 Mon Sep 17 00:00:00 2001 From: rishika-y-gaur Date: Tue, 22 Sep 2026 09:46:01 +0530 Subject: [PATCH 4/5] fix: added variables --- benchmark-scripts/benchmark_test.py | 49 +++++++++++------------ benchmark-scripts/windows_metrics.py | 58 ++-------------------------- 2 files changed, 29 insertions(+), 78 deletions(-) diff --git a/benchmark-scripts/benchmark_test.py b/benchmark-scripts/benchmark_test.py index 113281e..2a42e0e 100644 --- a/benchmark-scripts/benchmark_test.py +++ b/benchmark-scripts/benchmark_test.py @@ -17,31 +17,32 @@ class WindowsMetricsTesting(unittest.TestCase): - def test_python_discovery_launcher_fallback(self): - windows_path = r'C:\Users\intel\AppData\Local\Programs\Python\Python311\python.exe' - linux_path = '/mnt/c/Users/intel/AppData/Local/Programs/Python/Python311/python.exe' - with mock.patch.object(windows_metrics.subprocess, 'check_output', side_effect=[ - subprocess.CalledProcessError(9009, 'python.exe'), - json.dumps(windows_path), linux_path, json.dumps(windows_path)]) as probe: - self.assertEqual(windows_metrics.resolve_windows_python({}), linux_path) - self.assertEqual(probe.call_args_list[1].args[0][:2], ['py.exe', '-3.11']) - self.assertEqual(probe.call_args_list[-1].args[0][0], linux_path) - - def test_python_discovery_explicit_path_with_spaces(self): + def test_configured_python_path_with_spaces(self): executable = '/mnt/c/Program Files/Python311/python.exe' - with mock.patch.object(windows_metrics.subprocess, 'check_output', - return_value=json.dumps(executable)) as probe: - self.assertEqual(windows_metrics.resolve_windows_python( - {'WINDOWS_PYTHON': executable}), executable) - self.assertEqual(probe.call_args_list[0].args[0][0], executable) - - def test_python_discovery_failure_and_override(self): - for env, count in (({}, 3), ({'WINDOWS_PYTHON': '/missing/python.exe'}, 1)): - with mock.patch.object(windows_metrics.subprocess, 'check_output', - side_effect=FileNotFoundError()) as probe: - with self.assertRaisesRegex(ValueError, 'No working Windows Python'): - windows_metrics.resolve_windows_python(env) - self.assertEqual(probe.call_count, count) + collector = windows_metrics.WindowsMetricsCollector() + with tempfile.TemporaryDirectory() as directory, \ + mock.patch.object(windows_metrics.subprocess, 'check_output', + side_effect=['C:\\helper.py', 'C:\\results']) as paths, \ + mock.patch.object(windows_metrics.subprocess, 'Popen') as launch: + launch.return_value.returncode = 0 + collector.start({'WSL2': 'true', 'RESULTS_DIR': directory, + 'WINDOWS_PYTHON': executable}) + self.assertEqual(launch.call_args.args[0][0], executable) + self.assertEqual(paths.call_count, 2) + collector.stop() + + def test_configured_python_failure_does_not_try_fallback(self): + collector = windows_metrics.WindowsMetricsCollector() + with tempfile.TemporaryDirectory() as directory, \ + mock.patch.object(windows_metrics.subprocess, 'check_output', + side_effect=['C:\\helper.py', 'C:\\results']) as paths, \ + mock.patch.object(windows_metrics.subprocess, 'Popen', + side_effect=FileNotFoundError()) as launch: + collector.start({'WSL2': 'true', 'RESULTS_DIR': directory, + 'WINDOWS_PYTHON': '/missing/python.exe'}) + launch.assert_called_once() + self.assertEqual(paths.call_count, 2) + self.assertIsNone(collector.process) def test_gpu_power_rejects_cpu_and_preserves_missing_values(self): reader = windows_metrics.GpuPowerReader.__new__(windows_metrics.GpuPowerReader) diff --git a/benchmark-scripts/windows_metrics.py b/benchmark-scripts/windows_metrics.py index a7c2fbd..2fab183 100644 --- a/benchmark-scripts/windows_metrics.py +++ b/benchmark-scripts/windows_metrics.py @@ -257,42 +257,6 @@ def collect(directory, stop, init_duration=0, pcm_exe=None, temporary.replace(directory / 'windows_metrics.json') -def resolve_windows_python(env_vars): - override = env_vars.get('WINDOWS_PYTHON', '').strip() - probe = 'import json, os, sys; assert os.name == "nt"; print(json.dumps(sys.executable))' - - def linux_path(path): - if re.match(r'^[A-Za-z]:[\\/]', path) or path.startswith('\\\\'): - return subprocess.check_output( - ['wslpath', '-u', path], env=env_vars, text=True, - stderr=subprocess.PIPE, timeout=10).strip() - return path - - candidates = [[override]] if override else [ - ['python.exe'], ['py.exe', '-3.11'], ['py.exe', '-3']] - for candidate in candidates: - try: - command = [linux_path(candidate[0]), *candidate[1:]] - result = subprocess.check_output( - [*command, '-c', probe], env=env_vars, text=True, - stderr=subprocess.PIPE, timeout=15) - executable = json.loads(result.strip()) - if not isinstance(executable, str) or not executable: - raise ValueError('Windows Python returned no executable path') - executable = linux_path(executable) - subprocess.check_output( - [executable, '-c', probe], env=env_vars, text=True, - stderr=subprocess.PIPE, timeout=15) - return executable - except (OSError, subprocess.SubprocessError, ValueError): - continue - raise ValueError( - 'No working Windows Python found. Install Windows Python with the py.exe launcher, ' - 'or set WINDOWS_PYTHON to its executable path (for example ' - '/mnt/c/Users/intel/AppData/Local/Programs/Python/Python311/python.exe). ' - 'The Microsoft Store placeholder is not a Python installation.') - - class WindowsMetricsCollector: def __init__(self): self.process = None @@ -313,7 +277,9 @@ def start(self, env_vars): for path in (Path(__file__).resolve(), directory): paths.append(subprocess.check_output( ['wslpath', '-w', str(path)], text=True, timeout=5).strip()) - executable = resolve_windows_python(env_vars) + executable = env_vars.get('WINDOWS_PYTHON', 'python.exe').strip() + if not executable: + raise ValueError('WINDOWS_PYTHON must name a Windows Python executable') command = [executable, '-u', paths[0], '--output-dir', paths[1]] if env_vars.get('WINDOWS_PCM_EXE'): @@ -356,10 +322,7 @@ def stop(self): if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) - setup = parser.add_mutually_exclusive_group() - setup.add_argument('--resolve-python', action='store_true') - setup.add_argument('--install-dependencies', action='store_true') - parser.add_argument('--output-dir') + parser.add_argument('--output-dir', required=True) parser.add_argument('--init-duration', type=float, default=0) parser.add_argument('--pcm-exe') parser.add_argument('--lhm-dll') @@ -367,19 +330,6 @@ def stop(self): parser.add_argument('--gpu-power-adapter') parser.add_argument('--list-gpu-power-sensors', action='store_true') args = parser.parse_args() - if args.resolve_python or args.install_dependencies: - if os.name == 'nt' or os.environ.get('WSL2', '').lower() != 'true': - parser.error('Windows Python discovery must be invoked from WSL with WSL2=true') - try: - executable = resolve_windows_python(os.environ.copy()) - print(executable, flush=True) - if args.install_dependencies: - subprocess.run([executable, '-m', 'pip', 'install', 'psutil', 'pywin32'], check=True) - except (OSError, subprocess.SubprocessError, ValueError) as error: - parser.exit(1, f'ERROR: {error}\n') - sys.exit(0) - if not args.output_dir: - parser.error('--output-dir is required for collection or sensor discovery') if os.name != 'nt': parser.error('This worker must run under Windows Python, launched by the WSL benchmark') if args.list_gpu_power_sensors: From 0163b59c25fee0d1b3d1b48cb16cde2efe4a7efd Mon Sep 17 00:00:00 2001 From: rishika-y-gaur Date: Fri, 25 Sep 2026 06:59:06 +0530 Subject: [PATCH 5/5] fix: cpu enabled as well --- benchmark-scripts/windows_metrics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/benchmark-scripts/windows_metrics.py b/benchmark-scripts/windows_metrics.py index 2fab183..660f8e1 100644 --- a/benchmark-scripts/windows_metrics.py +++ b/benchmark-scripts/windows_metrics.py @@ -37,6 +37,7 @@ def __init__(self, dll): clr.AddReference(str(library)) from LibreHardwareMonitor.Hardware import Computer self.computer = Computer() + self.computer.IsCpuEnabled = True self.computer.IsGpuEnabled = True try: self.computer.Open()