├── .circleci └── config.yml ├── .gitignore ├── CITATION.cff ├── LICENSE ├── README.md ├── docs ├── API │ ├── API.rst │ ├── Device_API.rst │ ├── Handler_API.rst │ └── main_api.rst ├── Makefile ├── conf.py ├── devices │ ├── devices.rst │ ├── intel_cpu.rst │ └── nvidia_gpu.rst ├── generate.sh ├── handlers │ ├── csv_handler.rst │ ├── handlers.rst │ ├── mongo_handler.rst │ ├── pandas_handler.rst │ └── print_handler.rst ├── index.rst ├── make.bat ├── requirements.txt └── usages │ ├── context_manager.rst │ ├── decorator.rst │ ├── manual_usage.rst │ └── usage.rst ├── pyJoules ├── __init__.py ├── device │ ├── __init__.py │ ├── device.py │ ├── device_factory.py │ ├── domain.py │ ├── nvidia_device.py │ └── rapl_device.py ├── energy_meter.py ├── energy_trace.py ├── exception.py └── handler │ ├── __init__.py │ ├── csv_handler.py │ ├── handler.py │ ├── mongo_handler.py │ ├── pandas_handler.py │ └── print_handler.py ├── rapl_domains.png ├── scenario.md ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── acceptation ├── __init__.py ├── test_measure_trace_with_class.py ├── test_measure_trace_with_decorator.py ├── test_measure_trace_with_decorator_output_to_csv_file.py ├── test_measure_trace_with_energy_context.py └── test_measure_trace_with_energy_context_output_to_csv_file.py ├── integration └── energy_handler │ └── test_MongoHandler.py ├── unit ├── __init__.py ├── device │ ├── __init__.py │ ├── nvidia_device │ │ ├── __init__.py │ │ ├── test_NvidiaDevice.py │ │ └── test_NvidiaGPUDomain.py │ ├── rapl_device │ │ ├── __init__.py │ │ ├── test_RaplDevice.py │ │ └── test_RaplDomain.py │ └── test_EnergyDeviceFactory.py ├── energy_meter │ ├── __init__.py │ ├── test_EnergyMeter.py │ └── test_EnergyState.py ├── energy_sample │ └── test_EnergyTrace.py └── handler │ ├── test_CSVHandler.py │ ├── test_PandasHandler.py │ └── test_trace_to_dict.py └── utils ├── __init__.py ├── fake_api.py ├── fake_nvidia_api.py ├── rapl_fs.py └── sample.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # PyJoules CircleCI configuration file 2 | version: 2.1 3 | jobs: 4 | build: 5 | docker: 6 | # Language image for build and unit tests 7 | - image: circleci/python:3.7 8 | 9 | # Service image for integration tests 10 | - image: circleci/mongo:4.0 11 | 12 | working_directory: ~/repo 13 | 14 | steps: 15 | - checkout 16 | 17 | # Download and cache dependencies 18 | - restore_cache: 19 | keys: 20 | - v1-dependencies-{{ checksum "setup.cfg" }} 21 | # fallback to using the latest cache if no exact match is found 22 | - v1-dependencies- 23 | 24 | - run: 25 | name: install dependencies 26 | command: | 27 | python3 -m venv venv 28 | . venv/bin/activate 29 | pip3 install -e ".[nvidia, pandas, mongodb]" 30 | - save_cache: 31 | paths: 32 | - ./venv 33 | key: v1-dependencies-{{ checksum "setup.cfg" }} 34 | 35 | # Run unit and integration tests 36 | - run: 37 | name: run tests 38 | command: | 39 | . venv/bin/activate 40 | python3 setup.py test 41 | publish-release: 42 | docker: 43 | - image: circleci/python:3.7 44 | 45 | steps: 46 | - checkout 47 | 48 | - run: 49 | name: check git tag with package version 50 | command: | 51 | python3 -c "import os, pyJoules; exit(os.environ.get('CIRCLE_TAG', '?')[1:] != pyJoules .__version__)" 52 | - run: 53 | name: prepare environment 54 | command: | 55 | python3 -m venv venv 56 | . venv/bin/activate 57 | pip3 install -U pip twine 58 | - run: 59 | name: init .pypirc 60 | command: | 61 | echo -e "[pypi]" >> ~/.pypirc 62 | echo -e "username = powerapi" >> ~/.pypirc 63 | echo -e "password = $PYPI_PASSWORD" >> ~/.pypirc 64 | - run: 65 | name: generate package 66 | command: | 67 | python3 setup.py sdist bdist_wheel 68 | - run: 69 | name: upload to pypi 70 | command: | 71 | . venv/bin/activate 72 | twine upload dist/* 73 | workflows: 74 | version: 2 75 | build-n-publish: 76 | jobs: 77 | - build: 78 | filters: 79 | tags: 80 | only: /.*/ 81 | 82 | - publish-release: 83 | requires: 84 | - build 85 | filters: 86 | branches: 87 | ignore: /.*/ 88 | tags: 89 | only: /^v.*/ 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/vim,emacs,python,intellij+all,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=vim,emacs,python,intellij+all,visualstudiocode 4 | 5 | ### Emacs ### 6 | # -*- mode: gitignore; -*- 7 | *~ 8 | \#*\# 9 | /.emacs.desktop 10 | /.emacs.desktop.lock 11 | *.elc 12 | auto-save-list 13 | tramp 14 | .\#* 15 | 16 | # Org-mode 17 | .org-id-locations 18 | *_archive 19 | 20 | # flymake-mode 21 | *_flymake.* 22 | 23 | # eshell files 24 | /eshell/history 25 | /eshell/lastdir 26 | 27 | # elpa packages 28 | /elpa/ 29 | 30 | # reftex files 31 | *.rel 32 | 33 | # AUCTeX auto folder 34 | /auto/ 35 | 36 | # cask packages 37 | .cask/ 38 | dist/ 39 | 40 | # Flycheck 41 | flycheck_*.el 42 | 43 | # server auth directory 44 | /server/ 45 | 46 | # projectiles files 47 | .projectile 48 | 49 | # directory configuration 50 | .dir-locals.el 51 | 52 | # network security 53 | /network-security.data 54 | 55 | 56 | ### Intellij+all ### 57 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 58 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 59 | 60 | # User-specific stuff 61 | .idea/**/workspace.xml 62 | .idea/**/tasks.xml 63 | .idea/**/usage.statistics.xml 64 | .idea/**/dictionaries 65 | .idea/**/shelf 66 | 67 | # Generated files 68 | .idea/**/contentModel.xml 69 | 70 | # Sensitive or high-churn files 71 | .idea/**/dataSources/ 72 | .idea/**/dataSources.ids 73 | .idea/**/dataSources.local.xml 74 | .idea/**/sqlDataSources.xml 75 | .idea/**/dynamic.xml 76 | .idea/**/uiDesigner.xml 77 | .idea/**/dbnavigator.xml 78 | 79 | # Gradle 80 | .idea/**/gradle.xml 81 | .idea/**/libraries 82 | 83 | # Gradle and Maven with auto-import 84 | # When using Gradle or Maven with auto-import, you should exclude module files, 85 | # since they will be recreated, and may cause churn. Uncomment if using 86 | # auto-import. 87 | # .idea/modules.xml 88 | # .idea/*.iml 89 | # .idea/modules 90 | # *.iml 91 | # *.ipr 92 | 93 | # CMake 94 | cmake-build-*/ 95 | 96 | # Mongo Explorer plugin 97 | .idea/**/mongoSettings.xml 98 | 99 | # File-based project format 100 | *.iws 101 | 102 | # IntelliJ 103 | out/ 104 | 105 | # mpeltonen/sbt-idea plugin 106 | .idea_modules/ 107 | 108 | # JIRA plugin 109 | atlassian-ide-plugin.xml 110 | 111 | # Cursive Clojure plugin 112 | .idea/replstate.xml 113 | 114 | # Crashlytics plugin (for Android Studio and IntelliJ) 115 | com_crashlytics_export_strings.xml 116 | crashlytics.properties 117 | crashlytics-build.properties 118 | fabric.properties 119 | 120 | # Editor-based Rest Client 121 | .idea/httpRequests 122 | 123 | # Android studio 3.1+ serialized cache file 124 | .idea/caches/build_file_checksums.ser 125 | 126 | ### Intellij+all Patch ### 127 | # Ignores the whole .idea folder and all .iml files 128 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 129 | 130 | .idea/ 131 | 132 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 133 | 134 | *.iml 135 | modules.xml 136 | .idea/misc.xml 137 | *.ipr 138 | 139 | # Sonarlint plugin 140 | .idea/sonarlint 141 | 142 | ### Python ### 143 | # Byte-compiled / optimized / DLL files 144 | __pycache__/ 145 | *.py[cod] 146 | *$py.class 147 | 148 | # C extensions 149 | *.so 150 | 151 | # Distribution / packaging 152 | .Python 153 | build/ 154 | develop-eggs/ 155 | downloads/ 156 | eggs/ 157 | .eggs/ 158 | lib/ 159 | lib64/ 160 | parts/ 161 | sdist/ 162 | var/ 163 | wheels/ 164 | pip-wheel-metadata/ 165 | share/python-wheels/ 166 | *.egg-info/ 167 | .installed.cfg 168 | *.egg 169 | MANIFEST 170 | 171 | # PyInstaller 172 | # Usually these files are written by a python script from a template 173 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 174 | *.manifest 175 | *.spec 176 | 177 | # Installer logs 178 | pip-log.txt 179 | pip-delete-this-directory.txt 180 | 181 | # Unit test / coverage reports 182 | htmlcov/ 183 | .tox/ 184 | .nox/ 185 | .coverage 186 | .coverage.* 187 | .cache 188 | nosetests.xml 189 | coverage.xml 190 | *.cover 191 | .hypothesis/ 192 | .pytest_cache/ 193 | 194 | # Translations 195 | *.mo 196 | *.pot 197 | 198 | # Scrapy stuff: 199 | .scrapy 200 | 201 | # Sphinx documentation 202 | docs/_build/ 203 | 204 | # PyBuilder 205 | target/ 206 | 207 | # pyenv 208 | .python-version 209 | 210 | # pipenv 211 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 212 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 213 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 214 | # install all needed dependencies. 215 | #Pipfile.lock 216 | 217 | # celery beat schedule file 218 | celerybeat-schedule 219 | 220 | # SageMath parsed files 221 | *.sage.py 222 | 223 | # Spyder project settings 224 | .spyderproject 225 | .spyproject 226 | 227 | # Rope project settings 228 | .ropeproject 229 | 230 | # Mr Developer 231 | .mr.developer.cfg 232 | .project 233 | .pydevproject 234 | 235 | # mkdocs documentation 236 | /site 237 | 238 | # mypy 239 | .mypy_cache/ 240 | .dmypy.json 241 | dmypy.json 242 | 243 | # Pyre type checker 244 | .pyre/ 245 | 246 | ### Vim ### 247 | # Swap 248 | [._]*.s[a-v][a-z] 249 | [._]*.sw[a-p] 250 | [._]s[a-rt-v][a-z] 251 | [._]ss[a-gi-z] 252 | [._]sw[a-p] 253 | 254 | # Session 255 | Session.vim 256 | Sessionx.vim 257 | 258 | # Temporary 259 | .netrwhist 260 | # Auto-generated tag files 261 | tags 262 | # Persistent undo 263 | [._]*.un~ 264 | 265 | ### VisualStudioCode ### 266 | .vscode/* 267 | !.vscode/settings.json 268 | !.vscode/tasks.json 269 | !.vscode/launch.json 270 | !.vscode/extensions.json 271 | 272 | ### VisualStudioCode Patch ### 273 | # Ignore all local history of files 274 | .history 275 | 276 | # End of https://www.gitignore.io/api/vim,emacs,python,intellij+all,visualstudiocode 277 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: "Pyjoules: Python library that measures python code snippets" 6 | message: Make your python code green again 7 | type: software 8 | date-released: 2019-11-19 9 | authors: 10 | - given-names: Mohammed chakib 11 | family-names: Belgaid 12 | email: chakib.belgaid@gmail.com 13 | orcid: 'https://orcid.org/0000-0002-5264-7426' 14 | affiliation: Inria university of Lille 15 | - given-names: Romain 16 | family-names: Rouvoy 17 | email: romain.rouvoy@inria.fr 18 | affiliation: inria university of lille 19 | orcid: 'https://orcid.org/0000-0003-1771-8791' 20 | - orcid: 'https://orcid.org/0000-0003-0006-6088' 21 | affiliation: 'Inria university of lille ' 22 | email: lionel.seinturier@univ-lille.fr 23 | family-names: Seinturier 24 | given-names: Lionel 25 | identifiers: 26 | - type: url 27 | value: 'https://pyjoules.readthedocs.io' 28 | repository-code: 'https://github.com/powerapi-ng/pyJoules' 29 | url: 'http://powerapi.org/' 30 | repository-artifact: 'https://pypi.org/project/pyJoules/' 31 | abstract: >- 32 | A tool to measure the energy consumption of python 33 | code snippets 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019, INRIA 4 | Copyright (c) 2019, University of Lille 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyJoules 2 | 3 | [![License: MIT](https://img.shields.io/pypi/l/pyRAPL)](https://spdx.org/licenses/MIT.html) 4 | [![Build Status](https://img.shields.io/circleci/build/github/powerapi-ng/pyJoules.svg)](https://circleci.com/gh/powerapi-ng/pyjoules) 5 | [![Doc Status](https://readthedocs.org/projects/pyjoules/badge/?version=latest)](https://pyjoules.readthedocs.io/en/latest/) 6 | 7 | # About 8 | **pyJoules** is a software toolkit to measure the energy footprint of a host machine along the execution of a piece of Python code. 9 | It monitors the energy consumed by specific device of the host machine such as : 10 | 11 | - intel CPU socket package 12 | - RAM (for intel server architectures) 13 | - intel integrated GPU (for client architectures) 14 | - nvidia GPU 15 | 16 | ## Limitation 17 | 18 | ### CPU, RAM and integrated GPU 19 | **pyJoules** uses the Intel "_Running Average Power Limit_" (RAPL) technology that estimates power consumption of the CPU, ram and integrated GPU. 20 | This technology is available on Intel CPU since the [Sandy Bridge generation](https://fr.wikipedia.org/wiki/Intel#Historique_des_microprocesseurs_produits)(2010). 21 | 22 | ### Nvidia GPU 23 | **pyJoules** uses the nvidia "_Nvidia Management Library_" technology to measure energy consumption of nvidia devices. The energy measurement API is only available on nvidia GPU with [Volta architecture](https://en.wikipedia.org/wiki/Volta_(microarchitecture))(2018) 24 | 25 | ### Windows and MacOS 26 | Only GNU/Linux support is available for the moment. We are working on Mac support 27 | 28 | ## Known issues 29 | RAPL energy counters overflow after several minutes or hours, potentially causing false-negative energy readings. 30 | 31 | pyJoules takes this into account and adds the counter's maximum possible value, `max_energy_range_uj`, to negative energy measurements. However, if a counter overflows twice during a single energy measurement, the reported energy will be `max_energy_range_uj` less than the expected value. 32 | 33 | 34 | # Installation 35 | 36 | ### Measurement frequency 37 | PyJoule use hardware measurement tools (intel RAPL, nvidia GPU tools, ...) to measure device energy consumption. Theses tools have a mesasurement frequency that depend of the device. Thus, you can't use Pyjoule to measure energy consumption during a period shorter than the device energy measurement frequency. Pyjoule will return null values if the measurement period is to short. 38 | 39 | ## Requirements 40 | 41 | - python >= 3.7 42 | - [nvml](https://developer.nvidia.com/nvidia-management-library-nvml) (if you want nvidia GPU support) 43 | 44 | ## Installation 45 | You can install **pyJoules** with pip: `pip install pyJoules` 46 | 47 | if you want to use pyJoule to also measure nvidia GPU energy consumption, you have to install it with nvidia driver support using this command : `pip install pyJoules[nvidia]`. 48 | 49 | # Basic usage 50 | 51 | This Readme describe basic usage of pyJoules. For more in depth description, read the documentation [here](https://pyjoules.readthedocs.io/en/latest/) 52 | 53 | Here are some basic usages of **pyJoules**. Please note that the reported energy consumption is not only the energy consumption of the code you are running. This includes the _global energy consumption_ of all the process running on the machine during this period, thus including the operating system and other applications. 54 | That is why we recommend to eliminate any extra programs that may alter the energy consumption of the machine hosting experiments and to keep _only_ the code under measurement (_i.e._, no extra applications, such as graphical interface, background running task...). This will give the closest measure to the real energy consumption of the measured code. 55 | 56 | ## Decorate a function to measure its energy consumption 57 | 58 | To measure the energy consumed by the machine during the execution of the function `foo()` run the following code: 59 | ```python 60 | from pyJoules.energy_meter import measure_energy 61 | 62 | @measure_energy 63 | def foo(): 64 | # Instructions to be evaluated. 65 | 66 | foo() 67 | ``` 68 | 69 | This will print on the console the recorded energy consumption of all the monitorable devices during the execution of function `foo`. 70 | 71 | ### Output description 72 | decorator basic usage will print iformation with this format : 73 | 74 | `begin timestamp : XXX; tag : YYY; duration : ZZZ;device_name: AAAA` 75 | 76 | with : 77 | - `begin timestamp` : monitored function launching time 78 | - `tag`: tag of the measure, if nothing is specified, this will be the function name 79 | - `duration`: function execution duration 80 | - `device_name`: power consumption of the device `device_name` in uJ 81 | 82 | for cpu and ram devices, device_name match the RAPL domain described on the image below plus the CPU socket id. Rapl domain are described [here](https://github.com/powerapi-ng/pyJoules/blob/master/README.md#rapl-domain-description) 83 | 84 | ## Configure the decorator specifying the device to monitor 85 | 86 | You can easily configure which device to monitor using the parameters of the `measureit` decorator. 87 | For example, the following example only monitors the CPU power consumption on the CPU socket `1` and the Nvidia GPU `0`. 88 | By default, **pyJoules** monitors all the available devices of the CPU sockets. 89 | ```python 90 | from pyJoules.energy_meter import measure_energy 91 | from pyJoules.device.rapl_device import RaplPackageDomain 92 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 93 | 94 | @measure_energy(domains=[RaplPackageDomain(1), NvidiaGPUDomain(0)]) 95 | def foo(): 96 | # Instructions to be evaluated. 97 | 98 | foo() 99 | ``` 100 | 101 | You can append the following domain list to monitor them : 102 | 103 | - `pyJoules.device.rapl_device.RaplPackageDomain` : CPU (specify the socket id in parameter) 104 | - `pyJoules.device.rapl_device.RaplDramDomain` : RAM (specify the socket id in parameter) 105 | - `pyJoules.device.rapl_device.RaplUncoreDomain` : integrated GPU (specify the socket id in parameter) 106 | - `pyJoules.device.rapl_device.RaplCoreDomain` : RAPL Core domain (specify the socket id in parameter) 107 | - `pyJoules.device.nvidia_device.NvidiaGPUDomain` : Nvidia GPU (specify the socket id in parameter) 108 | 109 | to understand which par of the cpu each RAPL domain monitor, see this [section](https://github.com/powerapi-ng/pyJoules/blob/master/README.md#rapl-domain-description) 110 | 111 | ## Configure the output of the decorator 112 | 113 | If you want to handle data with different output than the standard one, you can configure the decorator with an `EnergyHandler` instance from the `pyJoules.handler` module. 114 | 115 | As an example, if you want to write the recorded energy consumption in a .csv file: 116 | ```python 117 | from pyJoules.energy_meter import measure_energy 118 | from pyJoules.handler.csv_handler import CSVHandler 119 | 120 | csv_handler = CSVHandler('result.csv') 121 | 122 | @measure_energy(handler=csv_handler) 123 | def foo(): 124 | # Instructions to be evaluated. 125 | 126 | for _ in range(100): 127 | foo() 128 | 129 | csv_handler.save_data() 130 | ``` 131 | 132 | This will produce a csv file of 100 lines. Each line containing the energy 133 | consumption recorded during one execution of the function `foo`. 134 | Other predefined `Handler` classes exist to export data to *MongoDB* and *Panda* 135 | dataframe. 136 | 137 | ## Use a context manager to add tagged "_breakpoint_" in your measurment 138 | 139 | If you want to know where is the "_hot spots_" where your python code consume the 140 | most energy you can add "_breakpoints_" during the measurement process and tag 141 | them to know amount of energy consumed between this breakpoints. 142 | 143 | For this, you have to use a context manager to measure the energy 144 | consumption. It is configurable as the decorator. For example, here we use an 145 | `EnergyContext` to measure the power consumption of CPU `1` and nvidia gpu `0` 146 | and report it in a csv file : 147 | 148 | ```python 149 | from pyJoules.energy_meter import EnergyContext 150 | from pyJoules.device.rapl_device import RaplPackageDomain 151 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 152 | from pyJoules.handler.csv_handler import CSVHandler 153 | 154 | csv_handler = CSVHandler('result.csv') 155 | 156 | with EnergyContext(handler=csv_handler, domains=[RaplPackageDomain(1), NvidiaGPUDomain(0)], start_tag='foo') as ctx: 157 | foo() 158 | ctx.record(tag='bar') 159 | bar() 160 | 161 | csv_handler.save_data() 162 | ``` 163 | 164 | This will record the energy consumed : 165 | 166 | - between the beginning of the `EnergyContext` and the call of the `ctx.record` method 167 | - between the call of the `ctx.record` method and the end of the `EnergyContext` 168 | 169 | Each measured part will be written in the csv file. One line per part. 170 | 171 | # RAPL domain description 172 | 173 | RAPL domains match part of the cpu socket as described in this image : 174 | 175 | ![](https://raw.githubusercontent.com/powerapi-ng/pyJoules/master/rapl_domains.png) 176 | 177 | - Package : correspond to the wall cpu energy consumption 178 | - core : correpond to the sum of all cpu core energy consumption 179 | - uncore : correspond to the integrated GPU 180 | 181 | # Output 182 | 183 | The output structure of all domains are enabled including their respective units are as follows: 184 | 185 | | name | timestamp | tag | duration | package | dram | core | uncore | nvidia_gpu | 186 | |---|---|---|---|---|---|---|---|---| 187 | | **type** | datetime | str | ms | uJ | uJ | uJ | uJ | **mJ** | 188 | 189 | # Miscellaneous 190 | 191 | ## About 192 | 193 | **pyJoules** is an open-source project developed by the [Spirals research group](https://team.inria.fr/spirals) (University of Lille and Inria) that is part of the [PowerAPI](http://powerapi.org) initiative. 194 | 195 | The documentation is available [here](https://pyJoules.readthedocs.io/en/latest/). 196 | 197 | ## Mailing list 198 | 199 | You can follow the latest news and asks questions by subscribing to our mailing list. 200 | 201 | ## Contributing 202 | 203 | If you would like to contribute code, you can do so via GitHub by forking the repository and sending a pull request. 204 | 205 | When submitting code, please make every effort to follow existing coding conventions and style in order to keep the code as readable as possible. 206 | -------------------------------------------------------------------------------- /docs/API/API.rst: -------------------------------------------------------------------------------- 1 | API 2 | *** 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | main_api 8 | Handler_API 9 | Device_API 10 | -------------------------------------------------------------------------------- /docs/API/Device_API.rst: -------------------------------------------------------------------------------- 1 | Device API 2 | ********** 3 | .. automodule:: pyJoules.device 4 | 5 | Abstract Class 6 | ============== 7 | .. autoclass:: pyJoules.device.Domain 8 | :members: 9 | 10 | .. autoclass:: pyJoules.device.Device 11 | :members: 12 | 13 | RAPL Device Classes 14 | =================== 15 | .. autoclass:: pyJoules.device.rapl_device.RaplDevice 16 | :members: 17 | 18 | .. autoclass:: pyJoules.device.rapl_device.RaplDomain 19 | :members: 20 | 21 | .. autoclass:: pyJoules.device.rapl_device.RaplCoreDomain 22 | :members: 23 | 24 | .. autoclass:: pyJoules.device.rapl_device.RaplUncoreDomain 25 | :members: 26 | 27 | .. autoclass:: pyJoules.device.rapl_device.RaplDramDomain 28 | :members: 29 | 30 | .. autoclass:: pyJoules.device.rapl_device.RaplPackageDomain 31 | :members: 32 | 33 | 34 | Nvidia GPU Device Classes 35 | ========================= 36 | .. autoclass:: pyJoules.device.nvidia_device.NvidiaGPUDevice 37 | :members: 38 | 39 | .. autoclass:: pyJoules.device.nvidia_device.NvidiaGPUDomain 40 | :members: 41 | 42 | Exception 43 | ========= 44 | .. autoexception:: pyJoules.device.NotConfiguredDeviceException 45 | :members: 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/API/Handler_API.rst: -------------------------------------------------------------------------------- 1 | Handler API 2 | *********** 3 | 4 | .. automodule:: pyJoules.handler 5 | 6 | Abstract Class 7 | ============== 8 | .. autoclass:: pyJoules.handler.EnergyHandler 9 | :members: 10 | 11 | Class 12 | ===== 13 | .. autoclass:: pyJoules.handler.print_handler.PrintHandler 14 | :members: 15 | 16 | .. autoclass:: pyJoules.handler.csv_handler.CSVHandler 17 | :members: 18 | 19 | .. automethod:: __init__ 20 | 21 | .. autoclass:: pyJoules.handler.mongo_handler.MongoHandler 22 | :members: 23 | 24 | .. automethod:: __init__ 25 | 26 | .. autoclass:: pyJoules.handler.pandas_handler.PandasHandler 27 | :members: 28 | -------------------------------------------------------------------------------- /docs/API/main_api.rst: -------------------------------------------------------------------------------- 1 | Core modules 2 | ************ 3 | 4 | Decorator 5 | ========= 6 | .. autodecorator:: pyJoules.energy_meter.measure_energy 7 | 8 | Context 9 | ======= 10 | .. autoclass:: pyJoules.energy_meter.EnergyContext 11 | :members: 12 | 13 | .. automethod:: __init__ 14 | 15 | Class 16 | ===== 17 | .. autoclass:: pyJoules.energy_meter.EnergyMeter 18 | :members: 19 | 20 | .. automethod:: __init__ 21 | 22 | .. autoclass:: pyJoules.energy_trace.EnergyTrace 23 | :members: 24 | 25 | .. automethod:: __init__ 26 | 27 | .. autoclass:: pyJoules.energy_trace.EnergySample 28 | :members: 29 | 30 | .. autoclass:: pyJoules.device.device_factory.DeviceFactory 31 | :members: 32 | 33 | 34 | Exception 35 | ========= 36 | 37 | .. autoexception:: pyJoules.exception.PyJoulesException 38 | :members: 39 | .. autoexception:: pyJoules.exception.NoSuchDomainError 40 | :members: 41 | .. autoexception:: pyJoules.exception.NoSuchDeviceError 42 | :members: 43 | .. autoexception:: pyJoules.energy_meter.NoNextStateException 44 | :members: 45 | .. autoexception:: pyJoules.energy_meter.StateIsNotFinalError 46 | :members: 47 | .. autoexception:: pyJoules.energy_meter.EnergyMeterNotStartedError 48 | :members: 49 | .. autoexception:: pyJoules.energy_meter.EnergyMeterNotStoppedError 50 | :members: 51 | .. autoexception:: pyJoules.energy_meter.SampleNotFoundError 52 | :members: 53 | 54 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.append('../') 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'pyJoules' 21 | copyright = '2019, INRIA, University of Lille' 22 | author = 'INRIA, University of Lille' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.2.0' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.autodoc', 34 | 'sphinx_autodoc_typehints', 35 | 'sphinx_rtd_theme'] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = "sphinx_rtd_theme" 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ['_static'] 57 | -------------------------------------------------------------------------------- /docs/devices/devices.rst: -------------------------------------------------------------------------------- 1 | Devices 2 | ******* 3 | 4 | .. toctree:: 5 | intel_cpu 6 | nvidia_gpu 7 | -------------------------------------------------------------------------------- /docs/devices/intel_cpu.rst: -------------------------------------------------------------------------------- 1 | Intel CPU 2 | ********* 3 | 4 | PyJoules support energy consumption monitoring of intel cpu using the "Running Average Power Limit" (RAPL) technology. RAPL is available on CPU since the `Sandy Bridge generation`__ (2010) 5 | 6 | __ https://fr.wikipedia.org/wiki/Intel#Historique_des_microprocesseurs_produits 7 | 8 | Domains 9 | ======= 10 | You can monitor energy consumption from several part of a CPU, called domain. 11 | 12 | Each monitorable domain is described on this image : 13 | 14 | .. image:: https://raw.githubusercontent.com/powerapi-ng/pyJoules/master/rapl_domains.png 15 | 16 | With : 17 | 18 | - Package : correspond to the wall cpu energy consumption 19 | - core : correpond to the sum of all cpu core energy consumption 20 | - uncore : correspond to the integrated GPU 21 | 22 | Usage 23 | ===== 24 | To configure your function decorator, context manager or energy meter to measure specific part of a CPU, pass as ``domain`` attribute a list of instance of a subClass of ``pyJoules.device.rapl_device.RaplDomain`` corresponding to the domain you want to monitor. 25 | 26 | For example, if you want to configure a context manager to measure the energy consumed by the Core domain follow this example : 27 | 28 | .. code-block:: python 29 | 30 | from pyJoules.device.rapl_device import RaplCoreDomain 31 | with EnergyContext(domains=[RaplCoreDomain(0)): 32 | foo() 33 | 34 | You can use the following class to select the list of domain you want to monitor : 35 | 36 | - ``RaplPackageDomain`` : whole CPU socket (specify the socket id in parameter) 37 | - ``RaplDramDomain`` : RAM (specify the socket id in parameter) 38 | - ``RaplUncoreDomain`` : integrated GPU (specify the socket id in parameter) 39 | - ``RaplCoreDomain`` : RAPL Core domain (specify the socket id in parameter) 40 | -------------------------------------------------------------------------------- /docs/devices/nvidia_gpu.rst: -------------------------------------------------------------------------------- 1 | Nvidia GPU 2 | ********** 3 | **pyJoules** uses the nvidia "*Nvidia Management Library*" technology to measure energy consumption of nvidia devices. The energy measurement API is only available on nvidia GPU with `Volta architecture`__ (2018) 4 | 5 | __ https://en.wikipedia.org/wiki/Volta_(microarchitecture) 6 | 7 | Usage 8 | ===== 9 | To configure your function decorator, context manager or energy meter to measure the energy consumption of a GPU, pass as :code:`domain` attribute a list of instance of :code:`pyJoules.device.rapl_device.NvidiaGPUDomain`. 10 | 11 | For example, if you want to configure a context manager to measure the energy consumed by the gpu of id :code:`0` follow this example : 12 | 13 | .. code-block:: python 14 | 15 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 16 | with EnergyContext(domains=[NvidiaGPUDomain(0)): 17 | foo() 18 | -------------------------------------------------------------------------------- /docs/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -R _build 3 | virtualenv ../venv 4 | source ../venv/bin/activate 5 | # pip install sphinx 6 | # pip install sphinx-autodoc-typehints 7 | # pip install sphinx-rtd-theme 8 | # pip install pymongo 9 | # pip install pandas 10 | # pip install pynvml 11 | # sphinx-build ./ _build/ 12 | make html 13 | deactivate 14 | # rm -R ../venv 15 | -------------------------------------------------------------------------------- /docs/handlers/csv_handler.rst: -------------------------------------------------------------------------------- 1 | CSV Handler 2 | *********** 3 | 4 | This handler save the measured energy sample on a csv file 5 | 6 | How to Use it 7 | ------------- 8 | 9 | Create a ``CSVHandler`` instance and pass it as a parameter of the context manager or the function decorator. You have to specify the filename of the file that will store the energy sample. 10 | 11 | When the measure is done, you have to use the ``save_data`` method to write the data on disk. 12 | 13 | Example : 14 | 15 | .. code-block:: python 16 | 17 | from pyJoules.handler.csv_handler import CSVHandler 18 | csv_handler = CSVHandler('result.csv') 19 | 20 | with EnergyContext(handler=csv_handler, domains=[RaplPackageDomain(1), NvidiaGPUDomain(0)], start_tag='foo') as ctx: 21 | foo() 22 | ctx.record(tag='bar') 23 | bar() 24 | 25 | csv_handler.save_data() 26 | 27 | Output 28 | ------ 29 | 30 | The previous example will produce the following csv file ``result.csv`` 31 | 32 | .. code-block:: 33 | 34 | timestamp;tag;duration;package_0;nvidia_gpu_0 35 | AAAA;foo;BBBB;CCCC;DDDD 36 | AAAA2;bar;BBBB2;CCCC2;DDDD2 37 | 38 | with : 39 | 40 | - AAAA* : timestamp of the measured interval beginning 41 | - BBBB* duration of the measured interval (in seconds) 42 | - CCCC* energy consumed by CPU 0 during the measured interval 43 | - DDDD* energy consumed by GPU 0 during the measured interval 44 | -------------------------------------------------------------------------------- /docs/handlers/handlers.rst: -------------------------------------------------------------------------------- 1 | Handlers 2 | ******** 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | print_handler 8 | csv_handler 9 | pandas_handler 10 | mongo_handler 11 | -------------------------------------------------------------------------------- /docs/handlers/mongo_handler.rst: -------------------------------------------------------------------------------- 1 | MongoDB Handler 2 | *************** 3 | 4 | This handler save the measured energy sample on a mongoDB database 5 | 6 | How to Use it 7 | ------------- 8 | 9 | Create a ``MongoHandler`` instance and pass it as a parameter of the context manager or the function decorator. You have to specify the uri of the database, the database and the collection name. 10 | 11 | When the measure is done, you have to use the ``save_data`` method to store the data on base. 12 | 13 | Example : 14 | 15 | .. code-block:: python 16 | 17 | from pyJoules.handler.mongo_handler import MongoHandler 18 | mongo_handler = MongoHandler(uri='mongodb://localhost', database_name='db', collection_name='collection') 19 | 20 | with EnergyContext(handler=mongo_handler, domains=[RaplPackageDomain(1), NvidiaGPUDomain(0)], start_tag='foo') as ctx: 21 | foo() 22 | ctx.record(tag='bar') 23 | bar() 24 | 25 | mongo_handler.save_data() 26 | 27 | Output 28 | ------ 29 | 30 | The previous example will store the following record on mongo db database 31 | 32 | .. code-block:: JSON 33 | 34 | { 35 | "name":"trace_0", 36 | "trace":[ 37 | { 38 | "timestamp":"AAAA", 39 | "tag":"foo", 40 | "duration":"BBBB", 41 | "energy":{ 42 | "package_0":"CCCC", 43 | "nvidia_gpu_0":"DDDD" 44 | } 45 | }, 46 | { 47 | "timestamp":"AAAA2", 48 | "tag":"bar", 49 | "duration":"BBBB2", 50 | "energy":{ 51 | "package_0":"CCCC2", 52 | "nvidia_gpu_0":"DDDD2" 53 | } 54 | } 55 | ] 56 | } 57 | 58 | with : 59 | 60 | - AAAA* : timestamp of the measured interval beginning 61 | - BBBB* duration of the measured interval (in seconds) 62 | - CCCC* energy consumed by CPU 0 during the measured interval 63 | - DDDD* energy consumed by GPU 0 during the measured interval 64 | 65 | Trace name 66 | ^^^^^^^^^^ 67 | 68 | Each trace stored in the database is named. Trace name is computed by adding an integer (which is incremented each time a new trace is stored) to a string prefix. By default, this prefix is ``trace`` so the first trace you store will be named ``trace_0``, the second ``trace_1``. 69 | 70 | You can change this default prefix by specifying the ``trace_name_prefix`` with the prefix you want to use. 71 | -------------------------------------------------------------------------------- /docs/handlers/pandas_handler.rst: -------------------------------------------------------------------------------- 1 | Pandas Handler 2 | ************** 3 | 4 | This Handler save the measured energy sample on a panda Dataframe 5 | 6 | How to Use it 7 | ------------- 8 | 9 | Create a ``PandasHandler`` instance and pass it as a parameter of the context manager or the function decorator. 10 | 11 | When the measure is done, you can retrieve the dataframe using the ``get_dataframe`` method 12 | 13 | Example : 14 | 15 | .. code-block:: python 16 | 17 | from pyJoules.handler.pandas_handler import PandasHandler 18 | pandas_handler = PandasHandler() 19 | 20 | with EnergyContext(handler=pandas_handler, domains=[RaplPackageDomain(1), NvidiaGPUDomain(0)], start_tag='foo') as ctx: 21 | foo() 22 | ctx.record(tag='bar') 23 | bar() 24 | 25 | df = pandas_handler.get_dataframe() 26 | 27 | Output 28 | ------ 29 | 30 | This will produce the following dataframe : 31 | 32 | .. code-block:: 33 | 34 | timestamp tag duration package_0 nvidia_gpu_0 35 | 0 AAAA foo BBBB CCCC DDDD 36 | 1 AAAA2 bar BBBB2 CCCC2 DDDD2 37 | 38 | with : 39 | 40 | - AAAA* : timestamp of the measured interval beginning 41 | - BBBB* duration of the measured interval (in seconds) 42 | - CCCC* energy consumed by CPU 0 during the measured interval 43 | - DDDD* energy consumed by GPU 0 during the measured interval 44 | 45 | -------------------------------------------------------------------------------- /docs/handlers/print_handler.rst: -------------------------------------------------------------------------------- 1 | Print Handler 2 | ************* 3 | 4 | This handler print the measured energy sample on the standard output 5 | 6 | How to Use it 7 | ------------- 8 | 9 | This handler is the default handler. When you use a context manager and decorated function without specifying any handler, the ``PrintHandler`` will be used 10 | 11 | Example : 12 | 13 | .. code-block:: python 14 | 15 | with EnergyContext(domains=[RaplPackageDomain(1), NvidiaGPUDomain(0)], start_tag='foo') as ctx: 16 | foo() 17 | ctx.record(tag='bar') 18 | bar() 19 | 20 | Output 21 | ------ 22 | The previous example will produce the following result on the standard output 23 | 24 | 25 | .. code-block:: 26 | 27 | begin timestamp : AAAA; tag : foo; duration : BBBB; package_0 : CCCC; nvidia_gpu_0 : DDDD 28 | begin timestamp : AAAA2; tag : bar; duration : BBBB2; package_0 : CCCC2; nvidia_gpu_0 : DDDD2 29 | 30 | with : 31 | 32 | - AAAA* : timestamp of the measured interval beginning 33 | - BBBB* duration of the measured interval (in seconds) 34 | - CCCC* energy consumed by CPU 0 during the measured interval 35 | - DDDD* energy consumed by GPU 0 during the measured interval 36 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pyJoules documentation master file, created by 2 | sphinx-quickstart on Tue Oct 8 14:16:48 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. role:: raw-role(raw) 7 | :format: html latex 8 | 9 | Welcome to pyJoules's documentation! 10 | ************************************ 11 | 12 | About 13 | ===== 14 | 15 | **pyJoules** is a software toolkit to measure the energy footprint of a host machine along the execution of a piece of Python code. 16 | It monitors the energy consumed by specific device of the host machine such as : 17 | 18 | - intel CPU socket package 19 | - RAM (for intel server architectures) 20 | - intel integrated GPU (for client architectures) 21 | - nvidia GPU 22 | 23 | Limitation 24 | ---------- 25 | 26 | CPU, RAM and integrated GPU 27 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 28 | **pyJoules** uses the Intel "*Running Average Power Limit*" (RAPL) technology that estimates energy consumption of the CPU, ram and integrated GPU. 29 | This technology is available on Intel CPU since the `Sandy Bridge generation`__ (2010). 30 | 31 | __ https://fr.wikipedia.org/wiki/Intel#Historique_des_microprocesseurs_produits 32 | 33 | For the moment, **pyJoules** use the linux kernel API to get energy values reported by RAPL technology. That's why CPU, RAM and integrated GPU energy monitoring is not available on windows or MacOS. 34 | 35 | As RAPL is not provided by a virtual machine, **pyJoules** can't use it anymore to monitor energy consumption inside a virtual machine. 36 | 37 | Nvidia GPU 38 | ^^^^^^^^^^ 39 | **pyJoules** uses the nvidia "*Nvidia Management Library*" technology to measure energy consumption of nvidia devices. The energy measurement API is only available on nvidia GPU with `Volta architecture`__ (2018) 40 | 41 | __ https://en.wikipedia.org/wiki/Volta_(microarchitecture) 42 | 43 | Monitor only function energy consumption 44 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 45 | **pyjoules** monitor device energy consumption. The reported energy consumption is not only the energy consumption of the code you are running. This includes the *global energy consumption* of all the process running on the machine during this period, thus including the operating system and other applications. 46 | 47 | That is why we recommend to eliminate any extra programs that may alter the energy consumption of the machine hosting experiments and to keep only the code under measurement (*i.e.*, no extra applications, such as graphical interface, background running task...). This will give the closest measure to the real energy consumption of the measured code. 48 | 49 | Quickstart 50 | ========== 51 | 52 | Installation 53 | ------------ 54 | 55 | You can install **pyJoules** with pip : ``pip install pyJoules`` 56 | 57 | Decorate a function to measure its energy consumption 58 | ----------------------------------------------------- 59 | 60 | To measure the energy consumed by the machine during the execution of the 61 | function ``foo()`` run the following code:: 62 | 63 | To measure the energy consumed by the machine during the execution of the function ``foo()`` run the following code with the :raw-role:`` ``measure_energy`` :raw-role:`` decorator: 64 | 65 | .. code-block:: python 66 | 67 | from pyJoules.energy_meter import measure_energy 68 | 69 | @measure_energy 70 | def foo(): 71 | # Instructions to be evaluated. 72 | 73 | foo() 74 | 75 | 76 | This will print in the console the recorded energy consumption of all the monitorable devices during the execution of function ``foo``. 77 | 78 | 79 | 80 | Miscellaneous 81 | ============= 82 | 83 | PyJoules is an open-source project developed by the `Spirals research group`__ (University of Lille and Inria) that take part of the Powerapi_ initiative. 84 | 85 | .. _Powerapi: http://powerapi.org 86 | 87 | __ https://team.inria.fr/spirals 88 | 89 | Mailing list and contact 90 | ------------------------ 91 | 92 | You can contact the developer team with this address : :raw-role:`powerapi-staff@inria.fr` 93 | 94 | You can follow the latest news and asks questions by subscribing to our :raw-role:`mailing list` 95 | 96 | Contributing 97 | ------------ 98 | 99 | If you would like to contribute code you can do so via GitHub by forking the repository and sending a pull request. 100 | 101 | When submitting code, please make every effort to follow existing coding conventions and style in order to keep the code as readable as possible. 102 | 103 | 104 | Table of contents 105 | ================= 106 | 107 | .. toctree:: 108 | :maxdepth: 2 109 | 110 | About 111 | usages/usage 112 | handlers/handlers 113 | devices/devices 114 | API/API.rst 115 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-autodoc-typehints 2 | sphinx-rtd-theme 3 | pymongo 4 | pandas 5 | pynvml 6 | -------------------------------------------------------------------------------- /docs/usages/context_manager.rst: -------------------------------------------------------------------------------- 1 | Context manager 2 | *************** 3 | 4 | Use a context manager to add tagged "breakpoint" in your measurement 5 | -------------------------------------------------------------------- 6 | If you want to know where is the *hot spots* where your python code consume the 7 | most energy you can add *breakpoints* during the measurement process and tag 8 | them to know amount of energy consumed between this breakpoints. 9 | 10 | For this, you have to use a context manager to measure the energy 11 | consumption. It is configurable as the :raw-role:`` decorator :raw-role:``. For example, here we use an 12 | :raw-role:`` ``EnergyContext`` :raw-role:`` to measure the energy consumption of CPU ``1`` and nvidia gpu ``0`` 13 | and report it in a csv file 14 | 15 | .. code-block:: python 16 | 17 | from pyJoules.energy_meter import EnergyContext 18 | from pyJoules.device.rapl_device import RaplPackageDomain 19 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 20 | from pyJoules.handler.csv_handler import CSVHandler 21 | 22 | csv_handler = CSVHandler('result.csv') 23 | 24 | with EnergyContext(handler=csv_handler, domains=[RaplPackageDomain(1), NvidiaGPUDomain(0)], start_tag='foo') as ctx: 25 | foo() 26 | ctx.record(tag='bar') 27 | bar() 28 | 29 | csv_handler.save_data() 30 | 31 | This will record the energy consumed : 32 | 33 | - between the beginning of the :raw-role:`` ``EnergyContext`` :raw-role:`` and the call of the ``ctx.record`` method 34 | - between the call of the ``ctx.record`` method and the end of the :raw-role:`` ``EnergyContext`` :raw-role:`` 35 | 36 | Each measured part will be written in the csv file. One line per part. 37 | -------------------------------------------------------------------------------- /docs/usages/decorator.rst: -------------------------------------------------------------------------------- 1 | Decorator 2 | ********* 3 | 4 | Decorate a function to measure its energy consumption 5 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | 7 | To measure the energy consumed by the machine during the execution of the 8 | function ``foo()`` run the following code:: 9 | 10 | To measure the energy consumed by the machine during the execution of the function ``foo()`` run the following code with the :raw-role:`` ``measure_energy`` :raw-role:`` decorator: 11 | 12 | .. code-block:: python 13 | 14 | from pyJoules.energy_meter import measure_energy 15 | 16 | @measure_energy 17 | def foo(): 18 | # Instructions to be evaluated. 19 | 20 | foo() 21 | 22 | 23 | This will print in the console the recorded energy consumption of all the monitorable devices during the execution of function ``foo``. 24 | 25 | Configure the decorator specifying the device to monitor 26 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 27 | 28 | You can easily configure which device to monitor using the parameters of the :raw-role:`` ``measure_energy`` :raw-role:`` decorator. 29 | For example, the following example only monitors the CPU energy consumption on the CPU socket ``1`` and the Nvidia GPU ``0``. 30 | By default, **pyJoules** monitors all the available devices of the CPU sockets. 31 | 32 | __ free.fr 33 | 34 | .. code-block:: python 35 | 36 | from pyJoules.energy_meter import measure_energy 37 | from pyJoules.device.rapl_device import RaplPackageDomain 38 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 39 | 40 | @measure_energy(domains=[RaplPackageDomain(1), NvidiaGPUDomain(0)]) 41 | def foo(): 42 | # Instructions to be evaluated. 43 | 44 | foo() 45 | 46 | for more information about device you can monitor, see :raw-role:`` here :raw-role:``: 47 | 48 | 49 | Configure the output of the decorator 50 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 51 | 52 | If you want to handle data with different output than the standard one, you can configure the decorator with an :raw-role:`` ``EnergyHandler`` :raw-role:`` instance from the :raw-role:`` ``pyJoules.handler`` :raw-role:`` module. 53 | 54 | As an example, if you want to write the recorded energy consumption in a .csv file: 55 | 56 | .. code-block:: python 57 | 58 | from pyJoules.energy_meter import measure_energy 59 | from pyJoules.handler.csv_handler import CSVHandler 60 | 61 | csv_handler = CSVHandler('result.csv') 62 | 63 | @measure_energy(handler=csv_handler) 64 | def foo(): 65 | # Instructions to be evaluated. 66 | 67 | for _ in range(100): 68 | foo() 69 | 70 | csv_handler.save_data() 71 | 72 | This will produce a csv file of 100 lines. Each line containing the energy 73 | consumption recorded during one execution of the function ``foo``. 74 | Other predefined ``Handler`` classes exist to export data to *MongoDB* and *Panda* 75 | dataframe. 76 | -------------------------------------------------------------------------------- /docs/usages/manual_usage.rst: -------------------------------------------------------------------------------- 1 | Manual usage 2 | ************ 3 | 4 | Use a EnergyMeter to measure energy consumption without decorator or context manager 5 | ------------------------------------------------------------------------------------ 6 | 7 | If you want need more flexibility and measure energy consumption of piece of 8 | code that can't be bound inside a decorated function of a context manager, you 9 | can use an instance of :raw-role:`` ``EnergyMeter`` 11 | :raw-role:``. 12 | 13 | Instance of :raw-role:`` ``EnergyMeter`` 15 | :raw-role:`` is the underlayer tool used by context manager and 16 | decorator to measure energy consumption. 17 | 18 | Create the EnergyMeter 19 | ^^^^^^^^^^^^^^^^^^^^^^ 20 | 21 | Before using an energy meter, you have to create it with devices that it have to monitor. For this, use an :raw-role:`` ``DeviceFactory`` :raw-role:`` to create and configure the monitored devices 23 | 24 | The following piece of code show how to create an :raw-role:`` ``EnergyMeter`` 26 | :raw-role:`` that monitor CPU, DRAM and GPU energy consumption. 27 | 28 | .. code-block:: python 29 | 30 | domains = [RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)] 31 | devices = DeviceFactory.create_devices(domains) 32 | meter = EnergyMeter(devices) 33 | 34 | Tips : call the :raw-role:`` ``DeviceFactory.create_devices`` :raw-role:`` without parameter to get the list of all monitorable devices. 35 | 36 | Use the EnergyMeter 37 | ^^^^^^^^^^^^^^^^^^^ 38 | 39 | When you have your :raw-role:`` ``EnergyMeter`` 41 | :raw-role:`` you can use it to measure energy consumption of piece of code. 42 | 43 | An :raw-role:`` ``EnergyMeter`` 45 | :raw-role:`` have three main method : 46 | 47 | - :raw-role:`` ``start`` :raw-role:``: to start the energy consumption monitoring 48 | - :raw-role:`` ``record`` :raw-role:``: to tag a hotspot in monitored piece of code 49 | - :raw-role:`` ``stop`` :raw-role:``: to stop the energy consumption monitoring 50 | 51 | The following piece of code show how to use an :raw-role:`` ``EnergyMeter`` 53 | :raw-role:`` to monitor piece of code: 54 | 55 | .. code-block:: python 56 | 57 | meter.start(tag='foo') 58 | foo() 59 | meter.record(tag='bar') 60 | bar() 61 | meter.stop() 62 | 63 | Get the EnergyTrace 64 | ^^^^^^^^^^^^^^^^^^^ 65 | 66 | When you finished to measure the energy consumed during execution of your piece of code, you can retrieve its energy trace using the :raw-role:`` ``EnergyMeter.get_trace`` :raw-role:`` method 67 | 68 | This will return an iterator on some :raw-role:`` ``EnergySample`` :raw-role:``. Each energy sample contains energy consumption information measured between each call to :raw-role:`` ``start`` :raw-role:``, :raw-role:`` ``record`` :raw-role:`` and :raw-role:`` ``stop`` :raw-role:`` method. 69 | 70 | For example, the trace of the previous example contains two :raw-role:`` ``EnergySample`` :raw-role:``. One that contains the energy measured between :raw-role:`` ``start`` :raw-role:`` and :raw-role:`` ``record`` :raw-role:`` methods (during ``foo`` method execution) and the second that contains energy measured between :raw-role:`` ``record`` :raw-role:`` and :raw-role:`` ``stop`` :raw-role:`` method (during ``bar`` method execution) . 71 | 72 | Energy sample contains : 73 | 74 | - a tag 75 | - a timestamp (the beginning of the measure) 76 | - the duration of the measure 77 | - the energy consumed during the measure 78 | 79 | Full Example 80 | ^^^^^^^^^^^^ 81 | 82 | .. code-block:: python 83 | 84 | from pyJoules.device import DeviceFactory 85 | from pyJoules.device.rapl_device import RaplPackageDomain, RaplDramDomain 86 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 87 | from pyJoules.energy_meter import EnergyMeter 88 | 89 | domains = [RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)] 90 | devices = DeviceFactory.create_devices(domains) 91 | meter = EnergyMeter(devices) 92 | 93 | meter.start(tag='foo') 94 | foo() 95 | meter.record(tag='bar') 96 | bar() 97 | meter.stop() 98 | 99 | trace = meter.get_trace() 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/usages/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ***** 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | decorator 8 | context_manager 9 | manual_usage 10 | -------------------------------------------------------------------------------- /pyJoules/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | __version__ = '0.5.2' 21 | -------------------------------------------------------------------------------- /pyJoules/device/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from .domain import Domain 22 | from .device import Device, NotConfiguredDeviceException 23 | from .device_factory import DeviceFactory 24 | -------------------------------------------------------------------------------- /pyJoules/device/device.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import List, Optional 22 | 23 | from . import Domain 24 | from ..exception import PyJoulesException, NoSuchDomainError 25 | 26 | class NotConfiguredDeviceException(PyJoulesException): 27 | """ 28 | Exception raised when a user call a device method that need the device to be configured on a non configured device 29 | """ 30 | 31 | 32 | class Device: 33 | """ 34 | Interface to get energy consumption information about a specific device 35 | """ 36 | 37 | def __init__(self): 38 | self._configured_domains = None 39 | self._available_domains = self.available_domains() 40 | 41 | @staticmethod 42 | def available_domains() -> List[Domain]: 43 | """ 44 | Returns names of the domain that could be monitored on the Device 45 | :return: a list of domain names 46 | :raise NoSuchDeviceError: if the device is not available on this machine 47 | """ 48 | raise NotImplementedError() 49 | 50 | def configure(self, domains: List[Domain] = None): 51 | """ 52 | configure the device to return only the energy consumption of the given energy domain when calling the 53 | :py:meth:`pyJoules.device.Device.get_energy` method 54 | 55 | :param domains: domains to be monitored by the device, if None, all the available domain will be monitored 56 | :raise NoSuchDomainError: if one given domain could not be monitored on this machine 57 | """ 58 | if domains is None: 59 | domains = self._available_domains 60 | else: 61 | # check if given domain list are available 62 | for domain in domains: 63 | if domain not in self._available_domains: 64 | raise NoSuchDomainError(domain) 65 | 66 | self._configured_domains = domains 67 | 68 | def get_energy(self) -> List[float]: 69 | """ 70 | Get the energy consumption of the device since the last device reset 71 | 72 | :return: a list of each domain power consumption. Value order is the same than the domain order passed as 73 | argument to the :py:meth:`pyJoules.device.Device.configure` method. 74 | """ 75 | raise NotImplementedError() 76 | 77 | def get_configured_domains(self): 78 | """ 79 | Get the domains that was passed as argument to the configure function 80 | 81 | :return: A list of Domain 82 | :raise NotConfiguredDeviceException: if the device was not configured 83 | """ 84 | if self._configured_domains is None: 85 | raise NotConfiguredDeviceException 86 | 87 | return self._configured_domains 88 | -------------------------------------------------------------------------------- /pyJoules/device/device_factory.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import List, Optional 22 | import logging 23 | from operator import add 24 | from . import Domain, Device 25 | from .rapl_device import RaplDevice 26 | try: 27 | from .nvidia_device import NvidiaGPUDevice 28 | except ImportError: 29 | NvidiaGPUDevice=None 30 | logging.warning('pynvml not found you can\'t use NVIDIA devices') 31 | 32 | from ..exception import NoSuchDeviceError 33 | 34 | from functools import reduce 35 | 36 | 37 | class DeviceFactory: 38 | 39 | @staticmethod 40 | def _gen_all_available_domains() -> List[Device]: 41 | available_api = [RaplDevice] 42 | if NvidiaGPUDevice is not None : 43 | available_api.append(NvidiaGPUDevice) 44 | available_domains = [] 45 | for api in available_api: 46 | try: 47 | available_domains.append(api.available_domains()) 48 | except NoSuchDeviceError: 49 | pass 50 | flaten_available_domain_list = reduce(add, available_domains, []) 51 | return flaten_available_domain_list 52 | 53 | @staticmethod 54 | def create_devices(domains: Optional[Domain] = None) -> List[Device]: 55 | """ 56 | Create and configure the Device instance with the given Domains 57 | 58 | :param domains: a list of Domain instance that as to be monitored (if None, return a list of all 59 | monitorable devices) 60 | :return: a list of device configured with the given Domains 61 | :raise NoSuchDeviceError: if a domain depend on a device that doesn't exist on the current machine 62 | :raise NoSuchDomainError: if the given domain is not available on the device 63 | """ 64 | if domains is None: 65 | domains = DeviceFactory._gen_all_available_domains() 66 | 67 | grouped_domains = {} 68 | for device_type, domain in map(lambda d: (d.get_device_type(), d), domains): 69 | if device_type in grouped_domains: 70 | grouped_domains[device_type].append(domain) 71 | else: 72 | grouped_domains[device_type] = [domain] 73 | 74 | devices = [] 75 | 76 | for device_type in grouped_domains: 77 | device = device_type() 78 | device.configure(domains=grouped_domains[device_type]) 79 | devices.append(device) 80 | return devices 81 | -------------------------------------------------------------------------------- /pyJoules/device/domain.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | class Domain: 23 | """ 24 | Identify a domain, a monitorable sub-part of a device 25 | """ 26 | 27 | def __repr__(self) -> str: 28 | raise NotImplementedError() 29 | 30 | def get_device_type(self): 31 | raise NotImplementedError() 32 | -------------------------------------------------------------------------------- /pyJoules/device/nvidia_device.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import List 22 | 23 | 24 | from . import Device, Domain 25 | from ..exception import PyJoulesException, NoSuchDeviceError 26 | import pynvml 27 | 28 | class NvidiaGPUDomain(Domain): 29 | 30 | def __init__(self, device_id): 31 | Domain.__init__(self) 32 | self.device_id = device_id 33 | self._repr = 'nvidia_gpu_' + str(device_id) 34 | 35 | def __repr__(self): 36 | return self._repr 37 | 38 | def __eq__(self, other) -> bool: 39 | return isinstance(other, NvidiaGPUDomain) and self.__repr__() == other.__repr__() 40 | 41 | def __lt__(self, other) -> bool: 42 | if isinstance(other, NvidiaGPUDomain): 43 | return self.device_id < other.device_id 44 | raise ValueError() 45 | 46 | def __gt__(self, other) -> bool: 47 | if isinstance(other, NvidiaGPUDomain): 48 | return self.device_id > other.device_id 49 | raise ValueError() 50 | 51 | def get_device_type(self): 52 | return NvidiaGPUDevice 53 | 54 | 55 | class GPUDoesNotSupportEnergyMonitoringError(PyJoulesException): 56 | """ 57 | Exception raised when a NvidiaDevice is created but the GPU does not support energy monitoring 58 | """ 59 | 60 | 61 | class NvidiaGPUDevice(Device): 62 | """ 63 | Interface to get energy consumption of GPUs 64 | """ 65 | 66 | def __init__(self): 67 | """ 68 | :raise NoSuchDeviceError: if no Nvidia API is available on this machine 69 | :raise GPUDoesNotSupportEnergyMonitoringError: if the GPU does not support energy monitoring 70 | """ 71 | Device.__init__(self) 72 | self._handle = None 73 | 74 | def configure(self, domains: List[NvidiaGPUDomain] = None): 75 | Device.configure(self, domains) 76 | self._handle = [pynvml.nvmlDeviceGetHandleByIndex(domain.device_id) for domain in self._configured_domains] 77 | 78 | def get_energy(self): 79 | return [pynvml.nvmlDeviceGetTotalEnergyConsumption(handle) * 1000 for handle in self._handle] 80 | 81 | @staticmethod 82 | def available_domains() -> List[NvidiaGPUDomain]: 83 | try: 84 | pynvml.nvmlInit() 85 | except pynvml.NVMLError: 86 | raise NoSuchDeviceError() 87 | 88 | device_ids = pynvml.nvmlDeviceGetCount() 89 | domains = map(lambda device_id: NvidiaGPUDomain(device_id), range(device_ids)) 90 | return list(domains) 91 | -------------------------------------------------------------------------------- /pyJoules/device/rapl_device.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | import os 21 | from typing import List 22 | 23 | from . import Device, Domain, NotConfiguredDeviceException 24 | from pyJoules.exception import NoSuchDeviceError 25 | 26 | 27 | class RaplDomain(Domain): 28 | 29 | def __init__(self, socket: int): 30 | Domain.__init__(self) 31 | self.socket = socket 32 | self._repr = self.get_domain_name() + '_' + str(socket) 33 | 34 | def get_device_type(self): 35 | return RaplDevice 36 | 37 | def get_domain_name(self) -> str: 38 | """ 39 | :return: domain name without socket identifier 40 | """ 41 | raise NotImplementedError() 42 | 43 | def __repr__(self) -> str: 44 | """ 45 | :return: domain name with socket identifier 46 | """ 47 | return self._repr 48 | 49 | def __eq__(self, other) -> bool: 50 | return isinstance(other, RaplDomain) and self.__repr__() == other.__repr__() 51 | 52 | def __lt__(self, other) -> bool: 53 | if isinstance(other, RaplDomain): 54 | return self.__repr__() < other.__repr__() 55 | raise ValueError() 56 | 57 | def __gt__(self, other) -> bool: 58 | if isinstance(other, RaplDomain): 59 | return self.__repr__() > other.__repr__() 60 | raise ValueError() 61 | 62 | 63 | class RaplCoreDomain(RaplDomain): 64 | def get_domain_name(self): 65 | return "core" 66 | 67 | 68 | class RaplUncoreDomain(RaplDomain): 69 | def get_domain_name(self): 70 | return "uncore" 71 | 72 | 73 | class RaplDramDomain(RaplDomain): 74 | def get_domain_name(self): 75 | return "dram" 76 | 77 | 78 | class RaplPackageDomain(RaplDomain): 79 | def get_domain_name(self): 80 | return "package" 81 | 82 | 83 | RAPL_API_DIR = '/sys/class/powercap/intel-rapl' 84 | 85 | 86 | class RaplDevice(Device): 87 | """ 88 | Interface to get energy consumption of CPU domains 89 | """ 90 | 91 | def __init__(self): 92 | """ 93 | :raise NoSuchDeviceError: if no RAPL API is available on this machine 94 | """ 95 | Device.__init__(self) 96 | self._api_file_names = None 97 | 98 | @staticmethod 99 | def _rapl_api_available(): 100 | return os.path.exists(RAPL_API_DIR) 101 | 102 | @staticmethod 103 | def available_domains() -> List[RaplDomain]: 104 | """ 105 | return a the list of the available energy domains 106 | """ 107 | if not RaplDevice._rapl_api_available(): 108 | raise NoSuchDeviceError() 109 | 110 | return (RaplDevice.available_package_domains() + RaplDevice.available_dram_domains() + 111 | RaplDevice.available_core_domains() + RaplDevice.available_uncore_domains()) 112 | 113 | @staticmethod 114 | def _get_socket_id_list(): 115 | socket_id_list = [] 116 | socket_id = 0 117 | while True: 118 | name = RAPL_API_DIR + '/intel-rapl:' + str(socket_id) 119 | if os.path.exists(name): 120 | socket_id_list.append(socket_id) 121 | socket_id += 1 122 | else: 123 | return socket_id_list 124 | 125 | @staticmethod 126 | def available_package_domains() -> List[RaplPackageDomain]: 127 | """ 128 | return a the list of the available energy Package domains 129 | """ 130 | package_domains = [] 131 | 132 | for socket_id in RaplDevice._get_socket_id_list(): 133 | domain_name_file_str = RAPL_API_DIR + '/intel-rapl:' + str(socket_id) + '/name' 134 | if os.path.exists(domain_name_file_str): 135 | with open(domain_name_file_str) as domain_name_file: 136 | if domain_name_file.readline() == 'package-' + str(socket_id) + '\n': 137 | package_domains.append(RaplPackageDomain(socket_id)) 138 | return package_domains 139 | 140 | @staticmethod 141 | def _domain_exist_on_socket(socket_id, domain_name): 142 | domain_id = 0 143 | while True: 144 | domain_name_file_str = (RAPL_API_DIR + '/intel-rapl:' + str(socket_id) + '/intel-rapl:' + str(socket_id) + 145 | ':' + str(domain_id) + '/name') 146 | if os.path.exists(domain_name_file_str): 147 | with open(domain_name_file_str) as domain_name_file: 148 | if domain_name_file.readline() == domain_name + '\n': 149 | return True 150 | domain_id += 1 151 | else: 152 | return False 153 | 154 | @staticmethod 155 | def available_dram_domains() -> List[RaplDramDomain]: 156 | """ 157 | return a the list of the available energy Dram domains 158 | """ 159 | dram_domains = [] 160 | for socket_id in RaplDevice._get_socket_id_list(): 161 | if RaplDevice._domain_exist_on_socket(socket_id, 'dram'): 162 | dram_domains.append(RaplDramDomain(socket_id)) 163 | return dram_domains 164 | 165 | @staticmethod 166 | def available_core_domains() -> List[RaplCoreDomain]: 167 | """ 168 | return a the list of the available energy Core domains 169 | """ 170 | core_domains = [] 171 | for socket_id in RaplDevice._get_socket_id_list(): 172 | if RaplDevice._domain_exist_on_socket(socket_id, 'core'): 173 | core_domains.append(RaplCoreDomain(socket_id)) 174 | return core_domains 175 | 176 | @staticmethod 177 | def available_uncore_domains() -> List[RaplUncoreDomain]: 178 | """ 179 | return a the list of the available energy Uncore domains 180 | """ 181 | uncore_domains = [] 182 | for socket_id in RaplDevice._get_socket_id_list(): 183 | if RaplDevice._domain_exist_on_socket(socket_id, 'uncore'): 184 | uncore_domains.append(RaplUncoreDomain(socket_id)) 185 | return uncore_domains 186 | 187 | def _get_domain_file_name(self, domain): 188 | socket_id = domain.socket 189 | 190 | if isinstance(domain, RaplPackageDomain): 191 | return RAPL_API_DIR + '/intel-rapl:' + str(socket_id) + '/energy_uj' 192 | 193 | domain_id = 0 194 | while True: 195 | domain_name_file_str = (RAPL_API_DIR + '/intel-rapl:' + str(socket_id) + '/intel-rapl:' + str(socket_id) + 196 | ':' + str(domain_id) + '/name') 197 | if os.path.exists(domain_name_file_str): 198 | with open(domain_name_file_str) as domain_name_file: 199 | if domain_name_file.readline() == domain.get_domain_name() + '\n': 200 | return (RAPL_API_DIR + '/intel-rapl:' + str(socket_id) + '/intel-rapl:' + str(socket_id) + \ 201 | ':' + str(domain_id) + '/energy_uj') 202 | else: 203 | domain_id += 1 204 | else: 205 | raise ValueError() 206 | 207 | def _collect_domain_api_file_name(self, domain_list): 208 | return [self._get_domain_file_name(domain) for domain in domain_list] 209 | 210 | def configure(self, domains=None): 211 | Device.configure(self, domains) 212 | 213 | self._api_file_names = self._collect_domain_api_file_name(self._configured_domains) 214 | 215 | def _read_energy_value(self, api_file): 216 | return float(api_file.readline()) 217 | 218 | def get_energy(self): 219 | energies = [self._read_energy_value(open(api_file_name, 'r')) for api_file_name in self._api_file_names] 220 | return energies 221 | -------------------------------------------------------------------------------- /pyJoules/energy_meter.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | import time 21 | import operator 22 | import functools 23 | 24 | from functools import reduce 25 | from typing import List, Optional, Dict 26 | 27 | from .exception import PyJoulesException 28 | from .device import Device, Domain, DeviceFactory 29 | from .handler import EnergyHandler, PrintHandler 30 | from .energy_trace import EnergySample, EnergyTrace 31 | 32 | class NoNextStateException(PyJoulesException): 33 | """Exception raised when trying to compute duration or energy from a state 34 | which is the last state of an energy trace. 35 | """ 36 | 37 | 38 | class StateIsNotFinalError(PyJoulesException): 39 | """Exception raised when trying to add a state to a non final state on an energy trace 40 | """ 41 | 42 | 43 | class EnergyMeterNotStartedError(PyJoulesException): 44 | """ 45 | Exception raised when trying to stop or record on a non started EnergyMeter instance 46 | """ 47 | 48 | 49 | class EnergyMeterNotStoppedError(PyJoulesException): 50 | """ 51 | Exception raised when trying to get energy samples from non stopped EnergyMeter instance 52 | """ 53 | 54 | 55 | class SampleNotFoundError(PyJoulesException): 56 | """ 57 | Exception raised when trying to retrieve a sample that does not exist on trace 58 | """ 59 | 60 | 61 | class EnergyMeter: 62 | """ 63 | Tool used to record the energy consumption of given devices 64 | """ 65 | 66 | def __init__(self, devices: List[Device], default_tag: str = ''): 67 | """ 68 | :param devices: list of the monitored devices 69 | :param default_tag: tag given if no tag were given to a measure 70 | """ 71 | self.devices = devices 72 | self.default_tag = default_tag 73 | 74 | self._last_state = None 75 | self._first_state = None 76 | 77 | def _measure_new_state(self, tag): 78 | timestamp = time.time() 79 | values = [device.get_energy() for device in self.devices] 80 | 81 | return EnergyState(timestamp, tag if tag is not None else self.default_tag, values) 82 | 83 | def _append_new_state(self, new_state): 84 | self._last_state.add_next_state(new_state) 85 | self._last_state = new_state 86 | 87 | def _is_meter_started(self): 88 | return not self._first_state is None 89 | 90 | def _is_meter_stoped(self): 91 | return self._last_state.tag == '__stop__' 92 | 93 | def _reinit(self): 94 | self._first_state = None 95 | self._last_state = None 96 | 97 | def start(self, tag: Optional[str] = None): 98 | """ 99 | Begin a new energy trace 100 | 101 | :param tag: sample name 102 | """ 103 | new_state = self._measure_new_state(tag) 104 | self._first_state = new_state 105 | self._last_state = new_state 106 | 107 | def record(self, tag: Optional[str] = None): 108 | """ 109 | Add a new state to the Trace 110 | 111 | :param tag: sample name 112 | :raise EnergyMeterNotStartedError: if the energy meter isn't started 113 | """ 114 | if not self._is_meter_started(): 115 | raise EnergyMeterNotStartedError() 116 | 117 | new_state = self._measure_new_state(tag) 118 | self._append_new_state(new_state) 119 | 120 | def resume(self, tag: Optional[str] = None): 121 | """ 122 | resume the energy Trace (if no energy trace was launched, start a new one 123 | 124 | :param tag: sample name 125 | :raise EnergyMeterNotStoppedError: if the energy meter isn't stopped 126 | """ 127 | if not self._is_meter_started(): 128 | return self.start(tag) 129 | 130 | if not self._is_meter_stoped(): 131 | raise EnergyMeterNotStoppedError() 132 | 133 | new_state = self._measure_new_state(tag) 134 | self._append_new_state(new_state) 135 | 136 | def stop(self): 137 | """ 138 | Set the end of the energy trace 139 | 140 | :raise EnergyMeterNotStartedError: if the energy meter isn't started 141 | """ 142 | if not self._is_meter_started(): 143 | raise EnergyMeterNotStartedError() 144 | 145 | new_state = self._measure_new_state('__stop__') 146 | self._append_new_state(new_state) 147 | 148 | def get_trace(self) -> EnergyTrace: 149 | """ 150 | return the last trace measured 151 | 152 | :raise EnergyMeterNotStoppedError: if the energy meter isn't stopped 153 | """ 154 | if not self._is_meter_started(): 155 | return EnergyTrace([]) 156 | 157 | if not self._is_meter_stoped(): 158 | raise EnergyMeterNotStoppedError() 159 | 160 | return self._generate_trace() 161 | 162 | def _get_domain_list(self): 163 | """ 164 | return the list of all monitored domains for each monitored energy devices 165 | """ 166 | return reduce(operator.add, [device.get_configured_domains() for device in self.devices]) 167 | 168 | def _generate_trace(self): 169 | domains = self._get_domain_list() 170 | generator = TraceGenerator(self._first_state, domains) 171 | return generator.generate() 172 | 173 | def gen_idle(self, trace: EnergyTrace) -> List[Dict[str, float]]: 174 | """ 175 | generate idle values of an energy trace 176 | for each sample, wait for the duraction of a sample and measure the energy consumed during this period 177 | 178 | :return: the list of idle energy consumption for each sample in the trace 179 | """ 180 | self._reinit() 181 | idle_values = [] 182 | 183 | for sample in trace: 184 | self.resume() 185 | time.sleep(sample.duration / 1000000000) 186 | self.stop() 187 | 188 | for sample in self.get_trace(): 189 | idle_values.append(sample.energy) 190 | 191 | return idle_values 192 | 193 | 194 | class TraceGenerator: 195 | 196 | def __init__(self, first_state, domains): 197 | self.domains = domains 198 | self._current_state = first_state 199 | 200 | def generate(self): 201 | def generate_next(current_state, samples): 202 | if current_state.next_state is None: 203 | return samples 204 | if current_state.tag == '__stop__': 205 | return generate_next(current_state.next_state, samples) 206 | 207 | sample = self._gen_sample(current_state) 208 | samples.append(sample) 209 | return generate_next(current_state.next_state, samples) 210 | 211 | samples = generate_next(self._current_state, []) 212 | return EnergyTrace(samples) 213 | 214 | def _gen_sample(self, state): 215 | return EnergySample(state.timestamp, state.tag, state.compute_duration(), state.compute_energy(self.domains)) 216 | 217 | 218 | class EnergyState: 219 | """ 220 | Internal class that record the current energy state of the monitored device 221 | """ 222 | 223 | def __init__(self, timestamp: float, tag: str, values: List[Dict[str, float]]): 224 | """ 225 | :param timstamp: timestamp of the measure 226 | :param tag: tag of the measure 227 | :param values: energy consumption measure, this is the list of measured energy consumption values for each 228 | monitored device. This list contains the energy consumption since the last device reset to the 229 | end of this sample 230 | """ 231 | self.timestamp = timestamp 232 | self.tag = tag 233 | self.values = values 234 | self.next_state = None 235 | 236 | def is_last(self) -> bool: 237 | """ 238 | indicate if the current state is the last state of the trace or not 239 | :return: True if the current state is the last state of the trace False otherwise 240 | """ 241 | return self.next_state is None 242 | 243 | def compute_duration(self) -> float: 244 | """ 245 | :return: compute the time elipsed between the current state and the next state 246 | :raise NoNextStateException: if the state is the last state of the trace 247 | """ 248 | if self.next_state is None: 249 | raise NoNextStateException() 250 | 251 | return self.next_state.timestamp - self.timestamp 252 | 253 | def compute_energy(self, domains) -> List[float]: 254 | """ 255 | :return: compute the energy consumed between the current state and the next state 256 | :raise NoNextStateException: if the state is the last state of the trace 257 | """ 258 | if self.next_state is None: 259 | raise NoNextStateException() 260 | 261 | energy = [] 262 | for next_state_device, current_state_device in zip(self.next_state.values, self.values): 263 | for next_value, current_value in zip(next_state_device, current_state_device): 264 | energy.append(next_value - current_value) 265 | 266 | values_dict = {} 267 | for value, key in zip(energy, domains): 268 | if value < 0: 269 | device = key.get_device_type() 270 | domain_dirname = device._get_domain_file_name(None, key)[:-9] 271 | with open(domain_dirname + 'max_energy_range_uj', 'r') as file: 272 | value += float(file.readline()) 273 | values_dict[str(key)] = value 274 | return values_dict 275 | 276 | def add_next_state(self, state: 'EnergyState'): 277 | """ 278 | :param previous: next state for the same energy trace 279 | :raise StateIsNotFinalError: if there are already a next state 280 | """ 281 | if self.next_state is not None: 282 | raise StateIsNotFinalError() 283 | self.next_state = state 284 | 285 | 286 | def measure_energy(func=None ,handler: EnergyHandler = PrintHandler(), domains: Optional[List[Domain]] = None): 287 | """ 288 | Measure the energy consumption of monitored devices during the execution of the decorated function 289 | 290 | :param handler: handler instance that will receive the power consummation data 291 | :param domains: list of the monitored energy domains 292 | """ 293 | def decorator_measure_energy(func): 294 | 295 | devices = DeviceFactory.create_devices(domains) 296 | energy_meter = EnergyMeter(devices) 297 | 298 | @functools.wraps(func) 299 | def wrapper_measure(*args, **kwargs): 300 | energy_meter.start(tag=func.__name__) 301 | val = func(*args, **kwargs) 302 | energy_meter.stop() 303 | handler.process(energy_meter.get_trace()) 304 | return val 305 | return wrapper_measure 306 | 307 | if func is None: 308 | # to ensure the working system when you call it with parameters or without parameters 309 | return decorator_measure_energy 310 | else: 311 | return decorator_measure_energy(func) 312 | 313 | 314 | class EnergyContext(): 315 | 316 | def __init__(self, handler: EnergyHandler = PrintHandler(), domains: Optional[List[Domain]] = None, start_tag: str = 'start'): 317 | """ 318 | Measure the energy consumption of monitored devices during the execution of the contextualized code 319 | 320 | :param handler: handler instance that will receive the power consummation data 321 | :param domains: list of the monitored energy domains 322 | :param start_tag: first tag of the trace 323 | """ 324 | self.handler = handler 325 | self.start_tag = start_tag 326 | 327 | devices = DeviceFactory.create_devices(domains) 328 | self.energy_meter = EnergyMeter(devices) 329 | 330 | def __enter__(self) -> EnergyMeter: 331 | self.energy_meter.start(self.start_tag) 332 | return self.energy_meter 333 | 334 | def __exit__(self, type, value, traceback): 335 | self.energy_meter.stop() 336 | self.handler.process(self.energy_meter.get_trace()) 337 | -------------------------------------------------------------------------------- /pyJoules/energy_trace.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | from typing import Dict, Any, List, Callable 21 | from functools import reduce 22 | from operator import and_ 23 | 24 | 25 | class EnergySample: 26 | """ 27 | :var timestamp: begining timestamp 28 | :vartype timestamp: float 29 | :var tag: sample tag 30 | :vartype tag: str 31 | :var duration: duration of the sample in seconds 32 | :vartype duration: float 33 | :var energy: dictionary that contains the energy consumed during this sample 34 | :vartype energy: Dict[str, float] 35 | """ 36 | def __init__(self, timestamp: float, tag: str, duration: float, energy: Dict[str, float]): 37 | self.timestamp = timestamp 38 | self.tag = tag 39 | self.duration = duration 40 | self.energy = energy 41 | 42 | 43 | class EnergyTrace: 44 | """ 45 | Trace of all EnergySample collected by a meter 46 | """ 47 | def __init__(self, samples: List[EnergySample]): 48 | """ 49 | :param samples: samples containing in the trace 50 | """ 51 | self._samples = [] 52 | for sample in samples: 53 | self._samples.append(sample) 54 | 55 | def _get_sample_from_tag(self, tag): 56 | for sample in self._samples: 57 | if sample.tag == tag: 58 | return sample 59 | 60 | def __getitem__(self, key: Any) -> EnergySample: 61 | """ 62 | Return the n-th EnergySample on the trace or the first sample with the given tag 63 | 64 | :param key: integer to get the n-th EnergySample or the tag of the needed sample 65 | :return: An EnergySample 66 | :raise KeyError: if no sample match the given tag 67 | :raise IndexError: if no sample match the given index 68 | """ 69 | if isinstance(key, int): 70 | if key > len(self._samples): 71 | raise IndexError('Trace index out of range : ' + str(key)) 72 | return self._samples[key] 73 | 74 | sample = self._get_sample_from_tag(key) 75 | if sample is None: 76 | raise KeyError('this tag doesn\'t match any sample : ' + str(key)) 77 | return sample 78 | 79 | def __iter__(self): 80 | """ 81 | Iterate on the trace's samples 82 | """ 83 | return self._samples.__iter__() 84 | 85 | def __len__(self) -> int: 86 | """ 87 | return the number of sample in the trace 88 | """ 89 | return len(self._samples) 90 | 91 | def __contains__(self, key: str): 92 | return not self._get_sample_from_tag(key) is None 93 | 94 | def __add__(self, trace: 'EnergySample'): 95 | samples = self._samples + trace._samples 96 | return EnergyTrace(samples) 97 | 98 | def __iadd__(self, trace: 'EnergySample'): 99 | self._samples += trace._samples 100 | return self 101 | 102 | def append(self, sample: EnergySample): 103 | """ 104 | append a new sample to the trace 105 | """ 106 | self._samples.append(sample) 107 | 108 | def remove_idle(self, idle: List[Dict[str, float]]): 109 | """ 110 | substract idle energy values from the current trace 111 | 112 | :param idle: list of idle consumption values to substract to current trace 113 | idle consumption values must be grouped in a dictionary with their domain as key 114 | :raise ValueError: if the number of values in the list doesn't match the number of sample in the trace 115 | or if a domain of the trace is not in the idle values 116 | """ 117 | if len(idle) != len(self._samples): 118 | raise ValueError('idle list havn\'t the same length than the trace') 119 | 120 | for idle_energy, sample in zip(idle, self._samples): 121 | for domain in sample.energy: 122 | if domain not in idle_energy: 123 | raise ValueError('domain not present in idle values : ' + domain) 124 | sample.energy[domain] -= idle_energy[domain] 125 | 126 | def _sample_havnt_negative_values(self, sample): 127 | for val in sample.energy.values(): 128 | if val < 0: 129 | return False 130 | return True 131 | 132 | def clean_data(self, guards: List[Callable[[EnergySample], bool]] = []): 133 | """ 134 | Remove sample with negative energy values from the trace 135 | Guards can be added to specify rules to remove sample 136 | 137 | :param guards: list of function that is used as rules to remove samples. A guard is a function that take a 138 | sample as parameter and return True if it must be keept in the trace, False otherwise 139 | """ 140 | 141 | extended_guards = [lambda s: self._sample_havnt_negative_values(s)] + guards 142 | 143 | valid_samples = [] 144 | for sample in self._samples: 145 | 146 | validity_list = [guard(sample) for guard in extended_guards] 147 | if reduce(and_, validity_list): 148 | valid_samples.append(sample) 149 | self._samples = valid_samples 150 | -------------------------------------------------------------------------------- /pyJoules/exception.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | class PyJoulesException(Exception): 23 | """ 24 | PyJoules exceptions parent class 25 | """ 26 | 27 | 28 | class NoSuchDomainError(PyJoulesException): 29 | """ 30 | Exception raised when a user ask to monitor a domain that is not available on the given device 31 | """ 32 | 33 | def __init__(self, domain_name: str): 34 | """ 35 | :param domain_name: the domain name that is not available on this device 36 | """ 37 | PyJoulesException.__init__(self) 38 | #: the domain name that is not available on this device 39 | self.domain_name = domain_name 40 | 41 | 42 | class NoSuchDeviceError(PyJoulesException): 43 | """ 44 | Exception raised when a Device that does not exist on the current machine is created 45 | """ 46 | -------------------------------------------------------------------------------- /pyJoules/handler/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from .handler import EnergyHandler, UnconsistantSamplesError 22 | from .print_handler import PrintHandler 23 | -------------------------------------------------------------------------------- /pyJoules/handler/csv_handler.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | import os.path 21 | 22 | from . import EnergyHandler 23 | 24 | class CSVHandler(EnergyHandler): 25 | 26 | def __init__(self, filename: str): 27 | """ 28 | :param filename: file name to store processed trace 29 | """ 30 | EnergyHandler.__init__(self) 31 | 32 | self._filename = filename 33 | 34 | def _gen_header(self, first_sample): 35 | domain_names = first_sample.energy.keys() 36 | return 'timestamp;tag;duration;' + ';'.join(domain_names) 37 | 38 | def _gen_sample_line(self, sample, domain_names): 39 | line_begining = f'{sample.timestamp};{sample.tag};{sample.duration};' 40 | energy_values = [str(sample.energy[domain]) for domain in domain_names] 41 | return line_begining + ';'.join(energy_values) 42 | 43 | def _init_file(self, first_sample): 44 | if os.path.exists(self._filename): 45 | csv_file = open(self._filename, 'a+') 46 | return csv_file 47 | else: 48 | csv_file = open(self._filename, 'w+') 49 | csv_file.write(self._gen_header(first_sample) + '\n') 50 | return csv_file 51 | 52 | def save_data(self): 53 | """ 54 | append processed trace to the file 55 | """ 56 | flatened_trace = self._flaten_trace() 57 | first_sample = flatened_trace[0] 58 | domain_names = first_sample.energy.keys() 59 | 60 | csv_file = self._init_file(first_sample) 61 | 62 | for sample in flatened_trace: 63 | csv_file.write(self._gen_sample_line(sample, domain_names) + '\n') 64 | csv_file.close() 65 | self.traces = [] 66 | -------------------------------------------------------------------------------- /pyJoules/handler/handler.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from ..energy_trace import EnergyTrace 22 | 23 | 24 | class UnconsistantSamplesError(Exception): 25 | """ 26 | Exception raised when processed sample whith differents energy domain 27 | """ 28 | 29 | 30 | def _check_samples(samples): 31 | sample1 = samples[0] 32 | 33 | for sample in samples: 34 | if len(sample.energy) != len(sample1.energy): 35 | return False 36 | for domain_name, domain_name1 in zip(sample.energy, sample1.energy): 37 | if domain_name != domain_name1: 38 | return False 39 | return True 40 | 41 | 42 | class EnergyHandler: 43 | """ 44 | An object that can handle the measured value of an energy trace 45 | """ 46 | 47 | def __init__(self): 48 | self.traces = [] 49 | 50 | def process(self, trace: EnergyTrace): 51 | """ 52 | """ 53 | self.traces.append(trace) 54 | 55 | def _flaten_trace(self): 56 | flatened_trace = EnergyTrace([]) 57 | for trace in self.traces: 58 | flatened_trace += trace 59 | if not _check_samples(flatened_trace): 60 | raise UnconsistantSamplesError() 61 | return flatened_trace 62 | -------------------------------------------------------------------------------- /pyJoules/handler/mongo_handler.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | from typing import Dict, Iterable 21 | 22 | try: 23 | import pymongo 24 | except ImportError: 25 | import logging 26 | logging.getLogger().info("PyMongo is not installed.") 27 | 28 | from . import EnergyHandler 29 | from ..energy_trace import EnergySample 30 | 31 | 32 | def sample_to_dict(sample: EnergySample) -> Dict: 33 | """ 34 | convert a sample to a dictionary that could be inserted in a mongodb database 35 | """ 36 | return { 37 | 'timestamp': sample.timestamp, 38 | 'tag': sample.tag, 39 | 'duration': sample.duration, 40 | 'energy': sample.energy, 41 | } 42 | 43 | 44 | def trace_to_dict(trace: Iterable[EnergySample], trace_name: str) -> Dict: 45 | """ 46 | convert a trace to a dictionary that could be inserted in a mongodb database 47 | """ 48 | return { 49 | 'name': trace_name, 50 | 'trace': list(map(sample_to_dict, trace)) 51 | } 52 | 53 | 54 | class MongoInitError(Exception): 55 | """ 56 | Exception raised when mongoHandler cant be initialized due to error while 57 | handling mongo database 58 | """ 59 | 60 | 61 | class MongoHandler(EnergyHandler): 62 | 63 | def __init__(self, uri: str, database_name: str, collection_name: str, connected_timeout: int = 30000, trace_name_prefix: str = 'trace_'): 64 | """ 65 | Create a handler that will store data on mongo database 66 | 67 | :param uri: database uri using mongoDB uri format 68 | :param connection_timeout: Controls how long (in milliseconds) the driver will wait to find an available, 69 | appropriate server to carry out a database operation; while it is waiting, multiple 70 | server monitoring operations may be carried out, each controlled by connectTimeoutMS. 71 | Defaults to 30000 (30 seconds). 72 | :param trace_name_prefix: prefix of the trace name used to identify a trace in mongo database. The trace name is 73 | computed as follow : trace_name_prefix + trace_position (trace position is the 74 | position of the current trace in the trace list processed by the handler) 75 | 76 | """ 77 | EnergyHandler.__init__(self) 78 | 79 | self.collection = None 80 | self.trace_id = 0 81 | self.trace_name_prefix = trace_name_prefix 82 | 83 | self._init_database(uri, connected_timeout, database_name, collection_name) 84 | 85 | def _init_database(self, uri, connected_timeout, database_name, collection_name): 86 | try: 87 | database = pymongo.MongoClient(uri, connectTimeoutMS=connected_timeout, serverSelectionTimeoutMS=connected_timeout) 88 | database.server_info() 89 | self._collection = database[database_name][collection_name] 90 | except pymongo.errors.InvalidURI: 91 | raise MongoInitError('invalid uri : ' + uri) 92 | except pymongo.errors.ServerSelectionTimeoutError as exn: 93 | raise MongoInitError('unreachable server : ' + uri + ' driver msg : ' + str(exn)) 94 | except Exception as exn: 95 | raise MongoInitError(' driver msg : ' + str(exn)) 96 | 97 | def save_data(self): 98 | """ 99 | Save processed trace on the database 100 | """ 101 | documents = [] 102 | for trace in self.traces: 103 | documents.append(trace_to_dict(trace, self.trace_name_prefix + str(self.trace_id))) 104 | self.trace_id += 1 105 | self._collection.insert_many(documents) 106 | 107 | self.traces = [] 108 | -------------------------------------------------------------------------------- /pyJoules/handler/pandas_handler.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | from typing import Iterable 21 | 22 | try: 23 | import pandas 24 | except ImportError: 25 | import logging 26 | logging.getLogger().info("Pandas is not installed.") 27 | 28 | from . import EnergyHandler, UnconsistantSamplesError 29 | from ..energy_trace import EnergyTrace, EnergySample 30 | 31 | 32 | def _gen_column_names(samples): 33 | sample = samples[0] 34 | names = ['timestamp', 'tag', 'duration'] 35 | 36 | for domain_name in sample.energy: 37 | names.append(domain_name) 38 | return names 39 | 40 | 41 | def _gen_data(samples): 42 | data = [] 43 | for sample in samples: 44 | data.append(_gen_row(sample)) 45 | return data 46 | 47 | 48 | def _gen_row(sample): 49 | row = [sample.timestamp, sample.tag, sample.duration] 50 | 51 | for domain_name in sample.energy: 52 | row.append(sample.energy[domain_name]) 53 | return row 54 | 55 | 56 | def trace_to_dataframe(trace: Iterable[EnergySample]) -> pandas.DataFrame: 57 | """ 58 | convert an energy trace into a pandas DataFrame 59 | """ 60 | if len(trace) == 0: 61 | return pandas.DataFrame() 62 | 63 | return pandas.DataFrame(columns=_gen_column_names(trace), data=_gen_data(trace)) 64 | 65 | 66 | class NoSampleProcessedError(Exception): 67 | """ 68 | Exception raised when trying to get dataframe from pandas handler without process any sample before 69 | """ 70 | 71 | 72 | class PandasHandler(EnergyHandler): 73 | """ 74 | handle energy sample to convert them into pandas DataFrame 75 | """ 76 | def __init__(self): 77 | EnergyHandler.__init__(self) 78 | self.traces = [] 79 | 80 | def process(self, trace: EnergyTrace): 81 | self.traces.append(trace) 82 | 83 | def get_dataframe(self) -> pandas.DataFrame: 84 | """ 85 | return the DataFrame containing the processed samples 86 | """ 87 | if len(self.traces) > 0: 88 | faltened_trace = self._flaten_trace() 89 | return trace_to_dataframe(faltened_trace) 90 | raise NoSampleProcessedError() 91 | -------------------------------------------------------------------------------- /pyJoules/handler/print_handler.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from functools import reduce 22 | from operator import add 23 | 24 | from ..energy_trace import EnergyTrace 25 | from .handler import EnergyHandler 26 | 27 | 28 | class PrintHandler(EnergyHandler): 29 | 30 | def process(self, trace: EnergyTrace): 31 | """ 32 | Print the given sample on the standard output 33 | """ 34 | for sample in trace: 35 | begin_string = f'begin timestamp : {sample.timestamp}; tag : {sample.tag}; duration : {sample.duration}' 36 | energy_strings = [f'; {domain} : {value}' for domain, value in sample.energy.items()] 37 | print(reduce(add, energy_strings, begin_string)) 38 | -------------------------------------------------------------------------------- /rapl_domains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powerapi-ng/pyJoules/253b71642c1e82de68adce568e94a2d7cbc103d9/rapl_domains.png -------------------------------------------------------------------------------- /scenario.md: -------------------------------------------------------------------------------- 1 | # Proposal 2 | ```python3 3 | from pyJoules import * 4 | 5 | # Usage 1 (basic) 6 | with EnergyContext: 7 | foo() 8 | bar() 9 | 10 | # Usage 2 (basic) 11 | @measureit 12 | def foo(): 13 | pass 14 | 15 | # Usage 3 (configurable) 16 | with EnergyContext(ConsolePrinter, tag="bar", NvidiaDevice.GPU(1), RaplDevice.ALL) as ctx: 17 | foo() 18 | ctx.record(tag="foo") 19 | bar() 20 | 21 | # Usage 4 (configurable) 22 | @measureit(ConsolePrinter, NvidiaDevice.GPU(1), RaplDevice.ALL) 23 | def foo(): 24 | pass 25 | 26 | # Usage 5 (advanced) 27 | meter = EnergyMeter(CudaDevice.GPU, RaplDevice.CORE) 28 | meter.start() 29 | foo() 30 | meter.record(tag="foo") 31 | bar() 32 | meter.stop(tag="bar") 33 | samples = meter.compute() 34 | ConsolePrinter.process(samples) 35 | 36 | sample = samples.get_sample("bar") 37 | print sample.energy(CudaDevice.GPU) 38 | ``` 39 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pyJoules 3 | version = attr: pyJoules.__version__ 4 | description = 5 | long_description = file: README.md, LICENSE 6 | long_description_content_type= text/markdown 7 | keywords = energy 8 | platform = linux 9 | author = Chakib Belgaid, Arthur d'Azémar, Romain Rouvoy 10 | author_email = powerapi-staff@inria.fr 11 | license = MIT License 12 | classifiers = 13 | Programming Language :: Python :: 3.7 14 | License :: OSI Approved :: MIT License 15 | 16 | project_urls = 17 | Homepage = https://pyjoules.readthedocs.io/en/latest/ 18 | Source = https://github.com/powerapi-ng/pyJoules 19 | 20 | [options] 21 | zip_safe = False 22 | include_package_data = True 23 | python_requires = >= 3.7 24 | packages = find: 25 | test_suite = tests 26 | setup_requires = 27 | pytest-runner >=3.9.2 28 | install_requires = 29 | tests_require = 30 | pytest >=3.9.2 31 | pyfakefs >= 3.6 32 | mock >=2.0 33 | 34 | [options.extras_require] 35 | docs = 36 | sphinx >=1.8.1 37 | sphinx-autodoc-typehints >=1.6.0 38 | mongodb = 39 | pymongo >= 3.9.0 40 | pandas = 41 | pandas >= 0.25.1 42 | nvidia = 43 | pynvml >= 8.0.4 44 | [aliases] 45 | test = pytest 46 | 47 | [bdist_wheel] 48 | universal = true -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | setup() 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powerapi-ng/pyJoules/253b71642c1e82de68adce568e94a2d7cbc103d9/tests/__init__.py -------------------------------------------------------------------------------- /tests/acceptation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powerapi-ng/pyJoules/253b71642c1e82de68adce568e94a2d7cbc103d9/tests/acceptation/__init__.py -------------------------------------------------------------------------------- /tests/acceptation/test_measure_trace_with_class.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | """ 22 | Test to measure energy consumption of a trace only with the energy meter 23 | """ 24 | import pytest 25 | 26 | from mock import patch 27 | from pyJoules.device import DeviceFactory 28 | from pyJoules.device.rapl_device import RaplDevice, RaplPackageDomain, RaplDramDomain 29 | from pyJoules.device.nvidia_device import NvidiaGPUDevice, NvidiaGPUDomain 30 | from pyJoules.energy_meter import EnergyMeter 31 | from ..utils.rapl_fs import fs_pkg_dram_one_socket 32 | from ..utils.fake_nvidia_api import one_gpu_api 33 | from ..utils.fake_api import CorrectTrace 34 | from ..utils.sample import assert_sample_are_equals 35 | 36 | 37 | # We mock time.time function that is used by pyfakefs each time an operation on filesystem is done. we have to give 38 | # consistant time return value to time.time that will be used by pyfakefs 39 | INIT_TS = [0] * 5 40 | FIRST_TS = [1.1] * 7 41 | SECOND_TS = [2.2] * 7 42 | THIRD_TS = [3.3] * 7 43 | MOCKED_TIMESTAMP_TRACE = INIT_TS + FIRST_TS + SECOND_TS + THIRD_TS 44 | TIMESTAMP_TRACE = [1.1, 2.2, 3.3] 45 | 46 | 47 | @patch('time.time', side_effect=MOCKED_TIMESTAMP_TRACE) 48 | def test_measure_rapl_device_all_domains(_mocked_time_ns, fs_pkg_dram_one_socket, one_gpu_api): 49 | domains = [RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)] 50 | 51 | correct_trace = CorrectTrace(domains, [fs_pkg_dram_one_socket, one_gpu_api], TIMESTAMP_TRACE) # test 52 | 53 | rapl = RaplDevice() 54 | rapl.configure(domains=[RaplPackageDomain(0), RaplDramDomain(0)]) 55 | 56 | nvidia = NvidiaGPUDevice() 57 | nvidia.configure(domains=[NvidiaGPUDomain(0)]) 58 | 59 | meter = EnergyMeter([rapl, nvidia]) 60 | 61 | correct_trace.add_new_sample('foo') # test 62 | meter.start(tag="foo") 63 | 64 | correct_trace.add_new_sample('bar') # test 65 | meter.record(tag="bar") 66 | 67 | correct_trace.add_new_sample('') # test 68 | meter.stop() 69 | 70 | for sample1, sample2 in zip(correct_trace, meter.get_trace()): # test 71 | assert_sample_are_equals(sample1, sample2) # test 72 | 73 | 74 | @patch('time.time', side_effect=MOCKED_TIMESTAMP_TRACE) 75 | def test_measure_rapl_device_all_domains_configuration_with_factory(_mocked_time_ns, fs_pkg_dram_one_socket, one_gpu_api): 76 | domains = [RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)] 77 | 78 | correct_trace = CorrectTrace(domains, [fs_pkg_dram_one_socket, one_gpu_api], TIMESTAMP_TRACE) # test 79 | 80 | devices = DeviceFactory.create_devices(domains) 81 | 82 | meter = EnergyMeter(devices) 83 | 84 | correct_trace.add_new_sample('foo') # test 85 | meter.start(tag="foo") 86 | 87 | correct_trace.add_new_sample('bar') # test 88 | meter.record(tag="bar") 89 | 90 | correct_trace.add_new_sample('') # test 91 | meter.stop() 92 | 93 | for sample1, sample2 in zip(correct_trace, meter.get_trace()): # test 94 | assert_sample_are_equals(sample1, sample2) # test 95 | 96 | 97 | INIT_TS2 = [0] * 9 98 | MOCKED_TIMESTAMP_TRACE2 = INIT_TS2 + FIRST_TS + SECOND_TS + THIRD_TS 99 | 100 | @patch('time.time', side_effect=MOCKED_TIMESTAMP_TRACE2) 101 | def test_measure_rapl_device_all_domains_configuration_with_factory_with_default_values(_mocked_time_ns, fs_pkg_dram_one_socket, one_gpu_api): 102 | domains = [RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)] 103 | 104 | correct_trace = CorrectTrace(domains, [fs_pkg_dram_one_socket, one_gpu_api], TIMESTAMP_TRACE) # test 105 | 106 | devices = DeviceFactory.create_devices() 107 | 108 | meter = EnergyMeter(devices) 109 | 110 | correct_trace.add_new_sample('foo') # test 111 | meter.start(tag="foo") 112 | 113 | correct_trace.add_new_sample('bar') # test 114 | meter.record(tag="bar") 115 | 116 | correct_trace.add_new_sample('') # test 117 | meter.stop() 118 | 119 | for sample1, sample2 in zip(correct_trace, meter.get_trace()): # test 120 | assert_sample_are_equals(sample1, sample2) # test 121 | -------------------------------------------------------------------------------- /tests/acceptation/test_measure_trace_with_decorator.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | """ 22 | Test to measure energy consumption of a trace only with the energy meter 23 | """ 24 | import pytest 25 | 26 | from mock import patch 27 | 28 | from pyJoules.device.rapl_device import RaplDevice, RaplPackageDomain, RaplDramDomain 29 | from pyJoules.energy_meter import EnergyMeter, measure_energy 30 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 31 | from .. utils.rapl_fs import fs_pkg_dram_one_socket 32 | from ..utils.fake_nvidia_api import one_gpu_api 33 | from .. utils.fake_api import CorrectTrace 34 | from ..utils.sample import assert_sample_are_equals 35 | 36 | 37 | # We mock time.time function that is used by pyfakefs each time an operation on filesystem is done. we have to give 38 | # consistant time return value to time.time that will be used by pyfakefs 39 | INIT_TS = [0] * 5 40 | FIRST_TS = [1.1] * 7 41 | SECOND_TS = [2.2] * 7 42 | MOCKED_TIMESTAMP_TRACE = INIT_TS + FIRST_TS + SECOND_TS 43 | TIMESTAMP_TRACE = [1.1, 2.2] 44 | 45 | 46 | @patch('pyJoules.handler.EnergyHandler') 47 | @patch('time.time', side_effect=MOCKED_TIMESTAMP_TRACE) 48 | def test_measure_rapl_device_all_domains(mocked_handler, _mocked_time, fs_pkg_dram_one_socket, one_gpu_api): 49 | 50 | domains = [RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)] 51 | 52 | correct_trace = CorrectTrace(domains, [fs_pkg_dram_one_socket, one_gpu_api], TIMESTAMP_TRACE) # test 53 | 54 | @measure_energy(handler=mocked_handler, domains=domains) 55 | def measured_function(val): 56 | correct_trace.add_new_sample('stop') # test 57 | return val + 1 58 | 59 | assert mocked_handler.process.call_count == 0 # test 60 | correct_trace.add_new_sample('measured_function') 61 | returned_value = measured_function(1) 62 | assert mocked_handler.process.call_count == 1 # test 63 | 64 | assert returned_value == 2 65 | assert len(correct_trace.get_trace()) == len(mocked_handler.process.call_args_list) 66 | 67 | for correct_sample, processed_arg in zip(correct_trace, mocked_handler.process.call_args_list): # test 68 | measured_sample = processed_arg[0][0][0] # test 69 | assert_sample_are_equals(correct_sample, measured_sample) # test 70 | -------------------------------------------------------------------------------- /tests/acceptation/test_measure_trace_with_decorator_output_to_csv_file.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | """ 22 | decorate a function and measure its energy consumption, output the energy consumption in a csv file 23 | """ 24 | import pytest 25 | from mock import patch 26 | 27 | from pyJoules.device.rapl_device import RaplDevice, RaplPackageDomain, RaplDramDomain 28 | from pyJoules.energy_meter import EnergyMeter, measure_energy 29 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 30 | from pyJoules.handler.csv_handler import CSVHandler 31 | from .. utils.rapl_fs import fs_pkg_dram_one_socket 32 | from ..utils.fake_nvidia_api import one_gpu_api 33 | from .. utils.fake_api import CorrectTrace 34 | from ..utils.sample import assert_sample_are_equals 35 | 36 | 37 | # We mock time.time function that is used by pyfakefs each time an operation on filesystem is done. we have to give 38 | # consistant time return value to time.time that will be used by pyfakefs 39 | INIT_TS = [0] * 5 40 | FIRST_TS = [1.1] * 7 41 | SECOND_TS = [2.2] * 7 42 | CSV_WRITING_TS = [1, 2, 3, 5, 6, 7, 8, 9] 43 | MOCKED_TIMESTAMP_TRACE = INIT_TS + FIRST_TS + SECOND_TS + CSV_WRITING_TS 44 | TIMESTAMP_TRACE = [1.1, 2.2] 45 | 46 | 47 | @patch('time.time', side_effect=MOCKED_TIMESTAMP_TRACE) 48 | def test_measure_rapl_device_all_domains(_mocked_time, fs_pkg_dram_one_socket, one_gpu_api): 49 | domains = [RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)] 50 | correct_trace = CorrectTrace(domains, [fs_pkg_dram_one_socket, one_gpu_api], TIMESTAMP_TRACE) 51 | 52 | csv_handler = CSVHandler('result.csv') 53 | @measure_energy(handler=csv_handler, domains=domains) 54 | def foo(): 55 | correct_trace.add_new_sample('stop') 56 | return 1 57 | correct_trace.add_new_sample('begin') 58 | foo() 59 | csv_handler.save_data() 60 | csv_file = open('result.csv', 'r') 61 | 62 | lines = [] 63 | 64 | for line in csv_file: 65 | lines.append(line.strip().split(';')) 66 | assert float(lines[1][0]) == TIMESTAMP_TRACE[0] 67 | assert lines[1][1] == 'foo' 68 | assert float(lines[1][2]) == TIMESTAMP_TRACE[1] - TIMESTAMP_TRACE[0] 69 | correct_sample = correct_trace.get_trace()[0] 70 | 71 | for key in correct_sample.energy: 72 | assert key in lines[0] 73 | assert correct_sample.energy[key] == float(lines[1][lines[0].index(key)]) 74 | -------------------------------------------------------------------------------- /tests/acceptation/test_measure_trace_with_energy_context.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | """ 22 | Test to measure energy consumption of a trace only with the energy meter 23 | """ 24 | import pytest 25 | 26 | from mock import patch 27 | 28 | from pyJoules.device.rapl_device import RaplDevice, RaplPackageDomain, RaplDramDomain 29 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 30 | from pyJoules.energy_meter import EnergyMeter, EnergyContext 31 | from .. utils.rapl_fs import fs_pkg_dram_one_socket 32 | from ..utils.fake_nvidia_api import one_gpu_api 33 | from .. utils.fake_api import CorrectTrace 34 | from ..utils.sample import assert_sample_are_equals 35 | 36 | 37 | # We mock time.time function that is used by pyfakefs each time an operation on filesystem is done. we have to give 38 | # consistant time return value to time.time that will be used by pyfakefs 39 | FIRST_TS = [1.1] * 16 40 | SECOND_TS = [2.2] * 7 41 | THIRD_TS = [3.3] * 7 42 | MOCKED_TIMESTAMP_TRACE = FIRST_TS + SECOND_TS + THIRD_TS 43 | TIMESTAMP_TRACE = [1.1, 2.2, 3.3] 44 | 45 | @patch('pyJoules.handler.EnergyHandler') 46 | @patch('time.time', side_effect=MOCKED_TIMESTAMP_TRACE) 47 | def test_measure_rapl_device_all_domains(mocked_handler, _mocked_time, fs_pkg_dram_one_socket, one_gpu_api): 48 | 49 | domains = [RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)] 50 | 51 | correct_trace = CorrectTrace(domains, [fs_pkg_dram_one_socket, one_gpu_api], TIMESTAMP_TRACE) # test 52 | 53 | correct_trace.add_new_sample('foo') # test 54 | with EnergyContext(mocked_handler, domains, start_tag='foo') as energy_context: 55 | correct_trace.add_new_sample('bar') # test 56 | energy_context.record(tag='bar') 57 | correct_trace.add_new_sample('') # test 58 | 59 | assert mocked_handler.process.call_count == 1 # test 60 | 61 | for correct_sample, processed_arg in zip(correct_trace, mocked_handler.process.call_args_list): # test 62 | measured_sample = processed_arg[0][0][0] # test 63 | 64 | assert_sample_are_equals(correct_sample, measured_sample) # test 65 | 66 | 67 | @patch('pyJoules.handler.EnergyHandler') 68 | @patch('time.time', side_effect=MOCKED_TIMESTAMP_TRACE) 69 | def test_measure_rapl_device_default_values(mocked_handler, _mocked_time, fs_pkg_dram_one_socket, one_gpu_api): 70 | 71 | correct_trace = CorrectTrace([RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)], 72 | [fs_pkg_dram_one_socket, one_gpu_api], TIMESTAMP_TRACE) # test 73 | 74 | correct_trace.add_new_sample('start') # test 75 | with EnergyContext(mocked_handler) as energy_context: 76 | correct_trace.add_new_sample('second_tag') # test 77 | energy_context.record(tag='second_tag') 78 | correct_trace.add_new_sample('') # test 79 | 80 | assert mocked_handler.process.call_count == 1 81 | 82 | for correct_sample, processed_arg in zip(correct_trace, mocked_handler.process.call_args_list): # test 83 | measured_sample = processed_arg[0][0][0] # test 84 | assert_sample_are_equals(correct_sample, measured_sample) # test 85 | -------------------------------------------------------------------------------- /tests/acceptation/test_measure_trace_with_energy_context_output_to_csv_file.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | """ 22 | Test to measure energy consumption of a trace only with the energy meter 23 | """ 24 | import pytest 25 | 26 | from mock import patch 27 | 28 | from pyJoules.device.rapl_device import RaplDevice, RaplPackageDomain, RaplDramDomain 29 | from pyJoules.device.nvidia_device import NvidiaGPUDomain 30 | from pyJoules.energy_meter import EnergyMeter, EnergyContext 31 | from pyJoules.handler.csv_handler import CSVHandler 32 | from .. utils.rapl_fs import fs_pkg_dram_one_socket 33 | from ..utils.fake_nvidia_api import one_gpu_api 34 | from .. utils.fake_api import CorrectTrace 35 | from ..utils.sample import assert_sample_are_equals 36 | 37 | 38 | # We mock time.time function that is used by pyfakefs each time an operation on filesystem is done. we have to give 39 | # consistant time return value to time.time that will be used by pyfakefs 40 | FIRST_TS = [1.1] * 12 41 | SECOND_TS = [2.2] * 7 42 | THIRD_TS = [3.3] * 7 43 | CSV_WRITING_TS = [1] * 3 44 | MOCKED_TIMESTAMP_TRACE = FIRST_TS + SECOND_TS + THIRD_TS + CSV_WRITING_TS 45 | TIMESTAMP_TRACE = [1.1, 2.2, 3.3] 46 | 47 | @patch('time.time', side_effect=MOCKED_TIMESTAMP_TRACE) 48 | @patch('pyJoules.handler.EnergyHandler') 49 | def test_measure_rapl_device_all_domains(mocked_handler, _mocked_time, fs_pkg_dram_one_socket, one_gpu_api): 50 | print('init--------------') 51 | domains = [RaplPackageDomain(0), RaplDramDomain(0), NvidiaGPUDomain(0)] 52 | 53 | correct_trace = CorrectTrace(domains, [fs_pkg_dram_one_socket, one_gpu_api], TIMESTAMP_TRACE) # test 54 | 55 | 56 | csv_handler = CSVHandler('result.csv') 57 | print('1--------------') 58 | correct_trace.add_new_sample('foo') 59 | with EnergyContext(handler=csv_handler, domains=domains, start_tag='foo') as ctx: 60 | print('2--------------') 61 | correct_trace.add_new_sample('bar') 62 | ctx.record(tag='bar') 63 | print('3--------------') 64 | correct_trace.add_new_sample('') 65 | print('save--------------') 66 | print(csv_handler.traces) 67 | csv_handler.save_data() 68 | print('test--------------') 69 | csv_file = open('result.csv', 'r') 70 | 71 | lines = [] 72 | 73 | for line in csv_file: 74 | lines.append(line.strip().split(';')) 75 | assert float(lines[1][0]) == TIMESTAMP_TRACE[0] 76 | assert lines[1][1] == 'foo' 77 | assert float(lines[1][2]) == TIMESTAMP_TRACE[1] - TIMESTAMP_TRACE[0] 78 | correct_sample = correct_trace.get_trace()[0] 79 | 80 | for key in correct_sample.energy: 81 | assert key in lines[0] 82 | assert correct_sample.energy[key] == float(lines[1][lines[0].index(key)]) 83 | 84 | 85 | assert float(lines[2][0]) == TIMESTAMP_TRACE[1] 86 | assert lines[2][1] == 'bar' 87 | assert float(lines[2][2]) == TIMESTAMP_TRACE[2] - TIMESTAMP_TRACE[1] 88 | correct_sample = correct_trace.get_trace()[1] 89 | 90 | for key in correct_sample.energy: 91 | assert key in lines[0] 92 | assert correct_sample.energy[key] == float(lines[2][lines[0].index(key)]) 93 | -------------------------------------------------------------------------------- /tests/integration/energy_handler/test_MongoHandler.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | import pytest 21 | import pymongo 22 | 23 | 24 | from pyJoules.energy_trace import EnergySample, EnergyTrace 25 | from pyJoules.handler.mongo_handler import MongoHandler, MongoInitError 26 | 27 | 28 | URI = 'mongodb://localhost:27017' 29 | DATABASE_NAME = 'test_pyjoules_db' 30 | COLLECTION_NAME = 'test_pyjoules_col' 31 | 32 | 33 | def clean_database(uri): 34 | """ 35 | drop test_pyjoules_* collections 36 | """ 37 | mongo = pymongo.MongoClient(uri) 38 | db_names = mongo.list_database_names() 39 | for name in db_names: 40 | if "test_pyjoules_" in name: 41 | for col in mongo[name].list_collection_names(): 42 | mongo[name][col].drop() 43 | mongo.close() 44 | 45 | 46 | @pytest.fixture 47 | def sample1(): 48 | return EnergySample(1, 'sample1', 2, {'d1': 1, 'd2': 2}) 49 | 50 | 51 | @pytest.fixture 52 | def sample2(): 53 | return EnergySample(2, 'sample2', 3, {'d1': 3, 'd2': 4}) 54 | 55 | 56 | @pytest.fixture 57 | def trace1(sample1): 58 | return EnergyTrace([sample1]) 59 | 60 | 61 | @pytest.fixture 62 | def trace2(sample1, sample2): 63 | return EnergyTrace([sample1, sample2]) 64 | 65 | 66 | @pytest.fixture 67 | def database(): 68 | yield pymongo.MongoClient(URI) 69 | clean_database(URI) 70 | 71 | 72 | def test_create_mongo_handler_with_bad_uri_must_raise_MongoInitError(database): 73 | with pytest.raises(MongoInitError): 74 | MongoHandler('mong://toto', DATABASE_NAME, COLLECTION_NAME) 75 | 76 | 77 | def test_create_mongo_handler_with_unreachable_host_must_raise_MongoInitError(database): 78 | with pytest.raises(MongoInitError): 79 | MongoHandler('mongodb://invalid_uri_pyjoules', DATABASE_NAME, COLLECTION_NAME, connected_timeout=100) 80 | 81 | 82 | def test_process_one_trace_and_record_produce_a_collection_with_one_item(database, trace1): 83 | handler = MongoHandler(URI, DATABASE_NAME, COLLECTION_NAME) 84 | handler.process(trace1) 85 | handler.save_data() 86 | 87 | assert database[DATABASE_NAME][COLLECTION_NAME].count_documents({}) == 1 88 | 89 | def test_process_one_trace_and_record_two_times_must_produce_a_collection_with_two_item(database, trace1): 90 | handler = MongoHandler(URI, DATABASE_NAME, COLLECTION_NAME) 91 | handler.process(trace1) 92 | handler.save_data() 93 | 94 | handler.process(trace1) 95 | handler.save_data() 96 | 97 | assert database[DATABASE_NAME][COLLECTION_NAME].count_documents({}) == 2 98 | 99 | 100 | def test_process_one_trace_and_record_two_times_must_produce_a_collection_with_two_item_with_different_trace_name(database, trace1): 101 | handler = MongoHandler(URI, DATABASE_NAME, COLLECTION_NAME) 102 | handler.process(trace1) 103 | handler.save_data() 104 | 105 | handler.process(trace1) 106 | handler.save_data() 107 | 108 | traces = database[DATABASE_NAME][COLLECTION_NAME].find() 109 | 110 | assert traces[0]['name'] != traces[1]['name'] 111 | 112 | 113 | def test_process_one_trace_and_record_must_produce_a_collection_with_one_item_with_correct_values(database, trace1, sample1): 114 | handler = MongoHandler(URI, DATABASE_NAME, COLLECTION_NAME) 115 | handler.process(trace1) 116 | handler.save_data() 117 | 118 | traces = database[DATABASE_NAME][COLLECTION_NAME].find() 119 | 120 | assert traces[0]['name'] == 'trace_0' 121 | assert traces[0]['trace'][0]['timestamp'] == sample1.timestamp 122 | assert traces[0]['trace'][0]['tag'] == sample1.tag 123 | assert traces[0]['trace'][0]['duration'] == sample1.duration 124 | assert traces[0]['trace'][0]['energy'] == sample1.energy 125 | 126 | 127 | def test_process_two_trace_and_record_must_produce_a_collection_with_two_item_with_correct_values(database, sample1, sample2): 128 | handler = MongoHandler(URI, DATABASE_NAME, COLLECTION_NAME) 129 | handler.process([sample1]) 130 | handler.save_data() 131 | 132 | handler.process([sample2]) 133 | handler.save_data() 134 | 135 | traces = database[DATABASE_NAME][COLLECTION_NAME].find() 136 | 137 | assert traces[0]['name'] == 'trace_0' 138 | assert traces[0]['trace'][0]['timestamp'] == sample1.timestamp 139 | assert traces[0]['trace'][0]['tag'] == sample1.tag 140 | assert traces[0]['trace'][0]['duration'] == sample1.duration 141 | assert traces[0]['trace'][0]['energy'] == sample1.energy 142 | 143 | assert traces[1]['name'] == 'trace_1' 144 | assert traces[1]['trace'][0]['timestamp'] == sample2.timestamp 145 | assert traces[1]['trace'][0]['tag'] == sample2.tag 146 | assert traces[1]['trace'][0]['duration'] == sample2.duration 147 | assert traces[1]['trace'][0]['energy'] == sample2.energy 148 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powerapi-ng/pyJoules/253b71642c1e82de68adce568e94a2d7cbc103d9/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/device/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powerapi-ng/pyJoules/253b71642c1e82de68adce568e94a2d7cbc103d9/tests/unit/device/__init__.py -------------------------------------------------------------------------------- /tests/unit/device/nvidia_device/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powerapi-ng/pyJoules/253b71642c1e82de68adce568e94a2d7cbc103d9/tests/unit/device/nvidia_device/__init__.py -------------------------------------------------------------------------------- /tests/unit/device/nvidia_device/test_NvidiaDevice.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import pytest 22 | import pynvml 23 | from mock import patch 24 | from ....utils.fake_nvidia_api import no_gpu_api, one_gpu_api, two_gpu_api 25 | from pyJoules.device.nvidia_device import NvidiaGPUDevice, NvidiaGPUDomain 26 | from pyJoules.device import NotConfiguredDeviceException 27 | from pyJoules.exception import NoSuchDeviceError, NoSuchDomainError 28 | 29 | ############## 30 | # INIT TESTS # 31 | ############## 32 | def test_create_NvidiaDevice_with_no_gpu_api_raise_NoSuchDeviceError(no_gpu_api): 33 | with pytest.raises(NoSuchDeviceError): 34 | NvidiaGPUDevice() 35 | 36 | ########################### 37 | # AVAILABLE DOMAINS TESTS # 38 | ########################### 39 | def test_available_domains_with_no_gpu_api_raise_NoSuchDeviceError(no_gpu_api): 40 | with pytest.raises(NoSuchDeviceError): 41 | NvidiaGPUDevice.available_domains() 42 | 43 | 44 | def test_available_domains_with_one_gpu_api_return_correct_values(one_gpu_api): 45 | returned_values = NvidiaGPUDevice.available_domains() 46 | correct_values = [NvidiaGPUDomain(0)] 47 | assert sorted(correct_values) == sorted(returned_values) 48 | 49 | 50 | def test_available_domains_with_two_gpu_api_return_correct_values(two_gpu_api): 51 | returned_values = NvidiaGPUDevice.available_domains() 52 | correct_values = [NvidiaGPUDomain(0), NvidiaGPUDomain(1)] 53 | assert sorted(correct_values) == sorted(returned_values) 54 | 55 | 56 | ################### 57 | # CONFIGURE TESTS # 58 | ################### 59 | def test_configure_device_to_get_second_gpu_energy_with_no_second_gpu_api_raise_NoSuchDomainError(one_gpu_api): 60 | device = NvidiaGPUDevice() 61 | 62 | with pytest.raises(NoSuchDomainError): 63 | device.configure([NvidiaGPUDomain(1)]) 64 | 65 | 66 | def test_get_configured_domains_on_non_configured_device_raise_NotConfiguredDeviceException(one_gpu_api): 67 | device = NvidiaGPUDevice() 68 | 69 | with pytest.raises(NotConfiguredDeviceException): 70 | device.get_configured_domains() 71 | 72 | 73 | def test_get_configured_domains_with_default_values_with_one_gpu_api_return_correct_values(one_gpu_api): 74 | configured_domains = [NvidiaGPUDomain(0)] 75 | device = NvidiaGPUDevice() 76 | device.configure() 77 | assert configured_domains == device.get_configured_domains() 78 | 79 | 80 | def test_get_configured_domains_with_default_values_on_two_gpu_api_return_correct_values(two_gpu_api): 81 | configured_domains = [NvidiaGPUDomain(0), NvidiaGPUDomain(1)] 82 | device = NvidiaGPUDevice() 83 | device.configure() 84 | assert configured_domains == device.get_configured_domains() 85 | 86 | 87 | ######################################## 88 | # GET ENERGY WITH DEFAULT VALUES TESTS # 89 | ######################################## 90 | def test_get_default_energy_values_with_one_gpu_api(one_gpu_api): 91 | device = NvidiaGPUDevice() 92 | device.configure() 93 | assert device.get_energy() == [one_gpu_api.domains_current_energy['nvidia_gpu_0']] 94 | 95 | 96 | def test_get_default_energy_values_with_two_gpu_api(two_gpu_api): 97 | device = NvidiaGPUDevice() 98 | device.configure() 99 | assert device.get_energy() == [two_gpu_api.domains_current_energy['nvidia_gpu_0'], 100 | two_gpu_api.domains_current_energy['nvidia_gpu_1']] 101 | 102 | 103 | ################################## 104 | # CONFIGURE AND GET ENERGY TESTS # 105 | ################################## 106 | def test_get_gpu0_energy_with_only_one_gpu_api_return_correct_value(one_gpu_api): 107 | """ 108 | Create a NvidiaGpuDevice instance on a machine with one gpu 109 | configure it to monitor the gpu 0 110 | use the `get_energy` method and check if: 111 | - the returned list contains one element 112 | - this element is the energy consumption of the gpu 0 113 | """ 114 | device = NvidiaGPUDevice() 115 | device.configure([NvidiaGPUDomain(0)]) 116 | assert device.get_energy() == [one_gpu_api.domains_current_energy['nvidia_gpu_0']] 117 | 118 | 119 | def test_get_gpu0_energy_with_two_gpu_api_return_correct_value(two_gpu_api): 120 | """ 121 | Create a NvidiaGpuDevice instance on a machine with two gpu 122 | configure it to monitor the gpu 0 123 | use the `get_energy` method and check if: 124 | - the returned list contains one element 125 | - this element is the energy consumption of the gpu 0 126 | """ 127 | device = NvidiaGPUDevice() 128 | device.configure([NvidiaGPUDomain(0)]) 129 | assert device.get_energy() == [two_gpu_api.domains_current_energy['nvidia_gpu_0']] 130 | 131 | def test_get_gpu0_and_gpu1_energy_with_two_gpu_api_return_correct_value(two_gpu_api): 132 | """ 133 | Create a NvidiaGpuDevice instance on a machine with two gpu 134 | configure it to monitor the gpu 0 and gpu 1 135 | use the `get_energy` method and check if: 136 | - the returned list contains two elements 137 | - theses elements are the energy consumption of the gpu 0 and 1 138 | """ 139 | device = NvidiaGPUDevice() 140 | device.configure([NvidiaGPUDomain(0), NvidiaGPUDomain(1)]) 141 | assert device.get_energy() == [two_gpu_api.domains_current_energy['nvidia_gpu_0'], 142 | two_gpu_api.domains_current_energy['nvidia_gpu_1']] 143 | -------------------------------------------------------------------------------- /tests/unit/device/nvidia_device/test_NvidiaGPUDomain.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import pytest 22 | 23 | from pyJoules.device.nvidia_device import NvidiaGPUDomain, NvidiaGPUDevice 24 | 25 | 26 | @pytest.fixture(params=[-1, 0, 1]) 27 | def integer_value(request): 28 | """parametrize a test function with negative, null and positive integers 29 | """ 30 | return request.param 31 | 32 | 33 | def test_repr_return_nvidia_gpu_underscore_device_id(integer_value): 34 | domain = NvidiaGPUDomain(integer_value) 35 | assert str(domain) == 'nvidia_gpu_' + str(integer_value) 36 | 37 | 38 | def test_get_device_type_return_NvidiaGPUDevice(): 39 | domain = NvidiaGPUDomain(0) 40 | assert domain.get_device_type() == NvidiaGPUDevice 41 | -------------------------------------------------------------------------------- /tests/unit/device/rapl_device/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powerapi-ng/pyJoules/253b71642c1e82de68adce568e94a2d7cbc103d9/tests/unit/device/rapl_device/__init__.py -------------------------------------------------------------------------------- /tests/unit/device/rapl_device/test_RaplDevice.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import pytest 22 | 23 | from .... utils.rapl_fs import * 24 | 25 | from pyJoules.device import NotConfiguredDeviceException 26 | from pyJoules.device.rapl_device import RaplDevice, RaplPackageDomain, RaplDramDomain, RaplCoreDomain 27 | from pyJoules.device.rapl_device import RaplUncoreDomain 28 | from pyJoules.exception import NoSuchDeviceError, NoSuchDomainError 29 | 30 | ############## 31 | # INIT TESTS # 32 | ############## 33 | def test_create_RaplDevice_with_no_rapl_api_raise_NoSuchDeviceError(empty_fs): 34 | with pytest.raises(NoSuchDeviceError): 35 | RaplDevice() 36 | 37 | def test_create_RaplDevice(fs_pkg_one_socket): 38 | device = RaplDevice() 39 | assert device is not None 40 | assert isinstance(device, RaplDevice) 41 | 42 | 43 | ########################### 44 | # AVAILABLE DOMAINS TESTS # 45 | ########################### 46 | def test_available_domains_with_no_rapl_api_raise_NoSuchDeviceError(empty_fs): 47 | with pytest.raises(NoSuchDeviceError): 48 | RaplDevice.available_domains() 49 | 50 | 51 | def test_available_domains_with_pkg_rapl_api_return_correct_values(fs_pkg_one_socket): 52 | returned_values = RaplDevice.available_domains() 53 | correct_values = [RaplPackageDomain(0)] 54 | assert sorted(correct_values) == sorted(returned_values) 55 | 56 | 57 | def test_available_domains_with_pkg_and_dram_rapl_api_return_correct_values(fs_pkg_dram_one_socket): 58 | returned_values = RaplDevice.available_domains() 59 | correct_values = [RaplPackageDomain(0), RaplDramDomain(0)] 60 | assert sorted(correct_values) == sorted(returned_values) 61 | 62 | def test_available_domains_with_pkg_core_and_dram_rapl_api_return_correct_values(fs_pkg_dram_core_one_socket): 63 | returned_values = RaplDevice.available_domains() 64 | correct_values = [RaplPackageDomain(0), RaplDramDomain(0), RaplCoreDomain(0)] 65 | assert sorted(correct_values) == sorted(returned_values) 66 | 67 | 68 | def test_available_domains_with_pkg_uncore_and_dram_rapl_api_return_correct_values(fs_pkg_dram_uncore_one_socket): 69 | returned_values = RaplDevice.available_domains() 70 | correct_values = [RaplPackageDomain(0), RaplDramDomain(0), RaplUncoreDomain(0)] 71 | assert sorted(correct_values) == sorted(returned_values) 72 | 73 | 74 | def test_available_domains_with_pkg_psys_rapl_api_return_correct_values(fs_pkg_psys_one_socket): 75 | returned_values = RaplDevice.available_domains() 76 | correct_values = [RaplPackageDomain(0)] 77 | assert sorted(correct_values) == sorted(returned_values) 78 | 79 | 80 | def test_available_domains_with_pkg_dram_rapl_api_two_cpu_return_correct_values(fs_pkg_dram_two_socket): 81 | returned_values = RaplDevice.available_domains() 82 | correct_values = [RaplPackageDomain(0), RaplDramDomain(0), RaplPackageDomain(1), RaplDramDomain(1)] 83 | assert sorted(correct_values) == sorted(returned_values) 84 | 85 | 86 | ################### 87 | # CONFIGURE TESTS # 88 | ################### 89 | def test_configure_device_to_get_dram_energy_with_no_rapl_dram_api_raise_NoSuchDomainError(fs_pkg_one_socket): 90 | device = RaplDevice() 91 | 92 | with pytest.raises(NoSuchDomainError): 93 | device.configure([RaplDramDomain(0)]) 94 | 95 | def test_configure_device_to_get_pkg_energy_on_cpu1_with_no_cpu1_raise_NoSuchDomainError(fs_pkg_dram_one_socket): 96 | device = RaplDevice() 97 | 98 | with pytest.raises(NoSuchDomainError): 99 | device.configure([RaplPackageDomain(1)]) 100 | 101 | def test_get_configured_domains_on_non_configured_device_raise_NotConfiguredDeviceException(fs_pkg_dram_one_socket): 102 | device = RaplDevice() 103 | 104 | with pytest.raises(NotConfiguredDeviceException): 105 | device.get_configured_domains() 106 | 107 | 108 | def test_get_configured_domains_with_default_values_on_pkg_rapl_api_return_correct_values(fs_pkg_one_socket): 109 | configured_domains = [RaplPackageDomain(0)] 110 | device = RaplDevice() 111 | device.configure() 112 | assert configured_domains == device.get_configured_domains() 113 | 114 | 115 | def test_get_configured_domains_with_default_values_on_pkg_dram_rapl_api_return_correct_values(fs_pkg_dram_one_socket): 116 | configured_domains = [RaplPackageDomain(0), RaplDramDomain(0)] 117 | device = RaplDevice() 118 | device.configure() 119 | assert configured_domains == device.get_configured_domains() 120 | 121 | 122 | def test_get_configured_domains_with_pkg_values_on_pkg_dram_rapl_api_return_correct_values(fs_pkg_dram_one_socket): 123 | configured_domains = [RaplPackageDomain(0)] 124 | device = RaplDevice() 125 | device.configure(configured_domains) 126 | assert configured_domains == device.get_configured_domains() 127 | 128 | 129 | ######################################## 130 | # GET ENERGY WITH DEFAULT VALUES TESTS # 131 | ######################################## 132 | def test_get_default_energy_values_with_pkg_rapl_api(fs_pkg_one_socket): 133 | device = RaplDevice() 134 | device.configure() 135 | assert device.get_energy() == [fs_pkg_one_socket.domains_current_energy['package_0']] 136 | 137 | 138 | def test_get_default_energy_values_with_pkg_dram_rapl_api(fs_pkg_dram_one_socket): 139 | device = RaplDevice() 140 | device.configure() 141 | assert device.get_energy() == [fs_pkg_dram_one_socket.domains_current_energy['package_0'], 142 | fs_pkg_dram_one_socket.domains_current_energy['dram_0']] 143 | 144 | 145 | def test_get_default_energy_values_with_pkg_dram_rapl_api_two_sockets(fs_pkg_dram_two_socket): 146 | device = RaplDevice() 147 | device.configure() 148 | assert device.get_energy() == [fs_pkg_dram_two_socket.domains_current_energy['package_0'], 149 | fs_pkg_dram_two_socket.domains_current_energy['package_1'], 150 | fs_pkg_dram_two_socket.domains_current_energy['dram_0'], 151 | fs_pkg_dram_two_socket.domains_current_energy['dram_1']] 152 | 153 | 154 | ################################## 155 | # CONFIGURE AND GET ENERGY TESTS # 156 | ################################## 157 | def test_get_package_energy_with_only_pkg_rapl_api_return_correct_value(fs_pkg_one_socket): 158 | """ 159 | Create a RaplDevice instance on a machine with package rapl api with on one socket 160 | configure it to monitor package domain 161 | use the `get_energy` method and check if: 162 | - the returned list contains one element 163 | - this element is the power consumption of the package on socket 0 164 | """ 165 | device = RaplDevice() 166 | device.configure([RaplPackageDomain(0)]) 167 | assert device.get_energy() == [fs_pkg_one_socket.domains_current_energy['package_0']] 168 | 169 | 170 | def test_get_dram_energy_with_pkg_dram_rapl_api_return_correct_value(fs_pkg_dram_one_socket): 171 | """ 172 | Create a RaplDevice instance on a machine with package and dram rapl api with on one socket 173 | configure it to monitor dram domain 174 | use the `get_energy` method and check if: 175 | - the returned list contains one element 176 | - this element is the power consumption of the dram on socket 0 177 | """ 178 | device = RaplDevice() 179 | device.configure([RaplDramDomain(0)]) 180 | assert device.get_energy() == [fs_pkg_dram_one_socket.domains_current_energy['dram_0']] 181 | 182 | 183 | def test_get_package_dram_energy_with_pkg_dram_rapl_api_return_correct_value(fs_pkg_dram_one_socket): 184 | """ 185 | Create a RaplDevice instance on a machine with package and dram rapl api with on one socket 186 | configure it to monitor package and dram domains 187 | use the `get_energy` method and check if: 188 | - the returned list contains two elements 189 | - these elements are the power consumption of the package and dram on socket 0 190 | """ 191 | device = RaplDevice() 192 | device.configure([RaplPackageDomain(0), RaplDramDomain(0)]) 193 | assert device.get_energy() == [fs_pkg_dram_one_socket.domains_current_energy['package_0'], 194 | fs_pkg_dram_one_socket.domains_current_energy['dram_0']] 195 | 196 | 197 | def test_get_dram_package_energy_with_pkg_dram_rapl_api_return_correct_values_in_correct_order(fs_pkg_dram_one_socket): 198 | """ 199 | Create a RaplDevice instance on a machine with package and dram rapl api with on one socket 200 | configure it to monitor dram and package domains 201 | use the `get_energy` method and check if: 202 | - the returned list contains two elements 203 | - these elements are the power consumption of the dram and package on socket 0 204 | """ 205 | device = RaplDevice() 206 | device.configure([RaplDramDomain(0), RaplPackageDomain(0)]) 207 | assert device.get_energy() == [fs_pkg_dram_one_socket.domains_current_energy['dram_0'], 208 | fs_pkg_dram_one_socket.domains_current_energy['package_0']] 209 | 210 | 211 | def test_get_package_dram_energy_with_pkg_dram_rapl_api_two_sockets_return_correct_value(fs_pkg_dram_two_socket): 212 | """ 213 | Create a RaplDevice instance on a machine with package and dram rapl api with on two socket 214 | configure it to monitor package and dram domains 215 | use the `get_energy` method and check if: 216 | - the returned list contains 4 elements 217 | - these elements are the power consumption of the package and dram on socket 0 and socket 1 218 | """ 219 | device = RaplDevice() 220 | device.configure([RaplPackageDomain(0), RaplDramDomain(0), RaplPackageDomain(1), RaplDramDomain(1)]) 221 | assert device.get_energy() == [fs_pkg_dram_two_socket.domains_current_energy['package_0'], 222 | fs_pkg_dram_two_socket.domains_current_energy['dram_0'], 223 | fs_pkg_dram_two_socket.domains_current_energy['package_1'], 224 | fs_pkg_dram_two_socket.domains_current_energy['dram_1']] 225 | 226 | 227 | def test_get_package0_and_all_dram_energy_with_pkg_dram_rapl_api_two_sockets_return_correct_value(fs_pkg_dram_two_socket): 228 | """ 229 | Create a RaplDevice instance on a machine with package and dram rapl api with on two socket 230 | configure it to monitor package domains on socket 0 and dram domains on socket 0 and 1 231 | use the `get_energy` method and check if: 232 | - the returned list contains 3 elements 233 | - these elements are the power consumption of the package on socket 0 and dram on socket 0 and socket 1 234 | """ 235 | device = RaplDevice() 236 | device.configure([RaplPackageDomain(0), RaplDramDomain(0), RaplDramDomain(1)]) 237 | assert device.get_energy() == [fs_pkg_dram_two_socket.domains_current_energy['package_0'], 238 | fs_pkg_dram_two_socket.domains_current_energy['dram_0'], 239 | fs_pkg_dram_two_socket.domains_current_energy['dram_1']] 240 | -------------------------------------------------------------------------------- /tests/unit/device/rapl_device/test_RaplDomain.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import pytest 22 | 23 | from pyJoules.device.rapl_device import RaplDevice, RaplCoreDomain, RaplDramDomain, RaplPackageDomain, RaplUncoreDomain 24 | 25 | 26 | @pytest.fixture(params=[-1, 0, 1]) 27 | def integer_value(request): 28 | """parametrize a test function with negative, null and positive integers 29 | """ 30 | return request.param 31 | 32 | 33 | def test_uncore_repr_return_uncore_underscore_socket_id(integer_value): 34 | domain = RaplUncoreDomain(integer_value) 35 | assert str(domain) == 'uncore_' + str(integer_value) 36 | 37 | 38 | def test_core_repr_return_core_underscore_socket_id(integer_value): 39 | domain = RaplCoreDomain(integer_value) 40 | assert str(domain) == 'core_' + str(integer_value) 41 | 42 | 43 | def test_package_repr_return_package_underscore_socket_id(integer_value): 44 | domain = RaplPackageDomain(integer_value) 45 | assert str(domain) == 'package_' + str(integer_value) 46 | 47 | 48 | def test_dram_repr_return_dram_underscore_socket_id(integer_value): 49 | domain = RaplDramDomain(integer_value) 50 | assert str(domain) == 'dram_' + str(integer_value) 51 | 52 | def test_uncore_get_device_type_return_RaplDevice(): 53 | domain = RaplUncoreDomain(0) 54 | assert domain.get_device_type() == RaplDevice 55 | 56 | def test_core_get_device_type_return_RaplDevice(): 57 | domain = RaplCoreDomain(0) 58 | assert domain.get_device_type() == RaplDevice 59 | 60 | 61 | def test_package_get_device_type_return_RaplDevice(): 62 | domain = RaplPackageDomain(0) 63 | assert domain.get_device_type() == RaplDevice 64 | 65 | 66 | def test_dram_get_device_type_return_RaplDevice(): 67 | domain = RaplDramDomain(0) 68 | assert domain.get_device_type() == RaplDevice 69 | -------------------------------------------------------------------------------- /tests/unit/device/test_EnergyDeviceFactory.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import pytest 22 | 23 | from pyJoules.exception import NoSuchDomainError, NoSuchDeviceError 24 | from pyJoules.device import DeviceFactory 25 | from pyJoules.device.rapl_device import RaplPackageDomain, RaplDramDomain, RaplDevice 26 | from pyJoules.device.nvidia_device import NvidiaGPUDevice, NvidiaGPUDomain 27 | from ...utils.fake_nvidia_api import no_gpu_api, one_gpu_api, two_gpu_api 28 | from ...utils.rapl_fs import fs_pkg_one_socket, fs_pkg_dram_one_socket, empty_fs 29 | 30 | 31 | def test_create_devices_with_one_rapl_package_domain_return_one_correctly_configured_rapl_device(fs_pkg_dram_one_socket): 32 | domains = [RaplPackageDomain(0)] 33 | devices = DeviceFactory.create_devices(domains) 34 | 35 | assert len(devices) == 1 36 | assert isinstance(devices[0], RaplDevice) 37 | assert devices[0].get_configured_domains() == domains 38 | 39 | 40 | def test_create_devices_with_rapl_package_and_dram_domains_return_one_correctly_configured_rapl_device(fs_pkg_dram_one_socket): 41 | domains = [RaplPackageDomain(0), RaplDramDomain(0)] 42 | devices = DeviceFactory.create_devices(domains) 43 | 44 | assert len(devices) == 1 45 | assert isinstance(devices[0], RaplDevice) 46 | assert devices[0].get_configured_domains() == domains 47 | 48 | 49 | def test_create_devices_with_dram_rapl_domain_without_dram_support_raise_NoSuchDomainError(fs_pkg_one_socket): 50 | with pytest.raises(NoSuchDomainError): 51 | DeviceFactory.create_devices([RaplDramDomain(0)]) 52 | 53 | def test_create_devices_to_monitor_rapl_without_rapl_api_raise_NoSuchDeviceError(empty_fs): 54 | with pytest.raises(NoSuchDeviceError): 55 | DeviceFactory.create_devices([RaplDramDomain(0)]) 56 | 57 | def test_create_devices_to_monitor_gpu0_with_one_gpu_api_return_one_correctly_configured_nvidia_gpu_device(one_gpu_api): 58 | domains = [NvidiaGPUDomain(0)] 59 | devices = DeviceFactory.create_devices(domains) 60 | 61 | assert len(devices) == 1 62 | assert isinstance(devices[0], NvidiaGPUDevice) 63 | assert devices[0].get_configured_domains() == domains 64 | 65 | 66 | def test_create_devices_to_monitor_gpu0_and_gpu1_with_two_gpu_api_return_one_correctly_configured_nvidia_gpu_device(two_gpu_api): 67 | domains = [NvidiaGPUDomain(0), NvidiaGPUDomain(1)] 68 | devices = DeviceFactory.create_devices(domains) 69 | 70 | assert len(devices) == 1 71 | assert isinstance(devices[0], NvidiaGPUDevice) 72 | assert devices[0].get_configured_domains() == domains 73 | 74 | 75 | def test_create_devices_to_monitor_gpu0_without_gpu_api_raise_NoSuchDeviceError(no_gpu_api): 76 | with pytest.raises(NoSuchDeviceError): 77 | DeviceFactory.create_devices([NvidiaGPUDomain(0)]) 78 | 79 | def test_create_devices_to_monitor_gpu1_with_only_one_gpu_api_raise_NoSuchDomainError(one_gpu_api): 80 | with pytest.raises(NoSuchDomainError): 81 | DeviceFactory.create_devices([NvidiaGPUDomain(1)]) 82 | 83 | def test_create_devices_to_monitor_gpu0_and_package_0_machine_with_two_gpu_and_package_dram_api_create_one_nvidia_gpu_device_and_rapl_device_configured_for_package(two_gpu_api, fs_pkg_dram_one_socket): 84 | devices = DeviceFactory.create_devices([RaplPackageDomain(0), NvidiaGPUDomain(0)]) 85 | 86 | assert len(devices) == 2 87 | assert isinstance(devices[1], NvidiaGPUDevice) 88 | assert devices[1].get_configured_domains() == [NvidiaGPUDomain(0)] 89 | assert isinstance(devices[0], RaplDevice) 90 | assert devices[0].get_configured_domains() == [RaplPackageDomain(0)] 91 | 92 | 93 | def test_create_devices_with_default_values_on_machine_with_only_rapl_pkg_and_dram_api_create_one_device_configured_for_dram_and_rapl(fs_pkg_dram_one_socket): 94 | devices = DeviceFactory.create_devices() 95 | 96 | assert len(devices) == 1 97 | assert isinstance(devices[0], RaplDevice) 98 | assert devices[0].get_configured_domains() == [RaplPackageDomain(0), RaplDramDomain(0)] 99 | 100 | 101 | def test_create_devices_with_default_values_on_machine_with_only_one_gpu_create_one_nvidia_gpu_device(one_gpu_api, empty_fs): 102 | devices = DeviceFactory.create_devices() 103 | 104 | assert len(devices) == 1 105 | assert isinstance(devices[0], NvidiaGPUDevice) 106 | assert devices[0].get_configured_domains() == [NvidiaGPUDomain(0)] 107 | 108 | 109 | def test_create_devices_with_default_values_on_machine_with_one_gpu_and_package_api_create_one_nvidia_gpu_device_and_rapl_device_configured_for_package(one_gpu_api, fs_pkg_one_socket): 110 | devices = DeviceFactory.create_devices() 111 | 112 | assert len(devices) == 2 113 | assert isinstance(devices[1], NvidiaGPUDevice) 114 | assert devices[1].get_configured_domains() == [NvidiaGPUDomain(0)] 115 | assert isinstance(devices[0], RaplDevice) 116 | assert devices[0].get_configured_domains() == [RaplPackageDomain(0)] 117 | -------------------------------------------------------------------------------- /tests/unit/energy_meter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powerapi-ng/pyJoules/253b71642c1e82de68adce568e94a2d7cbc103d9/tests/unit/energy_meter/__init__.py -------------------------------------------------------------------------------- /tests/unit/energy_meter/test_EnergyState.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import pytest 22 | 23 | from pyJoules.energy_meter import EnergyState, NoNextStateException, StateIsNotFinalError 24 | 25 | 26 | TS_FIRST = 1.0 27 | TS_SECOND = 2.0 28 | 29 | E_FIRST_DOMAIN0 = 1.0 30 | E_FIRST_DOMAIN1 = 7.2 31 | 32 | E_SECOND_DOMAIN0 = 2.0 33 | E_SECOND_DOMAIN1 = 9.4 34 | 35 | 36 | @pytest.fixture 37 | def alone_state(): 38 | return EnergyState(TS_FIRST, 'first', [[E_FIRST_DOMAIN0, E_FIRST_DOMAIN1]]) 39 | 40 | 41 | @pytest.fixture 42 | def two_states(alone_state): 43 | state2 = EnergyState(TS_SECOND, 'second', [[E_SECOND_DOMAIN0, E_SECOND_DOMAIN1]]) 44 | alone_state.add_next_state(state2) 45 | return alone_state 46 | 47 | 48 | ########### 49 | # IS_LAST # 50 | ########### 51 | def test_new_state_are_last_sate_of_trace(alone_state): 52 | assert alone_state.is_last() 53 | 54 | 55 | def test_add_a_state_to_new_state_make_it_not_being_last_state_of_trace(alone_state): 56 | assert alone_state.is_last() 57 | alone_state.add_next_state(EnergyState(1, 'second', [1, 1])) 58 | assert not alone_state.is_last() 59 | 60 | 61 | ########### 62 | # COMPUTE # 63 | ########### 64 | def test_compute_duration_from_last_state_raise_NoNextStateException(alone_state): 65 | with pytest.raises(NoNextStateException): 66 | alone_state.compute_duration() 67 | 68 | 69 | def test_compute_energy_from_last_state_raise_NoNextStateException(alone_state): 70 | with pytest.raises(NoNextStateException): 71 | alone_state.compute_energy([]) 72 | 73 | 74 | def test_compute_duration_between_two_state_return_correct_values(two_states): 75 | print(two_states.next_state) 76 | assert two_states.compute_duration() == TS_SECOND - TS_FIRST 77 | 78 | 79 | def test_compute_energy_between_two_state_return_correct_values(two_states): 80 | energy = two_states.compute_energy(['domain0', 'domain1']) 81 | assert len(energy) == 2 82 | assert energy['domain0'] == E_SECOND_DOMAIN0 - E_FIRST_DOMAIN0 83 | assert energy['domain1'] == E_SECOND_DOMAIN1 - E_FIRST_DOMAIN1 84 | 85 | 86 | ################## 87 | # ADD_NEXT_STATE # 88 | ################## 89 | def test_add_a_state_to_non_final_state_raise_StateIsNotFinalError(two_states): 90 | with pytest.raises(StateIsNotFinalError): 91 | two_states.add_next_state(EnergyState(1, 'third', [1, 2])) 92 | -------------------------------------------------------------------------------- /tests/unit/energy_sample/test_EnergyTrace.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | import pytest 21 | 22 | from pyJoules.energy_trace import EnergyTrace, EnergySample 23 | 24 | 25 | SAMPLE_1 = EnergySample('123', 'tag1', 10, {'domain1': 1, 'domain2': 2}) 26 | SAMPLE_2 = EnergySample('456', 'tag2', 20, {'domain2': 2, 'domain3': 3}) 27 | 28 | IDLE_1 = {'domain1': 1, 'domain2': 1} 29 | IDLE_2 = {'domain2': 1, 'domain3': 2} 30 | 31 | NEGATIVE_SAMPLE_1 = EnergySample('123', 'tag1', 10, {'domain1': -1, 'domain2': 2}) 32 | NEGATIVE_SAMPLE_2 = EnergySample('456', 'tag2', 20, {'domain2': 2, 'domain3': -3}) 33 | 34 | 35 | @pytest.fixture 36 | def trace_with_one_sample(): 37 | return EnergyTrace([SAMPLE_1]) 38 | 39 | 40 | @pytest.fixture 41 | def trace_with_two_sample(): 42 | return EnergyTrace([SAMPLE_1, SAMPLE_2]) 43 | 44 | 45 | @pytest.fixture 46 | def negative_trace(): 47 | return EnergyTrace([NEGATIVE_SAMPLE_1, NEGATIVE_SAMPLE_2]) 48 | 49 | 50 | @pytest.fixture 51 | def semi_negative_trace(): 52 | return EnergyTrace([SAMPLE_1, NEGATIVE_SAMPLE_2]) 53 | 54 | 55 | ########## 56 | # LENGTH # 57 | ########## 58 | def test_length_of_zero_element_trace_is_zero(): 59 | assert len(EnergyTrace([])) == 0 60 | 61 | 62 | def test_length_of_one_element_trace_is_one(trace_with_one_sample): 63 | assert len(trace_with_one_sample) 64 | 65 | 66 | def test_length_of_two_elements_trace_is_two(trace_with_two_sample): 67 | assert len(trace_with_two_sample) 68 | 69 | 70 | ################## 71 | # LIST INTERFACE # 72 | ################## 73 | def test_create_trace_with_one_sample_and_get_second_element_must_raise_IndexError(trace_with_one_sample): 74 | with pytest.raises(IndexError): 75 | trace_with_one_sample[1] 76 | 77 | 78 | def test_create_trace_with_one_sample_and_get_first_element_must_return_first_element(trace_with_one_sample): 79 | assert trace_with_one_sample[0] == SAMPLE_1 80 | 81 | 82 | def test_create_trace_with_two_sample_and_get_second_element_must_return_second_element(trace_with_two_sample): 83 | assert trace_with_two_sample[1] == SAMPLE_2 84 | 85 | 86 | def test_create_a_trace_from_a_localy_defined_list_and_modify_the_list_musnt_modify_the_trace(): 87 | locale_list = [SAMPLE_1] 88 | trace = EnergyTrace(locale_list) 89 | locale_list[0] = 0 90 | assert trace[0] != 0 91 | 92 | 93 | def test_append_sample_to_a_trace_with_one_sample_create_a_trace_with_two_sample(trace_with_one_sample): 94 | trace_with_one_sample.append(SAMPLE_1) 95 | assert len(trace_with_one_sample) == 2 96 | 97 | 98 | def test_append_sample_to_a_trace_with_one_sample_create_a_trace_with_two_correct_sample(trace_with_one_sample): 99 | trace_with_one_sample.append(SAMPLE_2) 100 | assert trace_with_one_sample[0] == SAMPLE_1 101 | assert trace_with_one_sample[1] == SAMPLE_2 102 | 103 | 104 | def test_append_sample_to_a_trace_with_one_sample_create_a_trace_with_two_correct_sample(trace_with_one_sample): 105 | trace_with_one_sample.append(SAMPLE_2) 106 | assert trace_with_one_sample[0] == SAMPLE_1 107 | assert trace_with_one_sample[1] == SAMPLE_2 108 | 109 | 110 | def test_add_two_trace_with_one_sample_produce_a_trace_of_length_two(trace_with_one_sample): 111 | assert len(trace_with_one_sample + trace_with_one_sample) == 2 112 | 113 | 114 | def test_add_one_trace_with_one_sample_and_one_trace_with_two_sample_tproduce_a_trace_of_length_three(trace_with_one_sample, trace_with_two_sample): 115 | assert len(trace_with_one_sample + trace_with_two_sample) == 3 116 | 117 | 118 | def test_add_two_trace_with_one_sample_produce_a_trace_with_correct_values(trace_with_one_sample): 119 | trace = trace_with_one_sample + trace_with_one_sample 120 | assert trace[0] == SAMPLE_1 121 | assert trace[1] == SAMPLE_1 122 | 123 | 124 | def test_add_one_trace_with_one_sample_and_one_trace_with_two_sample_tproduce_a_trace_with_correct_values(trace_with_one_sample, trace_with_two_sample): 125 | trace = trace_with_one_sample + trace_with_two_sample 126 | assert trace[0] == SAMPLE_1 127 | assert trace[1] == SAMPLE_1 128 | assert trace[2] == SAMPLE_2 129 | 130 | 131 | def test_iadd_a_trace_with_one_sample_to_a_trace_with_one_sample_create_a_trace_with_two_sample(trace_with_one_sample): 132 | trace_with_one_sample += trace_with_one_sample 133 | assert len(trace_with_one_sample) == 2 134 | 135 | 136 | def test_iadd_a_trace_with_one_sample_to_a_trace_with_one_sample_create_a_trace_with_two_correct_sample(trace_with_one_sample): 137 | trace_with_one_sample += trace_with_one_sample 138 | assert trace_with_one_sample[0] == SAMPLE_1 139 | assert trace_with_one_sample[1] == SAMPLE_1 140 | 141 | 142 | def test_iadd_a_trace_with_two_sample_to_a_trace_with_one_sample_create_a_trace_with_three_sample(trace_with_one_sample, trace_with_two_sample): 143 | trace_with_one_sample += trace_with_two_sample 144 | assert len(trace_with_one_sample) == 3 145 | 146 | 147 | def test_iadd_a_trace_with_two_sample_to_a_trace_with_one_sample_create_a_trace_with_three_correct_sample(trace_with_one_sample, trace_with_two_sample): 148 | trace_with_one_sample += trace_with_two_sample 149 | assert trace_with_one_sample[0] == SAMPLE_1 150 | assert trace_with_one_sample[1] == SAMPLE_1 151 | assert trace_with_one_sample[2] == SAMPLE_2 152 | 153 | ################## 154 | # DICT INTERFACE # 155 | ################## 156 | def test_create_trace_with_one_sample_and_get_bad_tag_must_raise_KeyError(trace_with_one_sample): 157 | with pytest.raises(KeyError): 158 | trace_with_one_sample['bad_tag'] 159 | 160 | 161 | def test_create_trace_with_one_sample_and_get_tag_of_the_first_element_must_return_first_element(trace_with_one_sample): 162 | assert trace_with_one_sample['tag1'] == SAMPLE_1 163 | 164 | 165 | def test_create_trace_with_two_sample_and_get_tag_of_the_second_element_must_return_second_element(trace_with_two_sample): 166 | assert trace_with_two_sample['tag2'] == SAMPLE_2 167 | 168 | 169 | def test_create_trace_with_one_sample_and_use_in_keyword_to_check_if_tag_is_present_return_true(trace_with_one_sample): 170 | assert 'tag1' in trace_with_one_sample 171 | 172 | 173 | def test_create_trace_with_one_sample_and_use_in_keyword_to_check_if_bad_tag_is_present_return_false(trace_with_one_sample): 174 | assert not 'bad_tag' in trace_with_one_sample 175 | 176 | 177 | def test_get_sample_on_a_two_sample_trace_with_same_names_return_first_sample(): 178 | s1 = EnergySample('123', 'tag1', 10, {'domain1': 1, 'domain2': 2}) 179 | s2 = EnergySample('456', 'tag1', 20, {'domain2': 2, 'domain3': 3}) 180 | 181 | trace = EnergyTrace([s1, s2]) 182 | 183 | assert trace['tag1'] == s1 184 | 185 | 186 | ########### 187 | # ITERATE # 188 | ########### 189 | def test_create_trace_with_one_sample_and_iter_on_it_must_iter_on_one_element(trace_with_one_sample): 190 | l = [] 191 | for s in trace_with_one_sample: 192 | l.append(s) 193 | assert l[0] == SAMPLE_1 194 | 195 | def test_create_trace_with_two_sample_and_iter_on_it_must_iter_on_two_elements(trace_with_two_sample): 196 | l = [] 197 | for s in trace_with_two_sample: 198 | l.append(s) 199 | assert l[0] == SAMPLE_1 200 | assert l[1] == SAMPLE_2 201 | 202 | 203 | ############### 204 | # REMOVE_IDLE # 205 | ############### 206 | def test_create_trace_with_two_element_and_remove_idle_with_one_value_must_raise_ValueError(trace_with_two_sample): 207 | with pytest.raises(ValueError): 208 | trace_with_two_sample.remove_idle([IDLE_1]) 209 | 210 | 211 | def test_create_trace_with_two_element_and_remove_idle_with_three_value_must_raise_ValueError(trace_with_two_sample): 212 | with pytest.raises(ValueError): 213 | trace_with_two_sample.remove_idle([IDLE_1, IDLE_1, IDLE_2]) 214 | 215 | 216 | def test_create_trace_with_two_element_and_remove_idle_with_value_with_bad_tag_must_raise_ValueError(trace_with_two_sample): 217 | with pytest.raises(ValueError): 218 | trace_with_two_sample.remove_idle([IDLE_2, IDLE_1]) 219 | 220 | 221 | def test_create_trace_with_two_element_and_remove_idle_must_return_good_values(trace_with_two_sample): 222 | trace_with_two_sample.remove_idle([IDLE_1, IDLE_2]) 223 | assert trace_with_two_sample[0].energy['domain1'] == 0 224 | assert trace_with_two_sample[0].energy['domain2'] == 1 225 | assert trace_with_two_sample[1].energy['domain2'] == 1 226 | assert trace_with_two_sample[1].energy['domain3'] == 1 227 | 228 | 229 | ############## 230 | # CLEAN_DATA # 231 | ############## 232 | def test_clean_trace_without_negative_value_must_return_the_same_trace(trace_with_two_sample): 233 | trace_with_two_sample.clean_data() 234 | assert len(trace_with_two_sample) == 2 235 | 236 | 237 | def test_create_trace_with_two_element_with_negative_energy_values_and_clean_data_must_remove_all_elements(negative_trace): 238 | negative_trace.clean_data() 239 | assert len(negative_trace) == 0 240 | 241 | 242 | def test_create_trace_with_two_element_with_one_element_with_negative_energy_values_and_clean_data_must_remove_one_element(semi_negative_trace): 243 | semi_negative_trace.clean_data() 244 | assert len(semi_negative_trace) == 1 245 | assert semi_negative_trace[0] == SAMPLE_1 246 | 247 | 248 | def test_clean_with_guard_a_trace_without_negative_sample_but_a_bad_sample_must_remove_this_sample(trace_with_two_sample): 249 | trace_with_two_sample.clean_data(guards=[lambda sample: False if 'domain3' in sample.energy else True]) 250 | assert len(trace_with_two_sample) == 1 251 | assert trace_with_two_sample[0] == SAMPLE_1 252 | 253 | 254 | def test_clean_with_guard_a_trace_without_negative_sample_but_two_bad_sample_must_remove_all_samples(trace_with_two_sample): 255 | trace_with_two_sample.clean_data(guards=[lambda sample: False if 'domain2' in sample.energy else True]) 256 | assert len(trace_with_two_sample) == 0 257 | 258 | 259 | def test_clean_with_guard_a_trace_with_a_negative_and_bad_sample_must_remove_this_samples(): 260 | trace = EnergyTrace([SAMPLE_1, NEGATIVE_SAMPLE_2]) 261 | trace.clean_data(guards=[lambda sample: False if 'domain3' in sample.energy else True]) 262 | assert len(trace) == 1 263 | assert trace[0] == SAMPLE_1 264 | 265 | def test_clean_with_guard_a_trace_with_a_negative_sample_and_a_bad_sample_must_remove_all_samples(): 266 | trace = EnergyTrace([SAMPLE_1, NEGATIVE_SAMPLE_2]) 267 | trace.clean_data(guards=[lambda sample: False if 'domain1' in sample.energy else True]) 268 | assert len(trace) == 0 269 | -------------------------------------------------------------------------------- /tests/unit/handler/test_CSVHandler.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import pytest 22 | import pyfakefs 23 | import os.path 24 | 25 | from pyJoules.handler.csv_handler import CSVHandler 26 | from pyJoules.handler import UnconsistantSamplesError 27 | from pyJoules.energy_trace import EnergySample, EnergyTrace 28 | 29 | 30 | @pytest.fixture 31 | def non_writable_folder(fs): 32 | """ 33 | A filesystem with a /dir directory that is not writable 34 | """ 35 | fs.create_dir('/dir', perm_bits=000) 36 | return fs 37 | 38 | 39 | @pytest.fixture 40 | def sample1(): 41 | return EnergySample(1, 'sample1', 2, {'d1': 1, 'd2': 2}) 42 | 43 | 44 | @pytest.fixture 45 | def sample2(): 46 | return EnergySample(2, 'sample2', 3, {'d1': 3, 'd2': 4}) 47 | 48 | 49 | @pytest.fixture 50 | def bad_sample(): 51 | return EnergySample(2, 'sample2', 3, {'d1': 3, 'd3': 4}) 52 | 53 | 54 | @pytest.fixture 55 | def bad_trace(bad_sample): 56 | return EnergyTrace([bad_sample]) 57 | 58 | 59 | @pytest.fixture 60 | def trace1(sample1): 61 | return EnergyTrace([sample1]) 62 | 63 | 64 | @pytest.fixture 65 | def trace2(sample1, sample2): 66 | return EnergyTrace([sample1, sample2]) 67 | 68 | 69 | def test_record_data_on_non_writable_file_raise_IOError(non_writable_folder, trace1): 70 | handler = CSVHandler('/dir/file.csv') 71 | handler.process(trace1) 72 | with pytest.raises(IOError): 73 | handler.save_data() 74 | 75 | 76 | def test_record_trace_with_unconsistent_sample_raise_UnconsistantSamplesError(fs, trace1, bad_trace): 77 | handler = CSVHandler('/file.csv') 78 | handler.process(trace1) 79 | handler.process(bad_trace) 80 | with pytest.raises(UnconsistantSamplesError): 81 | handler.save_data() 82 | 83 | 84 | def test_record_trace_with_unconsistent_sample_must_not_write_in_file(fs, trace1, bad_trace): 85 | handler = CSVHandler('/file.csv') 86 | handler.process(trace1) 87 | handler.process(bad_trace) 88 | try: 89 | handler.save_data() 90 | except UnconsistantSamplesError: 91 | pass 92 | assert not os.path.exists('/file.csv') 93 | 94 | def test_process_one_trace_and_record_produce_a_file_with_one_correct_line(fs, trace1): 95 | handler = CSVHandler('/file.csv') 96 | handler.process(trace1) 97 | handler.save_data() 98 | 99 | assert os.path.exists('/file.csv') 100 | with open('/file.csv', 'r') as csv: 101 | lines = [] 102 | for line in csv: 103 | lines.append(line) 104 | 105 | assert len(lines) == 2 106 | assert lines[0] == 'timestamp;tag;duration;d1;d2\n' 107 | assert lines[1] == '1;sample1;2;1;2\n' 108 | 109 | 110 | def test_process_two_trace_and_record_produce_a_file_with_two_correct_lines(fs, trace1): 111 | handler = CSVHandler('/file.csv') 112 | handler.process(trace1) 113 | handler.process(trace1) 114 | handler.save_data() 115 | 116 | assert os.path.exists('/file.csv') 117 | with open('/file.csv', 'r') as csv: 118 | lines = [] 119 | for line in csv: 120 | lines.append(line) 121 | 122 | assert len(lines) == 3 123 | assert lines[0] == 'timestamp;tag;duration;d1;d2\n' 124 | assert lines[1] == '1;sample1;2;1;2\n' 125 | assert lines[2] == '1;sample1;2;1;2\n' 126 | 127 | 128 | def test_process_one_trace_with_one_sample_and_one_trace_with_two_sample_and_record_produce_a_file_with_three_correct_lines(fs, trace1, trace2): 129 | handler = CSVHandler('/file.csv') 130 | handler.process(trace1) 131 | handler.process(trace2) 132 | handler.save_data() 133 | 134 | assert os.path.exists('/file.csv') 135 | with open('/file.csv', 'r') as csv: 136 | lines = [] 137 | for line in csv: 138 | lines.append(line) 139 | 140 | assert len(lines) == 4 141 | assert lines[0] == 'timestamp;tag;duration;d1;d2\n' 142 | assert lines[1] == '1;sample1;2;1;2\n' 143 | assert lines[2] == '1;sample1;2;1;2\n' 144 | assert lines[3] == '2;sample2;3;3;4\n' 145 | 146 | 147 | def test_process_one_trace_and_record_in_existing_file_only_append_one_correct_line(fs, trace1): 148 | fs.create_file('/file.csv', contents='timestamp;tag;duration;d1;d2\n1;sample1;2;1;2\n') 149 | 150 | handler = CSVHandler('/file.csv') 151 | handler.process(trace1) 152 | handler.save_data() 153 | 154 | assert os.path.exists('/file.csv') 155 | with open('/file.csv', 'r') as csv: 156 | lines = [] 157 | for line in csv: 158 | lines.append(line) 159 | 160 | assert len(lines) == 3 161 | assert lines[0] == 'timestamp;tag;duration;d1;d2\n' 162 | assert lines[1] == '1;sample1;2;1;2\n' 163 | assert lines[1] == '1;sample1;2;1;2\n' 164 | -------------------------------------------------------------------------------- /tests/unit/handler/test_PandasHandler.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import pytest 22 | import pandas 23 | 24 | from pyJoules.handler.pandas_handler import PandasHandler, NoSampleProcessedError, UnconsistantSamplesError 25 | from pyJoules.energy_trace import EnergySample, EnergyTrace 26 | 27 | 28 | @pytest.fixture 29 | def sample1(): 30 | return EnergySample(1, 'sample1', 2, {'d1': 1, 'd2': 2}) 31 | 32 | 33 | @pytest.fixture 34 | def sample2(): 35 | return EnergySample(2, 'sample2', 3, {'d1': 3, 'd2': 4}) 36 | 37 | 38 | @pytest.fixture 39 | def trace1(sample1): 40 | return EnergyTrace([sample1]) 41 | 42 | 43 | @pytest.fixture 44 | def trace2(sample1, sample2): 45 | return EnergyTrace([sample1, sample2]) 46 | 47 | @pytest.fixture 48 | def bad_trace(sample1): 49 | bad_sample = EnergySample(2, 'toto', 3, {'d3': 3}) 50 | return EnergyTrace([sample1, bad_sample]) 51 | 52 | 53 | def test_create_a_pandas_handler_and_get_dataframe_must_raise_NoSampleProcessedError(): 54 | handler = PandasHandler() 55 | with pytest.raises(NoSampleProcessedError): 56 | df = handler.get_dataframe() 57 | 58 | 59 | def test_process_one_sample_trace_and_get_dataframe_must_return_dataframe_of_len_1(trace1): 60 | handler = PandasHandler() 61 | handler.process(trace1) 62 | df = handler.get_dataframe() 63 | assert len(df) == 1 64 | 65 | 66 | def test_process_two_trace_and_get_dataframe_must_return_dataframe_of_len_2(trace1): 67 | handler = PandasHandler() 68 | handler.process(trace1) 69 | handler.process(trace1) 70 | df = handler.get_dataframe() 71 | assert len(df) == 2 72 | 73 | def test_process_one_trace_with_one_sample_and_one_trace_with_two_sample_and_get_dataframe_must_return_dataframe_of_len_3(trace1, trace2): 74 | handler = PandasHandler() 75 | handler.process(trace1) 76 | handler.process(trace2) 77 | df = handler.get_dataframe() 78 | assert len(df) == 3 79 | 80 | 81 | def test_process_one_sample_trace_and_get_dataframe_must_return_good_column_names(trace1, sample1): 82 | handler = PandasHandler() 83 | handler.process(trace1) 84 | df = handler.get_dataframe() 85 | assert df.columns[0] == 'timestamp' 86 | assert df.columns[1] == 'tag' 87 | assert df.columns[2] == 'duration' 88 | 89 | i = 3 90 | for domain_name in sample1.energy: 91 | assert df.columns[i] == domain_name 92 | i += 1 93 | 94 | def test_process_one_sample_trace_and_get_dataframe_must_return_good_values(trace1, sample1): 95 | handler = PandasHandler() 96 | handler.process(trace1) 97 | df = handler.get_dataframe() 98 | df['timestamp'][0] == sample1.timestamp 99 | df['tag'][0] == sample1.tag 100 | df['duration'][0] == sample1.duration 101 | 102 | for domain_name in sample1.energy: 103 | assert df[domain_name][0] == sample1.energy[domain_name] 104 | 105 | 106 | def test_process_one_trace_with_one_sample_and_one_trace_with_two_sample_get_dataframe_must_return_good_values_for_second_sample(trace1, trace2, sample1, sample2): 107 | handler = PandasHandler() 108 | handler.process(trace1) 109 | handler.process(trace2) 110 | df = handler.get_dataframe() 111 | df['timestamp'][1] == sample1.timestamp 112 | df['tag'][1] == sample1.tag 113 | df['duration'][1] == sample1.duration 114 | 115 | for domain_name in sample1.energy: 116 | assert df[domain_name][1] == sample1.energy[domain_name] 117 | 118 | df['timestamp'][2] == sample2.timestamp 119 | df['tag'][2] == sample2.tag 120 | df['duration'][2] == sample2.duration 121 | 122 | for domain_name in sample2.energy: 123 | assert df[domain_name][2] == sample2.energy[domain_name] 124 | 125 | 126 | def test_process_trace_with_two_different_sample_and_get_dataframe_must_raise_UnconsistantSamplesError(bad_trace): 127 | handler = PandasHandler() 128 | with pytest.raises(UnconsistantSamplesError): 129 | handler.process(bad_trace) 130 | 131 | df = handler.get_dataframe() 132 | -------------------------------------------------------------------------------- /tests/unit/handler/test_trace_to_dict.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | import pytest 21 | 22 | from pyJoules.handler.mongo_handler import trace_to_dict 23 | from pyJoules.energy_trace import EnergySample 24 | 25 | @pytest.fixture 26 | def sample1(): 27 | return EnergySample(1, 'sample1', 2, {'d1': 1, 'd2': 2}) 28 | 29 | 30 | @pytest.fixture 31 | def sample2(): 32 | return EnergySample(2, 'sample2', 3, {'d1': 3, 'd2': 4}) 33 | 34 | 35 | def test_converting_trace_list_to_dict_must_return_dict_good_name(sample1): 36 | d = trace_to_dict([sample1], 'trace1') 37 | assert d['name'] == 'trace1' 38 | 39 | 40 | def test_converting_empty_list_to_dict_must_return_dict_with_empty_trace(): 41 | d = trace_to_dict([], 'trace1') 42 | assert d['trace'] == [] 43 | 44 | 45 | def test_converting_one_trace_sample_to_dict_must_return_dict_with_one_sample(sample1): 46 | d = trace_to_dict([sample1], 'trace1') 47 | assert len(d['trace']) == 1 48 | 49 | 50 | def test_converting_one_trace_sample_to_dict_must_return_dict_with_correct_values(sample1): 51 | d = trace_to_dict([sample1], 'trace1') 52 | assert d['trace'][0]['timestamp'] == sample1.timestamp 53 | assert d['trace'][0]['tag'] == sample1.tag 54 | assert d['trace'][0]['duration'] == sample1.duration 55 | assert d['trace'][0]['energy'] == sample1.energy 56 | 57 | 58 | def test_converting_two_trace_sample_to_dict_must_return_dict_with_two_samples(sample1, sample2): 59 | d = trace_to_dict([sample1, sample2], 'trace1') 60 | assert len(d['trace']) == 2 61 | 62 | 63 | def test_converting_two_trace_sample_to_dict_must_return_dict_with_correct_values(sample1, sample2): 64 | d = trace_to_dict([sample1, sample2], 'trace1') 65 | 66 | assert d['trace'][0]['timestamp'] == sample1.timestamp 67 | assert d['trace'][0]['tag'] == sample1.tag 68 | assert d['trace'][0]['duration'] == sample1.duration 69 | assert d['trace'][0]['energy'] == sample1.energy 70 | 71 | assert d['trace'][1]['timestamp'] == sample2.timestamp 72 | assert d['trace'][1]['tag'] == sample2.tag 73 | assert d['trace'][1]['duration'] == sample2.duration 74 | assert d['trace'][1]['energy'] == sample2.energy 75 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/powerapi-ng/pyJoules/253b71642c1e82de68adce568e94a2d7cbc103d9/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/fake_api.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from pyJoules.energy_meter import EnergySample 22 | 23 | 24 | class FakeAPI: 25 | def reset_values(self): 26 | raise NotImplementedError() 27 | 28 | def get_device_type(self): 29 | raise NotImplementedError() 30 | 31 | 32 | class CorrectTrace: 33 | """ 34 | Generate the correct trace, consistent with given API 35 | """ 36 | 37 | def __init__(self, domains, fake_api_list, timestamps): 38 | self.domains = domains 39 | self.fake_api = {} 40 | for api in fake_api_list: 41 | self.fake_api[api.get_device_type()] = api 42 | self.energy_states = [] 43 | self.correct_timestamps = timestamps 44 | self.duration_trace = self._compute_duration_trace(timestamps) 45 | self.tag_trace = [] 46 | 47 | def _get_new_energy_values(self): 48 | new_values = {} 49 | for domain in self.domains: 50 | fake_api = self.fake_api[domain.get_device_type()] 51 | new_values[str(domain)] = fake_api.domains_current_energy[str(domain)] 52 | return new_values 53 | 54 | def _compute_duration_trace(self, timestamps): 55 | duration_trace = [] 56 | current_ts = timestamps[0] 57 | for next_ts in timestamps[1:]: 58 | duration_trace.append(next_ts - current_ts) 59 | current_ts = next_ts 60 | return duration_trace 61 | 62 | def add_new_sample(self, tag): 63 | for _, api in self.fake_api.items(): 64 | api.reset_values() 65 | self.tag_trace.append(tag) 66 | self.energy_states.append(self._get_new_energy_values()) 67 | 68 | def _compute_energy_trace(self): 69 | trace = [] 70 | current_state = self.energy_states[0] 71 | for next_state in self.energy_states[1:]: 72 | sample = {} 73 | for domain in self.domains: 74 | sample[str(domain)] = next_state[str(domain)] - current_state[str(domain)] 75 | current_state = next_state 76 | trace.append(sample) 77 | return trace 78 | 79 | def get_trace(self): 80 | trace = [] 81 | for sample in self: 82 | trace.append(sample) 83 | return trace 84 | 85 | def __iter__(self): 86 | trace = [] 87 | energy_trace = self._compute_energy_trace() 88 | zipped_list = zip(energy_trace, self.tag_trace, self.duration_trace, 89 | self.correct_timestamps) 90 | for values, tag, duration, timestamp in zipped_list: 91 | trace.append(EnergySample(timestamp, tag, duration, values)) 92 | return trace.__iter__() 93 | -------------------------------------------------------------------------------- /tests/utils/fake_nvidia_api.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import random 22 | import pytest 23 | from pynvml import NVMLError 24 | from pyJoules.device.nvidia_device import NvidiaGPUDomain, NvidiaGPUDevice 25 | from .fake_api import FakeAPI 26 | from mock import patch, Mock 27 | 28 | class FakeNvidiaAPI(FakeAPI): 29 | 30 | def __init__(self, number_of_device): 31 | self.number_of_device = number_of_device 32 | 33 | self.domains_current_energy = None 34 | 35 | def reset_values(self): 36 | self.domains_current_energy = {} 37 | for device_id in range(self.number_of_device): 38 | self.domains_current_energy[str(NvidiaGPUDomain(device_id))] = random.random() 39 | 40 | def get_energy_value(self, device_id): 41 | return self.domains_current_energy[str(NvidiaGPUDomain(device_id))] 42 | 43 | def get_device_type(self): 44 | return NvidiaGPUDevice 45 | 46 | @pytest.fixture 47 | def no_gpu_api(): 48 | patcher = patch('pynvml.nvmlInit', side_effect=NVMLError(0)) 49 | yield patcher.start() 50 | patcher.stop() 51 | 52 | 53 | class MockedHandle(Mock): 54 | 55 | def __init__(self, device_id): 56 | Mock.__init__(self) 57 | self.device_id = device_id 58 | 59 | 60 | @pytest.fixture 61 | def one_gpu_api(): 62 | fake_api = FakeNvidiaAPI(1) 63 | fake_api.reset_values() 64 | 65 | patcher_init = patch('pynvml.nvmlInit') 66 | patcher_device_count = patch('pynvml.nvmlDeviceGetCount', return_value=1) 67 | patcher_get_handle = patch('pynvml.nvmlDeviceGetHandleByIndex', side_effect=MockedHandle) 68 | patcher_get_energy = patch('pynvml.nvmlDeviceGetTotalEnergyConsumption', 69 | side_effect=lambda mocked_handle: fake_api.get_energy_value(mocked_handle.device_id)) 70 | 71 | patcher_init.start() 72 | patcher_device_count.start() 73 | patcher_get_handle.start() 74 | patcher_get_energy.start() 75 | yield fake_api 76 | patcher_init.stop() 77 | patcher_device_count.stop() 78 | patcher_get_handle.stop() 79 | patcher_get_energy.stop() 80 | 81 | 82 | @pytest.fixture 83 | def two_gpu_api(): 84 | fake_api = FakeNvidiaAPI(2) 85 | fake_api.reset_values() 86 | 87 | patcher_init = patch('pynvml.nvmlInit') 88 | patcher_device_count = patch('pynvml.nvmlDeviceGetCount', return_value=2) 89 | patcher_get_handle = patch('pynvml.nvmlDeviceGetHandleByIndex', side_effect=MockedHandle) 90 | patcher_get_energy = patch('pynvml.nvmlDeviceGetTotalEnergyConsumption', 91 | side_effect=lambda mocked_handle: fake_api.get_energy_value(mocked_handle.device_id)) 92 | 93 | patcher_init.start() 94 | patcher_device_count.start() 95 | patcher_get_handle.start() 96 | patcher_get_energy.start() 97 | yield fake_api 98 | patcher_init.stop() 99 | patcher_device_count.stop() 100 | patcher_get_handle.stop() 101 | patcher_get_energy.stop() 102 | -------------------------------------------------------------------------------- /tests/utils/rapl_fs.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import os 22 | import random 23 | import pytest 24 | import pyfakefs 25 | 26 | from pyJoules.device.rapl_device import RaplDevice 27 | from .fake_api import FakeAPI 28 | 29 | 30 | SOCKET_0_DIR_NAME = '/sys/class/powercap/intel-rapl/intel-rapl:0' 31 | SOCKET_1_DIR_NAME = '/sys/class/powercap/intel-rapl/intel-rapl:1' 32 | 33 | PKG_0_FILE_NAME = SOCKET_0_DIR_NAME + '/energy_uj' 34 | PKG_0_VALUE = 12345 35 | PKG_1_FILE_NAME = SOCKET_1_DIR_NAME + '/energy_uj' 36 | PKG_1_VALUE = 54321 37 | 38 | DRAM_0_DIR_NAME = SOCKET_0_DIR_NAME + '/intel-rapl:0:0' 39 | DRAM_1_DIR_NAME = SOCKET_1_DIR_NAME + '/intel-rapl:1:0' 40 | 41 | DRAM_0_FILE_NAME = DRAM_0_DIR_NAME + '/energy_uj' 42 | DRAM_0_VALUE = 6789 43 | DRAM_1_FILE_NAME = DRAM_1_DIR_NAME + '/energy_uj' 44 | DRAM_1_VALUE = 9876 45 | 46 | CORE_0_DIR_NAME = SOCKET_0_DIR_NAME + '/intel-rapl:0:1' 47 | CORE_1_DIR_NAME = SOCKET_1_DIR_NAME + '/intel-rapl:1:1' 48 | 49 | CORE_0_FILE_NAME = CORE_0_DIR_NAME + '/energy_uj' 50 | CORE_1_FILE_NAME = CORE_1_DIR_NAME + '/energy_uj' 51 | 52 | 53 | class RaplFS(FakeAPI): 54 | 55 | def __init__(self, fs): 56 | self.fs = fs 57 | self.domains_current_energy = {} 58 | self.domains_energy_file = {} 59 | 60 | def add_domain(self, domain_dir_name, domain_name, domain_id): 61 | self.fs.create_file(domain_dir_name + '/name', contents=domain_name + '\n') 62 | energy_value = random.random() 63 | self.fs.create_file(domain_dir_name + '/energy_uj', contents=str(energy_value) + '\n') 64 | self.domains_energy_file[domain_id] = domain_dir_name + '/energy_uj' 65 | self.domains_current_energy[domain_id] = energy_value 66 | 67 | def reset_values(self): 68 | for key in self.domains_energy_file: 69 | new_val = random.random() 70 | self.domains_current_energy[key] = new_val 71 | with open(self.domains_energy_file[key], 'w') as energy_file: 72 | energy_file.write(str(new_val) + '\n') 73 | 74 | def get_device_type(self): 75 | return RaplDevice 76 | 77 | 78 | 79 | @pytest.fixture 80 | def empty_fs(fs): 81 | """ 82 | filesystem describing a machine with one CPU but no RAPL API 83 | """ 84 | return RaplFS(fs) 85 | 86 | 87 | @pytest.fixture 88 | def fs_pkg_one_socket(fs): 89 | """ 90 | filesystem describing a machine with one CPU and RAPL API for package 91 | """ 92 | rapl_fs = RaplFS(fs) 93 | rapl_fs.add_domain(SOCKET_0_DIR_NAME, 'package-0', 'package_0') 94 | rapl_fs.reset_values() 95 | return rapl_fs 96 | 97 | 98 | @pytest.fixture 99 | def fs_pkg_dram_one_socket(fs): 100 | """ 101 | filesystem describing a machine with one CPU and RAPL API for package and dram 102 | """ 103 | rapl_fs = RaplFS(fs) 104 | rapl_fs.add_domain(SOCKET_0_DIR_NAME, 'package-0', 'package_0') 105 | rapl_fs.add_domain(DRAM_0_DIR_NAME, 'dram', 'dram_0') 106 | rapl_fs.reset_values() 107 | return rapl_fs 108 | 109 | 110 | @pytest.fixture 111 | def fs_pkg_psys_one_socket(fs): 112 | """ 113 | filesystem describing a machine with one CPU and RAPL API for package and psys 114 | """ 115 | rapl_fs = RaplFS(fs) 116 | rapl_fs.add_domain(SOCKET_0_DIR_NAME, 'package-0', 'package_0') 117 | rapl_fs.add_domain('/sys/class/powercap/intel-rapl/intel-rapl:1', 'psys', 'psys') 118 | rapl_fs.reset_values() 119 | return rapl_fs 120 | 121 | 122 | @pytest.fixture 123 | def fs_pkg_dram_core_one_socket(fs): 124 | """ 125 | filesystem describing a machine with one CPU and RAPL API for package dram and core 126 | """ 127 | rapl_fs = RaplFS(fs) 128 | rapl_fs.add_domain(SOCKET_0_DIR_NAME, 'package-0', 'package_0') 129 | rapl_fs.add_domain(DRAM_0_DIR_NAME, 'dram', 'dram_0') 130 | rapl_fs.add_domain(CORE_0_DIR_NAME, 'core', 'core_0') 131 | rapl_fs.reset_values() 132 | return rapl_fs 133 | 134 | @pytest.fixture 135 | def fs_pkg_dram_uncore_one_socket(fs): 136 | """ 137 | filesystem describing a machine with one CPU and RAPL API for package dram and core 138 | """ 139 | rapl_fs = RaplFS(fs) 140 | rapl_fs.add_domain(SOCKET_0_DIR_NAME, 'package-0', 'package_0') 141 | rapl_fs.add_domain(DRAM_0_DIR_NAME, 'dram', 'dram_0') 142 | rapl_fs.add_domain(CORE_0_DIR_NAME, 'uncore', 'uncore_0') 143 | rapl_fs.reset_values() 144 | return rapl_fs 145 | 146 | 147 | @pytest.fixture 148 | def fs_pkg_two_socket(fs): 149 | """ 150 | filesystem describing a machine with two CPU and RAPL API for package 151 | """ 152 | rapl_fs = RaplFS(fs) 153 | rapl_fs.add_domain(SOCKET_0_DIR_NAME, 'package-0', 'package_0') 154 | rapl_fs.add_domain(SOCKET_1_DIR_NAME, 'package-1', 'package_1') 155 | rapl_fs.reset_values() 156 | return rapl_fs 157 | 158 | 159 | @pytest.fixture 160 | def fs_pkg_dram_two_socket(fs): 161 | """ 162 | filesystem describing a machine with two CPU and RAPL API for package and dram 163 | """ 164 | rapl_fs = RaplFS(fs) 165 | rapl_fs.add_domain(SOCKET_0_DIR_NAME, 'package-0', 'package_0') 166 | rapl_fs.add_domain(SOCKET_1_DIR_NAME, 'package-1', 'package_1') 167 | rapl_fs.add_domain(DRAM_0_DIR_NAME, 'dram', 'dram_0') 168 | rapl_fs.add_domain(DRAM_1_DIR_NAME, 'dram', 'dram_1') 169 | rapl_fs.reset_values() 170 | return rapl_fs 171 | 172 | 173 | @pytest.fixture 174 | def fs_pkg_psys_two_socket(fs): 175 | """ 176 | filesystem describing a machine with two CPU and RAPL API for package 177 | """ 178 | rapl_fs = RaplFS(fs) 179 | rapl_fs.add_domain(SOCKET_0_DIR_NAME, 'package-0', 'package_0') 180 | rapl_fs.add_domain(SOCKET_1_DIR_NAME, 'package-1', 'package_1') 181 | 182 | rapl_fs.add_domain('/sys/class/powercap/intel-rapl/intel-rapl:2/name', 'psys', 'psys') 183 | rapl_fs.reset_values() 184 | return fs 185 | -------------------------------------------------------------------------------- /tests/utils/sample.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2019, INRIA 3 | # Copyright (c) 2019, University of Lille 4 | # All rights reserved. 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | def assert_sample_are_equals(sample1, sample2): 22 | print((sample1.timestamp, sample2.timestamp)) 23 | assert sample1.timestamp == sample2.timestamp 24 | print((sample1.tag, sample2.tag)) 25 | assert sample1.tag == sample2.tag 26 | print((sample1.duration, sample2.duration)) 27 | assert sample1.duration == sample2.duration 28 | 29 | print((len(sample1.energy), len(sample2.energy))) 30 | assert len(sample1.energy) == len(sample2.energy) 31 | for key in sample1.energy: 32 | print((key, sample2.energy)) 33 | assert key in sample2.energy 34 | print((sample1.energy[key], sample2.energy[key])) 35 | assert sample1.energy[key] == sample2.energy[key] 36 | --------------------------------------------------------------------------------