Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Improve Performance with Nested CDATA #243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Masamuneee opened this issue Feb 26, 2025 · 1 comment · Fixed by #244
Closed

Improve Performance with Nested CDATA #243

Masamuneee opened this issue Feb 26, 2025 · 1 comment · Fixed by #244

Comments

@Masamuneee
Copy link

I think we should improve the implementation to handle nested CDATA more efficiently.

require "rexml/document"

include REXML  

(1..1000000000).step(10000000) do |depth|
  puts "Trying depth #{depth}"
  string = "<?xml version=\"1.0\"?>\n" +
           "<root>Test</root>\n" +
           "<!" + "[CDATA[" * depth + "]]>\n"
  
  start = Time.now
  begin
    doc = Document.new(string)
  rescue Exception => e
    puts "Error at depth #{depth}: #{e}"
    break
  end
  elapsed_time = Time.now - start
  puts "Elapsed time: #{elapsed_time} seconds"
end

Result:

Elapsed time: 24.994068928 seconds
Trying depth 200000001
Elapsed time: 27.070803532 seconds
Trying depth 210000001
Elapsed time: 27.870487966 seconds
Trying depth 220000001
Elapsed time: 28.787843908 seconds
Trying depth 230000001
[1]    4336 killed     ruby test.rb
naitoh added a commit to naitoh/rexml that referenced this issue Mar 1, 2025
GitHub: fix rubyGH-243

## Benchmark 1
```
$ benchmark-driver benchmark/parse_cdata.yaml
Calculating -------------------------------------
                         before       after  before(YJIT)  after(YJIT)
                 dom     99.008     192.055        96.030      177.653 i/s -     100.000 times in 1.010020s 0.520685s 1.041338s 0.562894s
                 sax    101.858     205.223        99.488      194.657 i/s -     100.000 times in 0.981757s 0.487274s 1.005148s 0.513723s
                pull     99.614     205.030       100.284      196.212 i/s -     100.000 times in 1.003879s 0.487733s 0.997173s 0.509654s
              stream    102.506     205.073       100.324      195.893 i/s -     100.000 times in 0.975549s 0.487632s 0.996772s 0.510483s

Comparison:
                              dom
               after:       192.1 i/s
         after(YJIT):       177.7 i/s - 1.08x  slower
              before:        99.0 i/s - 1.94x  slower
        before(YJIT):        96.0 i/s - 2.00x  slower

                              sax
               after:       205.2 i/s
         after(YJIT):       194.7 i/s - 1.05x  slower
              before:       101.9 i/s - 2.01x  slower
        before(YJIT):        99.5 i/s - 2.06x  slower

                             pull
               after:       205.0 i/s
         after(YJIT):       196.2 i/s - 1.04x  slower
        before(YJIT):       100.3 i/s - 2.04x  slower
              before:        99.6 i/s - 2.06x  slower

                           stream
               after:       205.1 i/s
         after(YJIT):       195.9 i/s - 1.05x  slower
              before:       102.5 i/s - 2.00x  slower
        before(YJIT):       100.3 i/s - 2.04x  slower
```
- YJIT=ON : 1.84x - 1.95x faster
- YJIT=OFF : 1.94x - 2.04x faster

### Benchmark Code
- benchmark/parse_cdata.yaml
```ruby
loop_count: 100
contexts:
  - name: before
    gems:
      rexml: 3.4.1
    require: false
    prelude: require 'rexml'
  - name: after
    prelude: |
      $LOAD_PATH.unshift(File.expand_path("lib"))
      require 'rexml'
  - name: before(YJIT)
    gems:
      rexml: 3.4.1
    require: false
    prelude: |
      require 'rexml'
      RubyVM::YJIT.enable
  - name: after(YJIT)
    prelude: |
      $LOAD_PATH.unshift(File.expand_path("lib"))
      require 'rexml'
      RubyVM::YJIT.enable

prelude: |
  require 'rexml/document'
  require 'rexml/parsers/sax2parser'
  require 'rexml/parsers/pullparser'
  require 'rexml/parsers/streamparser'
  require 'rexml/streamlistener'

  def build_xml(n_depth)
    xml = "<?xml version=\"1.0\"?>\n" +
           "<root>Test</root>\n" +
           "<!" + "[CDATA[" * n_depth + "]]>\n"
  end
  xml = build_xml(100000)

  class Listener
    include REXML::StreamListener
  end

benchmark:
  'dom'        : REXML::Document.new(xml)
  'sax'        : REXML::Parsers::SAX2Parser.new(xml).parse
  'pull'       : |
    parser = REXML::Parsers::PullParser.new(xml)
    while parser.has_next?
      parser.pull
    end
  'stream'     : REXML::Parsers::StreamParser.new(xml, Listener.new).parse
```

## Benchmark 2

- depth 140000001 : 1.91x faster

- befofe
```
$ ruby nested_cdata.rb
Trying depth 1
Elapsed time: 0.000148 seconds
Trying depth 10000001
Elapsed time: 1.069345 seconds
Trying depth 20000001
Elapsed time: 2.08041 seconds
Trying depth 30000001
Elapsed time: 3.120498 seconds
Trying depth 40000001
Elapsed time: 4.169959 seconds
Trying depth 50000001
Elapsed time: 5.220279 seconds
Trying depth 60000001
Elapsed time: 6.343796 seconds
Trying depth 70000001
Elapsed time: 7.336923 seconds
Trying depth 80000001
Elapsed time: 8.357979 seconds
Trying depth 90000001
Elapsed time: 9.438741 seconds
Trying depth 100000001
Elapsed time: 10.564045 seconds
Trying depth 110000001
Elapsed time: 11.505168 seconds
Trying depth 120000001
Elapsed time: 12.696258 seconds
Trying depth 130000001
Elapsed time: 13.81369 seconds
Trying depth 140000001
Elapsed time: 14.772476 seconds
Trying depth 150000001
^CError at depth 150000001:
```

- after
```
$ ruby nested_cdata.rb
Trying depth 1
Elapsed time: 0.000135 seconds
Trying depth 10000001
Elapsed time: 0.546576 seconds
Trying depth 20000001
Elapsed time: 1.110255 seconds
Trying depth 30000001
Elapsed time: 1.592627 seconds
Trying depth 40000001
Elapsed time: 2.09499 seconds
Trying depth 50000001
Elapsed time: 2.700936 seconds
Trying depth 60000001
Elapsed time: 3.150459 seconds
Trying depth 70000001
Elapsed time: 3.665467 seconds
Trying depth 80000001
Elapsed time: 4.168667 seconds
Trying depth 90000001
Elapsed time: 4.999614 seconds
Trying depth 100000001
Elapsed time: 5.582072 seconds
Trying depth 110000001
Elapsed time: 5.971595 seconds
Trying depth 120000001
Elapsed time: 6.423684 seconds
Trying depth 130000001
Elapsed time: 7.108247 seconds
Trying depth 140000001
Elapsed time: 7.714127 seconds
Trying depth 150000001
^CError at depth 150000001:
```

### Benchmark Code
- nested_cdata.rb
```ruby
require "rexml/document"

include REXML

(1..1000000000).step(10000000) do |depth|
  puts "Trying depth #{depth}"
  string = "<?xml version=\"1.0\"?>\n" +
           "<root>Test</root>\n" +
           "<!" + "[CDATA[" * depth + "]]>\n"

  start = Time.now
  begin
    doc = Document.new(string)
  rescue Exception => e
    puts "Error at depth #{depth}: #{e}"
    break
  end
  elapsed_time = Time.now - start
  puts "Elapsed time: #{elapsed_time} seconds"
end
```
@tompng
Copy link
Member

tompng commented Mar 1, 2025

I don't think it is a problem. Large data takes time. Not related to nested cdata.

size = 1000000
nested_cdata = "<?xml version=\"1.0\"?><root>Test</root><!" + "[CDATA[" * size + "]]>"
normal_cdata = "<?xml version=\"1.0\"?><root>Test</root><![CDATA[" + 'aaaaaaa' * size + "]]>"
normal_text = "<?xml version=\"1.0\"?><root>Test" + 'aaaaaaa' * size + "</root>"
normal_tags = "<?xml version=\"1.0\"?><root>Test" + '<b></b>' * size + "</root>"

# Size are almost the same.
[nested_cdata, normal_cdata, normal_text, normal_tags].map(&:size)
# => [7000043, 7000050, 7000038, 7000038]

REXML::Document.new(nested_cdata);
# processing time: 0.130978s
REXML::Document.new(normal_cdata);
# processing time: 0.109913s
REXML::Document.new(normal_text);
# processing time: 0.144391s
REXML::Document.new(normal_tags);
# processing time: 5.863289s

The slowness is just caused by the input size. Time consumption of parsing nested cdata, non-nested normal cdata, normal text node is almost the same.

Trying depth 220000001
Elapsed time: 28.787843908 seconds
# depth=1000000 takes 0.13s
# estimated time 220000001/1000000 * 0.13 => 28.6

