Skip to content

refactor: drop property_tree dependency - #547

Open
Becheler wants to merge 1 commit into
boostorg:developfrom
Becheler:refactor/drop-property-tree-dependency
Open

refactor: drop property_tree dependency#547
Becheler wants to merge 1 commit into
boostorg:developfrom
Becheler:refactor/drop-property-tree-dependency

Conversation

@Becheler

@Becheler Becheler commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Before submitting

  • This PR targets the develop branch.
  • I searched for an existing PR or issue covering the same change.
  • My contribution is licensed under the Boost Software License 1.0.

Type of change

  • Bug fix
  • New feature or API addition
  • Refactor (no behavior change)
  • Documentation
  • Build, CI, or tooling
  • Other (specify below)

Does this PR introduce a breaking change?

  • Yes (describe migration impact below)
  • No

What this PR does

  • Remove dependency to Boost.PropertyTree and its 58 transitive dependencies
  • Vendor the core BGL uses (rapidxml header) with no behavioral change
  • Performance on local benchmark against develop:
    • x3.5 faster
    • x3.7 less memory

Motivation

  • GraphML was originally written in 2007 by Tiago de Paula Peixoto using expat, an external C XML library with a handmanaged SAX callback parser.
  • In 2009, Jeremiah Willcock rewrote it onto Boost.PropertyTree (commit e5aebbc, "fixes #3300"), which removed the external expat dependency and cut the parser from ~357 lines to ~97 by replacing the SAX state machine with a simple tree walk.
  • But in 2026 property_tree becomes the heavy dependency worth dropping.
  • Rapidxml.hpp is the only real dependency. There is no need to bring property_tree itself, on the contrary: it copies the whole tree again and is largely inefficient.

property_tree's value is a uniform generic tree with a string-path query API, the same interface we would use for JSON or INI. But GraphML never needs that generality: it only ever asks "tag name? this attribute? this text?", which a rapidxml node answers directly.

Under the hood, property_tree already parses XML with rapidxml (nodes are non owning, pointers to buffer), then copies the entire parse result into its generic ptree so the uniform API works (pure overhead with many allocations as nodes are recursive pairs string/ptree).

This PR keeps the exact same rapidxml parser but walks its DOM directly, deleting the ptree copy (and with it the property_tree dependency).

Testing

The legacy tests properly exercise the round trip and code path to guarantee no regression. The only legacy behavior we can't match is the exception type being raised. Malformed XML now throws boost::parse_error instead of property_tree's xml_parser_error/ptree_bad_path

Benchmark script to compile and run on develop then on the PR branch:

#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graphml.hpp>
#include <boost/graph/random.hpp>
#include <boost/random/mersenne_twister.hpp>

#include <algorithm>
#include <chrono>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

#include <sys/resource.h>

constexpr std::size_t NVERTICES = 100000;
constexpr std::size_t NEDGES = 1000000;
constexpr int REPS = 5;

using graph_t = boost::adjacency_list< boost::vecS, boost::vecS,
    boost::directedS,
    boost::property< boost::vertex_name_t, std::string,
        boost::property< boost::vertex_color_t, int > >,
    boost::property< boost::edge_weight_t, double > >;

static std::string make_graphml()
{
    graph_t g;
    boost::mt19937 rng(42);
    boost::generate_random_graph(g, NVERTICES, NEDGES, rng, false, false);

    std::size_t i = 0;
    graph_t::vertex_iterator vi, ve;
    for (boost::tie(vi, ve) = vertices(g); vi != ve; ++vi, ++i)
    {
        boost::put(boost::vertex_name_t(), g, *vi, "node <" + std::to_string(i) + "> & \"x\"");
        boost::put(boost::vertex_color_t(), g, *vi, int(i % 255));
    }
    double w = 0.5;
    graph_t::edge_iterator ei, ee;
    for (boost::tie(ei, ee) = edges(g); ei != ee; ++ei)
        boost::put(boost::edge_weight_t(), g, *ei, w += 0.001);

    boost::dynamic_properties dp;
    dp.property("name", boost::get(boost::vertex_name_t(), g));
    dp.property("color", boost::get(boost::vertex_color_t(), g));
    dp.property("weight", boost::get(boost::edge_weight_t(), g));
    std::ostringstream out;
    boost::write_graphml(out, g, dp, true);
    return out.str();
}

