Skip to content

Conversation

@DengY11
Copy link
Contributor

@DengY11 DengY11 commented May 25, 2025

PR Details

Add calculation cache to improve CalcCellValue performance

Description

Implements a calculation cache mechanism for CalcCellValue to significantly improve performance when calculating formulas repeatedly. The cache stores calculated results and automatically invalidates when cells are modified.

Related Issue

Fixes #2057 - Performance enhancement - cache calculated values

Motivation and Context

As described in #2057, Excelize doesn't cache calculated values from CalcCellValue, causing expensive formulas to be recalculated every time they are referenced by other cells. This leads to poor performance when dealing with complex formula dependencies.

Example scenario:

A1: 40
A2: =VERYEXPENSIVEFUNCTION()  // takes 100ms
A3: =A1+A2                    // takes 100ms (recalculates A2)

Solution implemented:

  • Cache calculated results in thread-safe sync.Map
  • Clear entire cache when any SetCellX() function is called
  • Cache key format: "sheet!cell"

Performance improvement:

  • First calculation: ~150µs (calculated and cached)
  • Second calculation: ~600ns (from cache, 250x faster)

How Has This Been Tested

go test -v -run "TestCalc.*Cache|TestSetFunctionsClearCache"
go test ./...

Test coverage includes:

  • ☑️ Basic cache hit/miss scenarios
  • ☑️ Cache invalidation on cell modifications
  • ☑️ Multi-cell formula dependencies
  • ☑️ All 12 Set functions properly clear cache
  • ☑️ Thread safety
  • ☑️ 3 new comprehensive test functions added

Types of changes

☑️ New feature (non-breaking change which adds functionality)
☐ Docs change / refactoring / dependency upgrade
☐ Bug fix (non-breaking change which fixes an issue)
☐ Breaking change (fix or feature that would cause existing functionality to change)

Checklist

☑️ My code follows the code style of this project.
☐ My change requires a change to the documentation.
☐ I have updated the documentation accordingly.
☑️ I have read the CONTRIBUTING document.
☑️ I have added tests to cover my changes.
☑️ All new and existing tests passed.

Implementation Details

Modified Files:

  • excelize.go: Added cache fields (calcCache sync.Map, calcCacheMu sync.RWMutex) to File struct
  • calc.go: Implemented caching logic in CalcCellValue() + added clearCalcCache() helper
  • cell.go: Added cache clearing to all 12 Set functions
  • calc_test.go: Added 3 comprehensive cache test functions

@xuri xuri added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label May 25, 2025
@codecov
Copy link

codecov bot commented May 25, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.47%. Comparing base (8a99deb) to head (83aeae4).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #2144   +/-   ##
=======================================
  Coverage   99.47%   99.47%           
=======================================
  Files          32       32           
  Lines       25704    25740   +36     
=======================================
+ Hits        25569    25605   +36     
  Misses         72       72           
  Partials       63       63           
Flag Coverage Δ
unittests 99.47% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@xuri xuri left a comment

Choose a reason for hiding this comment

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

Thanks for your PR. We need add any cache cautiously, after these changes we also need remove cache in following scenario:

  • RemoveCol
  • RemoveRow
  • DuplicateRow
  • DuplicateRowTo
  • SetSheetCol

And update or delete cache when:

  • SetSheetName
  • DeleteSheet

Consider if other scenario needed.

@DengY11
Copy link
Contributor Author

DengY11 commented May 27, 2025

Thanks for your PR. We need add any cache cautiously, after these changes we also need remove cache in following scenario:

  • RemoveCol
  • RemoveRow
  • DuplicateRow
  • DuplicateRowTo
  • SetSheetCol

And update or delete cache when:

  • SetSheetName
  • DeleteSheet

Consider if other scenario needed.

Thanks for your quick respond!
sorry for missing a few spots where we needed to clear the CalcCellValue cache in my first commit. After taking another look and with some great input, I think we've got all a_bases covered now.
Here’s a quick rundown of where we’ve added the f.clearCalcCache() calls:
These are the functions we sorted out earlier, based on what was initially asked for and our first round of checks:
RemoveCol ✅
RemoveRow ✅
DuplicateRow ✅
DuplicateRowTo ✅
SetSheetCol ✅
SetSheetName ✅
DeleteSheet ✅
SetCellValue ✅
SetCellInt ✅
SetCellUint ✅
SetCellBool ✅
SetCellFloat ✅
SetCellStr ✅
SetCellDefault ✅
SetCellFormula ✅
SetCellRichText ✅
SetCellHyperLink ✅

