|
10 | 10 | [excel-clj.style :as style]
|
11 | 11 | [clojure.walk :as walk]
|
12 | 12 | [taoensso.tufte :as tufte])
|
13 |
| - (:import (org.apache.poi.xssf.usermodel XSSFWorkbook XSSFRow XSSFSheet) |
14 |
| - (java.io Closeable) |
15 |
| - (org.apache.poi.ss.usermodel RichTextString Cell) |
| 13 | + (:import (java.io Closeable) |
| 14 | + (org.apache.poi.ss.usermodel RichTextString Sheet Cell Row Workbook) |
16 | 15 | (java.util Date Calendar)
|
17 |
| - (org.apache.poi.ss.util CellRangeAddress))) |
| 16 | + (org.apache.poi.ss.util CellRangeAddress) |
| 17 | + (org.apache.poi.xssf.streaming SXSSFWorkbook) |
| 18 | + (org.apache.poi.xssf.usermodel XSSFWorkbook))) |
18 | 19 |
|
19 | 20 |
|
20 | 21 | (set! *warn-on-reflection* true)
|
|
102 | 103 | (doto cell (.setCellValue ^String to-write))))))
|
103 | 104 |
|
104 | 105 |
|
105 |
| -(defn- ensure-row! [{:keys [^XSSFSheet sheet row row-cursor]}] |
| 106 | +(defn- ensure-row! [{:keys [^Sheet sheet row row-cursor]}] |
106 | 107 | (if-let [r @row]
|
107 | 108 | r
|
108 | 109 | (let [^int idx (vswap! row-cursor inc)]
|
109 | 110 | (vreset! row (.createRow sheet idx)))))
|
110 | 111 |
|
111 | 112 |
|
112 | 113 | (defrecord ^:private SheetWriter
|
113 |
| - [cell-style-cache ^XSSFSheet sheet row row-cursor col-cursor] |
| 114 | + [cell-style-cache ^Sheet sheet row row-cursor col-cursor] |
114 | 115 | IWorksheetWriter
|
115 | 116 | (write! [this value]
|
116 | 117 | (write! this value nil 1 1))
|
117 | 118 |
|
118 | 119 | (write! [this value style width height]
|
119 |
| - (let [^XSSFRow poi-row (ensure-row! this) |
| 120 | + (let [^Row poi-row (ensure-row! this) |
120 | 121 | ^int cidx (vswap! col-cursor inc)
|
121 | 122 | poi-cell (.createCell poi-row cidx)]
|
122 | 123 |
|
|
153 | 154 | Closeable
|
154 | 155 | (close [this]
|
155 | 156 | (tufte/p :set-print-settings
|
156 |
| - (.setFitToPage sheet true) |
157 |
| - (.setFitWidth (.getPrintSetup sheet) 1)) |
| 157 | + (.setFitToPage sheet true) |
| 158 | + (.setFitWidth (.getPrintSetup sheet) 1)) |
158 | 159 | this))
|
159 | 160 |
|
160 | 161 |
|
161 |
| -(defrecord ^:private WorkbookWriter [^XSSFWorkbook workbook path] |
| 162 | +(defrecord ^:private WorkbookWriter [^Workbook workbook path] |
162 | 163 | IWorkbookWriter
|
163 | 164 | (workbook* [this]
|
164 | 165 | workbook)
|
|
174 | 175 | (defn ^SheetWriter sheet-writer
|
175 | 176 | "Create a writer for an individual sheet within the workbook."
|
176 | 177 | [workbook-writer sheet-name]
|
177 |
| - (let [{:keys [^XSSFWorkbook workbook path]} workbook-writer |
| 178 | + (let [{:keys [^Workbook workbook path]} workbook-writer |
178 | 179 | cache (enc/memoize_
|
179 | 180 | (fn [style]
|
180 | 181 | (let [style (enc/nested-merge style/default-style style)]
|
|
188 | 189 |
|
189 | 190 |
|
190 | 191 | (defn ^WorkbookWriter writer
|
191 |
| - "Open a writer for Excel workbooks." |
192 |
| - [path] |
193 |
| - (->WorkbookWriter (XSSFWorkbook.) path)) |
| 192 | + "Open a writer for Excel workbooks. |
| 193 | +
|
| 194 | + If `streaming?` is true (default), uses Apache POI streaming implementations. |
| 195 | +
|
| 196 | + N.B. The streaming version is an order of magnitude faster than the |
| 197 | + alternative, so override this default only if you have a very good reason!" |
| 198 | + ([path] |
| 199 | + (writer path true)) |
| 200 | + ([path streaming?] |
| 201 | + (->WorkbookWriter (if streaming? (SXSSFWorkbook.) (XSSFWorkbook.)) path))) |
194 | 202 |
|
195 | 203 |
|
196 | 204 | (comment
|
|
205 | 213 |
|
206 | 214 | (newline! t)
|
207 | 215 | (write! t "Cell")
|
208 |
| - (write! t "Wide Cell" nil 2 1) |
| 216 | + (write! t "Wide Red Cell" {:font {:color :red}} 2 1) |
209 | 217 |
|
210 | 218 | (newline! t)
|
211 | 219 | (write! t "Tall Cell" nil 1 2)
|
|
228 | 236 |
|
229 | 237 | (defn performance-test
|
230 | 238 | "Write `n-rows` of data to `to-file` and see how long it takes."
|
231 |
| - [to-file n-rows] |
| 239 | + [to-file n-rows & {:keys [streaming?] :or {streaming? true}}] |
232 | 240 | (let [start (System/currentTimeMillis)
|
233 | 241 | header-style {:border-bottom :thin :font {:bold true}}]
|
234 |
| - (with-open [w (writer to-file) |
| 242 | + (with-open [w (writer to-file streaming?) |
235 | 243 | sh (sheet-writer w "Test")]
|
236 | 244 |
|
237 | 245 | (write! sh "Date" header-style 1 1)
|
|
250 | 258 |
|
251 | 259 | (println "Wrote rows after" (- (System/currentTimeMillis) start) "ms"))
|
252 | 260 |
|
253 |
| - (println "Wrote file after" (- (System/currentTimeMillis) start) "ms"))) |
| 261 | + (let [total (- (System/currentTimeMillis) start)] |
| 262 | + (println "Wrote file after" total "ms") |
| 263 | + total))) |
254 | 264 |
|
255 | 265 |
|
256 | 266 | (comment
|
| 267 | + "Testing overall performance, plus looking at streaming vs not streaming." |
| 268 | + |
| 269 | + ;; To get more detailed profiling output |
257 | 270 | (tufte/add-basic-println-handler! {})
|
258 |
| - (performance-test "test.xlsx" 1000) ; 103ms |
259 |
| - (tufte/profile {} (performance-test "test.xlsx" 10000)) ; 385ms |
260 |
| - (tufte/profile {} (performance-test "test.xlsx" 100000)) ; 4503ms |
261 |
| - (performance-test "test.xlsx" 150000) ; 9572ms |
262 |
| - (performance-test "test.xlsx" 200000) ; 11320ms |
263 |
| - (performance-test "test.xlsx" 300000) ; 19939ms |
264 |
| - (performance-test "test.xlsx" 350000) ; OOM error... haha |
| 271 | + |
| 272 | + ;;; 200,000 rows with and without streaming |
| 273 | + (tufte/profile {} (performance-test "test.xlsx" 200000 :streaming? true)) |
| 274 | + ;=> 2234 |
| 275 | + |
| 276 | + (tufte/profile {} (performance-test "test.xlsx" 200000 :streaming? false) ) |
| 277 | + ;=> 11187 |
| 278 | + |
| 279 | + |
| 280 | + ;;; 300,000 rows with and without streaming |
| 281 | + (tufte/profile {} (performance-test "test.xlsx" 500000 :streaming? true)) |
| 282 | + ;=> 5093 |
| 283 | + |
| 284 | + (tufte/profile {} (performance-test "test.xlsx" 500000 :streaming? false)) |
| 285 | + ; ... like a 2 minute delay and then OOM error (with my 8G of ram) ... haha |
265 | 286 | )
|
0 commit comments