Skip to content

Commit

Permalink
json: make the streamer a template class (envoyproxy#36001)
Browse files Browse the repository at this point in the history
Commit Message: json: make the streamer a template class
Additional Description:

This PR make Streamer a template to accept different types of output
(Buffer::Instance, std::string, etc.)

Risk Level: low.
Testing: unit.
Docs Changes: n/a.
Release Notes: n/a.
Platform Specific Features: n/a.

---------

Signed-off-by: wangbaiping <[email protected]>
Signed-off-by: wangbaiping <[email protected]>
  • Loading branch information
wbpcode authored Sep 10, 2024
1 parent b3a107f commit 2acf901
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 120 deletions.
1 change: 0 additions & 1 deletion source/common/buffer/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ envoy_cc_library(

envoy_cc_library(
name = "buffer_util_lib",
srcs = ["buffer_util.cc"],
hdrs = ["buffer_util.h"],
deps = [
"//envoy/buffer:buffer_interface",
Expand Down
45 changes: 0 additions & 45 deletions source/common/buffer/buffer_util.cc

This file was deleted.

38 changes: 37 additions & 1 deletion source/common/buffer/buffer_util.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
#pragma once

#include <charconv>
#include <cstddef>

#include "envoy/buffer/buffer.h"

#include "source/common/common/macros.h"

namespace Envoy {
namespace Buffer {

Expand All @@ -20,7 +25,38 @@ class Util {
* @param number the number to convert.
* @param buffer the buffer in which to write the double.
*/
static void serializeDouble(double number, Buffer::Instance& buffer);
template <class Output> static void serializeDouble(double number, Output& buffer) {
// Converting a double to a string: who would think it would be so complex?
// It's easy if you don't care about speed or accuracy :). Here we are measuring
// the speed with test/server/admin/stats_handler_speed_test
// --benchmark_filter=BM_HistogramsJson Here are some options:
// * absl::StrCat(number) -- fast (19ms on speed test) but loses precision (drops decimals).
// * absl::StrFormat("%.15g") -- works great but a bit slow (24ms on speed test)
// * `snprintf`(buf, sizeof(buf), "%.15g", ...) -- works but slow as molasses: 30ms.
// * fmt::format("{}") -- works great and is a little faster than absl::StrFormat: 21ms.
// * fmt::to_string -- works great and is a little faster than fmt::format: 19ms.
// * std::to_chars -- fast (16ms) and precise, but requires a few lines to
// generate the string_view, and does not work on all platforms yet.
//
// The accuracy is checked in buffer_util_test.
#if defined(_LIBCPP_VERSION) && _LIBCPP_VERSION >= 14000
// This version is awkward, and doesn't work on all platforms used in Envoy CI
// as of August 2023, but it is the fastest correct option on modern compilers.
char buf[100];
std::to_chars_result result = std::to_chars(buf, buf + sizeof(buf), number);
ENVOY_BUG(result.ec == std::errc{}, std::make_error_code(result.ec).message());
buffer.add(absl::string_view(buf, result.ptr - buf));

// Note: there is room to speed this up further by serializing the number directly
// into the buffer. However, buffer does not currently make it easy and fast
// to get (say) 100 characters of raw buffer to serialize into.
#else
// On older compilers, such as those found on Apple, and gcc, std::to_chars
// does not work with 'double', so we revert to the next fastest correct
// implementation.
buffer.add(fmt::to_string(number));
#endif
}
};

} // namespace Buffer
Expand Down
74 changes: 47 additions & 27 deletions source/common/json/json_streamer.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ namespace Json {
#define ASSERT_LEVELS_EMPTY ASSERT(this->levels_.empty())
#endif

// Simple abstraction that provide a output buffer for streaming JSON output.
// Buffer wrapper that implements the necessary abstraction for the template
// StreamerBase.
// This could be used to stream JSON output of StreamerBase to a Buffer.
class BufferOutput {
public:
void add(absl::string_view a) { buffer_.addFragments({a}); }
Expand All @@ -51,24 +53,41 @@ class BufferOutput {
Buffer::Instance& buffer_;
};

// String wrapper that implements the necessary abstraction for the template
// StreamerBase.
// This could be used to stream JSON output of StreamerBase to a single string.
class StringOutput {
public:
void add(absl::string_view a) { buffer_.append(a); }
void add(absl::string_view a, absl::string_view b, absl::string_view c) {
absl::StrAppend(&buffer_, a, b, c);
}
explicit StringOutput(std::string& output) : buffer_(output) {}

std::string& buffer_;
};

/**
* Provides an API for streaming JSON output, as an alternative to populating a
* JSON structure with an image of what you want to serialize, or using a
* protobuf with reflection. The advantage of this approach is that it does not
* require building an intermediate data structure with redundant copies of all
* strings, maps, and arrays.
*
* NOTE: This template take a type that can be used to stream output. This is either
* BufferOutput, StringOutput or any other types that have implemented
* add(absl::string_view) and
* add(absl::string_view, absl::string_view, absl::string_view) methods.
*/
class Streamer {
template <class OutputBufferType> class StreamerBase {
public:
using Value = absl::variant<absl::string_view, double, uint64_t, int64_t, bool>;

/**
* @param response The buffer in which to stream output. Note: this buffer can
* be flushed during population; it is not necessary to hold
* the entire json structure in memory before streaming it to
* the network.
* @param response The buffer in which to stream output.
* NOTE: The response must could be used to construct instance of OutputBufferType.
*/
explicit Streamer(Buffer::Instance& response) : response_(response) {}
template <class T> explicit StreamerBase(T& response) : response_(response) {}

class Array;
using ArrayPtr = std::unique_ptr<Array>;
Expand All @@ -81,15 +100,15 @@ class Streamer {
*/
class Level {
public:
Level(Streamer& streamer, absl::string_view opener, absl::string_view closer)
Level(StreamerBase& streamer, absl::string_view opener, absl::string_view closer)
: streamer_(streamer), closer_(closer) {
streamer_.addConstantString(opener);
streamer_.addWithoutSanitizing(opener);
#ifndef NDEBUG
streamer_.push(this);
#endif
}
virtual ~Level() {
streamer_.addConstantString(closer_);
streamer_.addWithoutSanitizing(closer_);
#ifndef NDEBUG
streamer_.pop(this);
#endif
Expand Down Expand Up @@ -180,7 +199,7 @@ class Streamer {
if (is_first_) {
is_first_ = false;
} else {
streamer_.addConstantString(",");
streamer_.addWithoutSanitizing(",");
}
}

Expand Down Expand Up @@ -223,11 +242,9 @@ class Streamer {
}
}

private:
friend Streamer;

protected:
bool is_first_{true}; // Used to control whether a comma-separator is added for a new entry.
Streamer& streamer_;
StreamerBase& streamer_;
absl::string_view closer_;
};
using LevelPtr = std::unique_ptr<Level>;
Expand All @@ -241,7 +258,7 @@ class Streamer {
using NameValue = std::pair<const absl::string_view, Value>;
using Entries = absl::Span<const NameValue>;

Map(Streamer& streamer) : Level(streamer, "{", "}") {}
Map(StreamerBase& streamer) : Level(streamer, "{", "}") {}

/**
* Initiates a new map key. This must be followed by rendering a value,
Expand Down Expand Up @@ -292,7 +309,7 @@ class Streamer {
*/
class Array : public Level {
public:
Array(Streamer& streamer) : Level(streamer, "[", "]") {}
Array(StreamerBase& streamer) : Level(streamer, "[", "]") {}
using Entries = absl::Span<const Value>;

/**
Expand Down Expand Up @@ -333,11 +350,6 @@ class Streamer {
return std::make_unique<Array>(*this);
}

private:
friend Level;
friend Map;
friend Array;

/**
* Takes a raw string, sanitizes it using JSON syntax, surrounds it
* with a prefix and suffix, and streams it out.
Expand All @@ -361,7 +373,7 @@ class Streamer {
if (std::isnan(d)) {
response_.add(Constants::Null);
} else {
Buffer::Util::serializeDouble(d, response_.buffer_);
Buffer::Util::serializeDouble(d, response_);
}
}
void addNumber(uint64_t u) { response_.add(absl::StrCat(u)); }
Expand All @@ -373,11 +385,14 @@ class Streamer {
void addBool(bool b) { response_.add(b ? Constants::True : Constants::False); }

/**
* Adds a constant string to the output stream. The string must outlive the
* Streamer object, and is intended for literal strings such as punctuation.
* Adds a pre-sanitized string or which doesn't require sanitizing to the output stream.
* NOTE: use this with care as it bypasses the sanitization process and may result in
* invalid JSON. If you are not sure if the string is already sanitized, use addString()
* or addSanitized() instead.
*/
void addConstantString(absl::string_view str) { response_.add(str); }
void addWithoutSanitizing(absl::string_view str) { response_.add(str); }

private:
#ifndef NDEBUG
/**
* @return the top Level*. This is used for asserts.
Expand All @@ -399,7 +414,7 @@ class Streamer {

#endif

BufferOutput response_;
OutputBufferType response_;
std::string sanitize_buffer_;

#ifndef NDEBUG
Expand All @@ -409,5 +424,10 @@ class Streamer {
#endif
};

/**
* A Streamer that streams to a Buffer::Instance.
*/
using Streamer = StreamerBase<BufferOutput>;

} // namespace Json
} // namespace Envoy
Loading

0 comments on commit 2acf901

Please sign in to comment.