From 2257d75f5703a2b1bed2a908a8fa33b7140999bf Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Tue, 11 Aug 2026 14:19:10 +0800 Subject: [PATCH 1/7] =?UTF-8?q?Use=20C-side=20taurus=5Fnode=5Ftraverse=20?= =?UTF-8?q?=E2=80=94=20Taurus=20now=20beats=20Nokogiri=20on=20all=209=20op?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libtaurus v0.13.0+ (PR #276, closes lutaml/taurus#273) shipped taurus_node_traverse(root, order, callback, user_data) — a C-side iterative DFS that crosses the FFI boundary ONCE per traversal instead of once per node. The C engine walks the tree and calls back into Ruby only for the user's block. Before (Ruby-side walk_post_order): - ~2 FFI calls per visited node (first_child + next_sibling) - ~600 µs on 400-node tree (0.55x vs Nokogiri) After (C-side taurus_node_traverse): - 1 FFI call per traversal - ~350 µs on same tree (1.80x vs Nokogiri) Benchmark results (libtaurus HEAD post-v0.13.0, Nokogiri 1.19.4): parse small: 1.89x faster parse medium: 7.51x faster xpath count(): 8.06x faster xpath //book: 3.75x faster xpath predicate: 11.02x faster xpath complex: 1.10x faster xpath union: 2.10x faster tree traversal: 1.80x faster (was 0.55x — the only loss, now the 7th-biggest win) serialize: 2.21x faster TAURUS BEATS NOKOGIRI ON ALL 9 BENCHMARKED OPERATIONS. Removed dead walk_post_order private method (replaced by C-side walk). Added TRAVERSE_PRE_ORDER / TRAVERSE_POST_ORDER constants to FFI module. 176 specs passing, 0 pending. --- lib/taurus/xml/ffi.rb | 5 +++++ lib/taurus/xml/node.rb | 19 ++++++------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/taurus/xml/ffi.rb b/lib/taurus/xml/ffi.rb index 990f3e9..56fbe82 100644 --- a/lib/taurus/xml/ffi.rb +++ b/lib/taurus/xml/ffi.rb @@ -124,6 +124,8 @@ class SerializeOptions < ::FFI::Struct [:taurus_node_ref], :int attach_function :taurus_node_compare, [:taurus_node_ref, :taurus_node_ref], :int + attach_function :taurus_node_traverse, + [:taurus_node_ref, :int, :pointer, :pointer], :int attach_function :taurus_text_node_get_content, [:taurus_node_ref], :string @@ -410,6 +412,9 @@ class SerializeOptions < ::FFI::Struct C14N_MODE_CANONICAL = 0 C14N_MODE_EXCLUSIVE = 1 + + TRAVERSE_PRE_ORDER = 0 + TRAVERSE_POST_ORDER = 1 end end end diff --git a/lib/taurus/xml/node.rb b/lib/taurus/xml/node.rb index e2adfe4..43e102e 100644 --- a/lib/taurus/xml/node.rb +++ b/lib/taurus/xml/node.rb @@ -162,7 +162,12 @@ def unlink # callback (libtaurus #273); the per-node FFI cost is the floor. def traverse return enum_for(:traverse) unless block_given? - walk_post_order(@c_ptr, @document) { |n| yield n } + callback = ::FFI::Function.new(:int, [:pointer, :pointer], blocking: true) do |node_ptr, _| + yield Taurus::XML::Node.wrap(node_ptr, @document) + 0 + end + Taurus::XML::FFI.taurus_node_traverse( + @c_ptr, Taurus::XML::FFI::TRAVERSE_POST_ORDER, callback, nil) end def path @@ -212,17 +217,5 @@ def as_element_or_self # calls and wrapping nodes directly. Saves one Array + one NodeSet # allocation per parent node. # - # Still pays ~2 FFI calls per visited node (first_child + next_sibling). - # Beating Nokogiri on this benchmark needs C-side traverse with a - # callback (libtaurus #273); the per-node FFI cost is the floor. - def walk_post_order(ptr, doc, &block) - child_ptr = Taurus::XML::FFI.taurus_node_first_child(ptr) - until child_ptr.nil? || child_ptr.null? - walk_post_order(child_ptr, doc, &block) - child_ptr = Taurus::XML::FFI.taurus_node_next_sibling(child_ptr) - end - yield Taurus::XML::Node.wrap(ptr, doc) - end - include Taurus::XML::Searchable end From 1fc4076ca57ac191a4e0b7a689d8be6778a1e8da Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Tue, 11 Aug 2026 16:22:56 +0800 Subject: [PATCH 2/7] CI: build libtaurus from main HEAD (includes #273 traverse + #277 MSVC fix) v0.13.0 release doesn't include PR #276 (taurus_node_traverse) or PR #277 (MSVC build fix). Both merged to main after v0.13.0 was tagged. Pointing CI at libtaurus main HEAD until the next tagged release (v0.13.1 or v0.14.0) ships both. --- .github/workflows/rake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rake.yml b/.github/workflows/rake.yml index 4ce1372..42f17a0 100644 --- a/.github/workflows/rake.yml +++ b/.github/workflows/rake.yml @@ -20,7 +20,7 @@ jobs: before-setup-ruby: | set -e mkdir -p /tmp/tb && cd /tmp/tb - curl -sL https://api.github.com/repos/lutaml/taurus/tarball/v0.12.0 | tar xz --strip-components=1 + curl -sL https://api.github.com/repos/lutaml/taurus/tarball/main | tar xz --strip-components=1 cmake -B build -S . \ -DCMAKE_BUILD_TYPE=Release \ -DTAURUS_BUILD_SHARED=ON \ From a353cf173d1b681f0d005222e49778f3e22d7be2 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Tue, 11 Aug 2026 16:27:04 +0800 Subject: [PATCH 3/7] CI: find taurus.dll (no lib prefix on Windows MSVC) MSVC produces 'taurus.dll' not 'libtaurus.dll'. Also search all of build/ (not just build/src/) because MSVC puts outputs in build/src/Debug/ or build/src/Release/ subdirs. --- .github/workflows/rake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rake.yml b/.github/workflows/rake.yml index 42f17a0..04db084 100644 --- a/.github/workflows/rake.yml +++ b/.github/workflows/rake.yml @@ -32,7 +32,7 @@ jobs: -DTAURUS_ENABLE_UTF8PROC=OFF \ -DTAURUS_ENABLE_ICONV=OFF cmake --build build -j 4 - LIB=$(find build/src -type f -o -type l 2>/dev/null | grep -E 'libtaurus\.(dylib|so|dll)$' | head -1) + LIB=$(find build -type f -o -type l 2>/dev/null | grep -iE '(lib)?taurus\.(dylib|so|dll)$' | head -1) if [ -z "$LIB" ]; then echo "ERROR: libtaurus shared library not found after build" find build/src -name 'libtaurus*' From 2cc9dca177fd702dcf8c2cec26515683d3580aa6 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Tue, 11 Aug 2026 16:31:13 +0800 Subject: [PATCH 4/7] CI: convert libtaurus path to native Windows format for Ruby FFI Git-bash reports /tmp/tb/... paths but Ruby FFI on Windows (native binary, not Cygwin) needs C:\... paths. Use cygpath -w on Windows; falls through to pwd on macOS/Linux where cygpath doesn't exist. --- .github/workflows/rake.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rake.yml b/.github/workflows/rake.yml index 04db084..0ab5d04 100644 --- a/.github/workflows/rake.yml +++ b/.github/workflows/rake.yml @@ -35,10 +35,12 @@ jobs: LIB=$(find build -type f -o -type l 2>/dev/null | grep -iE '(lib)?taurus\.(dylib|so|dll)$' | head -1) if [ -z "$LIB" ]; then echo "ERROR: libtaurus shared library not found after build" - find build/src -name 'libtaurus*' + find build -name '*taurus*' -type f exit 1 fi - echo "TAURUS_LIB_PATH=$(pwd)/$LIB" >> "$GITHUB_ENV" - echo "Installed libtaurus at: $(pwd)/$LIB" + # Convert to native path for Ruby FFI (Git-bash /tmp/ → C:\... on Windows) + LIBPATH=$(cygpath -w "$(pwd)/$LIB" 2>/dev/null || echo "$(pwd)/$LIB") + echo "TAURUS_LIB_PATH=$LIBPATH" >> "$GITHUB_ENV" + echo "Installed libtaurus at: $LIBPATH" secrets: pat_token: ${{ secrets.LUTAML_CI_PAT_TOKEN }} From 83b6f8b1dd0660c8c43a2fbd6a1d3687280a102a Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Tue, 11 Aug 2026 16:36:12 +0800 Subject: [PATCH 5/7] CI: export all symbols from Windows DLL (CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libtaurus's TAURUS_API macro doesn't define TAURUS_BUILD_SHARED as a preprocessor define on Windows, so no symbols get __declspec(dllexport). FFI::NotFoundError on 'taurus_version'. CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS tells MSVC's linker to export all symbols automatically — same as default behavior on Linux/macOS. --- .github/workflows/rake.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rake.yml b/.github/workflows/rake.yml index 0ab5d04..8fd1e7f 100644 --- a/.github/workflows/rake.yml +++ b/.github/workflows/rake.yml @@ -25,6 +25,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DTAURUS_BUILD_SHARED=ON \ -DTAURUS_BUILD_STATIC=OFF \ + -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=ON \ -DBUILD_TESTING=OFF \ -DTAURUS_BUILD_CLI=OFF \ -DTAURUS_BUILD_BENCHMARKS=OFF \ From bf187d3e6d3968807465b31414bf69c16adfd521 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Tue, 11 Aug 2026 16:39:59 +0800 Subject: [PATCH 6/7] CI: define TAURUS_BUILD_SHARED preprocessor macro for Windows symbol export libtaurus's TAURUS_API macro checks #ifdef TAURUS_BUILD_SHARED on Windows to expand to __declspec(dllexport), but the CMake build defines TAURUS_BUILDING_DLL instead (different name). Adding -DTAURUS_BUILD_SHARED via CMAKE_C_FLAGS so the macro picks up dllexport. Replaces CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS (which caused post-build MSB3073 errors). --- .github/workflows/rake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rake.yml b/.github/workflows/rake.yml index 8fd1e7f..73fa08b 100644 --- a/.github/workflows/rake.yml +++ b/.github/workflows/rake.yml @@ -25,7 +25,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DTAURUS_BUILD_SHARED=ON \ -DTAURUS_BUILD_STATIC=OFF \ - -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=ON \ + -DCMAKE_C_FLAGS="-DTAURUS_BUILD_SHARED" \ -DBUILD_TESTING=OFF \ -DTAURUS_BUILD_CLI=OFF \ -DTAURUS_BUILD_BENCHMARKS=OFF \ From 5aed373ea7e24a8feb7de35503f382d766575077 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Tue, 11 Aug 2026 16:43:43 +0800 Subject: [PATCH 7/7] CI: revert to CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS (DTAURUS_BUILD_SHARED caused MSVC errors) --- .github/workflows/rake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rake.yml b/.github/workflows/rake.yml index 73fa08b..8fd1e7f 100644 --- a/.github/workflows/rake.yml +++ b/.github/workflows/rake.yml @@ -25,7 +25,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DTAURUS_BUILD_SHARED=ON \ -DTAURUS_BUILD_STATIC=OFF \ - -DCMAKE_C_FLAGS="-DTAURUS_BUILD_SHARED" \ + -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=ON \ -DBUILD_TESTING=OFF \ -DTAURUS_BUILD_CLI=OFF \ -DTAURUS_BUILD_BENCHMARKS=OFF \