xlsxrb 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +1 -1
  3. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
  4. data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
  5. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
  6. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
  7. data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +16 -0
  8. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +96 -0
  9. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
  10. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +68 -0
  11. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
  12. data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +4 -0
  13. data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +47 -0
  14. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
  15. data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +9 -0
  16. data/.gem_rbs_collection/csv/3.3/csv.rbs +3871 -0
  17. data/.gem_rbs_collection/csv/3.3/manifest.yaml +3 -0
  18. data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +9 -0
  19. data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +48 -0
  20. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
  21. data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
  22. data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
  23. data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +9 -0
  24. data/.gem_rbs_collection/logger/1.7/formatter.rbs +45 -0
  25. data/.gem_rbs_collection/logger/1.7/log_device.rbs +100 -0
  26. data/.gem_rbs_collection/logger/1.7/logger.rbs +796 -0
  27. data/.gem_rbs_collection/logger/1.7/manifest.yaml +2 -0
  28. data/.gem_rbs_collection/logger/1.7/period.rbs +17 -0
  29. data/.gem_rbs_collection/logger/1.7/severity.rbs +34 -0
  30. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
  31. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
  32. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
  33. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
  34. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
  35. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
  36. data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
  37. data/.gem_rbs_collection/parser/3.2/parser.rbs +194 -0
  38. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
  39. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
  40. data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
  41. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
  42. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
  43. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
  44. data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
  45. data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
  46. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
  47. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
  48. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
  49. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +208 -0
  50. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
  51. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +903 -0
  52. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
  53. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
  54. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
  55. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
  56. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
  57. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
  58. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
  59. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
  60. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
  61. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
  62. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
  63. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
  64. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
  65. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
  66. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
  67. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
  68. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
  69. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
  70. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
  71. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
  72. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
  73. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
  74. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
  75. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
  76. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
  77. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
  78. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
  79. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
  80. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
  81. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
  82. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
  83. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
  84. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
  85. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
  86. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
  87. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
  88. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
  89. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
  90. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
  91. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
  92. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
  93. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
  94. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
  95. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
  96. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
  97. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
  98. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
  99. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
  100. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
  101. data/CHANGELOG.md +42 -3
  102. data/README.md +130 -49
  103. data/Rakefile +23 -2
  104. data/Steepfile +17 -0
  105. data/benchmark.rb +324 -348
  106. data/docs/ARCHITECTURE.md +24 -0
  107. data/docs/DEVELOPMENT.md +58 -0
  108. data/docs/QUALITY_ASSURANCE.md +26 -0
  109. data/docs/visual/VisualGallery.md +893 -938
  110. data/docs/wasm/ruby.wasm +0 -0
  111. data/lib/xlsxrb/elements/cell.rb +113 -8
  112. data/lib/xlsxrb/elements/column.rb +2 -0
  113. data/lib/xlsxrb/elements/row.rb +52 -4
  114. data/lib/xlsxrb/elements/types.rb +6 -0
  115. data/lib/xlsxrb/elements/workbook.rb +29 -0
  116. data/lib/xlsxrb/elements/worksheet.rb +98 -1
  117. data/lib/xlsxrb/elements.rb +2 -0
  118. data/lib/xlsxrb/ooxml/reader.rb +8 -6
  119. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +106 -40
  120. data/lib/xlsxrb/ooxml/styles_parser.rb +2 -0
  121. data/lib/xlsxrb/ooxml/utils.rb +2 -0
  122. data/lib/xlsxrb/ooxml/workbook_parser.rb +6 -1
  123. data/lib/xlsxrb/ooxml/workbook_writer.rb +15 -4
  124. data/lib/xlsxrb/ooxml/worksheet_parser.rb +156 -23
  125. data/lib/xlsxrb/ooxml/worksheet_writer.rb +207 -152
  126. data/lib/xlsxrb/ooxml/writer.rb +217 -5
  127. data/lib/xlsxrb/ooxml/xml_builder.rb +18 -10
  128. data/lib/xlsxrb/ooxml/xml_parser.rb +2 -0
  129. data/lib/xlsxrb/ooxml/zip_generator.rb +5 -8
  130. data/lib/xlsxrb/ooxml/zip_reader.rb +179 -57
  131. data/lib/xlsxrb/ooxml/zip_writer.rb +65 -25
  132. data/lib/xlsxrb/ooxml.rb +3 -1
  133. data/lib/xlsxrb/style_builder.rb +284 -36
  134. data/lib/xlsxrb/version.rb +3 -1
  135. data/lib/xlsxrb.rb +1761 -275
  136. data/rbs_collection.lock.yaml +252 -0
  137. data/rbs_collection.yaml +19 -0
  138. data/sig/generated/xlsxrb/elements/cell.rbs +30 -0
  139. data/sig/generated/xlsxrb/elements/column.rbs +30 -0
  140. data/sig/generated/xlsxrb/elements/row.rbs +32 -0
  141. data/sig/generated/xlsxrb/elements/types.rbs +74 -0
  142. data/sig/generated/xlsxrb/elements/workbook.rbs +25 -0
  143. data/sig/generated/xlsxrb/elements/worksheet.rbs +27 -0
  144. data/sig/generated/xlsxrb/elements.rbs +8 -0
  145. data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
  146. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
  147. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
  148. data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
  149. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
  150. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
  151. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
  152. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
  153. data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
  154. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
  155. data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
  156. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
  157. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
  158. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
  159. data/sig/generated/xlsxrb/ooxml.rbs +23 -0
  160. data/sig/generated/xlsxrb/style_builder.rbs +250 -0
  161. data/sig/generated/xlsxrb/version.rbs +5 -0
  162. data/sig/generated/xlsxrb.rbs +1276 -0
  163. data/sig/rexml.rbs +4 -0
  164. metadata +145 -4
  165. data/measure_memory.rb +0 -42
  166. data/sig/xlsxrb.rbs +0 -23
