diff options
| author | Martin Hořeňovský | 2025-08-21 23:36:22 +0200 |
|---|---|---|
| committer | Martin Hořeňovský | 2025-08-22 17:00:40 +0200 |
| commit | fb2e4fbe413824a5cad4101c0e94cb10b7828f0d (patch) | |
| tree | 8da4e08aeeea0d85f71f5db767a832085a9f8666 | |
| parent | 78a9518a2847f82cb65124d65eaf0b0be8d90cdc (diff) | |
| download | catch3-fb2e4fbe413824a5cad4101c0e94cb10b7828f0d.tar.gz | |
Improve performance of writing JSON values
The old code was exceedingly simple, as it went char-by-char and
decided whether to write it to the output stream as-is, or escaped.
This caused a _lot_ of stream writes of individual characters.
The new code instead looks for characters that need escaping, and
bulk-writes the non-escaped characters in between them. This leads
to about the same performance for strings that comprise of only
escaped characters, and 3-10x improvement for strings without any
escaping needed.
In practice, we should expect the former rather than the latter,
but this is still nice improvement.
| -rw-r--r-- | src/catch2/internal/catch_jsonwriter.cpp | 61 | ||||
| -rw-r--r-- | tests/SelfTest/IntrospectiveTests/Json.tests.cpp | 28 |
2 files changed, 66 insertions, 23 deletions
diff --git a/src/catch2/internal/catch_jsonwriter.cpp b/src/catch2/internal/catch_jsonwriter.cpp index 1a96e348..6915c9c3 100644 --- a/src/catch2/internal/catch_jsonwriter.cpp +++ b/src/catch2/internal/catch_jsonwriter.cpp @@ -7,8 +7,36 @@ // SPDX-License-Identifier: BSL-1.0 #include <catch2/internal/catch_enforce.hpp> #include <catch2/internal/catch_jsonwriter.hpp> +#include <catch2/internal/catch_unreachable.hpp> namespace Catch { + + namespace { + static bool needsEscape( char c ) { + return c == '"' || c == '\\' || c == '\b' || c == '\f' || + c == '\n' || c == '\r' || c == '\t'; + } + + static Catch::StringRef makeEscapeStringRef( char c ) { + if ( c == '"' ) { + return "\\\""_sr; + } else if ( c == '\\' ) { + return "\\\\"_sr; + } else if ( c == '\b' ) { + return "\\b"_sr; + } else if ( c == '\f' ) { + return "\\f"_sr; + } else if ( c == '\n' ) { + return "\\n"_sr; + } else if ( c == '\r' ) { + return "\\r"_sr; + } else if ( c == '\t' ) { + return "\\t"_sr; + } + Catch::Detail::Unreachable(); + } + } // namespace + void JsonUtils::indent( std::ostream& os, std::uint64_t level ) { for ( std::uint64_t i = 0; i < level; ++i ) { os << " "; @@ -118,30 +146,19 @@ namespace Catch { void JsonValueWriter::writeImpl( Catch::StringRef value, bool quote ) { if ( quote ) { m_os << '"'; } - for (char c : value) { - // Escape list taken from https://www.json.org/json-en.html, - // string definition. - // Note that while forward slash _can_ be escaped, it does - // not have to be, if JSON is not further embedded somewhere - // where forward slash is meaningful. - if ( c == '"' ) { - m_os << "\\\""; - } else if ( c == '\\' ) { - m_os << "\\\\"; - } else if ( c == '\b' ) { - m_os << "\\b"; - } else if ( c == '\f' ) { - m_os << "\\f"; - } else if ( c == '\n' ) { - m_os << "\\n"; - } else if ( c == '\r' ) { - m_os << "\\r"; - } else if ( c == '\t' ) { - m_os << "\\t"; - } else { - m_os << c; + size_t current_start = 0; + for ( size_t i = 0; i < value.size(); ++i ) { + if ( needsEscape( value[i] ) ) { + if ( current_start < i ) { + m_os << value.substr( current_start, i - current_start ); + } + m_os << makeEscapeStringRef( value[i] ); + current_start = i + 1; } } + if ( current_start < value.size() ) { + m_os << value.substr( current_start, value.size() - current_start ); + } if ( quote ) { m_os << '"'; } } diff --git a/tests/SelfTest/IntrospectiveTests/Json.tests.cpp b/tests/SelfTest/IntrospectiveTests/Json.tests.cpp index bee85c99..fae1c843 100644 --- a/tests/SelfTest/IntrospectiveTests/Json.tests.cpp +++ b/tests/SelfTest/IntrospectiveTests/Json.tests.cpp @@ -7,6 +7,8 @@ // SPDX-License-Identifier: BSL-1.0 #include <catch2/catch_test_macros.hpp> +#include <catch2/benchmark/catch_benchmark.hpp> +#include <catch2/generators/catch_generators.hpp> #include <catch2/internal/catch_jsonwriter.hpp> #include <catch2/matchers/catch_matchers_string.hpp> @@ -17,7 +19,6 @@ namespace { static std::ostream& operator<<( std::ostream& os, Custom const& ) { return os << "custom"; } -} // namespace TEST_CASE( "JsonWriter", "[JSON][JsonWriter]" ) { @@ -150,3 +151,28 @@ TEST_CASE( "JsonWriter escapes characters in strings properly", "[JsonWriter]" ) REQUIRE( sstream.str() == "\"\\\\/\\t\\r\\n\"" ); } } + +TEST_CASE( "JsonWriter benchmarks", "[JsonWriter][!benchmark]" ) { + const auto input_length = GENERATE( as<size_t>{}, 10, 100, 10'000 ); + std::string test_input( input_length, 'a' ); + BENCHMARK_ADVANCED( "write string, no-escaping, len=" + + std::to_string( input_length ) )( + Catch::Benchmark::Chronometer meter ) { + std::stringstream sstream; + meter.measure( [&]( int ) { + Catch::JsonValueWriter( sstream ).write( test_input ); + } ); + }; + + std::string escape_input( input_length, '\b' ); + BENCHMARK_ADVANCED( "write string, all-escaped, len=" + + std::to_string( input_length ) )( + Catch::Benchmark::Chronometer meter ) { + std::stringstream sstream; + meter.measure( [&]( int ) { + Catch::JsonValueWriter( sstream ).write( escape_input ); + } ); + }; +} + +} // namespace |