Skip to content

Commit eb806ad

Browse files
authored
Merge pull request #587 from prathzzzz/style-cache-performance-improvement
perf: enhance StyleCache with reverse index for style lookup
2 parents 7037073 + 3358bc2 commit eb806ad

File tree

3 files changed

+301
-2
lines changed

3 files changed

+301
-2
lines changed

fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleCache.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ final class StyleCache {
3636
private final ConcurrentMap<Fill, Integer> fills = new ConcurrentHashMap<>();
3737
private final ConcurrentMap<Border, Integer> borders = new ConcurrentHashMap<>();
3838
private final ConcurrentMap<Style, Integer> styles = new ConcurrentHashMap<>();
39+
private final ConcurrentMap<Integer, Style> styleIndexToStyle = new ConcurrentHashMap<>();
3940
private final ConcurrentMap<DifferentialFormat, Integer> dxfs = new ConcurrentHashMap<>();
4041

4142
/**
@@ -60,6 +61,19 @@ private static <T> int cacheStuff(ConcurrentMap<T, Integer> cache, T t, Function
6061
return cache.computeIfAbsent(t, indexFunction);
6162
}
6263

64+
/**
65+
* Cache a style and maintain reverse index for O(1) lookup.
66+
*
67+
* @param style Style to cache.
68+
* @param indexFunction Function to compute the index of the newly cached style.
69+
* @return Index of the cached style.
70+
*/
71+
private int cacheStyle(Style style, Function<Style, Integer> indexFunction) {
72+
Integer index = styles.computeIfAbsent(style, indexFunction);
73+
styleIndexToStyle.putIfAbsent(index, style);
74+
return index;
75+
}
76+
6377
/**
6478
* Caching method returning zero-based indexes.
6579
*
@@ -126,9 +140,9 @@ int cacheDxf(DifferentialFormat f) {
126140
}
127141

128142
int mergeAndCacheStyle(int currentStyle, String numberingFormat, Font font, Fill fill, Border border, Alignment alignment, Protection protection) {
129-
Style original = styles.entrySet().stream().filter(e -> e.getValue().equals(currentStyle)).map(Entry::getKey).findFirst().orElse(null);
143+
Style original = styleIndexToStyle.get(currentStyle);
130144
Style s = new Style(original, cacheValueFormatting(numberingFormat), cacheFont(font), cacheFill(fill), cacheBorder(border), alignment, protection);
131-
return cacheStuff(styles, s);
145+
return cacheStyle(s, k -> styles.size());
132146
}
133147

134148
void replaceDefaultFont(Font font) {
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package org.dhatim.fastexcel;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.IOException;
7+
import java.util.Map.Entry;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
import java.util.concurrent.ConcurrentMap;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class StyleCacheBeforeAfterTest {
14+
15+
private static class OldStyleCache {
16+
private final ConcurrentMap<Style, Integer> styles = new ConcurrentHashMap<>();
17+
18+
int mergeAndCacheStyleOldWay(int currentStyle, String numberingFormat, Font font,
19+
Fill fill, Border border, Alignment alignment, Protection protection,
20+
StyleCacheHelper helper) {
21+
Style original = styles.entrySet().stream()
22+
.filter(e -> e.getValue().equals(currentStyle))
23+
.map(Entry::getKey)
24+
.findFirst()
25+
.orElse(null);
26+
27+
Style s = new Style(original,
28+
helper.cacheValueFormatting(numberingFormat),
29+
helper.cacheFont(font),
30+
helper.cacheFill(fill),
31+
helper.cacheBorder(border),
32+
alignment, protection);
33+
34+
return styles.computeIfAbsent(s, k -> styles.size());
35+
}
36+
}
37+
38+
private static class NewStyleCache {
39+
private final ConcurrentMap<Style, Integer> styles = new ConcurrentHashMap<>();
40+
private final ConcurrentMap<Integer, Style> styleIndexToStyle = new ConcurrentHashMap<>();
41+
42+
int mergeAndCacheStyleNewWay(int currentStyle, String numberingFormat, Font font,
43+
Fill fill, Border border, Alignment alignment, Protection protection,
44+
StyleCacheHelper helper) {
45+
Style original = styleIndexToStyle.get(currentStyle);
46+
47+
Style s = new Style(original,
48+
helper.cacheValueFormatting(numberingFormat),
49+
helper.cacheFont(font),
50+
helper.cacheFill(fill),
51+
helper.cacheBorder(border),
52+
alignment, protection);
53+
54+
Integer index = styles.computeIfAbsent(s, k -> styles.size());
55+
styleIndexToStyle.putIfAbsent(index, s);
56+
return index;
57+
}
58+
}
59+
60+
private interface StyleCacheHelper {
61+
int cacheValueFormatting(String s);
62+
int cacheFont(Font f);
63+
int cacheFill(Fill f);
64+
int cacheBorder(Border b);
65+
}
66+
67+
@Test
68+
void compareOldVsNewImplementation() {
69+
int numStyles = 1000;
70+
int mergesPerStyle = 10;
71+
72+
StyleCacheHelper helper = new StyleCacheHelper() {
73+
@Override
74+
public int cacheValueFormatting(String s) { return s != null ? 1 : 0; }
75+
@Override
76+
public int cacheFont(Font f) { return 0; }
77+
@Override
78+
public int cacheFill(Fill f) { return 0; }
79+
@Override
80+
public int cacheBorder(Border b) { return 0; }
81+
};
82+
83+
OldStyleCache oldCache = new OldStyleCache();
84+
NewStyleCache newCache = new NewStyleCache();
85+
86+
for (int i = 0; i < 100; i++) {
87+
oldCache.mergeAndCacheStyleOldWay(0, null, Font.DEFAULT, Fill.NONE, Border.NONE, null, null, helper);
88+
newCache.mergeAndCacheStyleNewWay(0, null, Font.DEFAULT, Fill.NONE, Border.NONE, null, null, helper);
89+
}
90+
91+
int currentStyle = 0;
92+
for (int i = 0; i < numStyles; i++) {
93+
currentStyle = oldCache.mergeAndCacheStyleOldWay(
94+
currentStyle, "fmt" + i, Font.DEFAULT, Fill.NONE, Border.NONE, null, null, helper);
95+
newCache.mergeAndCacheStyleNewWay(
96+
currentStyle, "fmt" + i, Font.DEFAULT, Fill.NONE, Border.NONE, null, null, helper);
97+
}
98+
99+
long oldStart = System.nanoTime();
100+
currentStyle = 0;
101+
for (int styleIdx = 0; styleIdx < numStyles; styleIdx++) {
102+
for (int merge = 0; merge < mergesPerStyle; merge++) {
103+
currentStyle = oldCache.mergeAndCacheStyleOldWay(
104+
currentStyle,
105+
"format" + (styleIdx % 10),
106+
Font.DEFAULT,
107+
Fill.NONE,
108+
Border.NONE,
109+
null, null, helper);
110+
}
111+
}
112+
long oldDuration = System.nanoTime() - oldStart;
113+
114+
long newStart = System.nanoTime();
115+
currentStyle = 0;
116+
for (int styleIdx = 0; styleIdx < numStyles; styleIdx++) {
117+
for (int merge = 0; merge < mergesPerStyle; merge++) {
118+
currentStyle = newCache.mergeAndCacheStyleNewWay(
119+
currentStyle,
120+
"format" + (styleIdx % 10),
121+
Font.DEFAULT,
122+
Fill.NONE,
123+
Border.NONE,
124+
null, null, helper);
125+
}
126+
}
127+
long newDuration = System.nanoTime() - newStart;
128+
129+
System.out.println("\n=== Style Cache Performance Comparison ===");
130+
System.out.printf("Styles in cache: %d%n", numStyles);
131+
System.out.printf("Merges per style: %d%n", mergesPerStyle);
132+
System.out.printf("Total operations: %d%n", numStyles * mergesPerStyle);
133+
System.out.println("\nOLD (O(n) linear search):");
134+
System.out.printf(" Time: %d ms (%.2f ns per operation)%n",
135+
oldDuration / 1_000_000, (double) oldDuration / (numStyles * mergesPerStyle));
136+
System.out.println("\nNEW (O(1) hash map lookup):");
137+
System.out.printf(" Time: %d ms (%.2f ns per operation)%n",
138+
newDuration / 1_000_000, (double) newDuration / (numStyles * mergesPerStyle));
139+
140+
double speedup = (double) oldDuration / newDuration;
141+
System.out.printf("\nSpeedup: %.2fx faster%n", speedup);
142+
143+
assertThat(newDuration).isLessThanOrEqualTo(oldDuration);
144+
assertThat(speedup).isGreaterThan(1.5);
145+
}
146+
147+
@Test
148+
void demonstrateRealWorldScenario() throws IOException {
149+
int numUniqueStyles = 2000;
150+
151+
System.out.println("\n=== Real-World Scenario: Many Unique Styles ===");
152+
System.out.printf("Creating workbook with %d unique styles...%n", numUniqueStyles);
153+
154+
long startTime = System.nanoTime();
155+
156+
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
157+
Workbook wb = new Workbook(out, "PerfTest", "1.0")) {
158+
159+
Worksheet ws = wb.newWorksheet("Sheet 1");
160+
161+
for (int styleIdx = 0; styleIdx < numUniqueStyles; styleIdx++) {
162+
StyleSetter styleSetter = ws.range(styleIdx % 100, styleIdx % 10,
163+
(styleIdx % 100) + 1, (styleIdx % 10) + 1)
164+
.style()
165+
.fontName("Font" + (styleIdx % 50))
166+
.fontSize(10 + (styleIdx % 20))
167+
.fillColor(String.format("#%06X", styleIdx % 0xFFFFFF));
168+
if (styleIdx % 3 == 0) {
169+
styleSetter.bold();
170+
}
171+
if (styleIdx % 4 == 0) {
172+
styleSetter.italic();
173+
}
174+
styleSetter.horizontalAlignment(styleIdx % 2 == 0 ? "center" : "left").set();
175+
}
176+
177+
for (int i = 0; i < 100; i++) {
178+
ws.value(i, 0, "Data " + i);
179+
}
180+
}
181+
182+
long duration = System.nanoTime() - startTime;
183+
long durationMs = duration / 1_000_000;
184+
185+
System.out.printf("Completed in %d ms%n", durationMs);
186+
System.out.printf("Average: %.2f ms per style%n", (double) durationMs / numUniqueStyles);
187+
188+
assertThat(durationMs).isLessThan(5000);
189+
}
190+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package org.dhatim.fastexcel;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.IOException;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class StyleCachePerformanceTest {
11+
12+
@Test
13+
void styleCacheScalesWellWithManyStyles() throws IOException {
14+
int numStyles = 1000;
15+
int mergesPerStyle = 10;
16+
17+
long startTime = System.nanoTime();
18+
19+
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
20+
Workbook wb = new Workbook(out, "PerfTest", "1.0")) {
21+
22+
Worksheet ws = wb.newWorksheet("Sheet 1");
23+
24+
for (int styleIdx = 0; styleIdx < numStyles; styleIdx++) {
25+
String fontName = "Font" + (styleIdx % 20);
26+
int fontSize = 10 + (styleIdx % 5);
27+
String fillColor = String.format("#%06X", (styleIdx * 1000) % 0xFFFFFF);
28+
29+
for (int merge = 0; merge < mergesPerStyle; merge++) {
30+
StyleSetter styleSetter = ws.range(styleIdx % 100, (styleIdx * 2) % 50,
31+
(styleIdx % 100) + 1, ((styleIdx * 2) % 50) + 1)
32+
.style()
33+
.fontName(fontName)
34+
.fontSize(fontSize)
35+
.fillColor(fillColor);
36+
if (styleIdx % 2 == 0) {
37+
styleSetter.bold();
38+
}
39+
if (styleIdx % 3 == 0) {
40+
styleSetter.italic();
41+
}
42+
styleSetter.horizontalAlignment(styleIdx % 2 == 0 ? "center" : "left").set();
43+
}
44+
}
45+
46+
for (int i = 0; i < 100; i++) {
47+
ws.value(i, 0, "Data " + i);
48+
}
49+
}
50+
51+
long duration = System.nanoTime() - startTime;
52+
long durationMs = duration / 1_000_000;
53+
54+
assertThat(durationMs).isLessThan(5000);
55+
56+
System.out.println(String.format(
57+
"Created %d styles with %d merges each in %d ms (%.2f ms per style merge)",
58+
numStyles, mergesPerStyle, durationMs,
59+
(double) durationMs / (numStyles * mergesPerStyle)
60+
));
61+
}
62+
63+
@Test
64+
void styleCacheHandlesManyUniqueStyles() throws IOException {
65+
int numStyles = 2000;
66+
67+
long startTime = System.nanoTime();
68+
69+
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
70+
Workbook wb = new Workbook(out, "PerfTest", "1.0")) {
71+
72+
Worksheet ws = wb.newWorksheet("Sheet 1");
73+
74+
for (int styleIdx = 0; styleIdx < numStyles; styleIdx++) {
75+
ws.range(styleIdx % 100, styleIdx % 10,
76+
(styleIdx % 100) + 1, (styleIdx % 10) + 1)
77+
.style()
78+
.fontName("Font" + styleIdx)
79+
.fontSize(10 + (styleIdx % 20))
80+
.fillColor(String.format("#%06X", styleIdx % 0xFFFFFF))
81+
.set();
82+
}
83+
}
84+
85+
long duration = System.nanoTime() - startTime;
86+
long durationMs = duration / 1_000_000;
87+
88+
assertThat(durationMs).isLessThan(3000);
89+
90+
System.out.println(String.format(
91+
"Created %d unique styles in %d ms (%.2f ms per style)",
92+
numStyles, durationMs, (double) durationMs / numStyles
93+
));
94+
}
95+
}

0 commit comments

Comments
 (0)