New additions in this commit:
While double-checking things more recently, we found a few more functions that also mess with the sheet in ways that would need the cache to be cleared. So, I've added the cache clearing to these in the latest changes:
MergeCell ✅
UnmergeCell ✅
InsertCols ✅
InsertRows ✅
CopySheet ✅
MoveSheet ✅

Copy link
Member

@xuri xuri left a comment

Choose a reason for hiding this comment

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

Code conflicts need resolve.

@DengY11
Copy link
Contributor Author

DengY11 commented Jun 2, 2025

Code conflicts need resolve.

so sorry, the conflict was caused by my local auto-formatting. I've resolved it by keeping the original content from master

@DengY11 DengY11 requested a review from xuri June 2, 2025 12:06
@ar090
Copy link

ar090 commented Jun 17, 2025

This is really exciting! I have some very large workbooks this should help out on a ton.

@ar090
Copy link

ar090 commented Jun 26, 2025

Hi @DengY11 and @xuri thanks so much for working on this! What is left to get this merged and released?

@paolobarbolini
Copy link
Contributor

Ping @xuri

@xuri
Copy link
Member

xuri commented Oct 14, 2025

Will take review for this.

Copy link
Member

@xuri xuri left a comment

Choose a reason for hiding this comment

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

Sorry for late reply, I've left some comments.

// err := f.MoveSheet("Sheet2", "Sheet1")
func (f *File) MoveSheet(source, target string) error {
f.clearCalcCache()
if strings.EqualFold(source, target) {
Copy link
Member

Choose a reason for hiding this comment

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

Does clear calc cache on move sheet order is required?

sheet.go Outdated
// sheet name in the formula or reference associated with the cell. So there
// may be problem formula error or reference missing.
func (f *File) SetSheetName(source, target string) error {
f.clearCalcCache()
Copy link
Member

Choose a reason for hiding this comment

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

Clear calc cache after sheet name checking.

// as formulas, charts, and so on. If there is any referenced value of the
// worksheet, it will cause a file error when you open it. The excelize only
// partially updates these references currently.
func (f *File) DuplicateRow(sheet string, row int) error {
Copy link
Member

@xuri xuri Nov 27, 2025

Choose a reason for hiding this comment

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

For the DuplicateRow, DuplicateRowTo, InsertCol and InsertRow functions, we can clear calc cache in f.adjustHelper function instead of call f.clearCalcCache on each functions.

Copy link
Member

@xuri xuri left a comment

Choose a reason for hiding this comment

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

Due to formula function may reference defined name or table name, so we need also clear calc cache in SetDefinedName, DeleteDefinedName, AddPivotTable, DeletePivotTable, AddTable and DeleteTable functions.

cell.go Outdated
//
// err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location")
func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...HyperlinkOpts) error {
f.clearCalcCache()
Copy link
Member

Choose a reason for hiding this comment

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

Need to clear calc cache on set cell hyperlink.

cell.go Outdated
//
// err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2})
func (f *File) SetSheetRow(sheet, cell string, slice interface{}) error {
f.clearCalcCache()
Copy link
Member

Choose a reason for hiding this comment

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

The SetSheetCol and SetSheetRow will call SetCellValue, so we needn't clear calc cache at here.

col.go Outdated
// worksheet, it will cause a file error when you open it. The excelize only
// partially updates these references currently.
func (f *File) InsertCols(sheet, col string, n int) error {
f.clearCalcCache()
Copy link
Member

Choose a reason for hiding this comment

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

Clear calc cache in f.adjustHelper function would be better approach.

col.go Outdated
// worksheet, it will cause a file error when you open it. The excelize only
// partially updates these references currently.
func (f *File) RemoveCol(sheet, col string) error {
f.clearCalcCache()
Copy link
Member

Choose a reason for hiding this comment

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

Same as above, clear calc cache in f.adjustHelper function.

rows.go Outdated
// worksheet, it will cause a file error when you open it. The excelize only
// partially updates these references currently.
func (f *File) InsertRows(sheet string, row, n int) error {
f.clearCalcCache()
Copy link
Member

Choose a reason for hiding this comment

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

Clear calc cache in f.adjustHelper.

sheet.go Outdated
// }
// err = f.CopySheet(0, index)
func (f *File) CopySheet(from, to int) error {
f.clearCalcCache()
Copy link
Member

Choose a reason for hiding this comment

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

Clear calc in the f.copySheet function.

calc.go Outdated
func (f *File) clearCalcCache() {
f.calcCacheMu.Lock()
defer f.calcCacheMu.Unlock()
f.calcCache.Range(func(key, value interface{}) bool {
Copy link
Member

Choose a reason for hiding this comment

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

Can we using f.calcCache.Clear() instead of range each key?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance enhancement - cache calculated values

4 participants