data/benchmark.rb CHANGED
@@ -1,400 +1,376 @@
1
1
  # frozen_string_literal: true
2
+ # rubocop:disable all
2
3
 
3
- ENV["GEM_HOME"] = File.expand_path("tmp/vendor/bundle", __dir__)
4
- ENV["GEM_PATH"] = ENV.fetch("GEM_HOME", nil)
5
-
4
+ require "json"
5
+ require "open3"
6
+ require "fileutils"
6
7
  require "bundler/inline"
7
8
 
8
- gemfile do
9
+ puts "Ensuring benchmark peer ecosystem gems are available (bundler/inline)..."
10
+ gemfile(true) do
9
11
  source "https://rubygems.org"
10
- gem "benchmark", "0.5.0"
11
- gem "rexml", "3.4.4"
12
- gem "zlib", "3.2.3"
12
+ gem "caxlsx", "4.5.0"
13
13
  gem "xlsxtream", "3.1.0"
14
- gem "caxlsx", "4.4.2"
15
- gem "rubyXL", "3.4.35"
16
- gem "fast_excel", "0.5.0"
14
+ gem "fast_excel", "0.5.0", platform: :mri
15
+ gem "rubyXL", "3.4.38"
17
16
  gem "roo", "3.0.0"
18
17
  gem "creek", "2.6.3"
19
- gem "csv", "3.3.2"
20
- gem "xsv", "1.4.0"
21
- gem "base64", "0.2.0"
22
- gem "opentelemetry-sdk", "1.11.0"
18
+ gem "xsv", "1.4.1"
23
19
  end
24
20
 
25
- require "json"
26
- require "benchmark"
27
- require "fileutils"
28
- require "objspace"
29
- require_relative "lib/xlsxrb"
30
-
31
- args = ARGV.dup
32
- KEEP_FILES = args.delete("--keep-files")
33
-
34
- ROWS = (args[0] || 10_000).to_i
35
- COLS = 10
36
- ITERATIONS = 5
37
- READ_TEST_FILE = "tmp/benchmark_read_test.xlsx"
38
-
39
- OUTPUT_FILES = {
40
- "xlsxrb (Streaming)" => "tmp/write_xlsxrb_stream.xlsx",
41
- "xlsxrb (In-Memory)" => "tmp/write_xlsxrb_mem.xlsx",
42
- "caxlsx (In-Memory)" => "tmp/write_caxlsx.xlsx",
43
- "xlsxtream (Streaming)" => "tmp/write_xlsxtream.xlsx",
44
- "fast_excel (Streaming)" => "tmp/write_fast_excel.xlsx",
45
- "rubyXL (In-Memory)" => "tmp/write_rubyXL.xlsx",
46
- "Read source file" => READ_TEST_FILE
47
- }.freeze
48
-
49
- puts "Target: #{ROWS} rows x #{COLS} columns (#{ROWS * COLS} cells)"
50
- puts "Iterations: #{ITERATIONS}"
51
- puts "Keep generated files: #{KEEP_FILES ? "yes" : "no"}"
52
- puts "Preparing test file for read benchmarks..."
53
-
54
- # Prepare test file for read benchmarks
55
- Xlsxrb.generate(READ_TEST_FILE) do |w|
56
- w.add_sheet("Test") do |s|
57
- ROWS.times do |r|
58
- row_data = Array.new(COLS) { |c| (r * COLS) + c }
59
- s.add_row(row_data)
21
+ RUNS = (ENV["RUNS"] || "3").to_i
22
+ ROWS = (ARGV[0] || "100000").to_i
23
+ COLS = (ARGV[1] || "10").to_i
24
+
25
+ AVAILABLE_GEMS = {
26
+ "xlsxrb_stream" => true,
27
+ "xlsxrb_inmemory" => true,
28
+ "xlsxtream" => true,
29
+ "fast_excel" => true,
30
+ "caxlsx" => true,
31
+ "rubyXL" => true,
32
+ "creek" => true,
33
+ "roo" => true,
34
+ "xsv" => true
35
+ }
36
+
37
+ puts "=" * 80
38
+ puts "Benchmarking Excel Libraries (#{ROWS} rows x #{COLS} cols = #{ROWS * COLS} cells)"
39
+ puts "Runs per benchmark: #{RUNS} (Median reported, Mean calculated)"
40
+ puts "Ruby: #{RUBY_DESCRIPTION}"
41
+ puts "=" * 80
42
+
43
+ RUNNER_SCRIPT = <<~'RUBY'
44
+ require "json"
45
+ require "stringio"
46
+
47
+ def measure
48
+ gc_before = GC.stat[:count]
49
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
+
51
+ yield
52
+
53
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+ gc_after = GC.stat[:count]
55
+
56
+ # Peak memory in MB via /proc/self/status or getrusage
57
+ peak_mb = 0.0
58
+ if File.exist?("/proc/self/status")
59
+ status = File.read("/proc/self/status")
60
+ if status =~ /VmHWM:\s+(\d+)\s+kB/i
61
+ peak_mb = $1.to_f / 1024.0
62
+ elsif status =~ /VmRSS:\s+(\d+)\s+kB/i
63
+ peak_mb = $1.to_f / 1024.0
64
+ end
60
65
  end
66
+
67
+ {
68
+ time: (t1 - t0),
69
+ peak_memory_mb: peak_mb,
70
+ gc_count: (gc_after - gc_before)
71
+ }
61
72
  end
62
- end
63
73
 
64
- def run_in_subprocess(_name, &block)
65
- File.write("tmp/bench_task.rb", <<~RUBY)
66
- ENV['BUNDLE_GEMFILE'] = ''
67
- ENV['BUNDLE_IGNORE_CONFIG'] = '1'
68
- ENV['GEM_HOME'] = File.expand_path('tmp/vendor/bundle', __dir__)
69
- ENV['GEM_PATH'] = ENV['GEM_HOME']
70
-
71
- require 'bundler/inline'
72
- gemfile do
73
- source 'https://rubygems.org'
74
- gem 'benchmark', '0.5.0'
75
- gem 'rexml', '3.4.4'
76
- gem 'zlib', '3.2.3'
77
- gem 'xlsxtream', '3.1.0'
78
- gem 'caxlsx', '4.4.2'
79
- gem 'rubyXL', '3.4.35'
80
- gem 'fast_excel', '0.5.0'
81
- gem 'roo', '3.0.0'
82
- gem 'creek', '2.6.3'
83
- gem 'csv', '3.3.2'
84
- gem 'xsv', '1.4.0'
85
- gem 'base64', '0.2.0'
86
- gem 'opentelemetry-sdk', '1.11.0'
74
+ def generate_row(r, cols)
75
+ base = [r + 1, "User #{r + 1}", 123.45, true, "Active", (r + 1) * 10, "Tokyo", 99.9, false, "Item #{r % 50}"]
76
+ if cols <= base.size
77
+ base.first(cols)
78
+ else
79
+ base + Array.new(cols - base.size) { |c| "col_#{c}_#{r}" }
87
80
  end
88
- require 'csv'
89
- require 'base64'
90
- require 'json'
91
- require 'benchmark'
92
- require 'opentelemetry/sdk'
93
- require_relative '../lib/xlsxrb'
94
-
95
- ROWS = #{ROWS}
96
- COLS = 10
97
- READ_TEST_FILE = "tmp/benchmark_read_test.xlsx"
98
-
99
- def get_io_stats
100
- return { rchar: 0, wchar: 0, read_bytes: 0, write_bytes: 0 } unless File.exist?('/proc/self/io')
81
+ end
101
82
 
