Skip to content

Improve batched Column performance - #2260

Open
mvilliger wants to merge 1 commit into
releases/26.2from
features/mvi/26.2/smartColumnPerformance
Open

Improve batched Column performance#2260
mvilliger wants to merge 1 commit into
releases/26.2from
features/mvi/26.2/smartColumnPerformance

Conversation

@mvilliger

Copy link
Copy Markdown
Member

SmartColumns with batched LookupCall already collected all keys and then updated the cell text as soon as the async LookupCall completed. Therefore, there is only one promise for the LookupCall execution returning the texts for all keys.
Unfortunately the SmartColumn then splits this promise into one promise for each cell containing only the value for the specific cell. This leads to thousands of promises on large tables.
These promises are collected in the TableUpdateBuffer and removed as they resolve. This operation was O(n^2) as the buffer internally used an array to store the promises.

Processing lots of such promises together with the inefficient handling of the buffer leads to unnecessary computation time on the client. Because of some async overhead, executing a promise for each cell is significantly slower than executing one promise handling all cells. On large tables with thousands of rows and multiple SmartColumns this may sum up to several seconds just to process these promises.

This commit changes the following:

  • Introduce a helper class BatchCall to collect keys and using an async batch callback executed for all the keys.
  • Column uses this new BatchCall in formatValue() if available. This allows each column to support batch processing more easily. Implementors only need to set the _batchFormat member defining the batch callback.
  • The deferred Cell text update in batch mode always updates all texts of the column instead of only one cell. Accordingly, _formatValue and formatValue() in the async case now return all texts instead of only the one for a single cell.
  • StaticLookupCall & LookupCallColumn now supports batching to benefit from the new features.

410165, 399939

@fschinkel
fschinkel force-pushed the features/mvi/26.2/smartColumnPerformance branch from e33f8c3 to 68ba14a Compare July 30, 2026 14:10
}

protected _serializeKey(key: TKey): string {
return dataObjects.stringify(key);

@fschinkel fschinkel Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe deserialize the key first. Consider a SmartColumn<SomeDo> using the RestLookupCall to lookup its values. Depending on whether SomeDo is a BaseDoEntity and/or how the data of the table is loaded the value of the cell might be a BaseDoEntity. But as the resulting key of the RestLookupCall will always be a pojo simply using dataObjects.stringify will result in different values if the pojo has e.g. a _typeVersion set.

@mvilliger
mvilliger force-pushed the features/mvi/26.2/smartColumnPerformance branch 2 times, most recently from d96b915 to ab8eef4 Compare August 10, 2026 11:35
@fschinkel
fschinkel force-pushed the features/mvi/26.2/smartColumnPerformance branch from ab8eef4 to 05fbb7a Compare August 11, 2026 15:29
}

protected _batchFormatValues(keys: TValue[][]): JQuery.Promise<Map<TValue[], string>> {
const allKeys = [...new Set(keys.flat())];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work for complex objects?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be ok, because key-identity is handled by BatchCall. Correct?

this._tableColumnsChangedHandler = this._onTableColumnsChanged.bind(this);
this._realWidth = null;

this._batchFormat = null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialization to null not necessary for this internal property (or can be merged with the property declaration).

// If every cell of a row is empty the row would collapse, using nbsp makes sure the row is as height as the others even if it is empty
content = '&nbsp;';
cssClass = strings.join(' ', cssClass, 'empty');
} else if (cell.flowsLeft) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event shorter:

} else {
  content = cell.flowsLeft ? text + icon : icon + text;
}

});

setCellTextDeferred(promise: JQuery.Promise<BatchCallResult<TValue, unknown>>) {
// (then) promises always resolve asynchronously which means the text will always be set later after row is initialized and will generate an update row event.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does (then) at the start of this sentence mean?

this._batchFormat.addKey(value);
return this._batchFormat.promise();
}
return scout.nvl(value, '');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If value is not a string, the result will also not be a string, which contradicts the declared return type of this method. Maybe just convert it to String, same as in _batchFormatResultToCellText()?

return;
}

promise

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are not returning the result, it should be safe to use then/catch instead of done/fail.


protected _batchFormatValues(keys: TValue[]): JQuery.Promise<Map<TValue, string>> {
const lookupCall = this.lookupCall.cloneForKeys(keys);
this.trigger('prepareLookupCall', {lookupCall});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous implementation in _formatValue() triggered this event including the row property. Because we are now in a batch context, this can no longer be set. However, listeners still may rely on the property. Isn't this a problem?

*
* SPDX-License-Identifier: EPL-2.0
*/
import {scout} from '../scout';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import from index, not from individual files.

if (arrays.empty(keys)) {
return $.resolvedPromise();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

import {dataObjects} from '../dataobject/dataObjects';
import $ from 'jquery';

export class BatchCall<TKey, TValue> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add some documentation? (see old BatchLookup implementation).

}

/**
* Same key might point to the same values! Do not modify (read only!)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this documentation? It does not seem to be related to this method. Modifying the content of keys is never a good idea, is an explicit warning really needed?

@mvilliger
mvilliger force-pushed the features/mvi/26.2/smartColumnPerformance branch 4 times, most recently from 0b5fbb1 to 16eff29 Compare August 20, 2026 14:11
@mvilliger
mvilliger force-pushed the features/mvi/26.2/smartColumnPerformance branch from 16eff29 to de239fe Compare August 31, 2026 11:23
SmartColumns with batched LookupCall already collected all keys and then
updated the cell text as soon as the async LookupCall completed.
Therefore, there is only one promise for the LookupCall execution
returning the texts for all keys.
Unfortunately the SmartColumn then splits this promise into one promise
for each cell containing only the value for the specific cell. This
leads to thousands of promises on large tables.
These promises are collected in the TableUpdateBuffer and removed as
they resolve. This operation was O(n^2) as the buffer internally used an
array to store the promises.

Processing lots of such promises together with the inefficient handling
of the buffer leads to unnecessary computation time on the client.
Because of some async overhead, executing a promise for each cell is
significantly slower than executing one promise handling all cells. On
large tables with thousands of rows and multiple SmartColumns this may
sum up to several seconds just to process these promises.

This commit changes the following:
- Introduce a helper class BatchCall to collect keys and using an async
  batch callback executed for all the keys.
- Column uses this new BatchCall in formatValue() if available. This
  allows each column to support batch processing more easily.
  Implementors only need to set the _batchFormat member defining the
  batch callback.
- The deferred Cell text update in batch mode always updates all texts
  of the column instead of only one cell. Accordingly, _formatValue and
  formatValue() in the async case now return all texts instead of only
  the one for a single cell.
- StaticLookupCall & LookupCallColumn now supports batching to benefit
  from the new features.

410165, 399939
@mvilliger
mvilliger force-pushed the features/mvi/26.2/smartColumnPerformance branch from de239fe to 3ae3ebf Compare September 2, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants