Browse Source

Trim notebook large output for better performance (#10129)

Gonzalo Peña-Castellanos 4 years ago
parent
commit
aeaf7c9c34

+ 7 - 1
packages/cells/src/widget.ts

@@ -567,6 +567,11 @@ export namespace Cell {
      * Whether to send an update request to the editor when it is shown.
      */
     updateEditorOnShow?: boolean;
+
+    /**
+     * The maximum number of output items to display in cell output.
+     */
+    maxNumberOutputs?: number;
   }
 
   /**
@@ -703,7 +708,8 @@ export class CodeCell extends Cell<ICodeCellModel> {
     const output = (this._output = new OutputArea({
       model: model.outputs,
       rendermime,
-      contentFactory: contentFactory
+      contentFactory: contentFactory,
+      maxNumberOutputs: options.maxNumberOutputs
     }));
     output.addClass(CELL_OUTPUT_AREA_CLASS);
     // Set a CSS if there are no outputs, and connect a signal for future

+ 6 - 0
packages/notebook-extension/schema/tracker.json

@@ -374,6 +374,12 @@
       "description": "Should timing data be recorded in cell metadata",
       "type": "boolean",
       "default": false
+    },
+    "maxNumberOutputs": {
+      "title": "The maximum number of output cells to to be rendered in the output area. Set to 0 to have the complete display.",
+      "description": "Defines the maximum number of output cells to to to be rendered in the output area for cells with many outputs. The output area will have a head and a tail, and the outputs between will be trimmed and not displayed unless the user clicks on the information message.",
+      "type": "number",
+      "default": 50
     }
   },
   "additionalProperties": false,

+ 2 - 1
packages/notebook-extension/src/index.ts

@@ -1027,7 +1027,8 @@ function activateNotebookHandler(
     factory.notebookConfig = {
       scrollPastEnd: settings.get('scrollPastEnd').composite as boolean,
       defaultCell: settings.get('defaultCell').composite as nbformat.CellType,
-      recordTiming: settings.get('recordTiming').composite as boolean
+      recordTiming: settings.get('recordTiming').composite as boolean,
+      maxNumberOutputs: settings.get('maxNumberOutputs').composite as number
     };
     factory.shutdownOnClose = settings.get('kernelShutdown')
       .composite as boolean;

+ 10 - 2
packages/notebook/src/widget.ts

@@ -495,7 +495,9 @@ export class StaticNotebook extends Widget {
       model,
       rendermime,
       contentFactory,
-      updateEditorOnShow: false
+      updateEditorOnShow: false,
+      placeholder: false,
+      maxNumberOutputs: this.notebookConfig.maxNumberOutputs
     };
     const cell = this.contentFactory.createCodeCell(options, this);
     cell.syncCollapse = true;
@@ -773,6 +775,11 @@ export namespace StaticNotebook {
      * Should timing be recorded in metadata
      */
     recordTiming: boolean;
+
+    /**
+     * Defines the maximum number of outputs per cell.
+     */
+    maxNumberOutputs: number;
   }
   /**
    * Default configuration options for notebooks.
@@ -780,7 +787,8 @@ export namespace StaticNotebook {
   export const defaultNotebookConfig: INotebookConfig = {
     scrollPastEnd: true,
     defaultCell: 'code',
-    recordTiming: false
+    recordTiming: false,
+    maxNumberOutputs: 50
   };
 
   /**

+ 5 - 0
packages/outputarea/src/model.ts

@@ -83,6 +83,11 @@ export interface IOutputAreaModel extends IDisposable {
    * Serialize the model to JSON.
    */
   toJSON(): nbformat.IOutput[];
+
+  /**
+   * The maximum number of output items to display on top and bottom of cell output.
+   */
+  maxNumberOutputs?: number;
 }
 
 /**

+ 92 - 1
packages/outputarea/src/widget.ts

@@ -107,6 +107,10 @@ export class OutputArea extends Widget {
     this.contentFactory =
       options.contentFactory || OutputArea.defaultContentFactory;
     this.layout = new PanelLayout();
+    this.trimmedOutputModels = new Array<IOutputModel>();
+    this.maxNumberOutputs = options.maxNumberOutputs || 0;
+    this.headTailNumberOutputs = Math.round(this.maxNumberOutputs / 2);
+    this.headEndIndex = this.headTailNumberOutputs;
     for (let i = 0; i < model.length; i++) {
       const output = model.get(i);
       this._insertOutput(i, output);
@@ -130,6 +134,28 @@ export class OutputArea extends Widget {
    */
   readonly rendermime: IRenderMimeRegistry;
 
