From eff273bf090518d41ba46b8a52b91f20cf9d7074 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 24 Jun 2020 12:18:24 +0200 Subject: [PATCH] fix(frontend): improve performance of table-related utils Fixes #603 --- frontend/src/lib/table/table.js | 153 ++++++++++++++++++ .../services/util-registry/util-registry.js | 2 +- frontend/src/utils/check-all/check-all.js | 40 ++--- .../src/utils/hide-columns/hide-columns.js | 75 ++------- package-lock.json | 5 + package.json | 3 +- 6 files changed, 191 insertions(+), 87 deletions(-) create mode 100644 frontend/src/lib/table/table.js diff --git a/frontend/src/lib/table/table.js b/frontend/src/lib/table/table.js new file mode 100644 index 000000000..8178c0b74 --- /dev/null +++ b/frontend/src/lib/table/table.js @@ -0,0 +1,153 @@ +const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0; + +import * as defer from 'lodash.defer'; + +class Overhang { + colSpan; + rowSpan; + + constructor(colSpan, rowSpan) { + this.colSpan = colSpan; + this.rowSpan = rowSpan; + + if (new.target === Overhang) + Object.freeze(this); + } + + nextLine() { + return new Overhang(this.colSpan, Math.max(0, this.rowSpan - 1)); + } + + isHole() { + return this.rowSpan <= 0; + } +} + +const instanceCache = new Map(); + +export class TableIndices { + _table; + + _cellToIndices = new Map(); + _indicesToCell = new Array(); + + colSpan = cell => cell ? Math.max(1, cell.colSpan || 1) : 1; + rowSpan = cell => cell ? Math.max(1, cell.rowSpan || 1) : 1; + + maxRow = 0; + maxCol = 0; + + constructor(table, overrides) { + const prev = instanceCache.get(table); + if ( prev?.instance && + overrides?.colSpan === prev.overrides?.colSpan && + overrides?.rowSpan === prev.overrides?.rowSpan + ) { + if (DEBUG_MODE > 0) + console.log('Reusing existing TableIndices', table, overrides, prev); + + return prev.instance; + } + + if (overrides && overrides.colSpan) + this.colSpan = overrides.colSpan; + if (overrides && overrides.rowSpan) + this.rowSpan = overrides.rowSpan; + + this._table = table; + + + let overhangs = new Array(); + let currentRow = 0; + + for (const rowParent of this._table.rows) { + let currentOverhangs = Array.from(overhangs); + let newOverhangs = new Array(); + + let cellBefore = 0; + + for (const cell of rowParent.cells) { + let i; + + for (i = 0; i < currentOverhangs.length; i++) { + const overhang = currentOverhangs[i]; + + if (overhang.isHole()) + break; + else + newOverhangs.push(overhang.nextLine()); + + cellBefore += overhang.colSpan; + } + + currentOverhangs = currentOverhangs.slice(i); + + this._cellToIndices.set(cell, { row: currentRow, col: cellBefore }); + + let rows = range(currentRow, currentRow + this.rowSpan(cell)); + let columns = range(cellBefore, cellBefore + this.colSpan(cell)); + + if (DEBUG_MODE > 0) { + cell.dataset.rows = JSON.stringify(rows); + cell.dataset.columns = JSON.stringify(columns); + } + + for (const row of rows) { + for (const col of columns) { + if (!this._indicesToCell[row]) + this._indicesToCell[row] = new Array(); + + this._indicesToCell[row][col] = cell; + + this.maxRow = Math.max(row, this.maxRow); + this.maxCol = Math.max(col, this.maxCol); + } + } + + newOverhangs.push(new Overhang(this.colSpan(cell), this.rowSpan(cell) - 1)); + cellBefore += this.colSpan(cell); + } + + overhangs = newOverhangs; + currentRow++; + } + + if (DEBUG_MODE > 1) { + console.log(this._cellToIndices); + console.table(this._indicesToCell); + } + + instanceCache.set(table, { overrides: overrides, instance: this }); + defer(() => { instanceCache.delete(table); } ); + } + + colIndex(cell) { + return this.getIndices(cell)?.col; + } + + rowIndex(cell) { + return this.getIndices(cell)?.row; + } + + getIndices(cell) { + const res = this._cellToIndices.get(cell); + + if (DEBUG_MODE > 2) + console.log('getIndices', cell, res); + + return res; + } + + getCell(row, col) { + const cell = this._indicesToCell[row]?.[col]; + + if (DEBUG_MODE > 2) + console.log('getCell', row, col, cell); + + return cell; + } +} + +function range(from, to) { + return [...Array(to - from).keys()].map(n => n + from); +} diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index 57789622e..65e9326cc 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -1,6 +1,6 @@ import * as toposort from 'toposort'; -const DEBUG_MODE = /localhost/.test(window.location.href) ? 2 : 0; +const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0; export class UtilRegistry { diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index 6c7a8e441..05a9bd98d 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { TableIndices } from '../../lib/table/table'; const CHECKBOX_SELECTOR = '[type="checkbox"]'; @@ -10,8 +11,10 @@ const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized'; export class CheckAll { _element; - _columns = []; - _checkAllColumns = []; + _columns = new Array(); + _checkAllColumns = new Array(); + + _tableIndices; constructor(element, app) { if (!element) { @@ -24,6 +27,8 @@ export class CheckAll { return false; } + this._tableIndices = new TableIndices(this._element); + this._gatherColumns(); this._findCheckboxColumns().forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId]))); @@ -32,28 +37,23 @@ export class CheckAll { } _gatherColumns() { - const rows = Array.from(this._element.rows); - const cols = []; - rows.forEach((tr) => { - const cells = Array.from(tr.cells); - cells.forEach(cell => { - let i = 0; - for (const sibling of cells.slice(0, cell.cellIndex)) - i += Math.max(1, sibling.colSpan) || 1; + for (const rowIndex of new Array(this._tableIndices.maxRow + 1)) { + for (const colIndex of new Array(this._tableIndices.maxCol + 1)) { + const cell = this._tableIndices.getCell(rowIndex, colIndex); - for (let j = i; j < i + cell.colSpan; j++) { - if (!cols[j]) { - cols[j] = []; - } - cols[j].push(cell); - } - }); - }); - this._columns = cols; + if (!cell) + continue; + + if (!this._columns[colIndex]) + this._columns[colIndex] = new Array(); + + this._columns[colIndex][rowIndex] = cell; + } + } } _findCheckboxColumns() { - let checkboxColumnIds = []; + let checkboxColumnIds = new Array(); this._columns.forEach((col, i) => { if (this._isCheckboxColumn(col)) { checkboxColumnIds.push(i); diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 29a99670a..0c214ee38 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -1,7 +1,8 @@ import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import './hide-columns.sass'; -import * as memoize from 'lodash.memoize'; + +import { TableIndices } from '../../lib/table/table'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; const TABLE_HEADER_IDENT = 'uw-hide-column-header'; @@ -37,6 +38,8 @@ export class HideColumns { _mutationObserver; + _tableIndices; + headerToHider = new Map(); hiderToHeader = new Map(); @@ -60,6 +63,8 @@ export class HideColumns { this._element = element; + this._tableIndices = new TableIndices(this._element); + const hideColumnsContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}]`); if (!hideColumnsContainer) { throw new Error('Hide Columns utility needs to be setup on a table inside a hide columns container!'); @@ -250,8 +255,7 @@ export class HideColumns { // console.debug('_tableMutated', { mutationList }); - this._colIndexMemoized = undefined; - this._getColMemoized = undefined; + this._tableIndices = new TableIndices(this._element, { colSpan: this.colSpan.bind(this) }); Array.from(this._element.rows) .flatMap(row => Array.from(row.cells)) @@ -354,71 +358,12 @@ export class HideColumns { return originalColspan ? Math.max(colspan, originalColspan) : colspan; } - colIndexDirect(cell) { - if (!cell) - return 0; - - const rowParent = cell.closest('tr'); - - if (!rowParent) - return 0; - - let rowBefore = 0; - for (const cell of Array.from(rowParent.cells).slice(0, cell.cellIndex)) { - if (!cell) - continue; - - rowBefore += this.colSpan(cell); - } - - let i = 0; - for (const pRow of Array.from(this._element.rows).slice(0, rowParent.rowIndex + 1)) { - if (!pRow) - continue; - - let space = 0; - for (const cell of pRow === rowParent ? Array.from(pRow.cells).slice(0, cell.cellIndex) : pRow.cells) { - if (!cell) - continue; - - const rowSpan = cell.rowSpan || 1; - if (rowParent.rowIndex - pRow.rowIndex < rowSpan) - i += this.colSpan(cell); - else if (pRow !== rowParent) - space += this.colSpan(cell); - - if (space > rowBefore) - break; - } - } - - // console.debug({ rowParent, cell, rowBefore, i }); - - return i; - } - - _colIndexMemoized; - colIndex(cell) { - if (!this._colIndexMemoized) - this._colIndexMemoized = memoize(this.colIndexDirect.bind(this)); - - return this._colIndexMemoized(cell); + return this._tableIndices.colIndex(cell); } - getColDirect(row, columnIndex) { - for (const cell of row.cells) - if (cell && this.colIndex(cell) <= columnIndex && this.colIndex(cell) + this.colSpan(cell) > columnIndex) - return cell; - } - - _getColMemoized; - - getCol(row, columnIndex) { - if (!this._getColMemoized) - this._getColMemoized = memoize(this.getColDirect.bind(this), (row, columnIndex) => Array.of(row.rowIndex, columnIndex).toString()); - - return this._getColMemoized(row, columnIndex); + getCol(row, col) { + return this._tableIndices.getCell(row.rowIndex, col); } } diff --git a/package-lock.json b/package-lock.json index 4c694a23d..99d24f647 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10952,6 +10952,11 @@ "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", "dev": true }, + "lodash.defer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.defer/-/lodash.defer-4.1.0.tgz", + "integrity": "sha1-6cFYqWHeGkbqJP2jRoW0zN01jz8=" + }, "lodash.filter": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", diff --git a/package.json b/package.json index 7beec49d2..7c215e0b8 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,6 @@ "karma-mocha-reporter": "^2.2.5", "karma-webpack": "^3.0.5", "lint-staged": "^8.2.1", - "lodash.debounce": "^4.0.8", "mini-css-extract-plugin": "^0.8.2", "npm-run-all": "^4.1.5", "null-loader": "^2.0.0", @@ -120,6 +119,8 @@ "@juggle/resize-observer": "^2.5.0", "core-js": "^3.6.5", "js-cookie": "^2.2.1", + "lodash.debounce": "^4.0.8", + "lodash.defer": "^4.1.0", "lodash.throttle": "^4.1.1", "moment": "^2.25.3", "npm": "^6.14.5",