naitoh added a commit to naitoh/rexml that referenced this issue Mar 2, 2025
## Why?

See: ruby#243

## Benchmark (Comparison with rexml 3.4.1)
```
$ benchmark-driver benchmark/parse_cdata.yaml
Calculating -------------------------------------
                     rexml 3.4.1      master  3.4.1(YJIT)  master(YJIT)
                 dom     648.361      1.178k      591.590        1.046k i/s -     100.000 times in 0.154235s 0.084913s 0.169036s 0.095627s
                 sax     699.061      1.378k      651.148        1.196k i/s -     100.000 times in 0.143049s 0.072549s 0.153575s 0.083611s
                pull     699.271      1.379k      660.275        1.210k i/s -     100.000 times in 0.143006s 0.072527s 0.151452s 0.082622s
              stream     701.725      1.383k      659.483        1.228k i/s -     100.000 times in 0.142506s 0.072307s 0.151634s 0.081455s

Comparison:
                              dom
              master:      1177.7 i/s
        master(YJIT):      1045.7 i/s - 1.13x  slower
         rexml 3.4.1:       648.4 i/s - 1.82x  slower
         3.4.1(YJIT):       591.6 i/s - 1.99x  slower

                              sax
              master:      1378.4 i/s
        master(YJIT):      1196.0 i/s - 1.15x  slower
         rexml 3.4.1:       699.1 i/s - 1.97x  slower
         3.4.1(YJIT):       651.1 i/s - 2.12x  slower

                             pull
              master:      1378.8 i/s
        master(YJIT):      1210.3 i/s - 1.14x  slower
         rexml 3.4.1:       699.3 i/s - 1.97x  slower
         3.4.1(YJIT):       660.3 i/s - 2.09x  slower

                           stream
              master:      1383.0 i/s
        master(YJIT):      1227.7 i/s - 1.13x  slower
         rexml 3.4.1:       701.7 i/s - 1.97x  slower
         3.4.1(YJIT):       659.5 i/s - 2.10x  slower
```
- YJIT=ON : 1.76x - 1.83x faster
- YJIT=OFF : 1.82x - 1.97x faster
naitoh added a commit to naitoh/rexml that referenced this issue Mar 2, 2025
## Why?

GitHub: fix ruby#243

## Benchmark (Comparison with rexml 3.4.1)
```
$ benchmark-driver benchmark/parse_cdata.yaml
Calculating -------------------------------------
                     rexml 3.4.1      master  3.4.1(YJIT)  master(YJIT)
                 dom     648.361      1.178k      591.590        1.046k i/s -     100.000 times in 0.154235s 0.084913s 0.169036s 0.095627s
                 sax     699.061      1.378k      651.148        1.196k i/s -     100.000 times in 0.143049s 0.072549s 0.153575s 0.083611s
                pull     699.271      1.379k      660.275        1.210k i/s -     100.000 times in 0.143006s 0.072527s 0.151452s 0.082622s
              stream     701.725      1.383k      659.483        1.228k i/s -     100.000 times in 0.142506s 0.072307s 0.151634s 0.081455s

Comparison:
                              dom
              master:      1177.7 i/s
        master(YJIT):      1045.7 i/s - 1.13x  slower
         rexml 3.4.1:       648.4 i/s - 1.82x  slower
         3.4.1(YJIT):       591.6 i/s - 1.99x  slower

                              sax
              master:      1378.4 i/s
        master(YJIT):      1196.0 i/s - 1.15x  slower
         rexml 3.4.1:       699.1 i/s - 1.97x  slower
         3.4.1(YJIT):       651.1 i/s - 2.12x  slower

                             pull
              master:      1378.8 i/s
        master(YJIT):      1210.3 i/s - 1.14x  slower
         rexml 3.4.1:       699.3 i/s - 1.97x  slower
         3.4.1(YJIT):       660.3 i/s - 2.09x  slower

                           stream
              master:      1383.0 i/s
        master(YJIT):      1227.7 i/s - 1.13x  slower
         rexml 3.4.1:       701.7 i/s - 1.97x  slower
         3.4.1(YJIT):       659.5 i/s - 2.10x  slower
```
- YJIT=ON : 1.76x - 1.83x faster
- YJIT=OFF : 1.82x - 1.97x faster

Co-authored-by: Sutou Kouhei <[email protected]>
@kou kou closed this as completed in #244 Mar 2, 2025
@kou kou closed this as completed in 64a709e Mar 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants