Repository: deshaw/jupyterlab-execute-time (language: python)

Issue:
Display cell Outputs-per-Second metric
Every so often when working with a SQL Cell in Jupyterlab I wish to know the estimated QPS for that query formulation. It is difficult to do it using %%timeit and you would have to replicate this for every SQL cell. So, it would be a nice QoL improvement if this plugin had an EPS metric on top of the regular elapsed time display.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d23639..42d11b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## [3.2.0](https://github.com/deshaw/jupyterlab-execute-time/compare/v3.1.2...v3.2.0) (unreleased)
+
+### Added
+
+- Add an option to show the cell outputs per second [#116](https://github.com/deshaw/jupyterlab-execute-time/pull/116)
+
 ## [3.1.2](https://github.com/deshaw/jupyterlab-execute-time/compare/v3.1.0...v3.1.2) (2024-02-14)
 
 ### Fixed
diff --git a/schema/settings.json b/schema/settings.json
index 675150f..cf1a23b 100644
--- a/schema/settings.json
+++ b/schema/settings.json
@@ -59,6 +59,12 @@
       "description": "Show the formatted date string in the given format. For example \"yyy-MM-dd HH:mm:ss\" results in \"2023-02-28 14:25:57 \", \"dd/MM/YYY HH:mm:ss (O)\" results in \"28/02/2023 14:25:57 (GMT +3)\". Find out more on unicode date field symbols on this link [https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table]",
       "default": "yyy-MM-dd HH:mm:ss",
       "pattern": "[yGuqQMLwdeEcabBhHkKmsSzOxX\\W].*"
+    },
+    "showOutputsPerSecond": {
+      "type": "boolean",
+      "title": "Show Outputs Per Second",
+      "description": "After a cell has finished running, show the outputs per second calculated from the last cell execution time and number of outputs.",
+      "default": false
     }
   }
 }
diff --git a/src/ExecuteTimeWidget.ts b/src/ExecuteTimeWidget.ts
index 1d40d63..439bc28 100644
--- a/src/ExecuteTimeWidget.ts
+++ b/src/ExecuteTimeWidget.ts
@@ -32,6 +32,7 @@ export interface IExecuteTimeSettings {
   showDate: boolean;
   historyCount: number;
   dateFormat: string;
+  showOutputsPerSecond: boolean;
 }
 
 export default class ExecuteTimeWidget extends Widget {
@@ -290,11 +291,13 @@ export default class ExecuteTimeWidget extends Widget {
       if (isLikelyAborted) {
         msg = '';
       } else if (endTime) {
-        if (
-          this._settings.minTime <=
-          differenceInMilliseconds(endTime, startTime) / 1000.0
-        ) {
+        const executionTimeMillis = differenceInMilliseconds(
+          endTime,
+          startTime
+        );
+        if (this._settings.minTime <= executionTimeMillis / 1000.0) {
           const executionTime = getTimeDiff(endTime, startTime);
+          const executionsPerSecond = 1000.0 / executionTimeMillis;
           const lastExecutionTime = executionTimeNode.getAttribute(
             PREV_DATA_EXECUTION_TIME_ATTR
           );
@@ -324,6 +327,15 @@ export default class ExecuteTimeWidget extends Widget {
             msg += ` at ${getTimeString(endTime, this._settings.dateFormat)}`;
           }
           msg += ` in ${executionTime}`;
+
+          const numberOfOutputs = cell.model.outputs.length;
+          if (this._settings.showOutputsPerSecond && numberOfOutputs > 0) {
+            const outputsPerSecond = executionsPerSecond / numberOfOutputs;
+            msg += `, ${numberOfOutputs} output${
+              numberOfOutputs === 1 ? '' : 's'
+            }`;
+            msg += ` at ${outputsPerSecond.toFixed(2)}/s`;
+          }
         }
       } else if (startTime) {
         if (this._settings.showLiveExecutionTime) {
@@ -429,6 +441,9 @@ export default class ExecuteTimeWidget extends Widget {
       );
     }
 
+    this._settings.showOutputsPerSecond = settings.get('showOutputsPerSecond')
+      .composite as boolean;
+
     const cells = this._panel.context.model.cells;
     if (this._settings.enabled) {
       cells.changed.connect(this.updateConnectedCell);
@@ -513,5 +528,6 @@ export default class ExecuteTimeWidget extends Widget {
     showDate: true,
     historyCount: 5,
     dateFormat: 'yyy-MM-dd HH:mm:ss',
+    showOutputsPerSecond: false,
   };
 }


Test patch:
diff --git a/ui-tests/notebooks/Timing_outputs_outcomes.ipynb b/ui-tests/notebooks/Timing_outputs_outcomes.ipynb
new file mode 100644
index 0000000..ac2ecd1
--- /dev/null
+++ b/ui-tests/notebooks/Timing_outputs_outcomes.ipynb
@@ -0,0 +1,55 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "98ce2d4e-e422-40ea-aa89-5adabbb8d0f0",
+   "metadata": {},
+   "source": [
+    "Notes:\n",
+    "- Each cells is self-contained to allow parallel testing.\n",
+    "- Cell indices are used in tests."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f28cb5ea-94a4-47d3-9860-4cf20a29d690",
+   "metadata": {},
+   "source": [
+    "#### Last executed at with outputs:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3cfa2867-8f80-4793-9a4d-d773b06136e1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for i in range(5):\n",
+    "    display(1)\n",
+    "    sleep(0.01)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/ui-tests/tests/timing_outputs_outcomes.spec.ts b/ui-tests/tests/timing_outputs_outcomes.spec.ts
new file mode 100644
index 0000000..7fea776
--- /dev/null
+++ b/ui-tests/tests/timing_outputs_outcomes.spec.ts
@@ -0,0 +1,34 @@
+import { expect, galata, test } from '@jupyterlab/galata';
+import { openNotebook, cleanup, maskedScreenshot } from './utils';
+
+const SETTINGS_ID = 'jupyterlab-execute-time:settings';
+
+test.describe('Timing outcomes with ', () => {
+  test.beforeEach(openNotebook('Timing_outputs_outcomes.ipynb'));
+  test.afterEach(cleanup);
+  // Disable flashing highlight for screenshot consistency
+  test.use({
+    mockSettings: {
+      ...galata.DEFAULT_SETTINGS,
+      [SETTINGS_ID]: {
+        ...galata.DEFAULT_SETTINGS[SETTINGS_ID],
+        highlight: false,
+        showOutputsPerSecond: true,
+      },
+    },
+  });
+
+  test('"Last executed at" state', async ({ page }) => {
+    const cell = await page.notebook.getCell(2);
+
+    // Execute cell and wait for it to complete
+    await page.notebook.runCell(2);
+
+    const widget = await cell.waitForSelector('.execute-time');
+    expect(await widget.textContent()).toContain('Last executed at');
+    expect(await widget.textContent()).toContain('outputs at');
+    expect(await maskedScreenshot(widget)).toMatchSnapshot(
+      'last-executed-outputs.png'
+    );
+  });
+});
diff --git a/ui-tests/tests/timing_outputs_outcomes.spec.ts-snapshots/last-executed-outputs-linux.png b/ui-tests/tests/timing_outputs_outcomes.spec.ts-snapshots/last-executed-outputs-linux.png
new file mode 100644
index 0000000..18f42b0
Binary files /dev/null and b/ui-tests/tests/timing_outputs_outcomes.spec.ts-snapshots/last-executed-outputs-linux.png differ