├── .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 | [](https://spdx.org/licenses/MIT.html)
4 | [](https://circleci.com/gh/powerapi-ng/pyjoules)
5 | [](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 | 
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 |
--------------------------------------------------------------------------------