h2load: Add req/s min, max, mean and sd for clients
This commit is contained in:
parent
d1d1c83e56
commit
6beaf4d9f3
|
@ -86,6 +86,21 @@ time for 1st byte (of (decrypted in case of TLS) application data)
|
||||||
deviation range (mean +/- sd) against total number of successful
|
deviation range (mean +/- sd) against total number of successful
|
||||||
connections.
|
connections.
|
||||||
|
|
||||||
|
req/s (client)
|
||||||
|
min
|
||||||
|
The minimum request per second among all clients.
|
||||||
|
max
|
||||||
|
The maximum request per second among all clients.
|
||||||
|
mean
|
||||||
|
The mean request per second among all clients.
|
||||||
|
sd
|
||||||
|
The standard deviation of request per second among all clients.
|
||||||
|
server.
|
||||||
|
+/- sd
|
||||||
|
The fraction of the number of connections within standard
|
||||||
|
deviation range (mean +/- sd) against total number of successful
|
||||||
|
connections.
|
||||||
|
|
||||||
FLOW CONTROL
|
FLOW CONTROL
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|
|
@ -309,6 +309,7 @@ int Client::make_socket(addrinfo *addr) {
|
||||||
int Client::connect() {
|
int Client::connect() {
|
||||||
int rv;
|
int rv;
|
||||||
|
|
||||||
|
record_client_start_time();
|
||||||
clear_connect_times();
|
clear_connect_times();
|
||||||
record_connect_start_time();
|
record_connect_start_time();
|
||||||
|
|
||||||
|
@ -391,6 +392,8 @@ void Client::fail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::disconnect() {
|
void Client::disconnect() {
|
||||||
|
record_client_end_time();
|
||||||
|
|
||||||
ev_timer_stop(worker->loop, &conn_inactivity_watcher);
|
ev_timer_stop(worker->loop, &conn_inactivity_watcher);
|
||||||
ev_timer_stop(worker->loop, &conn_active_watcher);
|
ev_timer_stop(worker->loop, &conn_active_watcher);
|
||||||
ev_timer_stop(worker->loop, &request_timeout_watcher);
|
ev_timer_stop(worker->loop, &request_timeout_watcher);
|
||||||
|
@ -610,6 +613,8 @@ void Client::on_stream_close(int32_t stream_id, bool success,
|
||||||
if (success) {
|
if (success) {
|
||||||
req_stat->completed = true;
|
req_stat->completed = true;
|
||||||
++worker->stats.req_success;
|
++worker->stats.req_success;
|
||||||
|
auto &cstat = worker->stats.client_stats[id];
|
||||||
|
++cstat.req_success;
|
||||||
}
|
}
|
||||||
++worker->stats.req_done;
|
++worker->stats.req_done;
|
||||||
++req_done;
|
++req_done;
|
||||||
|
@ -1006,6 +1011,26 @@ void Client::clear_connect_times() {
|
||||||
cstat.ttfb = std::chrono::steady_clock::time_point();
|
cstat.ttfb = std::chrono::steady_clock::time_point();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Client::record_client_start_time() {
|
||||||
|
auto &cstat = worker->stats.client_stats[id];
|
||||||
|
|
||||||
|
// Record start time only once at the very first connection is going
|
||||||
|
// to be made.
|
||||||
|
if (recorded(cstat.client_start_time)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cstat.client_start_time = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::record_client_end_time() {
|
||||||
|
auto &cstat = worker->stats.client_stats[id];
|
||||||
|
|
||||||
|
// Unlike client_start_time, we overwrite client_end_time. This
|
||||||
|
// handles multiple connect/disconnect for HTTP/1.1 benchmark.
|
||||||
|
cstat.client_end_time = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
void Client::signal_write() { ev_io_start(worker->loop, &wev); }
|
void Client::signal_write() { ev_io_start(worker->loop, &wev); }
|
||||||
|
|
||||||
void Client::try_new_connection() { new_connection_requested = true; }
|
void Client::try_new_connection() { new_connection_requested = true; }
|
||||||
|
@ -1065,9 +1090,7 @@ void Worker::run() {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Returns percentage of number of samples within mean +/- sd.
|
// Returns percentage of number of samples within mean +/- sd.
|
||||||
template <typename Duration>
|
double within_sd(const std::vector<double> &samples, double mean, double sd) {
|
||||||
double within_sd(const std::vector<Duration> &samples, const Duration &mean,
|
|
||||||
const Duration &sd) {
|
|
||||||
if (samples.size() == 0) {
|
if (samples.size() == 0) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
@ -1075,7 +1098,7 @@ double within_sd(const std::vector<Duration> &samples, const Duration &mean,
|
||||||
auto upper = mean + sd;
|
auto upper = mean + sd;
|
||||||
auto m = std::count_if(
|
auto m = std::count_if(
|
||||||
std::begin(samples), std::end(samples),
|
std::begin(samples), std::end(samples),
|
||||||
[&lower, &upper](const Duration &t) { return lower <= t && t <= upper; });
|
[&lower, &upper](double t) { return lower <= t && t <= upper; });
|
||||||
return (m / static_cast<double>(samples.size())) * 100;
|
return (m / static_cast<double>(samples.size())) * 100;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
@ -1083,32 +1106,31 @@ double within_sd(const std::vector<Duration> &samples, const Duration &mean,
|
||||||
namespace {
|
namespace {
|
||||||
// Computes statistics using |samples|. The min, max, mean, sd, and
|
// Computes statistics using |samples|. The min, max, mean, sd, and
|
||||||
// percentage of number of samples within mean +/- sd are computed.
|
// percentage of number of samples within mean +/- sd are computed.
|
||||||
template <typename Duration>
|
SDStat compute_time_stat(const std::vector<double> &samples) {
|
||||||
TimeStat<Duration> compute_time_stat(const std::vector<Duration> &samples) {
|
|
||||||
if (samples.empty()) {
|
if (samples.empty()) {
|
||||||
return {Duration::zero(), Duration::zero(), Duration::zero(),
|
return {0.0, 0.0, 0.0, 0.0, 0.0};
|
||||||
Duration::zero(), 0.0};
|
|
||||||
}
|
}
|
||||||
// standard deviation calculated using Rapid calculation method:
|
// standard deviation calculated using Rapid calculation method:
|
||||||
// http://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods
|
// http://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods
|
||||||
double a = 0, q = 0;
|
double a = 0, q = 0;
|
||||||
size_t n = 0;
|
size_t n = 0;
|
||||||
int64_t sum = 0;
|
double sum = 0;
|
||||||
auto res = TimeStat<Duration>{Duration::max(), Duration::min()};
|
auto res = SDStat{std::numeric_limits<double>::max(),
|
||||||
|
std::numeric_limits<double>::min()};
|
||||||
for (const auto &t : samples) {
|
for (const auto &t : samples) {
|
||||||
++n;
|
++n;
|
||||||
res.min = std::min(res.min, t);
|
res.min = std::min(res.min, t);
|
||||||
res.max = std::max(res.max, t);
|
res.max = std::max(res.max, t);
|
||||||
sum += t.count();
|
sum += t;
|
||||||
|
|
||||||
auto na = a + (t.count() - a) / n;
|
auto na = a + (t - a) / n;
|
||||||
q += (t.count() - a) * (t.count() - na);
|
q += (t - a) * (t - na);
|
||||||
a = na;
|
a = na;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(n > 0);
|
assert(n > 0);
|
||||||
res.mean = Duration(sum / n);
|
res.mean = sum / n;
|
||||||
res.sd = Duration(static_cast<typename Duration::rep>(sqrt(q / n)));
|
res.sd = sqrt(q / n);
|
||||||
res.within_sd = within_sd(samples, res.mean, res.sd);
|
res.within_sd = within_sd(samples, res.mean, res.sd);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
@ -1116,19 +1138,20 @@ TimeStat<Duration> compute_time_stat(const std::vector<Duration> &samples) {
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
TimeStats
|
SDStats
|
||||||
process_time_stats(const std::vector<std::unique_ptr<Worker>> &workers) {
|
process_time_stats(const std::vector<std::unique_ptr<Worker>> &workers) {
|
||||||
size_t nrequest_times = 0;
|
size_t nrequest_times = 0;
|
||||||
for (const auto &w : workers) {
|
for (const auto &w : workers) {
|
||||||
nrequest_times += w->stats.req_stats.size();
|
nrequest_times += w->stats.req_stats.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::chrono::microseconds> request_times;
|
std::vector<double> request_times;
|
||||||
request_times.reserve(nrequest_times);
|
request_times.reserve(nrequest_times);
|
||||||
|
|
||||||
std::vector<std::chrono::microseconds> connect_times, ttfb_times;
|
std::vector<double> connect_times, ttfb_times, rps_values;
|
||||||
connect_times.reserve(config.nclients);
|
connect_times.reserve(config.nclients);
|
||||||
ttfb_times.reserve(config.nclients);
|
ttfb_times.reserve(config.nclients);
|
||||||
|
rps_values.reserve(config.nclients);
|
||||||
|
|
||||||
for (const auto &w : workers) {
|
for (const auto &w : workers) {
|
||||||
for (const auto &req_stat : w->stats.req_stats) {
|
for (const auto &req_stat : w->stats.req_stats) {
|
||||||
|
@ -1136,13 +1159,22 @@ process_time_stats(const std::vector<std::unique_ptr<Worker>> &workers) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
request_times.push_back(
|
request_times.push_back(
|
||||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
std::chrono::duration_cast<std::chrono::duration<double>>(
|
||||||
req_stat.stream_close_time - req_stat.request_time));
|
req_stat.stream_close_time - req_stat.request_time).count());
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto &stat = w->stats;
|
const auto &stat = w->stats;
|
||||||
|
|
||||||
for (const auto &cstat : stat.client_stats) {
|
for (const auto &cstat : stat.client_stats) {
|
||||||
|
if (recorded(cstat.client_start_time) &&
|
||||||
|
recorded(cstat.client_end_time)) {
|
||||||
|
auto t = std::chrono::duration_cast<std::chrono::duration<double>>(
|
||||||
|
cstat.client_end_time - cstat.client_start_time).count();
|
||||||
|
if (t > 1e-9) {
|
||||||
|
rps_values.push_back(cstat.req_success / t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We will get connect event before FFTB.
|
// We will get connect event before FFTB.
|
||||||
if (!recorded(cstat.connect_start_time) ||
|
if (!recorded(cstat.connect_start_time) ||
|
||||||
!recorded(cstat.connect_time)) {
|
!recorded(cstat.connect_time)) {
|
||||||
|
@ -1150,21 +1182,21 @@ process_time_stats(const std::vector<std::unique_ptr<Worker>> &workers) {
|
||||||
}
|
}
|
||||||
|
|
||||||
connect_times.push_back(
|
connect_times.push_back(
|
||||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
std::chrono::duration_cast<std::chrono::duration<double>>(
|
||||||
cstat.connect_time - cstat.connect_start_time));
|
cstat.connect_time - cstat.connect_start_time).count());
|
||||||
|
|
||||||
if (!recorded(cstat.ttfb)) {
|
if (!recorded(cstat.ttfb)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ttfb_times.push_back(
|
ttfb_times.push_back(
|
||||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
std::chrono::duration_cast<std::chrono::duration<double>>(
|
||||||
cstat.ttfb - cstat.connect_start_time));
|
cstat.ttfb - cstat.connect_start_time).count());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {compute_time_stat(request_times), compute_time_stat(connect_times),
|
return {compute_time_stat(request_times), compute_time_stat(connect_times),
|
||||||
compute_time_stat(ttfb_times)};
|
compute_time_stat(ttfb_times), compute_time_stat(rps_values)};
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
@ -2220,7 +2252,11 @@ time for request: )" << std::setw(10) << util::format_duration(ts.request.min)
|
||||||
<< util::format_duration(ts.ttfb.max) << " " << std::setw(10)
|
<< util::format_duration(ts.ttfb.max) << " " << std::setw(10)
|
||||||
<< util::format_duration(ts.ttfb.mean) << " " << std::setw(10)
|
<< util::format_duration(ts.ttfb.mean) << " " << std::setw(10)
|
||||||
<< util::format_duration(ts.ttfb.sd) << std::setw(9)
|
<< util::format_duration(ts.ttfb.sd) << std::setw(9)
|
||||||
<< util::dtos(ts.ttfb.within_sd) << "%" << std::endl;
|
<< util::dtos(ts.ttfb.within_sd) << "%"
|
||||||
|
<< "\nreq/s (client) : " << std::setw(10) << ts.rps.min << " "
|
||||||
|
<< std::setw(10) << ts.rps.max << " " << std::setw(10)
|
||||||
|
<< ts.rps.mean << " " << std::setw(10) << ts.rps.sd << std::setw(9)
|
||||||
|
<< util::dtos(ts.rps.within_sd) << "%" << std::endl;
|
||||||
|
|
||||||
SSL_CTX_free(ssl_ctx);
|
SSL_CTX_free(ssl_ctx);
|
||||||
|
|
||||||
|
|
30
src/h2load.h
30
src/h2load.h
|
@ -125,6 +125,18 @@ struct RequestStat {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ClientStat {
|
struct ClientStat {
|
||||||
|
// time client started (i.e., first connect starts)
|
||||||
|
std::chrono::steady_clock::time_point client_start_time;
|
||||||
|
// time client end (i.e., client somehow processed all requests it
|
||||||
|
// is responsible for, and disconnected)
|
||||||
|
std::chrono::steady_clock::time_point client_end_time;
|
||||||
|
// The number of requests completed successfull, but not necessarily
|
||||||
|
// means successful HTTP status code.
|
||||||
|
size_t req_success;
|
||||||
|
|
||||||
|
// The following 3 numbers are overwritten each time when connection
|
||||||
|
// is made.
|
||||||
|
|
||||||
// time connect starts
|
// time connect starts
|
||||||
std::chrono::steady_clock::time_point connect_start_time;
|
std::chrono::steady_clock::time_point connect_start_time;
|
||||||
// time to connect
|
// time to connect
|
||||||
|
@ -133,24 +145,24 @@ struct ClientStat {
|
||||||
std::chrono::steady_clock::time_point ttfb;
|
std::chrono::steady_clock::time_point ttfb;
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename Duration> struct TimeStat {
|
struct SDStat {
|
||||||
// min, max, mean and sd (standard deviation)
|
// min, max, mean and sd (standard deviation)
|
||||||
Duration min, max, mean, sd;
|
double min, max, mean, sd;
|
||||||
// percentage of samples inside mean -/+ sd
|
// percentage of samples inside mean -/+ sd
|
||||||
double within_sd;
|
double within_sd;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TimeStats {
|
struct SDStats {
|
||||||
// time for request
|
// time for request
|
||||||
TimeStat<std::chrono::microseconds> request;
|
SDStat request;
|
||||||
// time for connect
|
// time for connect
|
||||||
TimeStat<std::chrono::microseconds> connect;
|
SDStat connect;
|
||||||
// time to first byte (TTFB)
|
// time to first byte (TTFB)
|
||||||
TimeStat<std::chrono::microseconds> ttfb;
|
SDStat ttfb;
|
||||||
|
// request per second for each client
|
||||||
|
SDStat rps;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum TimeStatType { STAT_REQUEST, STAT_CONNECT, STAT_FIRST_BYTE };
|
|
||||||
|
|
||||||
struct Stats {
|
struct Stats {
|
||||||
Stats(size_t req_todo, size_t nclients);
|
Stats(size_t req_todo, size_t nclients);
|
||||||
// The total number of requests
|
// The total number of requests
|
||||||
|
@ -314,6 +326,8 @@ struct Client {
|
||||||
void record_connect_time();
|
void record_connect_time();
|
||||||
void record_ttfb();
|
void record_ttfb();
|
||||||
void clear_connect_times();
|
void clear_connect_times();
|
||||||
|
void record_client_start_time();
|
||||||
|
void record_client_end_time();
|
||||||
|
|
||||||
void signal_write();
|
void signal_write();
|
||||||
};
|
};
|
||||||
|
|
14
src/util.cc
14
src/util.cc
|
@ -1092,6 +1092,20 @@ std::string format_duration(const std::chrono::microseconds &u) {
|
||||||
return dtos(static_cast<double>(t) / d) + unit;
|
return dtos(static_cast<double>(t) / d) + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string format_duration(double t) {
|
||||||
|
const char *unit = "us";
|
||||||
|
if (t >= 1.) {
|
||||||
|
unit = "s";
|
||||||
|
} else if (t >= 0.001) {
|
||||||
|
t *= 1000.;
|
||||||
|
unit = "ms";
|
||||||
|
} else {
|
||||||
|
t *= 1000000.;
|
||||||
|
return utos(static_cast<int64_t>(t)) + unit;
|
||||||
|
}
|
||||||
|
return dtos(t) + unit;
|
||||||
|
}
|
||||||
|
|
||||||
std::string dtos(double n) {
|
std::string dtos(double n) {
|
||||||
auto f = utos(static_cast<int64_t>(round(100. * n)) % 100);
|
auto f = utos(static_cast<int64_t>(round(100. * n)) % 100);
|
||||||
return utos(static_cast<int64_t>(n)) + "." + (f.size() == 1 ? "0" : "") + f;
|
return utos(static_cast<int64_t>(n)) + "." + (f.size() == 1 ? "0" : "") + f;
|
||||||
|
|
|
@ -585,6 +585,9 @@ std::string duration_str(double t);
|
||||||
// fractional digits follow.
|
// fractional digits follow.
|
||||||
std::string format_duration(const std::chrono::microseconds &u);
|
std::string format_duration(const std::chrono::microseconds &u);
|
||||||
|
|
||||||
|
// Just like above, but this takes |t| as seconds.
|
||||||
|
std::string format_duration(double t);
|
||||||
|
|
||||||
// Creates "host:port" string using given |host| and |port|. If
|
// Creates "host:port" string using given |host| and |port|. If
|
||||||
// |host| is numeric IPv6 address (e.g., ::1), it is enclosed by "["
|
// |host| is numeric IPv6 address (e.g., ::1), it is enclosed by "["
|
||||||
// and "]". If |port| is 80 or 443, port part is omitted.
|
// and "]". If |port| is 80 or 443, port part is omitted.
|
||||||
|
|
|
@ -341,6 +341,15 @@ void test_util_format_duration(void) {
|
||||||
util::format_duration(std::chrono::microseconds(1000000)));
|
util::format_duration(std::chrono::microseconds(1000000)));
|
||||||
CU_ASSERT("1.05s" ==
|
CU_ASSERT("1.05s" ==
|
||||||
util::format_duration(std::chrono::microseconds(1050000)));
|
util::format_duration(std::chrono::microseconds(1050000)));
|
||||||
|
|
||||||
|
CU_ASSERT("0us" == util::format_duration(0.));
|
||||||
|
CU_ASSERT("999us" == util::format_duration(0.000999));
|
||||||
|
CU_ASSERT("1.00ms" == util::format_duration(0.001));
|
||||||
|
CU_ASSERT("1.09ms" == util::format_duration(0.00109));
|
||||||
|
CU_ASSERT("1.01ms" == util::format_duration(0.001009));
|
||||||
|
CU_ASSERT("999.99ms" == util::format_duration(0.99999));
|
||||||
|
CU_ASSERT("1.00s" == util::format_duration(1.));
|
||||||
|
CU_ASSERT("1.05s" == util::format_duration(1.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_util_starts_with(void) {
|
void test_util_starts_with(void) {
|
||||||
|
|
Loading…
Reference in New Issue