Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/formatter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Formatter

on: [push, pull_request]

jobs:
formatter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rickstaa/action-black@v1
with:
black_args: "-l 79 ."
22 changes: 22 additions & 0 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Linter

on:
workflow_run:
workflows: [Formatter]
types: [completed]

jobs:
linter:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Check out source repository
uses: actions/checkout@v4
- name: Set up Python environment
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: flake8 Lint
uses: py-actions/flake8@v2
with:
path: "jumper_extension"
33 changes: 33 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Tests

on:
- push
- pull_request
- workflow_dispatch # Running workflow manually

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: "3.11"
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[test]

- name: Run tests with coverage
run: |
pytest --cov=jumper_extension --cov-report=xml tests/

- name: Generate coverage badge
uses: tj-actions/coverage-badge-py@v2
with:
output: coverage.svg
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
__pycache__/
__pycache__/
*.egg-info/
.coverage
htmlcov/
.pytest_cache/
coverage.xml
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# JUmPER Extension

![Coverage](./coverage.svg)

This is JUmPER IPython extension for real-time performance monitoring in IPython environments and Jupyter notebooks. It allows you to gather performance data on CPU usage, memory consumption, GPU utilization, and I/O operations for individual cells and present it in the notebook/IPython session either as text report or as a plot. The extension can be naturally integrated with [JUmPER Jupyter kernel](https://github.com/score-p/scorep_jupyter_kernel_python/) for most comprehensive analysis of notebook.

## Installation
Expand Down
5 changes: 4 additions & 1 deletion jumper_extension/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
from .perfmonitor_magics import load_ipython_extension, unload_ipython_extension
from .perfmonitor_magics import (load_ipython_extension,
unload_ipython_extension)

__all__ = ["load_ipython_extension", "unload_ipython_extension"]
31 changes: 17 additions & 14 deletions jumper_extension/cell_history.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
import time
import json
import time


class CellHistory:
def __init__(self):
self.cells = []
self.cell_counter = 0
self.current_cell = None
self.cell_timestamps = []

def start_cell(self, raw_cell):
self.current_cell = {
'number': self.cell_counter,
'raw_cell': raw_cell,
'start_time': time.time(),
'end_time': None,
"number": self.cell_counter,
"raw_cell": raw_cell,
"start_time": time.time(),
"end_time": None,
}
self.cell_counter += 1

def end_cell(self, result):
if self.current_cell:
self.current_cell['end_time'] = time.time()
self.current_cell["end_time"] = time.time()
self.cells.append(self.current_cell)
self.cell_timestamps.append((self.current_cell['start_time'], self.current_cell['end_time']))
self.cell_timestamps.append(
(self.current_cell["start_time"], self.current_cell["end_time"])
)
self.current_cell = None

def print(self):
for cell in self.cells:
duration = cell['end_time'] - cell['start_time']
duration = cell["end_time"] - cell["start_time"]
print(f"Cell #{cell['number']} - Duration: {duration:.2f}s")
print("-" * 40)
print(cell['raw_cell'])
print(cell["raw_cell"])
print("=" * 40)

def export(self, filename='cell_history.json'):
with open(filename, 'w') as f:
json.dump(self.cells, f, indent=2)
def export(self, filename="cell_history.json"):
with open(filename, "w") as f:
json.dump(self.cells, f, indent=2)
58 changes: 33 additions & 25 deletions jumper_extension/perfmonitor_magics.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from IPython.core.magic import Magics, magics_class, line_magic
from IPython.core.magic import Magics, line_magic, magics_class

from .cell_history import CellHistory
from .performance_monitor import PerformanceMonitor
from .performance_visualizer import PerformanceVisualizer
from .cell_history import CellHistory

_perfmonitor_magics = None


@magics_class
class perfmonitorMagics(Magics):
def __init__(self, shell):
Expand All @@ -16,7 +18,7 @@ def __init__(self, shell):

def pre_run_cell(self, info):
self.cell_history.start_cell(info.raw_cell)

def post_run_cell(self, result):
self.cell_history.end_cell(result.result)
if self.monitor and self.print_perfreports:
Expand Down Expand Up @@ -55,19 +57,20 @@ def perfmonitor_start(self, line):
if self.monitor and self.monitor.running:
print("Performance monitoring already running")
return

interval = 1.0
if line:
try:
interval = float(line)
except ValueError:
print(f"Invalid interval value: {line}")
return

self.monitor = PerformanceMonitor(interval=interval)
self.monitor.start()
self.visualizer = PerformanceVisualizer(
self.monitor.cpu_handles, self.monitor.memory, self.monitor.gpu_memory)
self.monitor.cpu_handles, self.monitor.memory, self.monitor.gpu_memory
)

@line_magic
def perfmonitor_stop(self, line):
Expand All @@ -81,16 +84,16 @@ def perfmonitor_plot(self, line):
if not self.monitor:
print("No active performance monitoring session")
return

cell_marks = self._parse_cell_number(line)
if cell_marks is False: # Error parsing
return

df = self.monitor.data.to_dataframe()
if cell_marks:
start_mark, end_mark = cell_marks
df = df[(df['time'] >= start_mark) & (df['time'] <= end_mark)]
df['time'] -= self.monitor.start_time
df = df[(df["time"] >= start_mark) & (df["time"] <= end_mark)]
df["time"] -= self.monitor.start_time

if df.empty:
print("No performance data available")
Expand All @@ -110,13 +113,13 @@ def _perfreport(self, cell_marks=None):

if not cell_marks:
cell_marks = self.cell_history.cell_timestamps[-1]

start_mark, end_mark = cell_marks
duration = end_mark - start_mark
print(f"Duration: {duration:.2f}s")

df = self.monitor.data.to_dataframe()
df = df[(df['time'] >= start_mark) & (df['time'] <= end_mark)]
df = df[(df["time"] >= start_mark) & (df["time"] <= end_mark)]
if df.empty:
print("No performance data available")
return
Expand All @@ -126,20 +129,23 @@ def _perfreport(self, cell_marks=None):
("CPU Util (Across CPUs)", "cpu_util_avg", "-"),
("Memory (GB)", "memory_usage_gb", f"{self.monitor.memory:.2f}"),
("GPU Util (Across GPUs)", "gpu_util_avg", "-"),
("GPU Memory (GB)", "gpu_mem_avg", f"{self.monitor.gpu_memory:.2f}")
("GPU Memory (GB)", "gpu_mem_avg", f"{self.monitor.gpu_memory:.2f}"),
]

print(f"{'Metric':<25} {'AVG':<8} {'MIN':<8} {'MAX':<8} {'TOTAL':<8}")
print("-" * 65)
for name, col, total in metrics:
if col in df.columns:
print(f"{name:<25} {df[col].mean():<8.2f} {df[col].min():<8.2f} {df[col].max():<8.2f} {total:<8}")

print(
f"{name:<25} {df[col].mean():<8.2f} "
f"{df[col].min():<8.2f} {df[col].max():<8.2f} {total:<8}"
)

@line_magic
def perfmonitor_enable_perfreports(self, line):
self.print_perfreports = True
print("Performance reports enabled for each cell")

@line_magic
def perfmonitor_disable_perfreports(self, line):
self.print_perfreports = False
Expand All @@ -157,14 +163,14 @@ def perfmonitor_export_perfdata(self, line):
if not self.monitor:
print("No active performance monitoring session")
return
filename = line.strip() or 'performance_data.csv'
filename = line.strip() or "performance_data.csv"
self.monitor.data.export(filename)
print(f"Performance data exported to {filename}")

@line_magic
def perfmonitor_export_cell_history(self, line):
"""Export cell history"""
filename = line.strip() or 'cell_history.json'
filename = line.strip() or "cell_history.json"
self.cell_history.export(filename)
print(f"Cell history exported to {filename}")

Expand All @@ -174,33 +180,35 @@ def perfmonitor_help(self, line):
commands = [
"perfmonitor_help -- show this help",
"perfmonitor_resources -- show available hardware",
"cell_history -- show cell execution history",
"cell_history -- show cell execution history",
"perfmonitor_start [seconds] -- start monitoring",
"perfmonitor_stop -- stop monitoring",
"perfmonitor_perfreport [cell] -- show performance report",
"perfmonitor_plot [cell] -- plot performance data",
"perfmonitor_enable_perfreports -- enable auto-reports",
"perfmonitor_disable_perfreports -- disable auto-reports",
"perfmonitor_export_perfdata [filename] -- export data to CSV",
"perfmonitor_export_cell_history [filename] -- export history to JSON"
"perfmonitor_export_cell_history [filename] -- export history to JSON",
]
print("Available commands:")
for cmd in commands:
print(f" %{cmd}")


def load_ipython_extension(ipython):
global _perfmonitor_magics
_perfmonitor_magics = perfmonitorMagics(ipython)
ipython.events.register('pre_run_cell', _perfmonitor_magics.pre_run_cell)
ipython.events.register('post_run_cell', _perfmonitor_magics.post_run_cell)
ipython.events.register("pre_run_cell", _perfmonitor_magics.pre_run_cell)
ipython.events.register("post_run_cell", _perfmonitor_magics.post_run_cell)
ipython.register_magics(_perfmonitor_magics)
print("Perfmonitor extension loaded.")


def unload_ipython_extension(ipython):
global _perfmonitor_magics
if _perfmonitor_magics:
ipython.events.unregister('pre_run_cell', _perfmonitor_magics.pre_run_cell)
ipython.events.unregister('post_run_cell', _perfmonitor_magics.post_run_cell)
ipython.events.unregister("pre_run_cell", _perfmonitor_magics.pre_run_cell)
ipython.events.unregister("post_run_cell", _perfmonitor_magics.post_run_cell)
if _perfmonitor_magics.monitor:
_perfmonitor_magics.monitor.stop()
_perfmonitor_magics = None
Loading