102
- stats = {}
103
- File.readlines('/proc/self/io').each do |line|
104
- key, value = line.split(':')
105
- stats[key.strip.to_sym] = value.to_i if key && value
83
+ lib = ARGV[0]
84
+ mode = ARGV[1] # "write" or "read"
85
+ rows = ARGV[2].to_i
86
+ cols = ARGV[3].to_i
87
+ filename = ARGV[4]
88
+
89
+ result = case [lib, mode]
90
+ when ["xlsxtream", "write"]
91
+ require "xlsxtream"
92
+ measure do
93
+ Xlsxtream::Workbook.open(filename) do |wb|
94
+ wb.write_worksheet("Data") do |sheet|
95
+ rows.times do |r|
96
+ sheet << generate_row(r, cols)
97
+ end
98
+ end
106
99
  end
107
- stats
108
100
  end
109
-
110
- class BenchmarkSpanProcessor < OpenTelemetry::SDK::Trace::SpanProcessor
111
- def on_start(span, _parent_context)
112
- return unless span.name == 'benchmark_task'
113
- #{" "}
114
- span.set_attribute('bench.gc.count.start', GC.stat[:count])
115
- span.set_attribute('bench.alloc.objects.start', GC.stat[:total_allocated_objects])
116
- span.set_attribute('bench.time.start', Process.clock_gettime(Process::CLOCK_MONOTONIC))
117
- span.set_attribute('bench.cpu.utime.start', Process.times.utime)
118
- span.set_attribute('bench.cpu.stime.start', Process.times.stime)
119
- @io_start = get_io_stats
120
- #{" "}
121
- @max_mem_mb = 0.0
122
- @watcher = Thread.new do
123
- loop do
124
- mem = `ps -o rss= -p \#{Process.pid}`.to_f / 1024.0
125
- @max_mem_mb = mem if mem > @max_mem_mb
126
- sleep 0.05
101
+ when ["fast_excel", "write"]
102
+ require "fast_excel"
103
+ measure do
104
+ wb = FastExcel.open(filename)
105
+ sheet = wb.add_worksheet("Data")
106
+ rows.times do |r|
107
+ sheet.append_row(generate_row(r, cols))
108
+ end
109
+ wb.close
110
+ end
111
+ when ["caxlsx", "write"]
112
+ require "caxlsx"
113
+ measure do
114
+ p = Axlsx::Package.new
115
+ wb = p.workbook
116
+ wb.add_worksheet(name: "Data") do |sheet|
117
+ rows.times do |r|
118
+ sheet.add_row(generate_row(r, cols))
119
+ end
120
+ end
121
+ p.serialize(filename)
122
+ end
123
+ when ["rubyXL", "write"]
124
+ require "rubyXL"
125
+ measure do
126
+ wb = RubyXL::Workbook.new
127
+ sheet = wb[0]
128
+ sheet.sheet_name = "Data"
129
+ rows.times do |r|
130
+ row_data = generate_row(r, cols)
131
+ row_data.each_with_index do |val, c|
132
+ sheet.add_cell(r, c, val)
133
+ end
134
+ end
135
+ wb.write(filename)
136
+ end
137
+ when ["xlsxrb_stream", "write"]
138
+ require_relative "lib/xlsxrb"
139
+ measure do
140
+ Xlsxrb.generate(filename) do |wb|
141
+ wb.sheet("Data") do |sheet|
142
+ rows.times do |r|
143
+ sheet.row(generate_row(r, cols))
127
144
  end
128
145
  end
129
146
  end
130
-
131
- def on_finish(span)
132
- return unless span.name == 'benchmark_task'
133
- #{" "}
134
- @watcher.kill
135
- mem = `ps -o rss= -p \#{Process.pid}`.to_f / 1024.0
136
- @max_mem_mb = mem if mem > @max_mem_mb
137
-
138
- gc_start = span.attributes['bench.gc.count.start']
139
- alloc_start = span.attributes['bench.alloc.objects.start']
140
- time_start = span.attributes['bench.time.start']
141
- utime_start = span.attributes['bench.cpu.utime.start']
142
- stime_start = span.attributes['bench.cpu.stime.start']
143
- io_end = get_io_stats
144
- #{" "}
145
- gc_diff = GC.stat[:count] - gc_start
146
- alloc_diff = GC.stat[:total_allocated_objects] - alloc_start
147
-
148
- written_bytes = io_end[:wchar] - @io_start[:wchar]
149
- read_bytes = io_end[:rchar] - @io_start[:rchar]
150
-
151
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
152
- end_times = Process.times
153
-
154
- elapsed = end_time - time_start
155
- cpu_time = (end_times.utime - utime_start) + (end_times.stime - stime_start)
156
- cpu_percent = elapsed > 0 ? (cpu_time / elapsed) * 100 : 0.0
157
-
158
- puts JSON.dump({
159
- time: elapsed,
160
- cpu: cpu_percent,
161
- memory: @max_mem_mb,
162
- gc_count: gc_diff,
163
- alloc_objects: alloc_diff,
164
- rchar: read_bytes,
165
- wchar: written_bytes
166
- })
147
+ end
148
+ when ["xlsxrb_inmemory", "write"]
149
+ require_relative "lib/xlsxrb"
150
+ measure do
151
+ wb = Xlsxrb.build do |b|
152
+ b.sheet("Data") do |s|
153
+ rows.times do |r|
154
+ s.row(generate_row(r, cols))
155
+ end
156
+ end
167
157
  end
