From 8f26e2721de7096deded48b9a8e5592adac09a72 Mon Sep 17 00:00:00 2001 From: Greg Woolsey Date: Wed, 6 Dec 2017 00:15:51 +0000 Subject: [PATCH] Bug #61841 - Unnecessary long computation when evaluating VLOOKUP on all column reference Found some optimizations in the general evaluation framework related to blank cells in rows beyond the last defined row of a sheet. I don't see any issue with passing a bit of context down deeper into this framework, as it's all POI-internal and only had one calling path. See the above bug for the performance analysis. Not specifically related to VLOOKUP, but improves that case by more than 2/3 as well. git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1817252 13f79535-47bb-0310-9956-ffa450edef68 --- .../hssf/usermodel/HSSFEvaluationSheet.java | 13 ++++++++- .../poi/ss/formula/CellEvaluationFrame.java | 4 +-- .../poi/ss/formula/EvaluationSheet.java | 7 +++++ .../poi/ss/formula/EvaluationTracker.java | 4 +-- .../ss/formula/FormulaUsedBlankCellSet.java | 16 +++++++---- .../poi/ss/formula/WorkbookEvaluator.java | 2 +- .../eval/forked/ForkedEvaluationSheet.java | 11 +++++++- .../xssf/streaming/SXSSFEvaluationSheet.java | 13 ++++++++- .../xssf/usermodel/XSSFEvaluationSheet.java | 15 +++++++++++ .../poi/ss/formula/functions/TestVlookup.java | 25 ++++++++++++++++++ test-data/spreadsheet/VLookupFullColumn.xlsx | Bin 0 -> 13504 bytes 11 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestVlookup.java create mode 100644 test-data/spreadsheet/VLookupFullColumn.xlsx diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationSheet.java b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationSheet.java index f1b8d7bf3e..841daa6d33 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationSheet.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationSheet.java @@ -28,14 +28,25 @@ import org.apache.poi.util.Internal; final class HSSFEvaluationSheet implements EvaluationSheet { private final HSSFSheet _hs; + private int _lastDefinedRow = -1; public HSSFEvaluationSheet(HSSFSheet hs) { _hs = hs; + _lastDefinedRow = _hs.getLastRowNum(); } public HSSFSheet getHSSFSheet() { return _hs; } + + /* (non-Javadoc) + * @see org.apache.poi.ss.formula.EvaluationSheet#getlastRowNum() + * @since POI 4.0.0 + */ + public int getlastRowNum() { + return _lastDefinedRow; + } + @Override public EvaluationCell getCell(int rowIndex, int columnIndex) { HSSFRow row = _hs.getRow(rowIndex); @@ -54,6 +65,6 @@ final class HSSFEvaluationSheet implements EvaluationSheet { */ @Override public void clearAllCachedResultValues() { - // nothing to do + _lastDefinedRow = _hs.getLastRowNum(); } } diff --git a/src/java/org/apache/poi/ss/formula/CellEvaluationFrame.java b/src/java/org/apache/poi/ss/formula/CellEvaluationFrame.java index 7abfce3c80..46ab79b10f 100644 --- a/src/java/org/apache/poi/ss/formula/CellEvaluationFrame.java +++ b/src/java/org/apache/poi/ss/formula/CellEvaluationFrame.java @@ -64,11 +64,11 @@ final class CellEvaluationFrame { _sensitiveInputCells.toArray(result); return result; } - public void addUsedBlankCell(int bookIndex, int sheetIndex, int rowIndex, int columnIndex) { + public void addUsedBlankCell(EvaluationWorkbook evalWorkbook, int bookIndex, int sheetIndex, int rowIndex, int columnIndex) { if (_usedBlankCellGroup == null) { _usedBlankCellGroup = new FormulaUsedBlankCellSet(); } - _usedBlankCellGroup.addCell(bookIndex, sheetIndex, rowIndex, columnIndex); + _usedBlankCellGroup.addCell(evalWorkbook, bookIndex, sheetIndex, rowIndex, columnIndex); } public void updateFormulaResult(ValueEval result) { diff --git a/src/java/org/apache/poi/ss/formula/EvaluationSheet.java b/src/java/org/apache/poi/ss/formula/EvaluationSheet.java index b3e60e2b3a..e155d0f68f 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationSheet.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationSheet.java @@ -17,6 +17,7 @@ package org.apache.poi.ss.formula; +import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.util.Internal; /** @@ -42,4 +43,10 @@ public interface EvaluationSheet { * @since POI 3.15 beta 3 */ public void clearAllCachedResultValues(); + + /** + * @return last row index referenced on this sheet, for evaluation optimization + * @since POI 4.0.0 + */ + public int getlastRowNum(); } diff --git a/src/java/org/apache/poi/ss/formula/EvaluationTracker.java b/src/java/org/apache/poi/ss/formula/EvaluationTracker.java index 35b7f32ba0..a69386389c 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationTracker.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationTracker.java @@ -131,7 +131,7 @@ final class EvaluationTracker { } } - public void acceptPlainValueDependency(int bookIndex, int sheetIndex, + public void acceptPlainValueDependency(EvaluationWorkbook evalWorkbook, int bookIndex, int sheetIndex, int rowIndex, int columnIndex, ValueEval value) { // Tell the currently evaluating cell frame that it has a dependency on the specified int prevFrameIndex = _evaluationFrames.size() - 1; @@ -140,7 +140,7 @@ final class EvaluationTracker { } else { CellEvaluationFrame consumingFrame = _evaluationFrames.get(prevFrameIndex); if (value == BlankEval.instance) { - consumingFrame.addUsedBlankCell(bookIndex, sheetIndex, rowIndex, columnIndex); + consumingFrame.addUsedBlankCell(evalWorkbook, bookIndex, sheetIndex, rowIndex, columnIndex); } else { PlainValueCellCacheEntry cce = _cache.getPlainValueEntry(bookIndex, sheetIndex, rowIndex, columnIndex, value); diff --git a/src/java/org/apache/poi/ss/formula/FormulaUsedBlankCellSet.java b/src/java/org/apache/poi/ss/formula/FormulaUsedBlankCellSet.java index 69069f9056..18e88a8be9 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaUsedBlankCellSet.java +++ b/src/java/org/apache/poi/ss/formula/FormulaUsedBlankCellSet.java @@ -57,13 +57,17 @@ final class FormulaUsedBlankCellSet { private int _firstColumnIndex; private int _lastColumnIndex; private BlankCellRectangleGroup _currentRectangleGroup; + private int _lastDefinedRow; - public BlankCellSheetGroup() { + public BlankCellSheetGroup(int lastDefinedRow) { _rectangleGroups = new ArrayList<>(); _currentRowIndex = -1; + _lastDefinedRow = lastDefinedRow; } public void addCell(int rowIndex, int columnIndex) { + if (rowIndex > _lastDefinedRow) return; + if (_currentRowIndex == -1) { _currentRowIndex = rowIndex; _firstColumnIndex = columnIndex; @@ -89,6 +93,8 @@ final class FormulaUsedBlankCellSet { } public boolean containsCell(int rowIndex, int columnIndex) { + if (rowIndex > _lastDefinedRow) return true; + for (int i=_rectangleGroups.size()-1; i>=0; i--) { BlankCellRectangleGroup bcrg = _rectangleGroups.get(i); if (bcrg.containsCell(rowIndex, columnIndex)) { @@ -167,17 +173,17 @@ final class FormulaUsedBlankCellSet { _sheetGroupsByBookSheet = new HashMap<>(); } - public void addCell(int bookIndex, int sheetIndex, int rowIndex, int columnIndex) { - BlankCellSheetGroup sbcg = getSheetGroup(bookIndex, sheetIndex); + public void addCell(EvaluationWorkbook evalWorkbook, int bookIndex, int sheetIndex, int rowIndex, int columnIndex) { + BlankCellSheetGroup sbcg = getSheetGroup(evalWorkbook, bookIndex, sheetIndex); sbcg.addCell(rowIndex, columnIndex); } - private BlankCellSheetGroup getSheetGroup(int bookIndex, int sheetIndex) { + private BlankCellSheetGroup getSheetGroup(EvaluationWorkbook evalWorkbook, int bookIndex, int sheetIndex) { BookSheetKey key = new BookSheetKey(bookIndex, sheetIndex); BlankCellSheetGroup result = _sheetGroupsByBookSheet.get(key); if (result == null) { - result = new BlankCellSheetGroup(); + result = new BlankCellSheetGroup(evalWorkbook.getSheet(sheetIndex).getlastRowNum()); _sheetGroupsByBookSheet.put(key, result); } return result; diff --git a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java index 3c12dc5709..9ffbb8e09b 100644 --- a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java @@ -253,7 +253,7 @@ public final class WorkbookEvaluator { if (srcCell == null || srcCell.getCellType() != CellType.FORMULA) { ValueEval result = getValueFromNonFormulaCell(srcCell); if (shouldCellDependencyBeRecorded) { - tracker.acceptPlainValueDependency(_workbookIx, sheetIndex, rowIndex, columnIndex, result); + tracker.acceptPlainValueDependency(_workbook, _workbookIx, sheetIndex, rowIndex, columnIndex, result); } return result; } diff --git a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationSheet.java b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationSheet.java index bc5fdf1741..698fda5649 100644 --- a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationSheet.java +++ b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationSheet.java @@ -42,6 +42,7 @@ import org.apache.poi.util.Internal; final class ForkedEvaluationSheet implements EvaluationSheet { private final EvaluationSheet _masterSheet; + /** * Only cells which have been split are put in this map. (This has been done to conserve memory). */ @@ -51,7 +52,15 @@ final class ForkedEvaluationSheet implements EvaluationSheet { _masterSheet = masterSheet; _sharedCellsByRowCol = new HashMap<>(); } - + + /* (non-Javadoc) + * @see org.apache.poi.ss.formula.EvaluationSheet#getlastRowNum() + * @since POI 4.0.0 + */ + public int getlastRowNum() { + return _masterSheet.getlastRowNum(); + } + @Override public EvaluationCell getCell(int rowIndex, int columnIndex) { RowColKey key = new RowColKey(rowIndex, columnIndex); diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFEvaluationSheet.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFEvaluationSheet.java index 57d7cc57b9..ccb685690c 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFEvaluationSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFEvaluationSheet.java @@ -27,14 +27,25 @@ import org.apache.poi.util.Internal; @Internal final class SXSSFEvaluationSheet implements EvaluationSheet { private final SXSSFSheet _xs; + private int _lastDefinedRow = -1; public SXSSFEvaluationSheet(SXSSFSheet sheet) { _xs = sheet; + _lastDefinedRow = _xs.getLastRowNum(); } public SXSSFSheet getSXSSFSheet() { return _xs; } + + /* (non-Javadoc) + * @see org.apache.poi.ss.formula.EvaluationSheet#getlastRowNum() + * @since POI 4.0.0 + */ + public int getlastRowNum() { + return _lastDefinedRow; + } + @Override public EvaluationCell getCell(int rowIndex, int columnIndex) { SXSSFRow row = _xs.getRow(rowIndex); @@ -56,6 +67,6 @@ final class SXSSFEvaluationSheet implements EvaluationSheet { */ @Override public void clearAllCachedResultValues() { - // nothing to do + _lastDefinedRow = _xs.getLastRowNum(); } } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java index d8391ebf78..debb6c5dcb 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationSheet.java @@ -34,25 +34,40 @@ final class XSSFEvaluationSheet implements EvaluationSheet { private final XSSFSheet _xs; private Map _cellCache; + private int _lastDefinedRow = -1; public XSSFEvaluationSheet(XSSFSheet sheet) { _xs = sheet; + _lastDefinedRow = _xs.getLastRowNum(); } public XSSFSheet getXSSFSheet() { return _xs; } + /* (non-Javadoc) + * @see org.apache.poi.ss.formula.EvaluationSheet#getlastRowNum() + * @since POI 4.0.0 + */ + public int getlastRowNum() { + return _lastDefinedRow; + } + /* (non-JavaDoc), inherit JavaDoc from EvaluationWorkbook * @since POI 3.15 beta 3 */ @Override public void clearAllCachedResultValues() { _cellCache = null; + _lastDefinedRow = _xs.getLastRowNum(); } @Override public EvaluationCell getCell(int rowIndex, int columnIndex) { + // shortcut evaluation if reference is outside the bounds of existing data + // see issue #61841 for impact on VLOOKUP in particular + if (rowIndex > _lastDefinedRow) return null; + // cache for performance: ~30% speedup due to caching if (_cellCache == null) { _cellCache = new HashMap<>(_xs.getLastRowNum() * 3); diff --git a/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestVlookup.java b/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestVlookup.java new file mode 100644 index 0000000000..bfe31ea496 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/ss/formula/functions/TestVlookup.java @@ -0,0 +1,25 @@ +package org.apache.poi.ss.formula.functions; + +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.XSSFTestDataSamples; +import org.junit.Test; + +import junit.framework.TestCase; + +/** + * Test the VLOOKUP function + */ +public class TestVlookup extends TestCase { + + @Test + public void testFullColumnAreaRef61841() { + final Workbook wb = XSSFTestDataSamples.openSampleWorkbook("VLookupFullColumn.xlsx"); + FormulaEvaluator feval = wb.getCreationHelper().createFormulaEvaluator(); + feval.evaluateAll(); + assertEquals("Wrong lookup value", "Value1", feval.evaluate(wb.getSheetAt(0).getRow(3).getCell(1)).getStringValue()); + assertEquals("Lookup should return #N/A", CellType.ERROR, feval.evaluate(wb.getSheetAt(0).getRow(4).getCell(1)).getCellType()); + } + +} diff --git a/test-data/spreadsheet/VLookupFullColumn.xlsx b/test-data/spreadsheet/VLookupFullColumn.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..cf6b82b8090016dda63583f6d85fc974c61739ed GIT binary patch literal 13504 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 literal 0 HcmV?d00001