mirror of
https://github.com/dream-num/univer.git
synced 2026-10-02 04:24:34 +08:00
feat(sheets): split large set-range-values mutation (#6191)
This commit is contained in:
@@ -15,9 +15,10 @@
|
||||
*/
|
||||
|
||||
import type { IMutationInfo, IRange } from '@univerjs/core';
|
||||
import type { ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import { SetRangeValuesMutation } from '@univerjs/sheets';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getRepeatRange, mergeSetRangeValues } from '../utils';
|
||||
import { getRepeatRange, mergeSetRangeValues, spilitLargeSetRangeValuesMutations } from '../utils';
|
||||
|
||||
describe('test getRepeatRange', () => {
|
||||
it('repeat row 2 times', () => {
|
||||
@@ -195,3 +196,166 @@ describe('test "mergeSetRangeValues"', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test "spilitLargeSetRangeValuesMutations"', () => {
|
||||
it('should not split if mutation is not SetRangeValuesMutation', () => {
|
||||
const mutation: IMutationInfo<ISetRangeValuesMutationParams> = {
|
||||
id: 'other.mutation',
|
||||
params: { unitId: '1', subUnitId: '1', cellValue: {} },
|
||||
};
|
||||
const result = spilitLargeSetRangeValuesMutations(mutation);
|
||||
expect(result).toStrictEqual([mutation]);
|
||||
});
|
||||
|
||||
it('should not split if cellValue is empty', () => {
|
||||
const mutation: IMutationInfo<ISetRangeValuesMutationParams> = {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: { unitId: '1', subUnitId: '1', cellValue: undefined },
|
||||
};
|
||||
const result = spilitLargeSetRangeValuesMutations(mutation);
|
||||
expect(result).toStrictEqual([mutation]);
|
||||
});
|
||||
|
||||
it('should not split if cell count is below threshold', () => {
|
||||
const cellValue: Record<number, Record<number, { v: string }>> = {};
|
||||
// Create 100 cells (10x10)
|
||||
for (let row = 0; row < 10; row++) {
|
||||
cellValue[row] = {};
|
||||
for (let col = 0; col < 10; col++) {
|
||||
cellValue[row][col] = { v: `cell_${row}_${col}` };
|
||||
}
|
||||
}
|
||||
|
||||
const mutation: IMutationInfo<ISetRangeValuesMutationParams> = {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: { unitId: '1', subUnitId: '1', cellValue },
|
||||
};
|
||||
|
||||
const result = spilitLargeSetRangeValuesMutations(mutation, { threshold: 6000 });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toStrictEqual(mutation);
|
||||
});
|
||||
|
||||
it('should split large mutation into chunks based on maxCellsPerChunk', () => {
|
||||
const cellValue: Record<number, Record<number, { v: string }>> = {};
|
||||
// Create 150 cells (50 rows x 3 cols)
|
||||
for (let row = 0; row < 50; row++) {
|
||||
cellValue[row] = {};
|
||||
for (let col = 0; col < 3; col++) {
|
||||
cellValue[row][col] = { v: `cell_${row}_${col}` };
|
||||
}
|
||||
}
|
||||
|
||||
const mutation: IMutationInfo<ISetRangeValuesMutationParams> = {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: { unitId: '1', subUnitId: '1', cellValue },
|
||||
};
|
||||
|
||||
// With 3 cols and maxCellsPerChunk=30, each chunk should have 10 rows (30/3=10)
|
||||
// So 50 rows should be split into 5 chunks
|
||||
const result = spilitLargeSetRangeValuesMutations(mutation, {
|
||||
threshold: 100,
|
||||
maxCellsPerChunk: 30,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(5);
|
||||
result.forEach((chunk) => {
|
||||
expect(chunk.id).toBe(SetRangeValuesMutation.id);
|
||||
expect(chunk.params.unitId).toBe('1');
|
||||
expect(chunk.params.subUnitId).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve cell data correctly when splitting', () => {
|
||||
const cellValue: Record<number, Record<number, { v: string }>> = {};
|
||||
// Create 20 cells (10 rows x 2 cols)
|
||||
for (let row = 0; row < 10; row++) {
|
||||
cellValue[row] = {};
|
||||
for (let col = 0; col < 2; col++) {
|
||||
cellValue[row][col] = { v: `cell_${row}_${col}` };
|
||||
}
|
||||
}
|
||||
|
||||
const mutation: IMutationInfo<ISetRangeValuesMutationParams> = {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: { unitId: '1', subUnitId: '1', cellValue },
|
||||
};
|
||||
|
||||
// With 2 cols and maxCellsPerChunk=6, each chunk should have 3 rows (6/2=3)
|
||||
const result = spilitLargeSetRangeValuesMutations(mutation, {
|
||||
threshold: 10,
|
||||
maxCellsPerChunk: 6,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(4); // 10 rows / 3 rows per chunk = 4 chunks
|
||||
|
||||
// Verify first chunk has rows 0-2
|
||||
const firstChunk = result[0].params.cellValue;
|
||||
expect(firstChunk![0]![0]!.v).toBe('cell_0_0');
|
||||
expect(firstChunk![2]![1]!.v).toBe('cell_2_1');
|
||||
|
||||
// Verify last chunk has rows 9
|
||||
const lastChunk = result[3].params.cellValue;
|
||||
expect(lastChunk![9]![0]!.v).toBe('cell_9_0');
|
||||
expect(lastChunk![9]![1]!.v).toBe('cell_9_1');
|
||||
});
|
||||
|
||||
it('should handle sparse matrices correctly', () => {
|
||||
const cellValue: Record<number, Record<number, { v: string }>> = {
|
||||
0: { 0: { v: 'cell_0_0' } },
|
||||
5: { 5: { v: 'cell_5_5' } },
|
||||
10: { 10: { v: 'cell_10_10' } },
|
||||
};
|
||||
|
||||
const mutation: IMutationInfo<ISetRangeValuesMutationParams> = {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: { unitId: '1', subUnitId: '1', cellValue },
|
||||
};
|
||||
|
||||
const result = spilitLargeSetRangeValuesMutations(mutation, {
|
||||
threshold: 2,
|
||||
maxCellsPerChunk: 1,
|
||||
});
|
||||
|
||||
// Should be split into chunks
|
||||
expect(result.length).toBeGreaterThan(1);
|
||||
|
||||
// Verify all cells are preserved
|
||||
const allCells: Record<string, Record<string, { v: string }>> = {};
|
||||
result.forEach((chunk) => {
|
||||
const chunkCellValue = chunk.params.cellValue!;
|
||||
Object.keys(chunkCellValue).forEach((rowKey) => {
|
||||
if (!allCells[rowKey]) allCells[rowKey] = {};
|
||||
Object.keys(chunkCellValue[Number(rowKey)]).forEach((colKey) => {
|
||||
const cellData = chunkCellValue[Number(rowKey)][Number(colKey)]!;
|
||||
allCells[rowKey][colKey] = { v: cellData.v as string };
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
expect(allCells['0']['0'].v).toBe('cell_0_0');
|
||||
expect(allCells['5']['5'].v).toBe('cell_5_5');
|
||||
expect(allCells['10']['10'].v).toBe('cell_10_10');
|
||||
});
|
||||
|
||||
it('should handle custom options correctly', () => {
|
||||
const cellValue: Record<number, Record<number, { v: string }>> = {};
|
||||
// Create 100 cells (100 rows x 1 col)
|
||||
for (let row = 0; row < 100; row++) {
|
||||
cellValue[row] = { 0: { v: `cell_${row}_0` } };
|
||||
}
|
||||
|
||||
const mutation: IMutationInfo<ISetRangeValuesMutationParams> = {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: { unitId: '1', subUnitId: '1', cellValue },
|
||||
};
|
||||
|
||||
// With 1 col and maxCellsPerChunk=25, each chunk should have 25 rows
|
||||
const result = spilitLargeSetRangeValuesMutations(mutation, {
|
||||
threshold: 50,
|
||||
maxCellsPerChunk: 25,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(4); // 100 rows / 25 rows per chunk = 4 chunks
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ import type {
|
||||
Workbook,
|
||||
Worksheet,
|
||||
} from '@univerjs/core';
|
||||
import type { ISetSelectionsOperationParams } from '@univerjs/sheets';
|
||||
import type { ISetRangeValuesMutationParams, ISetSelectionsOperationParams } from '@univerjs/sheets';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { IDiscreteRange } from '../../controllers/utils/range-tools';
|
||||
import type {
|
||||
@@ -84,7 +84,7 @@ import { UniverPastePlugin } from './html-to-usm/paste-plugins/plugin-univer';
|
||||
import { WordPastePlugin } from './html-to-usm/paste-plugins/plugin-word';
|
||||
import { COPY_TYPE } from './type';
|
||||
import { USMToHtmlService } from './usm-to-html/convertor';
|
||||
import { convertTextToTable, discreteRangeContainsRange, htmlContainsImage, htmlIsFromExcel, mergeSetRangeValues, rangeIntersectWithDiscreteRange } from './utils';
|
||||
import { convertTextToTable, discreteRangeContainsRange, htmlContainsImage, htmlIsFromExcel, mergeSetRangeValues, rangeIntersectWithDiscreteRange, spilitLargeSetRangeValuesMutations } from './utils';
|
||||
|
||||
export const PREDEFINED_HOOK_NAME = {
|
||||
DEFAULT_COPY: 'default-copy',
|
||||
@@ -819,6 +819,21 @@ export class SheetClipboardService extends Disposable implements ISheetClipboard
|
||||
|
||||
redoMutationsInfo = mergeSetRangeValues(redoMutationsInfo);
|
||||
undoMutationsInfo = mergeSetRangeValues(undoMutationsInfo);
|
||||
|
||||
// Split large SetRangeValuesMutation to avoid performance issues
|
||||
redoMutationsInfo = redoMutationsInfo.flatMap((mutation) =>
|
||||
spilitLargeSetRangeValuesMutations(mutation as IMutationInfo<ISetRangeValuesMutationParams>, {
|
||||
threshold: 20000,
|
||||
maxCellsPerChunk: 10000,
|
||||
})
|
||||
);
|
||||
undoMutationsInfo = undoMutationsInfo.flatMap((mutation) =>
|
||||
spilitLargeSetRangeValuesMutations(mutation as IMutationInfo<ISetRangeValuesMutationParams>, {
|
||||
threshold: 20000,
|
||||
maxCellsPerChunk: 10000,
|
||||
})
|
||||
);
|
||||
|
||||
undoMutationsInfo.push({ id: SetWorksheetActiveOperation.id, params: { unitId: target.unitId, subUnitId: target.subUnitId } });
|
||||
|
||||
this._logService.log('[SheetClipboardService]', 'pasting mutations', {
|
||||
|
||||
@@ -180,6 +180,90 @@ export function mergeSetRangeValues(mutations: IMutationInfo[]) {
|
||||
return newMutations;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export function spilitLargeSetRangeValuesMutations(
|
||||
mutation: IMutationInfo<ISetRangeValuesMutationParams>,
|
||||
options: { maxCellsPerChunk?: number; threshold?: number; maxChunks?: number } = {}
|
||||
): IMutationInfo<ISetRangeValuesMutationParams>[] {
|
||||
const { maxCellsPerChunk = 2500, threshold = 6000, maxChunks = 10 } = options;
|
||||
|
||||
if (mutation.id !== SetRangeValuesMutation.id) {
|
||||
return [mutation];
|
||||
}
|
||||
const { cellValue } = mutation.params;
|
||||
if (!cellValue) {
|
||||
return [mutation];
|
||||
}
|
||||
|
||||
const matrix = new ObjectMatrix(cellValue);
|
||||
|
||||
// 计算单元格数量和边界
|
||||
let cellCount = 0;
|
||||
let minRow = Infinity;
|
||||
let maxRow = -Infinity;
|
||||
let minCol = Infinity;
|
||||
let maxCol = -Infinity;
|
||||
|
||||
matrix.forValue((row, col) => {
|
||||
cellCount++;
|
||||
minRow = Math.min(minRow, row);
|
||||
maxRow = Math.max(maxRow, row);
|
||||
minCol = Math.min(minCol, col);
|
||||
maxCol = Math.max(maxCol, col);
|
||||
});
|
||||
|
||||
// 如果没有数据或单元格数量小于阈值,不拆分
|
||||
if (minRow === Infinity || cellCount <= threshold) {
|
||||
return [mutation];
|
||||
}
|
||||
|
||||
const chunks: IMutationInfo<ISetRangeValuesMutationParams>[] = [];
|
||||
|
||||
// 根据列数计算每个块的行数
|
||||
const colCount = maxCol - minCol + 1;
|
||||
let rowsPerChunk = Math.max(1, Math.floor(maxCellsPerChunk / colCount));
|
||||
|
||||
// 确保不会拆分成超过 maxChunks 个块
|
||||
const totalRows = maxRow - minRow + 1;
|
||||
const estimatedChunks = Math.ceil(totalRows / rowsPerChunk);
|
||||
if (estimatedChunks > maxChunks) {
|
||||
// 重新计算 rowsPerChunk,使得拆分后的块数不超过 maxChunks
|
||||
rowsPerChunk = Math.ceil(totalRows / maxChunks);
|
||||
}
|
||||
|
||||
// 按照计算出的行数进行拆分
|
||||
for (let rowStart = minRow; rowStart <= maxRow; rowStart += rowsPerChunk) {
|
||||
const rowEnd = Math.min(rowStart + rowsPerChunk - 1, maxRow);
|
||||
|
||||
const chunkMatrix = new ObjectMatrix<Nullable<ICellData>>();
|
||||
let hasData = false;
|
||||
|
||||
// 提取当前块的数据
|
||||
for (let row = rowStart; row <= rowEnd; row++) {
|
||||
for (let col = minCol; col <= maxCol; col++) {
|
||||
const value = matrix.getValue(row, col);
|
||||
if (value !== undefined) {
|
||||
chunkMatrix.setValue(row, col, value);
|
||||
hasData = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 只有当块中有数据时才创建 mutation
|
||||
if (hasData) {
|
||||
chunks.push({
|
||||
...mutation,
|
||||
params: {
|
||||
...mutation.params,
|
||||
cellValue: chunkMatrix.getMatrix(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return chunks.length > 0 ? chunks : [mutation];
|
||||
}
|
||||
|
||||
export function rangeIntersectWithDiscreteRange(range: IRange, discrete: IDiscreteRange) {
|
||||
const { startRow, endRow, startColumn, endColumn } = range;
|
||||
for (let i = startRow; i <= endRow; i++) {
|
||||
|
||||
Reference in New Issue
Block a user