158
+ Xlsxrb.write(filename, wb)
168
159
  end
169
-
170
- OpenTelemetry::SDK.configure do |c|
171
- c.add_span_processor(BenchmarkSpanProcessor.new)
160
+ when ["xlsxrb_stream", "read"]
161
+ require_relative "lib/xlsxrb"
162
+ measure do
163
+ count = 0
164
+ Xlsxrb.foreach(filename) do |sheet|
165
+ sheet.each do |row|
166
+ row.cells.each do |cell|
167
+ _val = cell.value
168
+ count += 1
169
+ end
170
+ end
171
+ end
172
172
  end
173
-
174
- tracer = OpenTelemetry.tracer_provider.tracer('benchmark')
175
- GC.start
176
-
177
- tracer.in_span('benchmark_task') do
178
- # --- TASK START ---
179
- #{block.call}
180
- # --- TASK END ---
173
+ when ["xlsxrb_inmemory", "read"]
174
+ require_relative "lib/xlsxrb"
175
+ measure do
176
+ wb = Xlsxrb.read(filename)
177
+ count = 0
178
+ wb.sheets.each do |sheet|
179
+ sheet.rows.each do |row|
180
+ row.cells.each do |cell|
181
+ _val = cell.value
182
+ count += 1
183
+ end
184
+ end
185
+ end
181
186
  end
182
- RUBY
183
-
184
- output = Bundler.with_unbundled_env do
185
- `ruby tmp/bench_task.rb 2>/dev/null`
186
- end
187
- begin
188
- JSON.parse(output, symbolize_names: true)
189
- rescue StandardError => e
190
- puts "Error: #{e.message} with output: #{output.inspect}"
191
- nil
192
- end
193
- end
194
-
195
- def median(array)
196
- return 0.0 if array.empty?
197
-
198
- sorted = array.sort
199
- len = sorted.length
200
- (sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
201
- end
202
-
203
- def run_benchmark(name, snippet)
204
- print format("%-25s", name)
205
- results = ITERATIONS.times.map do
206
- print "."
207
- run_in_subprocess(name) { snippet }
208
- end
209
- puts
210
-
211
- valid_results = results.compact
212
-
213
- median_time = median(valid_results.map { |r| r[:time] }.compact)
214
- median_cpu = median(valid_results.map { |r| r[:cpu] }.compact)
215
- median_mem = median(valid_results.map { |r| r[:memory] }.compact)
216
- median_gc = median(valid_results.map { |r| r[:gc_count] }.compact)
217
- median_alloc = median(valid_results.map { |r| r[:alloc_objects] }.compact)
218
- median_wchar = median(valid_results.map { |r| r[:wchar] }.compact)
219
- median_rchar = median(valid_results.map { |r| r[:rchar] }.compact)
220
-
221
- {
222
- name: name,
223
- time: median_time,
224
- cpu: median_cpu,
225
- memory: median_mem,
226
- gc_count: median_gc,
227
- alloc_m: median_alloc / 1_000_000.0,
228
- wchar_mb: median_wchar / 1_048_576.0,
229
- rchar_mb: median_rchar / 1_048_576.0
230
- }
231
- end
232
-
233
- def format_row(result)
234
- format("| %-25<name>s | %8.2<time>f s | %9.1<memory>f MB | %8.1<gc_count>f |", result)
235
- end
236
-
237
- puts "\n--- Write Benchmarks ---"
238
- write_results = []
239
-
240
- write_results << run_benchmark("xlsxrb (Streaming)", <<~RUBY
241
- Xlsxrb.generate("tmp/write_xlsxrb_stream.xlsx") do |writer|
242
- writer.add_sheet("Sheet1") do |sheet|
243
- ROWS.times do |r|
244
- row_data = Array.new(COLS) { |c| r * COLS + c }
245
- sheet.add_row(row_data)
187
+ when ["creek", "read"]
188
+ require "creek"
189
+ measure do
190
+ creek = Creek::Book.new(filename)
191
+ count = 0
192
+ creek.sheets.each do |sheet|
193
+ sheet.rows.each do |row|
194
+ row.each_value do |_val|
195
+ count += 1
196
+ end
197
+ end
246
198
  end
247
199
  end
248
- end
249
- RUBY
250
- )
251
-
252
- write_results << run_benchmark("xlsxrb (In-Memory)", <<~RUBY
253
- workbook = Xlsxrb.build do |w|
254
- w.add_sheet("Sheet1") do |sheet|
255
- ROWS.times do |r|
256
- row_data = Array.new(COLS) { |c| r * COLS + c }
257
- sheet.add_row(row_data)
200
+ when ["roo", "read"]
201
+ require "roo"
202
+ measure do
203
+ xlsx = Roo::Excelx.new(filename)
204
+ count = 0
205
+ xlsx.each_row_streaming do |row|
206
+ row.each do |cell|
207
+ _val = cell&.value
208
+ count += 1
209
+ end
258
210
  end
259
211
  end
260
- end
261
- Xlsxrb.write("tmp/write_xlsxrb_mem.xlsx", workbook)
262
- RUBY
263
- )
264
-
265
- write_results << run_benchmark("caxlsx (In-Memory)", <<~RUBY
266
- begin
267
- p = Axlsx::Package.new
268
- sheet = p.workbook.add_worksheet(name: "Sheet1")
269
- ROWS.times do |r|
270
- sheet.add_row(Array.new(COLS) { |c| r * COLS + c })
212
+ when ["xsv", "read"]
213
+ require "xsv"
214
+ measure do
215
+ x = Xsv.open(filename)
216
+ count = 0
217
+ x.sheets.each do |sheet|
218
+ sheet.each do |row|
219
+ row.each do |_val|
220
+ count += 1
221
+ end
222
+ end
223
+ end
271
224
  end
272
- p.serialize("tmp/write_caxlsx.xlsx")
273
- rescue Zlib::BufError => e
274
- File.write("tmp/write_caxlsx.xlsx", "")
275
- end
276
- RUBY
277
- )
278
-
279
- write_results << run_benchmark("xlsxtream (Streaming)", <<~RUBY
280
- begin
281
- Xlsxtream::Workbook.open("tmp/write_xlsxtream.xlsx") do |workbook|
282
- workbook.write_worksheet("Sheet1") do |sheet|
283
- ROWS.times do |r|
284
- sheet << Array.new(COLS) { |c| r * COLS + c }
225
+ when ["rubyXL", "read"]
226
+ require "rubyXL"
227
+ measure do
228
+ wb = RubyXL::Parser.parse(filename)
229
+ count = 0
230
+ wb.worksheets.each do |sheet|
231
+ sheet.each do |row|
232
+ next unless row
233
+ row.cells.each do |cell|
234
+ _val = cell&.value
235
+ count += 1
236
+ end
285
237
  end
286
238
  end
287
239
  end
288
- rescue Zlib::BufError => e
289
- File.write("tmp/write_xlsxtream.xlsx", "")
240
+ else
241
+ raise "Unknown benchmark target: #{lib} #{mode}"
290
242
  end
291
- RUBY
292
- )
293
-
294
- write_results << run_benchmark("fast_excel (Streaming)", <<~RUBY
295
- File.delete("tmp/write_fast_excel.xlsx") if File.exist?("tmp/write_fast_excel.xlsx")
296
- workbook = FastExcel.open("tmp/write_fast_excel.xlsx", constant_memory: true)
297
- sheet = workbook.add_worksheet("Sheet1")
298
- ROWS.times do |r|
299
- sheet.append_row(Array.new(COLS) { |c| r * COLS + c })
300
- end
301
- workbook.close
302
- RUBY
303
- )
304
243
 
305
- write_results << run_benchmark("rubyXL (In-Memory)", <<~RUBY
306
- workbook = RubyXL::Workbook.new
307
- sheet = workbook.worksheets[0]
308
- ROWS.times do |r|
309
- COLS.times do |c|
310
- sheet.add_cell(r, c, r * COLS + c)
311
- end
312
- end
313
- workbook.write("tmp/write_rubyXL.xlsx")
244
+ puts result.to_json
314
245
  RUBY
315
- )
316
246
 
317
- puts "\n--- Read Benchmarks ---"
318
- read_results = []
247
+ runner_file = "benchmark_runner.rb"
248
+ File.write(runner_file, RUNNER_SCRIPT)
319
249
 
320
- read_results << run_benchmark("xlsxrb (Streaming)", <<~RUBY
321
- count = 0
322
- Xlsxrb.foreach(READ_TEST_FILE) do |row|
323
- count += row.cells.size
250
+ def run_isolated(lib, mode, rows, cols, filename)
251
+ cmd = ["ruby", "-Ilib", "benchmark_runner.rb", lib, mode, rows.to_s, cols.to_s, filename]
252
+ stdout, stderr, status = Bundler.with_unbundled_env do
253
+ Open3.capture3(*cmd)
324
254
  end
325
- RUBY
326
- )
327
-
328
- read_results << run_benchmark("xlsxrb (In-Memory)", <<~RUBY
329
- workbook = Xlsxrb.read(READ_TEST_FILE)
330
- count = 0
331
- workbook.sheets[0].rows.each do |row|
332
- count += row.cells.size
255
+ unless status.success?
256
+ warn "Failed to run #{lib} #{mode}: #{stderr}"
257
+ return nil
333
258
  end
334
- RUBY
335
- )
259
+ JSON.parse(stdout.strip, symbolize_names: true)
260
+ end
336
261
 
337
- read_results << run_benchmark("creek (Streaming)", <<~RUBY
338
- creek = Creek::Book.new(READ_TEST_FILE)
339
- sheet = creek.sheets[0]
340
- count = 0
341
- sheet.simple_rows.each do |row|
342
- count += row.size
262
+ def run_benchmark_series(name, lib, mode, rows, cols, filename, runs)
263
+ unless AVAILABLE_GEMS[lib]
264
+ puts "Skipping #{name} (gem not installed)"
265
+ return nil
343
266
  end
344
- RUBY
345
- )
346
267
 
347
- read_results << run_benchmark("roo (Streaming)", <<~RUBY
348
- xlsx = Roo::Excelx.new(READ_TEST_FILE)
349
- count = 0
350
- xlsx.each_row_streaming do |row|
351
- count += row.size
268
+ print "Running #{name} (#{runs} runs)... "
269
+ $stdout.flush
270
+ results = []
271
+ runs.times do |_i|
272
+ File.delete(filename) if File.exist?(filename) && mode == "write"
273
+ res = run_isolated(lib, mode, rows, cols, filename)
274
+ if res
275
+ results << res
276
+ print "#{res[:time].round(2)}s "
277
+ $stdout.flush
278
+ else
279
+ print "ERR "
280
+ $stdout.flush
281
+ end
352
282
  end
353
- RUBY
354
- )
283
+ puts
355
284
 
356
- read_results << run_benchmark("rubyXL (In-Memory)", <<~RUBY
357
- workbook = RubyXL::Parser.parse(READ_TEST_FILE)
358
- count = 0
359
- workbook.worksheets[0].each do |row|
360
- count += row.cells.size if row
361
- end
362
- RUBY
363
- )
285
+ return nil if results.empty?
364
286
 
365
- read_results << run_benchmark("xsv (Streaming)", <<~RUBY
366
- workbook = Xsv.open(READ_TEST_FILE)
367
- count = 0
368
- workbook[0].each do |row|
369
- count += row.size
370
- end
371
- RUBY
372
- )
287
+ times = results.map { |r| r[:time] }.sort
288
+ mems = results.map { |r| r[:peak_memory_mb] }.sort
289
+ gcs = results.map { |r| r[:gc_count] }.sort
373
290
 
374
- # Print markdown tables
375
- puts "\n\n### Write Performance (#{ROWS * COLS} cells)"
376
- puts "| Library | Time | Peak Memory | GC Count |"
377
- puts "|---------------------------|------------|--------------|----------|"
378
- write_results.sort_by { |r| r[:time] }.each { |r| puts format_row(r) }
291
+ median_time = times[times.size / 2]
292
+ mean_time = times.sum / times.size
293
+ median_mem = mems[mems.size / 2]
294
+ median_gc = gcs[gcs.size / 2]
379
295
 
380
- puts "\n### Read Performance (#{ROWS * COLS} cells)"
381
- puts "| Library | Time | Peak Memory | GC Count |"
382
- puts "|---------------------------|------------|--------------|----------|"
383
- read_results.sort_by { |r| r[:time] }.each { |r| puts format_row(r) }
296
+ {
297
+ name: name,
298
+ median_time: median_time,
299
+ mean_time: mean_time,
300
+ median_mem: median_mem,
301
+ median_gc: median_gc
302
+ }
303
+ end
304
+
305
+ # 1. Generate a standard reference file for reading benchmarks
306
+ ref_file = "bench_reference_data.xlsx"
307
+ puts "\n[Setup] Generating reference file (#{ROWS} x #{COLS}) for read benchmarks..."
308
+ run_isolated("xlsxrb_stream", "write", ROWS, COLS, ref_file)
309
+
310
+ # 2. Benchmark Write
311
+ puts "\n=== Benchmarking Write Performance ==="
312
+ write_targets = [
313
+ ["xlsxtream 3.1.0", "xlsxtream", "Streaming", "Inline String"],
314
+ ["xlsxrb (Streaming)", "xlsxrb_stream", "Streaming", "SST (Shared)"],
315
+ ["fast_excel 0.5.0 (C)", "fast_excel", "Streaming", "SST (Shared)"],
316
+ ["caxlsx 4.5.0", "caxlsx", "In-Memory", "Inline String"],
317
+ ["xlsxrb (In-Memory)", "xlsxrb_inmemory", "In-Memory", "SST (Shared)"],
318
+ ["rubyXL 3.4.38", "rubyXL", "In-Memory", "Inline String"]
319
+ ]
320
+
321
+ write_results = []
322
+ write_targets.each do |name, lib, model, storage|
323
+ target_file = "bench_write_#{lib}.xlsx"
324
+ res = run_benchmark_series(name, lib, "write", ROWS, COLS, target_file, RUNS)
325
+ if res
326
+ res[:model] = model
327
+ res[:storage] = storage
328
+ write_results << res
329
+ end
330
+ FileUtils.rm_f(target_file)
331
+ end
384
332
 
385
- puts "\n### Generated Files"
386
- puts "| Producer | File path | Status |"
387
- puts "|---------------------------|-------------------------------|----------|"
388
- OUTPUT_FILES.each do |name, path|
389
- status = File.exist?(path) ? "exists" : "missing"
390
- puts format("| %-25<name>s | %-29<path>s | %-8<status>s |", name: name, path: path, status: status)
333
+ # 3. Benchmark Read
334
+ puts "\n=== Benchmarking Read Performance ==="
335
+ read_targets = [
336
+ ["xlsxrb (Streaming)", "xlsxrb_stream", "Streaming"],
337
+ ["xlsxrb (In-Memory)", "xlsxrb_inmemory", "In-Memory"],
338
+ ["creek 2.6.3", "creek", "Streaming"],
339
+ ["roo 3.0.0", "roo", "Streaming"],
340
+ ["xsv 1.4.1", "xsv", "Streaming"],
341
+ ["rubyXL 3.4.38", "rubyXL", "In-Memory"]
342
+ ]
343
+
344
+ read_results = []
345
+ read_targets.each do |name, lib, model|
346
+ res = run_benchmark_series(name, lib, "read", ROWS, COLS, ref_file, RUNS)
347
+ if res
348
+ res[:model] = model
349
+ read_results << res
350
+ end
391
351
  end
392
352
 
393
353
  # Cleanup
394
- if KEEP_FILES
395
- puts "\nKeeping generated files because --keep-files was specified."
396
- else
397
- Dir.glob("tmp/write_*.xlsx").each { |f| FileUtils.rm(f) }
398
- FileUtils.rm(READ_TEST_FILE)
354
+ FileUtils.rm_f(ref_file)
355
+ FileUtils.rm_f(runner_file)
356
+
357
+ # Print Tables
358
+ puts "\n" + ("=" * 80)
359
+ puts "### Write Performance (#{ROWS * COLS} cells: #{ROWS} rows x #{COLS} cols)"
360
+ puts ""
361
+ puts "| Library | Model | Write String Storage | Time (Median) | Time (Mean) | Peak Memory | GC Count |"
362
+ puts "| ---------------------- | ----------- | -------------------- | ------------- | ----------- | ----------- | -------- |"
363
+ write_results.sort_by { |r| r[:median_time] }.each do |r|
364
+ printf "| %-22s | %-11s | %-20s | %6.2f s | %6.2f s | %7.1f MB | %6.1f |\n",
365
+ r[:name], r[:model], r[:storage], r[:median_time], r[:mean_time], r[:median_mem], r[:median_gc]
366
+ end
367
+
368
+ puts "\n### Read Performance (#{ROWS * COLS} cells: #{ROWS} rows x #{COLS} cols)"
369
+ puts ""
370
+ puts "| Library | Model | Time (Median) | Time (Mean) | Peak Memory | GC Count |"
371
+ puts "| ---------------------- | ----------- | ------------- | ----------- | ----------- | -------- |"
372
+ read_results.sort_by { |r| r[:median_time] }.each do |r|
373
+ printf "| %-22s | %-11s | %6.2f s | %6.2f s | %7.1f MB | %6.1f |\n",
374
+ r[:name], r[:model], r[:median_time], r[:mean_time], r[:median_mem], r[:median_gc]
399
375
  end
400
- FileUtils.rm_f("tmp/bench_task.rb")
376
+ puts "=" * 80