You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Large Memory and Speed performance regression from 2.5.3 to 2.6.3 #122
We upgraded to rails 7.1.3 and at the same time I upgraded our version of creek from 2.5.3 to 2.6.3.
After deploying the new rails version, we noticed our memory usage going up significantly as well as our time to process spreadsheets.
I tested the two versions in irb isolation and saw no difference, with a slight improvement for 2.6.3. However, when in docker the difference is massive.
(See #122 (comment), my testing methodology wasn't great for local Mac and the difference is less but still large. This is not just docker)
As you can see in the above, the problem appears to be the row generator. Also, the peak memory usage went from 709,000 KB to 3,852,000 KB, 5 times more memory!
changed the title [-][RAILS ONLY] Large Memory and Speed performance regression from 2.5.3 to 2.6.3[/-][+][Docker ONLY] Large Memory and Speed performance regression from 2.5.3 to 2.6.3[/+]on Feb 21, 2024
Found that this is actually docker not rails. I spun up a fresh rails instance and wasn't producing it locally. Then, I used the rails default dockerfile modified to strip it back closer to essentials.
This is on arm64 architecture but I have the same problem in our production amd64 architecture docker images too.
# syntax = docker/dockerfile:1# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and GemfileARG RUBY_VERSION=3.2.2
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
# Rails app lives hereWORKDIR /rails
# Set production environmentENV BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"# Throw-away build stage to reduce size of final imageFROM base as build
# Install packages needed to build gemsRUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git libvips pkg-config
# Copy application codeCOPY . .
# Final stage for app imageFROM base
# Install packages needed for deploymentRUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libsqlite3-0 libvips && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Copy built artifacts: gems, applicationCOPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails
# Run and own only the runtime files as a non-root user for securityRUN useradd rails --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER rails:rails
RUN gem install creek
CMD ["bash"]
Note: I tried this in an amd_64 version of the same container and got the same result. 3GB RAM usage in 2.6.3, 800MB RAM in 2.5.3. Still on Mac OS X host though
Nevermind, I'm not used to Mac OS X and had trouble figuring out the peak memory usage.
Got gtime working though and amd64 Mac OS X Sonoma 14.3.1 ruby 3.2.2 shows a similar though smaller effect.
So this is not docker or rails specific, I just failed at figuring out peak memory usage on Mac and the speed didn't get way slower like it did in docker.
It is better locally at least. Only about 2.6x more memory not 5x.
lilithcarpenter@Liliths-MacBook-Pro tmp-rails % gtime -v irb
irb(main):001> require 'creek'
=>true
irb(main):002> Creek::VERSION
=>"2.6.3"
irb(main):003>time = Time.now; creek = Creek::Book.new('./performance-test.xlsx'); puts(Time.now - time);
6.727392
irb(main):004>time = Time.now; creek.sheets[0].simple_rows.map(&:inspect); puts(Time.now - time);
32.480844
irb(main):005>
Command being timed: "irb"
User time (seconds): 36.39
System time (seconds): 2.42
Percent of CPU this job got: 21%
Elapsed (wall clock) time (h:mm:ss or m:ss): 2:56.84
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 1691680
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 14
Minor (reclaiming a frame) page faults: 654378
Voluntary context switches: 1737
Involuntary context switches: 20670
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 16384
Exit status: 0
lilithcarpenter@Liliths-MacBook-Pro tmp-rails % gtime -v irb
irb(main):001> gem 'creek', '2.5.3'
=>true
irb(main):002> require 'creek'
=>true
irb(main):003>time = Time.now; creek = Creek::Book.new('./performance-test.xlsx'); puts(Time.now - time);
6.498211
irb(main):004>time = Time.now; creek.sheets[0].simple_rows.map(&:inspect); puts(Time.now - time);
27.670476
irb(main):005>
Command being timed: "irb"
User time (seconds): 34.16
System time (seconds): 0.57
Percent of CPU this job got: 53%
Elapsed (wall clock) time (h:mm:ss or m:ss): 1:05.09
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 647200
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 15
Minor (reclaiming a frame) page faults: 44136
Voluntary context switches: 3867
Involuntary context switches: 1967
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 16384
Exit status: 0
changed the title [-][Docker ONLY] Large Memory and Speed performance regression from 2.5.3 to 2.6.3[/-][+]Large Memory and Speed performance regression from 2.5.3 to 2.6.3[/+]on Feb 21, 2024
The issue appears to be that calling node.namespaces (which looks for namespaces on this node, and all the way up the document hierarchy) is hugely expensive within nokogiri, rather than strings within creek - although they could be improved.
I've run a test using allocation_stats and mem GetProcessMem, reading 10k rows from a spreadsheet where I've observed this problem.
With the code as is results in a large number of hash allocations and lots of allocations within Nokogiri:
If I change the code so that we only do that call if the node appears to have a prefix then the tests still pass, but the memory allocations go right down...
if prefix.empty? && node.prefix && node.namespaces.any?
I've encountered the same issue and this commit was not enough to fix the issue in my case. I try to parse a 33MB xlsx file (500_000 rows) and it consumes 8GB. The culprit is the node.namespaces call on every row as @transoceanic2000 mentionned.
Indeed : if prefix.empty? && node.prefix && node.namespaces.any?
should work but from what I understand, nokogiri supply the node namespace_uri and prefix so we should be able to skip all node that are not from our namespace of interest and grab the prefix used the first time a node in our namespace is seen. I think it's a bit easier to understand and doesn't require the namespaces
e.g:
Nokogiri::XML::Reader.from_io(xml).each do |node|
next unless node.namespace_uri == SPREADSHEETML_URI
if prefix.empty? && node.prefix
prefix = node.prefix
name_row = "#{prefix}row"
name_c = "#{prefix}c"
name_v = "#{prefix}v"
name_t = "#{prefix}t"
end
I'll do some tests when times allow it and supply an MR unless someone has a better idea.
Thanks.
Activity
azrazalea-debtbook commentedon Feb 21, 2024
More system information:
Ruby 3.2.2
Docker arm64 alpine linux on Mac OS X host via Docker Desktop
azrazalea-debtbook commentedon Feb 21, 2024
Related to #121 but more specific and I was able to find some data I can show you to reproduce it!
[-][RAILS ONLY] Large Memory and Speed performance regression from 2.5.3 to 2.6.3[/-][+][Docker ONLY] Large Memory and Speed performance regression from 2.5.3 to 2.6.3[/+]azrazalea-debtbook commentedon Feb 21, 2024
Found that this is actually docker not rails. I spun up a fresh rails instance and wasn't producing it locally. Then, I used the rails default dockerfile modified to strip it back closer to essentials.
This is on arm64 architecture but I have the same problem in our production amd64 architecture docker images too.
And built it
docker build .Then ran docker
docker run -it <image sha>Finally inside docker I ran irb:
Inside irb I did:
In a separate shell to the same docker container (
docker exec -it <container SHA (NOT image sha> bash) I did:azrazalea-debtbook commentedon Feb 21, 2024
Note: I tried this in an amd_64 version of the same container and got the same result. 3GB RAM usage in 2.6.3, 800MB RAM in 2.5.3. Still on Mac OS X host though
azrazalea-debtbook commentedon Feb 21, 2024
Nevermind, I'm not used to Mac OS X and had trouble figuring out the peak memory usage.
Got
gtimeworking though and amd64 Mac OS X Sonoma 14.3.1 ruby 3.2.2 shows a similar though smaller effect.So this is not docker or rails specific, I just failed at figuring out peak memory usage on Mac and the speed didn't get way slower like it did in docker.
It is better locally at least. Only about 2.6x more memory not 5x.
[-][Docker ONLY] Large Memory and Speed performance regression from 2.5.3 to 2.6.3[/-][+]Large Memory and Speed performance regression from 2.5.3 to 2.6.3[/+]azrazalea-debtbook commentedon Feb 21, 2024
Identified commit 494ed05 as the culprit.
Version 2.6
Commit f8856ee
Commit 494ed05
transoceanic2000 commentedon Feb 18, 2025
The issue appears to be that calling
node.namespaces(which looks for namespaces on this node, and all the way up the document hierarchy) is hugely expensive within nokogiri, rather than strings within creek - although they could be improved.I've run a test using allocation_stats and mem GetProcessMem, reading 10k rows from a spreadsheet where I've observed this problem.
With the code as is results in a large number of hash allocations and lots of allocations within Nokogiri:
If I change the code so that we only do that call if the node appears to have a prefix then the tests still pass, but the memory allocations go right down...
if prefix.empty? && node.prefix && node.namespaces.any?I'm not sure that would work with all spreadsheets that caused this change to be added, but it narrows down the causes.
alx75 commentedon May 26, 2025
Hello,
I've encountered the same issue and this commit was not enough to fix the issue in my case. I try to parse a 33MB xlsx file (500_000 rows) and it consumes 8GB. The culprit is the
node.namespacescall on every row as @transoceanic2000 mentionned.Indeed :
if prefix.empty? && node.prefix && node.namespaces.any?should work but from what I understand, nokogiri supply the node
namespace_uriandprefixso we should be able to skip all node that are not from our namespace of interest and grab the prefix used the first time a node in our namespace is seen. I think it's a bit easier to understand and doesn't require thenamespacese.g:
I'll do some tests when times allow it and supply an MR unless someone has a better idea.
Thanks.
connorshea commentedon Sep 8, 2026
This should be fixed by #138 if it gets merged