Skip to content

Commit 1fbaeb5

Browse files
Add execution timing in code cell metadata for v4 spec (#32)
* Add execution timing in code cell metadata for v4 spec * Add record_timing trait * Add test
1 parent 9c4d49b commit 1fbaeb5

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-0
lines changed

nbclient/client.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import base64
23
from textwrap import dedent
34

@@ -17,6 +18,10 @@
1718
from .exceptions import CellTimeoutError, DeadKernelError, CellExecutionComplete, CellExecutionError
1819

1920

21+
def timestamp():
22+
return datetime.datetime.utcnow().isoformat() + 'Z'
23+
24+
2025
class NotebookClient(LoggingConfigurable):
2126
"""
2227
Encompasses a Client for executing cells in a notebook
@@ -144,6 +149,16 @@ class NotebookClient(LoggingConfigurable):
144149
),
145150
).tag(config=True)
146151

152+
record_timing = Bool(
153+
True,
154+
help=dedent(
155+
"""
156+
If `True` (default), then the execution timings of each cell will
157+
be stored in the metadata of the notebook.
158+
"""
159+
),
160+
).tag(config=True)
161+
147162
iopub_timeout = Integer(
148163
4,
149164
allow_none=False,
@@ -436,6 +451,8 @@ async def _poll_for_reply(self, msg_id, cell, timeout, task_poll_output_msg):
436451
try:
437452
msg = await self.kc.shell_channel.get_msg(timeout=timeout)
438453
if msg['parent_header'].get('msg_id') == msg_id:
454+
if self.record_timing:
455+
cell['metadata']['execution']['shell.execute_reply'] = timestamp()
439456
try:
440457
await asyncio.wait_for(task_poll_output_msg, self.iopub_timeout)
441458
except (asyncio.TimeoutError, Empty):
@@ -608,6 +625,9 @@ async def async_execute_cell(self, cell, cell_index, execution_count=None, store
608625
self.log.debug("Skipping non-executing cell %s", cell_index)
609626
return cell
610627

628+
if self.record_timing and 'execution' not in cell['metadata']:
629+
cell['metadata']['execution'] = {}
630+
611631
self.log.debug("Executing cell:\n%s", cell.source)
612632
parent_msg_id = self.kc.execute(
613633
cell.source, store_history=store_history, stop_on_error=not self.allow_errors
@@ -672,6 +692,15 @@ def process_message(self, msg, cell, cell_index):
672692
if 'execution_count' in content:
673693
cell['execution_count'] = content['execution_count']
674694

695+
if self.record_timing:
696+
if msg_type == 'status':
697+
if content['execution_state'] == 'idle':
698+
cell['metadata']['execution']['iopub.status.idle'] = timestamp()
699+
elif content['execution_state'] == 'busy':
700+
cell['metadata']['execution']['iopub.status.busy'] = timestamp()
701+
elif msg_type == 'execute_input':
702+
cell['metadata']['execution']['iopub.execute_input'] = timestamp()
703+
675704
if msg_type == 'status':
676705
if content['execution_state'] == 'idle':
677706
raise CellExecutionComplete()

nbclient/tests/files/Sleep1s.ipynb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"import time\n",
10+
"import datetime"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": null,
16+
"metadata": {},
17+
"outputs": [],
18+
"source": [
19+
"t0 = datetime.datetime.utcnow()\n",
20+
"time.sleep(1)\n",
21+
"t1 = datetime.datetime.utcnow()"
22+
]
23+
},
24+
{
25+
"cell_type": "code",
26+
"execution_count": null,
27+
"metadata": {},
28+
"outputs": [],
29+
"source": [
30+
"time_format = '%Y-%m-%dT%H:%M:%S.%fZ'\n",
31+
"print(t0.strftime(time_format), end='')"
32+
]
33+
},
34+
{
35+
"cell_type": "code",
36+
"execution_count": null,
37+
"metadata": {},
38+
"outputs": [],
39+
"source": [
40+
"print(t1.strftime(time_format), end='')"
41+
]
42+
}
43+
],
44+
"metadata": {
45+
"kernelspec": {
46+
"display_name": "Python 3",
47+
"language": "python",
48+
"name": "python3"
49+
},
50+
"language_info": {
51+
"codemirror_mode": {
52+
"name": "ipython",
53+
"version": 3
54+
},
55+
"file_extension": ".py",
56+
"mimetype": "text/x-python",
57+
"name": "python",
58+
"nbconvert_exporter": "python",
59+
"pygments_lexer": "ipython3",
60+
"version": "3.8.1"
61+
}
62+
},
63+
"nbformat": 4,
64+
"nbformat_minor": 2
65+
}

nbclient/tests/test_client.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
import threading
77
import asyncio
8+
import datetime
89

910
import nbformat
1011
import sys
@@ -354,6 +355,36 @@ def test_many_async_parallel_notebooks(capfd):
354355
assert captured.err == ""
355356

356357

358+
def test_execution_timing():
359+
"""Compare the execution timing information stored in the cell with the
360+
actual time it took to run the cell. Also check for the cell timing string
361+
format."""
362+
opts = dict(kernel_name="python")
363+
input_name = "Sleep1s.ipynb"
364+
input_file = os.path.join(current_dir, "files", input_name)
365+
res = notebook_resources()
366+
input_nb, output_nb = run_notebook(input_file, opts, res)
367+
368+
def get_time_from_str(s):
369+
time_format = '%Y-%m-%dT%H:%M:%S.%fZ'
370+
return datetime.datetime.strptime(s, time_format)
371+
372+
execution_timing = output_nb['cells'][1]['metadata']['execution']
373+
status_busy = get_time_from_str(execution_timing['iopub.status.busy'])
374+
execute_input = get_time_from_str(execution_timing['iopub.execute_input'])
375+
execute_reply = get_time_from_str(execution_timing['shell.execute_reply'])
376+
status_idle = get_time_from_str(execution_timing['iopub.status.idle'])
377+
378+
cell_start = get_time_from_str(output_nb['cells'][2]['outputs'][0]['text'])
379+
cell_end = get_time_from_str(output_nb['cells'][3]['outputs'][0]['text'])
380+
381+
delta = datetime.timedelta(milliseconds=100)
382+
assert status_busy - cell_start < delta
383+
assert execute_input - cell_start < delta
384+
assert execute_reply - cell_end < delta
385+
assert status_idle - cell_end < delta
386+
387+
357388
class TestExecute(NBClientTestsBase):
358389
"""Contains test functions for execute.py"""
359390

0 commit comments

Comments
 (0)