Skip to content

Commit 943345f

Browse files
authored
Merge pull request #201 from anmcgrath/improve-formula-performance
Improve formula performance by using caching of computed values.
2 parents 88baf27 + c793608 commit 943345f

File tree

15 files changed

+109
-154
lines changed

15 files changed

+109
-154
lines changed

src/BlazorDatasheet.Core/Commands/Data/SetCellValuesCommand.cs

+23-82
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@ namespace BlazorDatasheet.Core.Commands.Data;
77

88
public class SetCellValuesCommand : BaseCommand, IUndoableCommand
99
{
10-
private readonly object[][]? _values;
11-
private readonly CellValue[][]? _cellValues;
12-
private readonly IRegion? _region;
13-
private readonly object? _singleValue;
14-
private readonly CellValue? _singleCellValue;
10+
private readonly IEnumerable<IEnumerable<CellValue>> _cellValues;
1511
private readonly int _row;
1612
private readonly int _col;
1713
private readonly CellStoreRestoreData _restoreData = new();
@@ -22,46 +18,37 @@ public class SetCellValuesCommand : BaseCommand, IUndoableCommand
2218
/// <param name="values">The values to set. values[row] is the row offset from <paramref name="row"/>. Each values[row][col] is the col offset from <paramref name="col"/></param>
2319
/// <param name="row"></param>
2420
/// <param name="col"></param>
25-
public SetCellValuesCommand(int row, int col, object[][] values)
21+
public SetCellValuesCommand(int row, int col, CellValue[][] values)
2622
{
27-
_values = values;
23+
_cellValues = values;
2824
_row = row;
2925
_col = col;
3026
}
3127

28+
3229
/// <summary>
3330
/// Creates a command to set multiple cell values starting at position <paramref name="row"/>/<paramref name="col"/>
3431
/// </summary>
35-
/// <param name="values">The values to set. values[row] is the row offset from <paramref name="row"/>. Each values[row][col] is the col offset from <paramref name="col"/></param>
3632
/// <param name="row"></param>
3733
/// <param name="col"></param>
38-
public SetCellValuesCommand(int row, int col, CellValue[][] values)
34+
/// <param name="values"></param>
35+
public SetCellValuesCommand(int row, int col, IEnumerable<IEnumerable<CellValue>> values)
3936
{
4037
_cellValues = values;
4138
_row = row;
4239
_col = col;
4340
}
4441

45-
/// <summary>
46-
/// Sets all the cells in the region <paramref name="region"/> to <paramref name="value"/>
47-
/// </summary>
48-
/// <param name="region"></param>
49-
/// <param name="value"></param>
50-
public SetCellValuesCommand(IRegion region, object? value)
51-
{
52-
_region = region;
53-
_singleValue = value;
54-
}
55-
5642
/// <summary>
5743
/// Sets all the cells in the region <paramref name="region"/> to <paramref name="value"/>
5844
/// </summary>
5945
/// <param name="region"></param>
6046
/// <param name="value"></param>
6147
public SetCellValuesCommand(IRegion region, CellValue value)
6248
{
63-
_region = region;
64-
_singleCellValue = value;
49+
_row = region.Top;
50+
_col = region.Left;
51+
_cellValues = Enumerable.Repeat(Enumerable.Repeat(value, region.Width), region.Height);
6552
}
6653

6754
public override bool CanExecute(Sheet sheet) => true;
@@ -70,85 +57,39 @@ public override bool Execute(Sheet sheet)
7057
{
7158
sheet.ScreenUpdating = false;
7259
sheet.BatchUpdates();
73-
if (_values != null)
74-
ExecuteSetObjectArrayData(sheet);
75-
else if (_cellValues != null)
76-
ExecuteSetCellValueData(sheet);
77-
else if (_region != null && _singleCellValue != null)
78-
ExecuteSetRegionDataAsCellValue(sheet);
79-
else if (_region != null)
80-
ExecuteSetRegionData(sheet);
81-
else
82-
return false;
60+
ExecuteSetCellValueData(sheet);
8361
sheet.EndBatchUpdates();
8462
sheet.ScreenUpdating = true;
8563

8664
return true;
8765
}
8866

89-
private void ExecuteSetObjectArrayData(Sheet sheet)
90-
{
91-
var rowEnd = _row + _values!.Length;
92-
var colEnd = _col;
93-
94-
for (int i = 0; i < _values.Length; i++)
95-
{
96-
for (int j = 0; j < _values[i].Length; j++)
97-
{
98-
_restoreData.Merge(sheet.Cells.SetValueImpl(_row + i, _col + j, _values[i][j]));
99-
colEnd = Math.Max(colEnd, j + _col);
100-
}
101-
}
102-
103-
sheet.MarkDirty(new Region(_row, rowEnd, _col, colEnd));
104-
}
105-
10667
private void ExecuteSetCellValueData(Sheet sheet)
10768
{
108-
var rowEnd = _row + _cellValues!.Length;
69+
var rowEnd = _row;
10970
var colEnd = _col;
11071

111-
for (int i = 0; i < _cellValues.Length; i++)
112-
{
113-
for (int j = 0; j < _cellValues[i].Length; j++)
114-
{
115-
_restoreData.Merge(sheet.Cells.SetValueImpl(_row + i, _col + j, _cellValues[i][j]));
116-
colEnd = Math.Max(colEnd, j + _col);
117-
}
118-
}
72+
int rowOffset = 0;
11973

120-
sheet.MarkDirty(new Region(_row, rowEnd, _col, colEnd));
121-
}
122-
123-
124-
private void ExecuteSetRegionData(Sheet sheet)
125-
{
126-
for (int row = _region!.Top; row <= _region!.Bottom; row++)
74+
foreach (var row in _cellValues)
12775
{
128-
for (int col = _region.Left; col <= _region.Right; col++)
76+
int colOffset = 0;
77+
78+
foreach (var value in row)
12979
{
130-
_restoreData.Merge(sheet.Cells.SetValueImpl(row, col, _singleValue));
80+
_restoreData.Merge(sheet.Cells.SetValueImpl(_row + rowOffset, _col + colOffset, value));
81+
colEnd = Math.Max(colEnd, _col + colOffset);
82+
colOffset++;
13183
}
132-
}
133-
134-
sheet.MarkDirty(_region);
135-
}
136-
137-
private void ExecuteSetRegionDataAsCellValue(Sheet sheet)
138-
{
139-
sheet.ScreenUpdating = false;
14084

141-
for (int row = _region!.Top; row <= _region!.Bottom; row++)
142-
{
143-
for (int col = _region.Left; col <= _region.Right; col++)
144-
{
145-
_restoreData.Merge(sheet.Cells.SetValueImpl(row, col, _singleCellValue!));
146-
}
85+
rowEnd = Math.Max(rowEnd, _row + rowOffset);
86+
rowOffset++;
14787
}
14888

149-
sheet.MarkDirty(_region);
89+
sheet.MarkDirty(new Region(_row, rowEnd, _col, colEnd));
15090
}
15191

92+
15293
public bool Undo(Sheet sheet)
15394
{
15495
sheet.ScreenUpdating = false;

src/BlazorDatasheet.Core/Data/Cells/CellStore.Data.cs

+18-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public partial class CellStore
2525
/// <param name="col"></param>
2626
/// <param name="value"></param>
2727
/// <returns></returns>
28-
public bool SetValue(int row, int col, object value)
28+
public bool SetValue(int row, int col, object? value)
2929
{
3030
var cmd = new SetCellValueCommand(row, col, ConvertToCellValue(row, col, value));
3131
return Sheet.Commands.ExecuteCommand(cmd);
@@ -100,7 +100,9 @@ internal CellStoreRestoreData SetValueImpl(int row, int col, object? value)
100100
/// <returns></returns>
101101
public bool SetValues(int row, int col, object[][] values)
102102
{
103-
var cmd = new SetCellValuesCommand(row, col, values);
103+
var cmd = new SetCellValuesCommand(row, col,
104+
values.Select((r, rowOffset) =>
105+
r.Select((v, colOffset) => ConvertToCellValue(row + rowOffset, col + colOffset, v))));
104106
return Sheet.Commands.ExecuteCommand(cmd);
105107
}
106108

@@ -123,8 +125,20 @@ public bool SetValues(int row, int col, CellValue[][] values)
123125
/// <returns></returns>
124126
public bool SetValues(IRegion region, object? value)
125127
{
126-
var cmd = new SetCellValuesCommand(region, value);
127-
return Sheet.Commands.ExecuteCommand(cmd);
128+
if (region.IsSingleCell())
129+
return SetValue(region.Top, region.Left, value);
130+
131+
var cellValues = new CellValue[region.Height][];
132+
for (int i = 0; i < region.Height; i++)
133+
{
134+
cellValues[i] = new CellValue[region.Width];
135+
for (int j = 0; j < region.Width; j++)
136+
{
137+
cellValues[i][j] = ConvertToCellValue(i + region.Top, j + region.Left, value);
138+
}
139+
}
140+
141+
return SetValues(region.Top, region.Left, cellValues);
128142
}
129143

130144
/// <summary>

src/BlazorDatasheet.Core/Data/Cells/CellStore.Validation.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ internal void ValidateRegion(IRegion region)
2222
_validStore.Set(row, col, result.IsValid);
2323
}
2424

25-
Sheet.MarkDirty(cellsAffected);
25+
Sheet.MarkDirty(region);
2626
}
2727

2828
public bool IsValid(int row, int col)

src/BlazorDatasheet.Core/Data/Sheet.cs

+21-28
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ public bool ScreenUpdating
154154
public NamedRangeManager NamedRanges { get; }
155155

156156
/// <summary>
157-
/// If the sheet is batching dirty regions, they are stored here.
157+
/// If the sheet is batching dirty rows, they are stored here.
158158
/// </summary>
159-
private readonly ConsolidatedDataStore<bool> _dirtyRegions = new();
159+
private readonly Range1DStore<bool> _dirtyRows = new(false);
160160

161161
private Sheet(int defaultWidth, int defaultHeight)
162162
{
@@ -329,15 +329,6 @@ public bool IsCellVisible(int row, int col)
329329
return null;
330330
}
331331

332-
/// <summary>
333-
/// Mark the cells specified by positions dirty.
334-
/// </summary>
335-
/// <param name="positions"></param>
336-
internal void MarkDirty(IEnumerable<CellPosition> positions)
337-
{
338-
MarkDirty(positions.Select(p => new Region(p.row, p.col)));
339-
}
340-
341332
/// <summary>
342333
/// Marks the cell as dirty and requiring re-render
343334
/// </summary>
@@ -358,7 +349,9 @@ internal void MarkDirty(IRegion region)
358349
if (intersection == null)
359350
return;
360351

361-
MarkDirty(new List<IRegion>() { intersection });
352+
_dirtyRows.Set(region.Top, region.Bottom, true);
353+
if (!_isBatchingChanges)
354+
EmitSheetDirty();
362355
}
363356

364357
/// <summary>
@@ -373,17 +366,20 @@ internal void MarkDirty(IEnumerable<IRegion> regions)
373366
if (intersection == null)
374367
continue;
375368

376-
_dirtyRegions.Add(intersection, true);
369+
_dirtyRows.Set(region.Top, region.Bottom, true);
377370
}
378371

379372
if (!_isBatchingChanges)
373+
EmitSheetDirty();
374+
}
375+
376+
private void EmitSheetDirty()
377+
{
378+
SheetDirty?.Invoke(this, new()
380379
{
381-
SheetDirty?.Invoke(this, new DirtySheetEventArgs()
382-
{
383-
DirtyRegions = _dirtyRegions,
384-
});
385-
_dirtyRegions.Clear();
386-
}
380+
DirtyRows = _dirtyRows
381+
});
382+
_dirtyRows.Clear();
387383
}
388384

389385
private int _batchRequestNo;
@@ -400,7 +396,7 @@ public void BatchUpdates()
400396
if (_isBatchingChanges)
401397
return;
402398

403-
_dirtyRegions.Clear();
399+
_dirtyRows.Clear();
404400
Cells.BatchChanges();
405401
_isBatchingChanges = true;
406402
}
@@ -414,9 +410,12 @@ public void SortRange(IRegion? region, List<ColumnSortOptions>? sortOptions = nu
414410

415411
var beforeArgs = new BeforeRangeSortEventArgs(region, sortOptions);
416412
BeforeRangeSort?.Invoke(this, beforeArgs);
413+
417414
var cmd = new SortRangeCommand(region, sortOptions);
415+
418416
if (!beforeArgs.Cancel)
419417
Commands.ExecuteCommand(cmd);
418+
420419
var afterArgs = new RangeSortedEventArgs(region, cmd.SortedRegion, cmd.OldIndices);
421420
RangeSorted?.Invoke(this, afterArgs);
422421
}
@@ -435,16 +434,10 @@ public void EndBatchUpdates()
435434

436435
// Checks for batching changes here, because the cells changed event
437436
// may start batching more dirty changes again from subscribers of that event.
438-
if (_dirtyRegions.Any() && _isBatchingChanges)
439-
{
440-
SheetDirty?.Invoke(this, new DirtySheetEventArgs()
441-
{
442-
DirtyRegions = _dirtyRegions,
443-
});
444-
}
437+
if (_dirtyRows.Any() && _isBatchingChanges)
438+
EmitSheetDirty();
445439

446440
_isBatchingChanges = false;
447-
_dirtyRegions.Clear();
448441
}
449442

450443
/// <summary>

src/BlazorDatasheet.Core/Events/Visual/DirtySheetEventArgs.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ namespace BlazorDatasheet.Core.Events.Visual;
55

66
public class DirtySheetEventArgs
77
{
8-
public ConsolidatedDataStore<bool> DirtyRegions { get; init; } = default!;
8+
public Range1DStore<bool> DirtyRows { get; init; } = default!;
99
}

src/BlazorDatasheet.Core/FormulaEngine/FormulaEngine.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -183,14 +183,14 @@ public void CalculateSheet(bool calculateAll)
183183
{
184184
// check whether the formula has already been calculated in this scc group - may be the case if we lucked
185185
// out on the first value calculation and it wasn't a circular reference.
186-
if (scc.Count > 1 && executionContext.TryGetExecutedValue(vertex.Formula, out var result))
186+
if (executionContext.TryGetExecutedValue(vertex.Formula, out var result))
187187
{
188-
//TODO: This is never hit so we are always recalculating
189188
value = result;
190189
}
191190
else
192191
{
193192
value = _evaluator.Evaluate(vertex.Formula, executionContext);
193+
executionContext.RecordExecuted(vertex.Formula, value);
194194
if (value.IsError() && ((FormulaError)value.Data!).ErrorType == ErrorType.Circular)
195195
isCircularGroup = true;
196196
}

src/BlazorDatasheet.DataStructures/Intervals/MergeableIntervalStore.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ private void UpdateStartEndPositions()
411411
{
412412
if (_intervals.Any())
413413
{
414-
Start = _intervals.First().Value.Start;
415-
End = _intervals.Last().Value.End;
414+
Start = _intervals.Values[0].Start;
415+
End = _intervals.Values[_intervals.Count - 1].End;
416416
}
417417
}
418418

src/BlazorDatasheet.DataStructures/Store/IStore.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace BlazorDatasheet.DataStructures.Store;
55
public interface IStore<T, TRestoreData>
66
{
77
/// <summary>
8-
/// Returns whether the the store contains any non-empty data at the row, column specified.
8+
/// Returns whether store contains any non-empty data at the row, column specified.
99
/// </summary>
1010
/// <param name="row"></param>
1111
/// <param name="col"></param>

src/BlazorDatasheet.DataStructures/Store/Range1DStore.cs

+2
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ public int GetNextNonEmptyIndex(int index)
142142
var interval = Intervals.GetNextNonEmptyIndex(index);
143143
return interval;
144144
}
145+
146+
public bool Any() => Intervals.Any();
145147
}
146148

147149
public class OverwritingValue<R> : IMergeable<OverwritingValue<R>>

src/BlazorDatasheet.Formula.Core/Dependencies/DependencyManager.cs

+8
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,14 @@ public DependencyManagerRestoreData InsertRowColAt(int index, int count, Axis ax
270270

271271
private List<FormulaVertex> GetVerticesInRegion(IRegion region, string sheetName)
272272
{
273+
if (region.IsSingleCell())
274+
{
275+
var vertex = _dependencyGraph.GetVertex(new FormulaVertex(region, sheetName, null).SheetName);
276+
if (vertex != null)
277+
return [vertex];
278+
return new List<FormulaVertex>();
279+
}
280+
273281
var vertices = new List<FormulaVertex>();
274282
foreach (var v in _dependencyGraph.GetAll())
275283
{

0 commit comments

Comments
 (0)