Генерация больших Excel-файлов

Оглавление

Генерация больших Excel-файлов

Приступим

Для генерации Excel-файлов мы чаще всего используем roo или axlsx/caxlsx. Они прекрасно работают, имеют огромный функционал и активное комьюнити. Но иногда для счастья этого оказывается мало.

В качестве источника данных возьмем готовые таблицы:

1) users — 38832 записей
2) comments — 2728260 записей

Попробуем сгенерировать файл при помощи caxlsx:

p = Axlsx::Package.new
wb = p.workbook
wb.add_worksheet do |sheet|
  User.dataset.paged_each do |row|
    sheet.add_row row.values.values
  end
end
stream = p.to_stream
File.open('users.xlsx', 'wb') { |f| f.write(stream.read) }
  • Файл успешно сгенерировался, потребление оперативной памяти составило ~100 MB.
  • Теперь попробуем прогнать большую таблицу comments. К сожалению, нам не хватило ОЗУ, и OOM Killer прибил нас.

Первое, что приходит на ум — решить проблему инкрементальной записью чанков в файл, как мы это сделали бы в случае с csv:

file = File.open('users.csv', 'w')
User.dataset.paged_each do |row|
  file.write row.values.values.join(";") + "\n"
end
file.close

Но после того, как я увидел структуру файла .xlsx, у меня появились сомнения.

Генерация больших Excel-файлов

Эксперименты

Я изучил много инфы, Stack Overflow утверждал, что построчно записывать в файл нельзя, но решил, что решение должно быть, и я не один с этим столкнулся. Переключился на структуру файлов и поиск возможных решений на других языках. В итоге наткнулся на Libxlsxwriter на C, потом на статью и даже на гем.

Давайте посмотрим, что из этого вышло:

  • caxlsx
users - 91.05 MB
RAM USAGE (start): 446696 K
RAM USAGE (end): 539936 K

comments - ??? MB
RAM USAGE (start): 444460 K
RAM USAGE (end): Killed (кончилась память)
  • xlsxwriter:
users - 38.42 MB
RAM USAGE (start): 444292 K
RAM USAGE (end): 483636 K

comments - 1597.96 MB
RAM USAGE (start): 443776 K
RAM USAGE (end): 2080096 K

Уже лучше, но 1,5 ГБ — тоже много. Решил посмотреть, что можно с этим сделать, и нашел опцию — constant_memory.

  • xlsxwriter with constant_memory:
users - 8.26 MB
RAM USAGE (start): 445720 K
RAM USAGE (end): 454184 K

comments - 8.28 MB
RAM USAGE (start): 445780 K
RAM USAGE (end): 454260 K

Результат налицо: теперь генерация файла любого объема будет потреблять ~8 MB ОЗУ. Если сравнивать функциональность этого гема с axlsx, то xlsxwriter не имеет возможности считывать данные из текущего состояния и не имеет никакой интеграции с рельсами. Но я все равно считаю это отличным бустом производительности.

Полезные ссылки

Обсудить статью, задать вопрос или предложить тему — напишите мне в Telegram