diff --git a/README.md b/README.md
index 08be880..eb48a8c 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,57 @@ Open the result.html, and identify the problem (GIL visible, possible low instru
The dark red `S(GIL)` blocks indicate the threads/processes are in a waiting state due to the GIL, dark orange `S` is a due to other reasons (like `time.sleep(...)`). The regular pattern is due to Python switching threads after [`sys.getswitchinterval`](https://docs.python.org/3/library/sys.html#sys.getswitchinterval) (0.005 seconds)
+
+# Usage - Jupyter notebook
+
+First, load the magics
+```
+%load_ext per4m.cellmagic
+```
+
+Run a cell with the `%%giltrace` cell magic.
+```
+%%giltrace
+import threading
+import time
+import time
+
+
+def run():
+ total = 0
+ for i in range(1_000_000):
+ total += i
+ return total
+
+
+thread1 = threading.Thread(target=run)
+thread2 = threading.Thread(target=run)
+thread1.start()
+thread2.start()
+time.sleep(0.2)
+for thread in [thread1, thread2]:
+ thread.join()
+```
+Output:
+```
+Saving report to /tmp/tmp2rwf1xq3/viztracer.json ...
+Dumping trace data to json, total entries: 89, estimated json file size: 10.4KiB
+Report saved.
+
+[ perf record: Woken up 8 times to write data ]
+[ perf record: Captured and wrote 2,752 MB /tmp/tmp2rwf1xq3/perf.data (415 samples) ]
+
+Wait for perf to finish...
+Saving report to /home/maartenbreddels/github/maartenbreddels/per4m/result.html ...
+Dumping trace data to json, total entries: 167, estimated json file size: 19.6KiB
+Generating HTML report
+Report saved.
+Download result.html
+Open result.html in new tab (might not work due to security issue)
+```
+
+Click the download link to get the results.
+
# Usage - manual
## Step 1
diff --git a/per4m/cellmagic.py b/per4m/cellmagic.py
new file mode 100644
index 0000000..22eb045
--- /dev/null
+++ b/per4m/cellmagic.py
@@ -0,0 +1,41 @@
+import os
+import tempfile
+import viztracer
+from viztracer.report_builder import ReportBuilder
+from IPython.display import HTML, display
+
+from IPython.core.magic import (cell_magic,
+ magics_class,
+ Magics,
+ needs_local_scope,
+ )
+
+
+from .giltracer import GilTracer
+
+@magics_class
+class GilTraceMagic(Magics):
+ @needs_local_scope
+ @cell_magic
+ def giltrace(self, line, cell, local_ns):
+ temp_dir = tempfile.mkdtemp()
+ perf_path = os.path.join(temp_dir, 'perf.data')
+ viz_path = os.path.join(temp_dir, 'viztracer.json')
+ gil_path = os.path.join(temp_dir, 'giltracer.json')
+ out_path = 'result.html'
+ code = self.shell.transform_cell(cell)
+ with GilTracer(perf_path, gil_path) as gt:
+ with viztracer.VizTracer(output_file=viz_path):
+ exec(code, local_ns, local_ns)
+ builder = ReportBuilder([viz_path, gil_path])
+ builder.save(output_file=out_path)
+
+ download = HTML(f'''Download {out_path}''')
+ view = HTML(f'''Open {out_path} in new tab (might not work due to security issue)''')
+ display(download, view)
+
+def load_ipython_extension(ipython):
+ """
+ Use `%load_ext per4m.cellmagic`
+ """
+ ipython.register_magics(GilTraceMagic)
\ No newline at end of file
diff --git a/per4m/example2.py b/per4m/example2.py
new file mode 100644
index 0000000..66b2367
--- /dev/null
+++ b/per4m/example2.py
@@ -0,0 +1,20 @@
+# same as example1, but without explicit viztracer calls
+import threading
+import time
+import time
+
+
+def run():
+ total = 0
+ for i in range(1_000_000):
+ total += i
+ return total
+
+
+thread1 = threading.Thread(target=run)
+thread2 = threading.Thread(target=run)
+thread1.start()
+thread2.start()
+time.sleep(0.2)
+for thread in [thread1, thread2]:
+ thread.join()