int main()
{
    std::string xml = make_graphml();
    const double mb = xml.size() / (1024.0 * 1024.0);

    std::vector< double > ms;
    for (int r = 0; r < REPS; ++r)
    {
        graph_t g;
        boost::dynamic_properties dp(boost::ignore_other_properties);
        dp.property("name", boost::get(boost::vertex_name_t(), g));
        dp.property("color", boost::get(boost::vertex_color_t(), g));
        dp.property("weight", boost::get(boost::edge_weight_t(), g));
        std::istringstream in(xml);
        auto t0 = std::chrono::steady_clock::now();
        boost::read_graphml(in, g, dp);
        auto t1 = std::chrono::steady_clock::now();
        ms.push_back( td::chrono::duration< double, std::milli >(t1 - t0).count());
    }
    std::sort(ms.begin(), ms.end());

    rusage ru{};
    getrusage(RUSAGE_SELF, &ru);
#ifdef __APPLE__
    double peak_mb = ru.ru_maxrss / (1024.0 * 1024.0);
#else
    double peak_mb = ru.ru_maxrss / 1024.0;
#endif

    std::cout << "size=" << mb << "MB  read median=" << ms[ms.size() / 2]  << "ms  peakRSS=" << peak_mb << "MB\n";
    return 0;
}

On develop:

size=113.66MB  read median=6382.51ms  peakRSS=5388.23MB

On PR:

size=113.66MB  read median=1825.4ms  peakRSS=1463.05MB

Checklist

  • Existing tests pass (b2 in the test/ directory).
  • New behavior is covered by a test, or this is a docs / build / refactor change.
  • Documentation was updated if user-facing behavior changed.
  • No new compiler warnings on the platforms I built against.

@Becheler Becheler self-assigned this Aug 5, 2026
@Becheler Becheler added the technical debt Code that works but needs refactoring, cleanup, or modernization. Not user-facing. label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Boost dependency footprint vs develop (auto-generated).
PR run 31210887289 vs develop run 31207793080 (16aca6db40).

Header-inclusion weights (graph files pulling each direct dependency in):

Dependency develop PR Δ
property_tree 2 0 -2
optional 7 6 -1
assert 31 32 +1
config 94 95 +1

Transitive Boost modules: 68 → 67 (-1)

  • removed: property_tree

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Compiler-warning counts vs develop (auto-generated).
PR run 31210887189 vs develop run 31207793516 (16aca6db40).

Job Baseline After Delta
macos (clang, 14) 681 681 0
macos (clang, 17) 641 641 0
macos (clang, 20) 641 641 0
ubuntu (clang-19, 14) 681 681 0
ubuntu (clang-19, 17) 641 641 0
ubuntu (clang-19, 20) 641 641 0
ubuntu (clang-19, 23) 641 641 0
ubuntu (gcc-14, 14) 827 827 0
ubuntu (gcc-14, 17) 949 949 0
ubuntu (gcc-14, 20) 949 949 0
ubuntu (gcc-14, 23) 949 949 0
windows_msvc_14_3 (msvc-14.3) 971 971 0

@Becheler
Becheler force-pushed the refactor/drop-property-tree-dependency branch from dd803bf to 8d16ad5 Compare August 6, 2026 05:51
@Becheler Becheler added the performance Performant use of time and space. label Aug 6, 2026
@Becheler
Becheler force-pushed the refactor/drop-property-tree-dependency branch from 8d16ad5 to 520413c Compare August 6, 2026 05:56
@Becheler
Becheler marked this pull request as ready for review August 6, 2026 09:12
@Becheler
Becheler requested a review from jeremy-murphy as a code owner August 6, 2026 09:12
@Becheler
Becheler force-pushed the refactor/drop-property-tree-dependency branch from 520413c to 16aca6d Compare August 7, 2026 19:18
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.66667% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.60%. Comparing base (e8e3f01) to head (16aca6d).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
include/boost/graph/graphml.hpp 62.50% 6 Missing ⚠️
src/graphml.cpp 93.22% 4 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #547      +/-   ##
===========================================
- Coverage    92.45%   91.60%   -0.85%     
===========================================
  Files          392      393       +1     
  Lines        28214    28824     +610     
  Branches      8008     8124     +116     
===========================================
+ Hits         26086    26405     +319     
- Misses        2024     2311     +287     
- Partials       104      108       +4     
Files with missing lines Coverage Δ
include/boost/graph/detail/rapidxml.hpp 53.11% <ø> (ø)
src/graphml.cpp 85.81% <93.22%> (-2.76%) ⬇️
include/boost/graph/graphml.hpp 83.68% <62.50%> (-2.72%) ⬇️

... and 3 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8d8dc81...16aca6d. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performant use of time and space. technical debt Code that works but needs refactoring, cleanup, or modernization. Not user-facing.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant