Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c226497
Save state on origin timeing
oschaaf Jun 15, 2020
c6697de
Merge remote-tracking branch 'upstream/master' into origin-timings
oschaaf Jun 15, 2020
28e0d34
Fix clang-tidy, format, and test build
oschaaf Jun 16, 2020
94ad984
Merge remote-tracking branch 'upstream/master' into origin-timings
oschaaf Aug 19, 2020
933c589
stats naming
oschaaf Aug 19, 2020
a1691ec
Merge remote-tracking branch 'upstream/master' into origin-timings
oschaaf Aug 21, 2020
740d0ca
Track request receipt deltas. Configure response header name.
oschaaf Aug 21, 2020
1afe2f1
Save state before context switching
oschaaf Aug 21, 2020
ae0c08e
Merge remote-tracking branch 'upstream/master' into origin-timings
oschaaf Aug 21, 2020
3487e19
Merge remote-tracking branch 'upstream/master' into origin-timings
oschaaf Aug 24, 2020
1d1f03a
Fix format
oschaaf Aug 24, 2020
a9d99eb
Suppress clang-tidy on MUTABLE_CONSTRUCT_ON_FIRST_USE
oschaaf Aug 24, 2020
0aa8630
Proper locking, add TODOs and doc comments.
oschaaf Aug 24, 2020
db01e40
Merge remote-tracking branch 'upstream/master' into origin-timings
oschaaf Aug 24, 2020
5e48513
Small fixes
oschaaf Aug 24, 2020
d9569cb
Add unit test for StreamDecoder
oschaaf Aug 25, 2020
cf1170c
Review feedback
oschaaf Aug 26, 2020
421feb8
check_format introduced proto comment issues. Add punctuation.
oschaaf Aug 26, 2020
af148ea
Merge remote-tracking branch 'upstream/master' into origin-timings
oschaaf Aug 27, 2020
e813e75
Review feedback
oschaaf Aug 27, 2020
505495c
Save state on splitting out a separate extension and stopwatch
oschaaf Aug 27, 2020
5405dc9
Merge remote-tracking branch 'upstream/master' into origin-timings
oschaaf Aug 27, 2020
d858b55
Small corrections
oschaaf Aug 28, 2020
3cf32b8
Add threaded spam test for stopwatch. Add doc comments.
oschaaf Aug 28, 2020
35aee47
clang-tidy: fix for loop
oschaaf Aug 28, 2020
027c00f
Merge remote-tracking branch 'upstream/master' into origin-timings
oschaaf Aug 29, 2020
70502e1
Review feedback: doc comments
oschaaf Aug 29, 2020
7774e2d
review feedback
oschaaf Sep 1, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/server/response_options.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ message ResponseOptions {
// Concurrency based linear delay configuration.
ConcurrencyBasedLinearDelay concurrency_based_linear_delay = 5;
}
// If set, makes the extension echo timing data in the supplied response header name.
Comment thread
oschaaf marked this conversation as resolved.
Outdated
string emit_previous_request_delta_in_response_header = 6;
}
14 changes: 10 additions & 4 deletions source/client/benchmark_client_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ BenchmarkClientStatistic::BenchmarkClientStatistic(BenchmarkClientStatistic&& st
latency_3xx_statistic(std::move(statistic.latency_3xx_statistic)),
latency_4xx_statistic(std::move(statistic.latency_4xx_statistic)),
latency_5xx_statistic(std::move(statistic.latency_5xx_statistic)),
latency_xxx_statistic(std::move(statistic.latency_xxx_statistic)) {}
latency_xxx_statistic(std::move(statistic.latency_xxx_statistic)),
origin_latency_statistic(std::move(statistic.origin_latency_statistic)) {}

BenchmarkClientStatistic::BenchmarkClientStatistic(
StatisticPtr&& connect_stat, StatisticPtr&& response_stat,
StatisticPtr&& response_header_size_stat, StatisticPtr&& response_body_size_stat,
StatisticPtr&& latency_1xx_stat, StatisticPtr&& latency_2xx_stat,
StatisticPtr&& latency_3xx_stat, StatisticPtr&& latency_4xx_stat,
StatisticPtr&& latency_5xx_stat, StatisticPtr&& latency_xxx_stat)
StatisticPtr&& latency_5xx_stat, StatisticPtr&& latency_xxx_stat,
StatisticPtr&& origin_latency_stat)
: connect_statistic(std::move(connect_stat)), response_statistic(std::move(response_stat)),
response_header_size_statistic(std::move(response_header_size_stat)),
response_body_size_statistic(std::move(response_body_size_stat)),
Expand All @@ -46,7 +48,8 @@ BenchmarkClientStatistic::BenchmarkClientStatistic(
latency_3xx_statistic(std::move(latency_3xx_stat)),
latency_4xx_statistic(std::move(latency_4xx_stat)),
latency_5xx_statistic(std::move(latency_5xx_stat)),
latency_xxx_statistic(std::move(latency_xxx_stat)) {}
latency_xxx_statistic(std::move(latency_xxx_stat)),
origin_latency_statistic(std::move(origin_latency_stat)) {}

Envoy::Http::ConnectionPool::Cancellable*
Http1PoolImpl::newStream(Envoy::Http::ResponseDecoder& response_decoder,
Expand Down Expand Up @@ -97,6 +100,7 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl(
statistic_.latency_4xx_statistic->setId("benchmark_http_client.latency_4xx");
statistic_.latency_5xx_statistic->setId("benchmark_http_client.latency_5xx");
statistic_.latency_xxx_statistic->setId("benchmark_http_client.latency_xxx");
statistic_.origin_latency_statistic->setId("benchmark_http_client.origin_latency_statistic");
}

void BenchmarkClientHttpImpl::terminate() {
Expand All @@ -121,6 +125,7 @@ StatisticPtrMap BenchmarkClientHttpImpl::statistics() const {
statistics[statistic_.latency_4xx_statistic->id()] = statistic_.latency_4xx_statistic.get();
statistics[statistic_.latency_5xx_statistic->id()] = statistic_.latency_5xx_statistic.get();
statistics[statistic_.latency_xxx_statistic->id()] = statistic_.latency_xxx_statistic.get();
statistics[statistic_.origin_latency_statistic->id()] = statistic_.origin_latency_statistic.get();
return statistics;
};

Expand Down Expand Up @@ -160,7 +165,8 @@ bool BenchmarkClientHttpImpl::tryStartRequest(CompletionCallback caller_completi
dispatcher_, api_.timeSource(), *this, std::move(caller_completion_callback),
*statistic_.connect_statistic, *statistic_.response_statistic,
*statistic_.response_header_size_statistic, *statistic_.response_body_size_statistic,
request->header(), shouldMeasureLatencies(), content_length, generator_, http_tracer_);
*statistic_.origin_latency_statistic, request->header(), shouldMeasureLatencies(),
content_length, generator_, http_tracer_);
requests_initiated_++;
pool_ptr->newStream(*stream_decoder, *stream_decoder);
return true;
Expand Down
4 changes: 3 additions & 1 deletion source/client/benchmark_client_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ struct BenchmarkClientStatistic {
StatisticPtr&& response_body_size_stat, StatisticPtr&& latency_1xx_stat,
StatisticPtr&& latency_2xx_stat, StatisticPtr&& latency_3xx_stat,
StatisticPtr&& latency_4xx_stat, StatisticPtr&& latency_5xx_stat,
StatisticPtr&& latency_xxx_stat);
StatisticPtr&& latency_xxx_stat,
StatisticPtr&& origin_latency_statistic);

// These are declared order dependent. Changing ordering may trigger on assert upon
// destruction when tls has been involved during usage.
Expand All @@ -73,6 +74,7 @@ struct BenchmarkClientStatistic {
StatisticPtr latency_4xx_statistic;
StatisticPtr latency_5xx_statistic;
StatisticPtr latency_xxx_statistic;
StatisticPtr origin_latency_statistic;
};

class Http1PoolImpl : public Envoy::Http::Http1::ProdConnPoolImpl {
Expand Down
1 change: 1 addition & 0 deletions source/client/factories_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ BenchmarkClientPtr BenchmarkClientFactoryImpl::create(
std::make_unique<SinkableHdrStatistic>(scope, worker_id),
std::make_unique<SinkableHdrStatistic>(scope, worker_id),
std::make_unique<SinkableHdrStatistic>(scope, worker_id),
std::make_unique<SinkableHdrStatistic>(scope, worker_id),
std::make_unique<SinkableHdrStatistic>(scope, worker_id));
auto benchmark_client = std::make_unique<BenchmarkClientHttpImpl>(
api, dispatcher, scope, statistic, options_.h2(), cluster_manager, http_tracer, cluster_name,
Expand Down
13 changes: 13 additions & 0 deletions source/client/stream_decoder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b
response_header_sizes_statistic_.addValue(response_headers_->byteSize());
const uint64_t response_code = Envoy::Http::Utility::getResponseStatus(*response_headers_);
stream_info_.response_code_ = static_cast<uint32_t>(response_code);
const auto timing_header_name = Envoy::Http::LowerCaseString("x-nh-do-not-use-origin-timings");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: the odd name here is to scare people away from using this. My plan is to make a new option for nighthawk_client to allow configuration of this header name.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my concern here is i think we are introducing two different header namespaces now. See https://github.com/envoyproxy/nighthawk/blob/master/source/server/well_known_headers.h, where we have x-nighthawk-test-server-config. It would be nice if we only had the one namespace, to avoid confusion.

If this is the first introduction of x-nh-, can we change it to x-nighthawk-?

If not, can we create an issue to decide on one of those two, and at least make sure that one contains all of the headers, even if we support the other one for backwards compatibility.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separately, can you explain why having a configurable header name here is beneficial? just not sure why we wouldn't just dictate this

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: separately, can you explain why having a configurable header name here is beneficial? just not sure why we wouldn't just dictate this

Well, in my opinion, there's certainly something to say for dictating the names: less code, less configuration. Here are the thoughts I had that made me lean towards allow the end user control of the header names involved. I think this mostly applies to both the client and test-server aspects of it:

  1. Being able to configure the header names both on the server and client side of Nighthawk decouples them. This flexibility may come in handy when mixing Nighthawk components with other OSS or homegrown clients/proxies/test servers with similar capabilities (especially when these dictate the naming involved).
  2. If both the proxy and test server emit timing data using different header names, one can switch which one the clients should track
  3. If multiple proxies or test-servers are set up to emit latencies using different names in horizontal scaling, it could be used to track a single instance instead of all of them.
  4. If timing-filter gets extended to emit more timings, and we end up with multiple headers, clients can easily switch which one they track.
  5. In examples the header name could be a little bit longer for clarity, but in final testing, one might want to opt to make it as short as possible to minimise overhead.

Considering the above, let me know what you think;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense. Thanks for the explanation.

const auto* timing_header = response_headers_->get(timing_header_name);
Comment thread
oschaaf marked this conversation as resolved.
Outdated
if (timing_header != nullptr) {
auto timing_value = timing_header->value().getStringView();
int64_t origin_delta;
if (absl::SimpleAtoi(timing_value, &origin_delta)) {
origin_latency_statistic_.addValue(origin_delta);
} else {
// TODO(XXX): Can we make sure we avoid high frequency logging for this somehow?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Punted to #484, which has a candidate PoC solution. I remember there are other spots where this would be good to have in the existing code base as well.

ENVOY_LOG(warn, "Origin delta {} could not be interpreted as an integer.", timing_value);
}
}

if (complete_) {
onComplete(true);
}
Expand Down
6 changes: 4 additions & 2 deletions source/client/stream_decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder,
StreamDecoderCompletionCallback& decoder_completion_callback,
OperationCallback caller_completion_callback, Statistic& connect_statistic,
Statistic& latency_statistic, Statistic& response_header_sizes_statistic,
Statistic& response_body_sizes_statistic, HeaderMapPtr request_headers,
bool measure_latencies, uint32_t request_body_size,
Statistic& response_body_sizes_statistic, Statistic& origin_latency_statistic,
HeaderMapPtr request_headers, bool measure_latencies, uint32_t request_body_size,
Envoy::Random::RandomGenerator& random_generator,
Envoy::Tracing::HttpTracerSharedPtr& http_tracer)
: dispatcher_(dispatcher), time_source_(time_source),
Expand All @@ -53,6 +53,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder,
connect_statistic_(connect_statistic), latency_statistic_(latency_statistic),
response_header_sizes_statistic_(response_header_sizes_statistic),
response_body_sizes_statistic_(response_body_sizes_statistic),
origin_latency_statistic_(origin_latency_statistic),
request_headers_(std::move(request_headers)), connect_start_(time_source_.monotonicTime()),
complete_(false), measure_latencies_(measure_latencies),
request_body_size_(request_body_size), stream_info_(time_source_),
Expand Down Expand Up @@ -103,6 +104,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder,
Statistic& latency_statistic_;
Statistic& response_header_sizes_statistic_;
Statistic& response_body_sizes_statistic_;
Statistic& origin_latency_statistic_;
HeaderMapPtr request_headers_;
Envoy::Http::ResponseHeaderMapPtr response_headers_;
Envoy::Http::ResponseTrailerMapPtr trailer_headers_;
Expand Down
2 changes: 2 additions & 0 deletions source/server/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ envoy_cc_library(
":configuration_lib",
":well_known_headers_lib",
"//api/server:response_options_proto_cc_proto",
"@envoy//source/common/common:lock_guard_lib_with_external_headers",
"@envoy//source/common/common:thread_lib_with_external_headers",
"@envoy//source/exe:envoy_common_lib_with_external_headers",
],
)
Expand Down
28 changes: 28 additions & 0 deletions source/server/http_test_server_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "server/well_known_headers.h"

#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"

namespace Nighthawk {
namespace Server {
Expand All @@ -16,6 +17,22 @@ HttpTestServerDecoderFilterConfig::HttpTestServerDecoderFilterConfig(
nighthawk::server::ResponseOptions proto_config)
: server_config_(std::move(proto_config)) {}

uint64_t HttpTestServerDecoderFilterConfig::ThreadSafeMontonicTimeStopwatch::getElapsedNsAndReset(
Envoy::TimeSource& time_source) {
Envoy::Thread::LockGuard guard(lock_);
// Note that we obtain monotonic time under lock, to ensure that start_ will be updated
// monotonically.
const Envoy::MonotonicTime new_time = time_source.monotonicTime();
const uint64_t elapsed = start_ == Envoy::MonotonicTime::min() ? 0 : (new_time - start_).count();
Comment thread
oschaaf marked this conversation as resolved.
Outdated
start_ = new_time;

@eric846 eric846 Aug 25, 2020

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning: Attempting to reason about concurrency...

Do you see any weird effects running with concurrency > 1 on the test server?

I was imagining a situation with 2 test server threads operating almost in lockstep, but 1ns apart. Suppose the requests come in evenly every 1ms. If the test server got stalled for a few milliseconds and the OS ended up buffering several packets, and then the test server threads started consuming them round robin, wouldn't the timing reports end up alternating 0, 1ns, 999999ns, 1ns, 999999ns, 1ns, ...?

This would actually represent the true response timing coming out of the test server, so the code in this PR would be working as intended.

Extreme statistics like that could give the impression that there is extreme distortion from the intermediary proxy, but it's entirely an artifact of the Nighthawk test server having two threads and stalling for a few milliseconds.

(Also the intermediary might also introduce its own equally extreme irregularities, since it could also be a multithreaded Envoy.)

The obvious way to avoid this issue is to use concurrency=1 on the test server. Then we would only be measuring the distortions from intermediaries and the OS.

Do you think there's any future reporting that could diagnose this issue? For example, if we also had exactly this PR but with thread-local stopwatches, we would see perfect evenness rather than 1-999999 fluctuation. We would know that the 1-999999 in the cross-thread stats was the fault of our own multithreading, and any timing deviations would be our OS or the intermediary.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, maybe varying between 1ns and 1ms would be considered negligible distortion, especially if the intermediary added multiple milliseconds.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your reasoning here makes sense to me. The intent of this is to be able to sanity check part of our timings, so if it would catch those extremes where we don't expect them, I think it would be providing value.

The way I see it is that this is going to be particularly useful in A/B tests, for example:

  • across different clients
  • with and without an intermediary
  • with nighthawk client features that might perform relatively heavy lifting on cpu cores shared with the workers (e.g. stats flushing).

Some existing OSS clients can probably easily be tweaked to take these as inputs to their latency reports to figure out their request-release timing precisions.

Also, something like the delta between the expected and observed curves could perhaps serve as a means to quantify distortion; Indeed I think the extremes in the scenario you described would be signal in this case, and not noise (looking at you, adaptive load controller :-)).

As for tracking thread-local values: I've been thinking about that, and I think it's pretty doable doing that on the server side by adding a new Envoy histogram in the extension. On the client side however, I think it will take more consideration and work, as I think there would be some feature gaps:

  • be able to set multiple response headers for tracking, or create a means to read multiple values from the header.
  • if we want to be able to correlate thread id's we need to be able to tag values too, but this may not be nessecary.
  • having a means to dynamically create histograms or a histogram capable of grouping on a tag would be needed
    for the Nighthawk client to handle it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afterthought: maybe, if we'd have the per worker histograms on the test server side, plus a means to read/sample these histograms on the NH client side and propagate these to the output, that might be a relatively low effort means to achieve this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last thought: per-worker tracking may also yield data that's interesting in light of exposing unbalancedness.
I filed #487 to get this on the radar and track this / serve as a point for further discussion.

return elapsed;
}

uint64_t
HttpTestServerDecoderFilterConfig::getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source) {
return lastRequestStopwatch().getElapsedNsAndReset(time_source);
}

HttpTestServerDecoderFilter::HttpTestServerDecoderFilter(
HttpTestServerDecoderFilterConfigSharedPtr config)
: config_(std::move(config)) {}
Expand All @@ -32,6 +49,13 @@ void HttpTestServerDecoderFilter::sendReply() {
static_cast<Envoy::Http::Code>(200), response_body,
[this](Envoy::Http::ResponseHeaderMap& direct_response_headers) {
Configuration::applyConfigToResponseHeaders(direct_response_headers, base_config_);
const std::string previous_request_delta_in_response_header =
base_config_.emit_previous_request_delta_in_response_header();
if (!previous_request_delta_in_response_header.empty() && last_request_delta_ns_ > 0) {
direct_response_headers.appendCopy(
Envoy::Http::LowerCaseString(previous_request_delta_in_response_header),
absl::StrCat(last_request_delta_ns_));
}
},
absl::nullopt, "");
} else {
Expand Down Expand Up @@ -79,6 +103,10 @@ HttpTestServerDecoderFilter::decodeTrailers(Envoy::Http::RequestTrailerMap&) {
void HttpTestServerDecoderFilter::setDecoderFilterCallbacks(
Envoy::Http::StreamDecoderFilterCallbacks& callbacks) {
decoder_callbacks_ = &callbacks;
// TODO(oschaaf): this adds locking in the hot path. Consider moving this into a separate
// extension, which will also allow tracking multiple points via configuration.
last_request_delta_ns_ =
Comment thread
oschaaf marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we (or can we) verify the performance implication of this? How likely is this going to cause significant regressions in performance tests that use the test server?

If we can't verify it, we may be better off keeping this behind a feature flag to begin with? Or is it already on the path only when we ask for it by setting emit_previous_request_delta_in_response_header?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't measure a significant difference in manual tests. Having said that, I'd love to move this into its own extension because intuitively I'd say that there's no way that this can have a positive impact.
The locking now is unconditional, and it is hard to make it conditional because the configuration for it can be changed on a per-request basis.

But a separate extension would allow one to easily take this out of the equation altogether, and possibly also allow one to inject it into multiple places in the server side extension pipeline if desired so, emitting these timings from various places.
If you don't mind me bloating this PR a bit, I'd be up to move this into a new extension.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the unknown performance impact, I think it would be worthwhile to move this to a new extension in this PR. Thank you for looking into it.

config_->getElapsedNanosSinceLastRequest(callbacks.dispatcher().timeSource());
}

} // namespace Server
Expand Down
51 changes: 50 additions & 1 deletion source/server/http_test_server_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,68 @@

#include <string>

#include "envoy/common/time.h"
#include "envoy/server/filter_config.h"

#include "external/envoy/source/common/common/lock_guard.h"
#include "external/envoy/source/common/common/thread.h"

#include "api/server/response_options.pb.h"

namespace Nighthawk {
namespace Server {

// Basically this is left in as a placeholder for further configuration.
/**
* Filter configuration container class for the test server extension.
* Instances of this class will be shared accross instances of HttpTestServerDecoderFilter.
*/
class HttpTestServerDecoderFilterConfig {
public:
/**
* Constructs a new HttpTestServerDecoderFilterConfig instance.
*
* @param proto_config The proto configuration of the filter.
*/
HttpTestServerDecoderFilterConfig(nighthawk::server::ResponseOptions proto_config);

/**
* @return const nighthawk::server::ResponseOptions& read-only reference to the proto config
* object.
*/
const nighthawk::server::ResponseOptions& server_config() { return server_config_; }

/**
* Gets the number of elapsed nanoseconds since the last call (server wide).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How useful is this statistic if it is server wide as compared to having it per each session? I likely don't fully understand the use case for this statistic, so this may as well be working as intended.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I see it is that the current approach is going to be particularly useful in A/B tests, for example:

with and without an intermediary
with nighthawk client features that might perform relatively heavy lifting on cpu cores shared with the workers (e.g. stats flushing).
across different clients

So the use case is sanity checking and exposing a low effort means to dig up a potential root cause of latency measurement divergence.

I think per-worker or per-session tracking could also serve as a purpose to further dig into things as that would allow one to get a sense of balancedness across threads and sessions. Also see https://github.com/envoyproxy/nighthawk/pull/477/files#r476803233 for an earlier discussion related to this.

* Safe to use concurrently.
*
* @param time_source Time source that will be used to obain an updated monotonic time sample.
* @return uint64_t 0 on the first call, else the number of elapsed nanoseconds since the last
* call.
*/
static uint64_t getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source);

private:
/**
* Utility class for thread safe tracking of elapsed monotonic time.
*/
class ThreadSafeMontonicTimeStopwatch {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this class part of the HttpTestServerDecoderFilterConfig? is there anything the binds them together other than the fact that one uses the other? Or can we (for clarity and reusability) define it as a standalone one?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want other things to rely on it yet, but if we feel this is good to go for that, then we can move it out. Maybe it should go into a separate file then and have direct test coverage. Let me know what you think.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea of having it in its own file with test coverage is a good one and as a bonus we will end up with a simpler interface.

public:
ThreadSafeMontonicTimeStopwatch() : start_(Envoy::MonotonicTime::min()) {}
Comment thread
oschaaf marked this conversation as resolved.
Outdated
/**
* @param time_source used to obtain a sample of the current monotonic time.
* @return uint64_t 0 on the first invocation, and the number of elapsed nanoseconds since the
* last invocation otherwise.
*/
uint64_t getElapsedNsAndReset(Envoy::TimeSource& time_source);

private:
Envoy::Thread::MutexBasicLockable lock_;
Envoy::MonotonicTime start_ GUARDED_BY(lock_);
};

static ThreadSafeMontonicTimeStopwatch& lastRequestStopwatch() {
Comment thread
oschaaf marked this conversation as resolved.
Outdated
MUTABLE_CONSTRUCT_ON_FIRST_USE(ThreadSafeMontonicTimeStopwatch); // NOLINT
}
const nighthawk::server::ResponseOptions server_config_;
};

Expand Down Expand Up @@ -43,6 +91,7 @@ class HttpTestServerDecoderFilter : public Envoy::Http::StreamDecoderFilter {
bool json_merge_error_{false};
std::string error_message_;
absl::optional<std::string> request_headers_dump_;
uint64_t last_request_delta_ns_;
};

} // namespace Server
Expand Down
3 changes: 2 additions & 1 deletion test/benchmark_http_client_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ class BenchmarkClientHttpTest : public Test {
std::make_unique<StreamingStatistic>(), std::make_unique<StreamingStatistic>(),
std::make_unique<StreamingStatistic>(), std::make_unique<StreamingStatistic>(),
std::make_unique<StreamingStatistic>(), std::make_unique<StreamingStatistic>(),
std::make_unique<StreamingStatistic>(), std::make_unique<StreamingStatistic>()) {
std::make_unique<StreamingStatistic>(), std::make_unique<StreamingStatistic>(),
std::make_unique<StreamingStatistic>()) {
auto header_map_param = std::initializer_list<std::pair<std::string, std::string>>{
{":scheme", "http"}, {":method", "GET"}, {":path", "/"}, {":host", "localhost"}};
default_header_map_ =
Expand Down
14 changes: 11 additions & 3 deletions test/server/http_test_server_filter_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,13 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestTooLarge) {
}

TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) {
const std::string kPreviousRequestDeltaHeader = "x-prd";
Envoy::BufferingStreamDecoderPtr response = makeSingleRequest(
lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, "foo.com", "",
[](Envoy::Http::RequestHeaderMapImpl& request_headers) {
const std::string header_config =
R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})";
[kPreviousRequestDeltaHeader](Envoy::Http::RequestHeaderMapImpl& request_headers) {
const std::string header_config = fmt::format(
R"({{response_headers: [ {{ header: {{ key: "foo", value: "bar2"}}, append: true }} ], emit_previous_request_delta_in_response_header: "{}"}})",
kPreviousRequestDeltaHeader);
request_headers.addCopy(Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig,
header_config);
});
Expand All @@ -283,6 +285,12 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) {
EXPECT_EQ("bar2",
response->headers().get(Envoy::Http::LowerCaseString("foo"))->value().getStringView());
EXPECT_EQ("", response->body());
uint64_t dummy;
EXPECT_TRUE(absl::SimpleAtoi(response->headers()
.get(Envoy::Http::LowerCaseString(kPreviousRequestDeltaHeader))
->value()
.getStringView(),
&dummy));
}

TEST_P(HttpTestServerIntegrationNoConfigTest, BadTestHeaderConfig) {
Expand Down
Loading