+  /**
+   * The hidden output models.
+   */
+  private trimmedOutputModels: IOutputModel[];
+
+  /*
+   * The maximum outputs to show in the trimmed
+   * output area.
+   */
+  private maxNumberOutputs: number;
+
+  /*
+   * The maximum outputs to show in the trimmed
+   * output head and tail areas.
+   */
+  private headTailNumberOutputs: number;
+
+  /*
+   * The index for the end of the head in case of trim mode.
+   */
+  private headEndIndex: number;
+
   /**
    * A read-only sequence of the chidren widgets in the output area.
    */
@@ -287,6 +313,7 @@ export class OutputArea extends Widget {
    * Follow changes on the output model state.
    */
   protected onStateChanged(sender: IOutputAreaModel): void {
+    this.trimmedOutputModels = new Array<IOutputModel>();
     for (let i = 0; i < this.model.length; i++) {
       this._setOutput(i, this.model.get(i));
     }
@@ -413,16 +440,75 @@ export class OutputArea extends Widget {
 
   /**
    * Render and insert a single output into the layout.
+   *
+   * @param index - The index of the output to be inserted.
+   * @param model - The model of the output to be inserted.
    */
   private _insertOutput(index: number, model: IOutputModel): void {
+    if (index === 0) {
+      this.trimmedOutputModels = new Array<IOutputModel>();
+    }
+    if (index === this.maxNumberOutputs && this.maxNumberOutputs !== 0) {
+      // TODO Improve style of the display message.
+      const separatorModel = this.model.contentFactory.createOutputModel({
+        value: {
+          output_type: 'display_data',
+          data: {
+            'text/html': `
+              <a style="margin: 10px; text-decoration: none;">
+                <pre>Output of this cell has been trimmed on the initial display.</pre>
+                <pre>Displaying the first ${this.maxNumberOutputs} top and last bottom outputs.</pre>
+                <pre>Click on this message to get the complete output.</pre>
+              </a>
+              `
+          }
+        }
+      });
+      const onClick = () =>
+        this._showTrimmedOutputs(this.headTailNumberOutputs);
+      const separator = this.createOutputItem(separatorModel);
+      separator!.node.addEventListener('click', onClick);
+      const layout = this.layout as PanelLayout;
+      layout.insertWidget(this.headEndIndex, separator!);
+    }
+    const output = this._createOutput(model);
+    const layout = this.layout as PanelLayout;
+    if (index < this.maxNumberOutputs || this.maxNumberOutputs === 0) {
+      layout.insertWidget(index, output);
+    } else if (index >= this.maxNumberOutputs) {
+      layout.removeWidgetAt(this.headTailNumberOutputs + 1);
+      layout.insertWidget(index, output);
+    }
+    if (index >= this.headTailNumberOutputs && this.maxNumberOutputs !== 0) {
+      this.trimmedOutputModels.push(model);
+    }
+  }
+
+  private _createOutput(model: IOutputModel): Widget {
     let output = this.createOutputItem(model);
     if (output) {
       output.toggleClass(EXECUTE_CLASS, model.executionCount !== null);
     } else {
       output = new Widget();
     }
+    return output;
+  }
+
+  /**
+   * Remove the information message related to the trimmed output
+   * and show all previously trimmed outputs.
+   */
+  private _showTrimmedOutputs(headTailNumberOutputs: number) {
     const layout = this.layout as PanelLayout;
-    layout.insertWidget(index, output);
+    layout.removeWidgetAt(headTailNumberOutputs);
+    for (
+      let i = 0;
+      i < this.trimmedOutputModels.length - this.headTailNumberOutputs;
+      i++
+    ) {
+      const output = this._createOutput(this.trimmedOutputModels[i]);
+      layout.insertWidget(headTailNumberOutputs + i, output);
+    }
   }
 
   /**
@@ -611,6 +697,11 @@ export namespace OutputArea {
      * The rendermime instance used by the widget.
      */
     rendermime: IRenderMimeRegistry;
+
+    /**
+     * The maximum number of output items to display on top and bottom of cell output.
+     */
+    maxNumberOutputs?: number;
   }
 
   /**