From d808748cd62bec919fb6242fa700f848914dfec8 Mon Sep 17 00:00:00 2001 From: Andy Green Date: Fri, 23 Aug 2019 16:10:36 +0100 Subject: [PATCH] detailed latency stats Remove LWS_LATENCY. Add the option LWS_WITH_DETAILED_LATENCY, allowing lws to collect very detailed information on every read and write, and allow the user code to provide a callback to process events. --- CMakeLists.txt | 12 +- READMEs/README.detailed-latency.md | 117 +++++++++++++++ cmake/lws_config.h.in | 2 +- doc-assets/lws-detailed-latency-example.png | Bin 0 -> 50833 bytes include/libwebsockets.h | 1 + include/libwebsockets/lws-context-vhost.h | 5 + include/libwebsockets/lws-detailed-latency.h | 140 ++++++++++++++++++ lib/core-net/connect.c | 8 + lib/core-net/detailed-latency.c | 79 ++++++++++ lib/core-net/network.c | 7 +- lib/core-net/output.c | 47 ++++-- lib/core-net/pollfd.c | 6 +- lib/core-net/service.c | 13 +- lib/core-net/vhost.c | 11 +- lib/core-net/wsi.c | 41 ----- lib/core/context.c | 20 +-- lib/core/private-lib-core.h | 57 +++---- lib/plat/freertos/freertos-service.c | 10 ++ lib/plat/unix/unix-service.c | 9 ++ lib/roles/h2/http2.c | 11 +- lib/roles/http/client/client-handshake.c | 52 ++++++- lib/roles/http/client/client-http.c | 23 ++- lib/roles/http/header.c | 6 + lib/roles/listen/ops-listen.c | 3 - lib/tls/mbedtls/mbedtls-client.c | 3 - lib/tls/mbedtls/mbedtls-ssl.c | 12 +- lib/tls/openssl/openssl-client.c | 3 - lib/tls/openssl/openssl-ssl.c | 12 ++ lib/tls/tls-client.c | 21 +-- lib/tls/tls-server.c | 26 ++-- .../minimal-http-client-multi.c | 8 + 31 files changed, 601 insertions(+), 164 deletions(-) create mode 100644 READMEs/README.detailed-latency.md create mode 100644 doc-assets/lws-detailed-latency-example.png create mode 100644 include/libwebsockets/lws-detailed-latency.h create mode 100644 lib/core-net/detailed-latency.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 3779924ca..54a1b3739 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,6 @@ option(LWS_WITHOUT_EXTENSIONS "Don't compile with extensions" ON) option(LWS_WITHOUT_BUILTIN_GETIFADDRS "Don't use the BSD getifaddrs implementation from libwebsockets if it is missing (this will result in a compilation error) ... The default is to assume that your libc provides it. On some systems such as uclibc it doesn't exist." OFF) option(LWS_FALLBACK_GETHOSTBYNAME "Also try to do dns resolution using gethostbyname if getaddrinfo fails" OFF) option(LWS_WITHOUT_BUILTIN_SHA1 "Don't build the lws sha-1 (eg, because openssl will provide it" OFF) -option(LWS_WITH_LATENCY "Build latency measuring code into the library" OFF) option(LWS_WITHOUT_DAEMONIZE "Don't build the daemonization api" ON) option(LWS_SSL_SERVER_WITH_ECDH_CERT "Include SSL server use ECDH certificate" OFF) option(LWS_WITH_LEJP "With the Lightweight JSON Parser" ON) @@ -139,6 +138,7 @@ option(LWS_WITH_EXTERNAL_POLL "Support external POLL integration using callback option(LWS_WITH_LWS_DSH "Support lws_dsh_t Disordered Shared Heap" OFF) option(LWS_CLIENT_HTTP_PROXYING "Support external http proxies for client connections" ON) option(LWS_WITH_FILE_OPS "Support file operations vfs" ON) +option(LWS_WITH_DETAILED_LATENCY "Record detailed latency stats for each read and write" OFF) # # to use miniz, enable both LWS_WITH_ZLIB and LWS_WITH_MINIZ # @@ -677,10 +677,6 @@ if (LWS_SSL_CLIENT_USE_OS_CA_CERTS) set(LWS_SSL_CLIENT_USE_OS_CA_CERTS 1) endif() -if (LWS_WITH_LATENCY) - set(LWS_LATENCY 1) -endif() - if (LWS_WITHOUT_DAEMONIZE OR WIN32) set(LWS_NO_DAEMONIZE 1) endif() @@ -1030,6 +1026,11 @@ if (LWS_WITH_NETWORK) lib/system/async-dns/async-dns-parse.c) endif() + if (LWS_WITH_DETAILED_LATENCY) + list(APPEND SOURCES + lib/core-net/detailed-latency.c) + endif() + if (LWS_WITH_LWS_DSH) list(APPEND SOURCES lib/core-net/lws-dsh.c) @@ -2739,7 +2740,6 @@ message(" LWS_WITHOUT_TEST_SERVER_EXTPOLL = ${LWS_WITHOUT_TEST_SERVER_EXTPOLL}") message(" LWS_WITHOUT_TEST_PING = ${LWS_WITHOUT_TEST_PING}") message(" LWS_WITHOUT_TEST_CLIENT = ${LWS_WITHOUT_TEST_CLIENT}") message(" LWS_WITHOUT_EXTENSIONS = ${LWS_WITHOUT_EXTENSIONS}") -message(" LWS_WITH_LATENCY = ${LWS_WITH_LATENCY}") message(" LWS_WITHOUT_DAEMONIZE = ${LWS_WITHOUT_DAEMONIZE}") message(" LWS_WITH_LIBEV = ${LWS_WITH_LIBEV}") message(" LWS_WITH_LIBUV = ${LWS_WITH_LIBUV}") diff --git a/READMEs/README.detailed-latency.md b/READMEs/README.detailed-latency.md new file mode 100644 index 000000000..29e29d03a --- /dev/null +++ b/READMEs/README.detailed-latency.md @@ -0,0 +1,117 @@ +# lws detailed latency + +![lws detailed latency example plot](../doc-assets/lws-detailed-latency-example.png) + +## Introduction + +lws has the capability to make detailed latency measurements and +report them in realtime to a specified callback. + +A default callback is provided that renders the data as text in +space-separated format suitable for gnuplot, to a specified file. + +## Configuring + +Enable `LWS_WITH_DETAILED_LATENCY` at cmake. + +Create your context with something similar to this + +``` +#if defined(LWS_WITH_DETAILED_LATENCY) + info.detailed_latency_cb = lws_det_lat_plot_cb; + info.detailed_latency_filepath = "/tmp/lws-latency-results"; +#endif +``` + +`lws_det_lat_plot_cb` is provided by lws as a convenience to convert +the stuct data provided at the callback interface to space-separated +text data that is easy to process with shell commands and gnuplot. + +## `lws_det_lat_plot_cb` format + +``` +728239173547 N 23062 0 0 23062 0 0 0 +728239192554 C 18879 0 0 18879 0 0 0 +728239217894 T 25309 0 0 25309 0 0 0 +728239234998 r 0 0 0 0 271 172 256 +728239250611 r 0 0 0 0 69 934 4096 +728239255679 w 19 122 18 159 20 80 80 +728239275718 w 20 117 15 152 18 80 80 +728239295578 w 10 73 7 90 7 80 80 +728239315567 w 9 67 5 81 7 80 80 +728239335745 w 23 133 9 165 14 80 80 +... +``` + +Each event is shown in 9 columns + + - unix time in us + - event type + - N = Name resolution + - C = TCP Connection + - T = TLS negotiation server + - t = TLS negotiation client + - r = Read + - w = Write + - us duration, for w time client spent waiting to write + - us duration, for w time data spent in transit to proxy + - us duration, for w time proxy waited to send data + - as a convenience, sum of last 3 columns above + - us duration, time spent in callback + - last 2 are actual / requested size in bytes + +## Processing captured data with ministat + +Eg, to summarize overall latencies on all captured writes + +``` + $ cat /tmp/lws-latency-results | grep " w " | cut -d' ' -f6 | ministat +... + N Min Max Median Avg Stddev +x 1000 43 273 141 132.672 32.471693 +``` + +## Processing captured data with gnuplot + +### Gnuplot plotting script + +Create a gnuplot script, eg myscript.gp + +``` +reset +set term pngcairo enhanced nocrop font "OpenSans, 12" size 800,600#output terminal and file +set output "lws-latency.png" +#set yrange [0:10000] +#to put an empty boundary around the +#data inside an autoscaled graph. +set offset graph 0.05,0.05,0.05,0.0 +set style fill transparent solid 0.5 #fillstyle +set tics out nomirror +set xlabel "event" +set ylabel "latency (us)" +set format x "" +set title "Write latency" +set key invert reverse Right inside nobox +set key autotitle columnheader +set style data histogram +set style histogram rowstacked +set style fill solid border -1 +set boxwidth 0.75 +set style fill solid 1.00 noborder +set tic scale 0 +set grid ytics lc rgb "#505050" +unset border +unset xtics + +plot '/tmp/1' \ + using ($3 + $4 + $5):xtic(1) w boxes lt rgbcolor "blue" title 'prox wr wait', \ + '' using ($3 + $4):xtic(1) w boxes lt rgbcolor "green" title 'txfr to prox', \ + '' using 3:xtic(1) w boxes lt rgbcolor "red" title 'cli wri wait' +``` + +### gnuplot invocation + +``` + $ cat /tmp/lws-latency-results | grep " w " \>/tmp/1 ; gnuplot myscript.gp && eog lws-latency.png +``` + diff --git a/cmake/lws_config.h.in b/cmake/lws_config.h.in index ef120343b..9adf8f4ce 100644 --- a/cmake/lws_config.h.in +++ b/cmake/lws_config.h.in @@ -72,7 +72,6 @@ #cmakedefine LWS_HAVE_UV_VERSION_H #cmakedefine LWS_HAVE_X509_get_key_usage #cmakedefine LWS_HAVE_X509_VERIFY_PARAM_set1_host -#cmakedefine LWS_LATENCY #cmakedefine LWS_LIBRARY_VERSION "${LWS_LIBRARY_VERSION}" #cmakedefine LWS_MINGW_SUPPORT #cmakedefine LWS_NO_CLIENT @@ -100,6 +99,7 @@ #cmakedefine LWS_WITH_CGI #cmakedefine LWS_WITH_CUSTOM_HEADERS #cmakedefine LWS_WITH_DEPRECATED_LWS_DLL +#cmakedefine LWS_WITH_DETAILED_LATENCY #cmakedefine LWS_WITH_DIR #cmakedefine LWS_WITH_ESP32 #cmakedefine LWS_WITH_EXTERNAL_POLL diff --git a/doc-assets/lws-detailed-latency-example.png b/doc-assets/lws-detailed-latency-example.png new file mode 100644 index 0000000000000000000000000000000000000000..e23132b8762e95f7baf17b933e1ede4d5e316fb5 GIT binary patch literal 50833 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 +#include #include #include #include diff --git a/include/libwebsockets/lws-context-vhost.h b/include/libwebsockets/lws-context-vhost.h index dd9120057..2f1e9ec8d 100644 --- a/include/libwebsockets/lws-context-vhost.h +++ b/include/libwebsockets/lws-context-vhost.h @@ -677,6 +677,11 @@ struct lws_context_creation_info { const lws_system_ops_t *system_ops; /**< CONTEXT: hook up lws_system_ apis to system-specific * implementations */ + det_lat_buf_cb_t detailed_latency_cb; + /**< CONTEXT: NULL, or callback to receive detailed latency information + * collected for each read and write */ + const char *detailed_latency_filepath; + /**< CONTEXT: NULL, or filepath to put latency data into */ /* Add new things just above here ---^ * This is part of the ABI, don't needlessly break compatibility diff --git a/include/libwebsockets/lws-detailed-latency.h b/include/libwebsockets/lws-detailed-latency.h new file mode 100644 index 000000000..1b352c7a6 --- /dev/null +++ b/include/libwebsockets/lws-detailed-latency.h @@ -0,0 +1,140 @@ +/* + * libwebsockets - small server side websockets and web server implementation + * + * Copyright (C) 2010 - 2019 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * included from libwebsockets.h + */ + +enum { + + /* types of latency, all nonblocking except name resolution */ + + LDLT_READ, /* time taken to read LAT_DUR_PROXY_RX_TO_CLIENT_WRITE */ + LDLT_WRITE, + LDLT_NAME_RESOLUTION, /* BLOCKING: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */ + LDLT_CONNECTION, /* conn duration: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */ + LDLT_TLS_NEG_CLIENT, /* tls conn duration: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */ + LDLT_TLS_NEG_SERVER, /* tls conn duration: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */ + + LDLT_USER, + + /* interval / duration elements in latencies array */ + + LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE = 0, + /* us the client spent waiting to write to proxy */ + LAT_DUR_PROXY_CLIENT_WRITE_TO_PROXY_RX, + /* us the packet took to be received by proxy */ + LAT_DUR_PROXY_PROXY_REQ_TO_WRITE, + /* us the proxy has to wait before it could write */ + LAT_DUR_PROXY_RX_TO_ONWARD_TX, + /* us the proxy spent waiting to write to destination, or + * if nonproxied, then time between write request and write */ + + LAT_DUR_USERCB, /* us duration of user callback */ + + LAT_DUR_STEPS /* last */ +}; + +typedef struct lws_detlat { + lws_usec_t earliest_write_req; + lws_usec_t earliest_write_req_pre_write; + /**< use this for interval comparison */ + const char *aux; /* name for name resolution timing */ + int type; + uint32_t latencies[LAT_DUR_STEPS]; + size_t req_size; + size_t acc_size; +} lws_detlat_t; + +typedef int (*det_lat_buf_cb_t)(struct lws_context *context, + const lws_detlat_t *d); + +/** + * lws_det_lat_cb() - inject your own latency records + * + * \param context: the lws_context + * \param d: the lws_detlat_t you have prepared + * + * For proxying or similar cases where latency information is available from + * user code rather than lws itself, you can generate your own latency callback + * events with your own lws_detlat_t. + */ + +LWS_VISIBLE LWS_EXTERN int +lws_det_lat_cb(struct lws_context *context, lws_detlat_t *d); + +/* + * detailed_latency_plot_cb() - canned save to file in plottable format cb + * + * \p context: the lws_context + * \p d: the detailed latency event information + * + * This canned callback makes it easy to export the detailed latency information + * to a file. Just set the context creation members like this + * + * #if defined(LWS_WITH_DETAILED_LATENCY) + * info.detailed_latency_cb = lws_det_lat_plot_cb; + * info.detailed_latency_filepath = "/tmp/lws-latency-results"; + * #endif + * + * and you will get a file containing information like this + * + * 718823864615 N 10589 0 0 10589 0 0 0 + * 718823880837 C 16173 0 0 16173 0 0 0 + * 718823913063 T 32212 0 0 32212 0 0 0 + * 718823931835 r 0 0 0 0 232 30 256 + * 718823948757 r 0 0 0 0 40 30 256 + * 718823948799 r 0 0 0 0 83 30 256 + * 718823965602 r 0 0 0 0 27 30 256 + * 718823965617 r 0 0 0 0 43 30 256 + * 718823965998 r 0 0 0 0 12 28 256 + * 718823983887 r 0 0 0 0 74 3 4096 + * 718823986411 w 16 87 7 110 9 80 80 + * 718824006358 w 8 68 6 82 6 80 80 + * + * which is easy to grep and pass to gnuplot. + * + * The columns are + * + * - unix time in us + * - N = Name resolution, C = TCP Connection, T = TLS negotiation server, + * t = TLS negotiation client, r = Read, w = Write + * - us duration, for w time client spent waiting to write + * - us duration, for w time data spent in transit to proxy + * - us duration, for w time proxy waited to send data + * - as a convenience, sum of last 3 columns above + * - us duration, time spent in callback + * - last 2 are actual / requested size in bytes + */ +LWS_VISIBLE LWS_EXTERN int +lws_det_lat_plot_cb(struct lws_context *context, const lws_detlat_t *d); + +/** + * lws_det_lat_active() - indicates if latencies are being measured + * + * \context: lws_context + * + * Returns 0 if latency measurement has not been set up (the callback is NULL). + * Otherwise returns 1 + */ +LWS_VISIBLE LWS_EXTERN int +lws_det_lat_active(struct lws_context *context); diff --git a/lib/core-net/connect.c b/lib/core-net/connect.c index fd435c848..2bbcfaafe 100644 --- a/lib/core-net/connect.c +++ b/lib/core-net/connect.c @@ -61,6 +61,11 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) wsi->desc.sockfd = LWS_SOCK_INVALID; wsi->seq = i->seq; +#if defined(LWS_WITH_DETAILED_LATENCY) + if (i->context->detailed_latency_cb) + wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); +#endif + wsi->vhost = NULL; if (!i->vhost) lws_vhost_bind_wsi(i->context->vhost_list, wsi); @@ -91,6 +96,9 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) lwsl_info("%s: client binds to caller tsi %d\n", __func__, n); wsi->tsi = n; +#if defined(LWS_WITH_DETAILED_LATENCY) + wsi->detlat.tsi = n; +#endif break; } diff --git a/lib/core-net/detailed-latency.c b/lib/core-net/detailed-latency.c new file mode 100644 index 000000000..e176b4beb --- /dev/null +++ b/lib/core-net/detailed-latency.c @@ -0,0 +1,79 @@ +/* + * libwebsockets - small server side websockets and web server implementation + * + * Copyright (C) 2010 - 2019 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "private-lib-core.h" + +int +lws_det_lat_active(struct lws_context *context) +{ + return !!context->detailed_latency_cb; +} + +int +lws_det_lat_cb(struct lws_context *context, lws_detlat_t *d) +{ + int n; + + if (!context->detailed_latency_cb) + return 0; + + n = context->detailed_latency_cb(context, d); + + memset(&d->latencies, 0, sizeof(d->latencies)); + + return n; +} + +static const char types[] = "rwNCTt????"; +int +lws_det_lat_plot_cb(struct lws_context *context, const lws_detlat_t *d) +{ + char buf[80], *p = buf, *end = &p[sizeof(buf) - 1]; + + if (!context->detailed_latency_filepath) + return 1; + + if (context->latencies_fd == -1) { + context->latencies_fd = open(context->detailed_latency_filepath, + LWS_O_CREAT | LWS_O_TRUNC | LWS_O_WRONLY, 0600); + if (context->latencies_fd == -1) + return 1; + } + + p += lws_snprintf(p, lws_ptr_diff(end, p), + "%llu %c %u %u %u %u %u %zu %zu\n", + (unsigned long long)lws_now_usecs(), types[d->type], + d->latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE], + d->latencies[LAT_DUR_PROXY_CLIENT_WRITE_TO_PROXY_RX], + d->latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE], + d->latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] + + d->latencies[LAT_DUR_PROXY_CLIENT_WRITE_TO_PROXY_RX] + + d->latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE], + d->latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX], + d->acc_size, d->req_size); + + write(context->latencies_fd, buf, lws_ptr_diff(p, buf)); + + return 0; +} diff --git a/lib/core-net/network.c b/lib/core-net/network.c index 7c4250f04..7aa13587e 100644 --- a/lib/core-net/network.c +++ b/lib/core-net/network.c @@ -159,15 +159,11 @@ lws_get_peer_addresses(struct lws *wsi, lws_sockfd_type fd, char *name, struct sockaddr_in6 sin6; #endif struct sockaddr_in sin4; - struct lws_context *context = wsi->context; - int ret = -1; void *p; rip[0] = '\0'; name[0] = '\0'; - lws_latency_pre(context, wsi); - #ifdef LWS_WITH_IPV6 if (LWS_IPV6_ENABLED(wsi->vhost)) { len = sizeof(sin6); @@ -184,10 +180,9 @@ lws_get_peer_addresses(struct lws *wsi, lws_sockfd_type fd, char *name, goto bail; } - ret = lws_get_addresses(wsi->vhost, p, name, name_len, rip, rip_len); + lws_get_addresses(wsi->vhost, p, name, name_len, rip, rip_len); bail: - lws_latency(context, wsi, "lws_get_peer_addresses", ret, 1); #endif (void)wsi; (void)fd; diff --git a/lib/core-net/output.c b/lib/core-net/output.c index 1dac56eca..2838b7009 100644 --- a/lib/core-net/output.c +++ b/lib/core-net/output.c @@ -27,7 +27,8 @@ /* * notice this returns number of bytes consumed, or -1 */ -int lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len) +int +lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len) { struct lws_context *context = lws_get_context(wsi); struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi]; @@ -108,10 +109,8 @@ int lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len) n = (int)len; /* nope, send it on the socket directly */ - lws_latency_pre(context, wsi); - m = lws_ssl_capable_write(wsi, buf, n); - lws_latency(context, wsi, "send lws_issue_raw", n, n == m); + m = lws_ssl_capable_write(wsi, buf, n); lwsl_info("%s: ssl_capable_write (%d) says %d\n", __func__, n, m); /* something got written, it can have been truncated now */ @@ -220,10 +219,15 @@ int lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len) return (int)real_len; } -LWS_VISIBLE int lws_write(struct lws *wsi, unsigned char *buf, size_t len, - enum lws_write_protocol wp) +int +lws_write(struct lws *wsi, unsigned char *buf, size_t len, + enum lws_write_protocol wp) { struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi]; +#if defined(LWS_WITH_DETAILED_LATENCY) + lws_usec_t us; +#endif + int m; lws_stats_bump(pt, LWSSTATS_C_API_LWS_WRITE, 1); @@ -242,15 +246,38 @@ LWS_VISIBLE int lws_write(struct lws *wsi, unsigned char *buf, size_t len, if (wsi->vhost) wsi->vhost->conn_stats.tx += len; #endif +#if defined(LWS_WITH_DETAILED_LATENCY) + us = lws_now_usecs(); +#endif assert(wsi->role_ops); if (!wsi->role_ops->write_role_protocol) return lws_issue_raw(wsi, buf, len); - return wsi->role_ops->write_role_protocol(wsi, buf, len, &wp); + m = wsi->role_ops->write_role_protocol(wsi, buf, len, &wp); + if (m < 0) + return m; + +#if defined(LWS_WITH_DETAILED_LATENCY) + if (wsi->context->detailed_latency_cb) { + wsi->detlat.req_size = len; + wsi->detlat.acc_size = m; + wsi->detlat.type = LDLT_WRITE; + if (wsi->detlat.earliest_write_req_pre_write) + wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = + us - wsi->detlat.earliest_write_req_pre_write; + else + wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = 0; + wsi->detlat.latencies[LAT_DUR_USERCB] = lws_now_usecs() - us; + lws_det_lat_cb(wsi->context, &wsi->detlat); + + } +#endif + + return m; } -LWS_VISIBLE int +int lws_ssl_capable_read_no_ssl(struct lws *wsi, unsigned char *buf, int len) { struct lws_context *context = wsi->context; @@ -299,7 +326,7 @@ lws_ssl_capable_read_no_ssl(struct lws *wsi, unsigned char *buf, int len) return LWS_SSL_CAPABLE_ERROR; } -LWS_VISIBLE int +int lws_ssl_capable_write_no_ssl(struct lws *wsi, unsigned char *buf, int len) { int n = 0; @@ -339,7 +366,7 @@ lws_ssl_capable_write_no_ssl(struct lws *wsi, unsigned char *buf, int len) return LWS_SSL_CAPABLE_ERROR; } -LWS_VISIBLE int +int lws_ssl_pending_no_ssl(struct lws *wsi) { (void)wsi; diff --git a/lib/core-net/pollfd.c b/lib/core-net/pollfd.c index abdbff311..69ea94152 100644 --- a/lib/core-net/pollfd.c +++ b/lib/core-net/pollfd.c @@ -511,6 +511,11 @@ lws_callback_on_writable(struct lws *wsi) pt = &wsi->context->pt[(int)wsi->tsi]; +#if defined(LWS_WITH_DETAILED_LATENCY) + if (!wsi->detlat.earliest_write_req) + wsi->detlat.earliest_write_req = lws_now_usecs(); +#endif + lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB_REQ, 1); #if defined(LWS_WITH_STATS) if (!wsi->active_writable_req_us) { @@ -519,7 +524,6 @@ lws_callback_on_writable(struct lws *wsi) } #endif - if (wsi->role_ops->callback_on_writable) { if (wsi->role_ops->callback_on_writable(wsi)) return 1; diff --git a/lib/core-net/service.c b/lib/core-net/service.c index c2c02cd92..5f3f73d32 100644 --- a/lib/core-net/service.c +++ b/lib/core-net/service.c @@ -41,7 +41,17 @@ lws_callback_as_writeable(struct lws *wsi) wsi->active_writable_req_us = 0; } #endif +#if defined(LWS_WITH_DETAILED_LATENCY) + if (wsi->context->detailed_latency_cb) { + lws_usec_t us = lws_now_usecs(); + wsi->detlat.earliest_write_req_pre_write = + wsi->detlat.earliest_write_req; + wsi->detlat.earliest_write_req = 0; + wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = + ((uint32_t)us - wsi->detlat.earliest_write_req_pre_write); + } +#endif n = wsi->role_ops->writeable_cb[lwsi_role_server(wsi)]; m = user_callback_handle_rxflow(wsi->protocol->callback, @@ -356,8 +366,7 @@ lws_buflist_aware_read(struct lws_context_per_thread *pt, struct lws *wsi, /* stash what we read */ - n = lws_buflist_append_segment(&wsi->buflist, ebuf->token, - ebuf->len); + n = lws_buflist_append_segment(&wsi->buflist, ebuf->token, ebuf->len); if (n < 0) return -1; if (n) { diff --git a/lib/core-net/vhost.c b/lib/core-net/vhost.c index 096299fcf..1b7e18e9b 100644 --- a/lib/core-net/vhost.c +++ b/lib/core-net/vhost.c @@ -289,7 +289,7 @@ lws_protocol_init(struct lws_context *context) { struct lws_vhost *vh = context->vhost_list; const struct lws_protocol_vhost_options *pvo, *pvo1; - struct lws wsi; + struct lws *wsi = context->pt[0].fake_wsi; int n, any = 0; if (context->doing_protocol_init) @@ -297,13 +297,12 @@ lws_protocol_init(struct lws_context *context) context->doing_protocol_init = 1; - memset(&wsi, 0, sizeof(wsi)); - wsi.context = context; + wsi->context = context; lwsl_info("%s\n", __func__); while (vh) { - wsi.vhost = vh; + wsi->vhost = vh; /* only do the protocol init once for a given vhost */ if (vh->created_vhost_protocols || @@ -313,7 +312,7 @@ lws_protocol_init(struct lws_context *context) /* initialize supported protocols on this vhost */ for (n = 0; n < vh->count_protocols; n++) { - wsi.protocol = &vh->protocols[n]; + wsi->protocol = &vh->protocols[n]; if (!vh->protocols[n].name) continue; pvo = lws_vhost_protocol_options(vh, @@ -366,7 +365,7 @@ lws_protocol_init(struct lws_context *context) * NOTE the wsi is all zeros except for the context, vh * + protocol ptrs so lws_get_context(wsi) etc can work */ - if (vh->protocols[n].callback(&wsi, + if (vh->protocols[n].callback(wsi, LWS_CALLBACK_PROTOCOL_INIT, NULL, (void *)pvo, 0)) { if (vh->protocol_vh_privs[n]) { diff --git a/lib/core-net/wsi.c b/lib/core-net/wsi.c index 018ef126f..66dda34c5 100644 --- a/lib/core-net/wsi.c +++ b/lib/core-net/wsi.c @@ -751,47 +751,6 @@ lws_get_context(const struct lws *wsi) return wsi->context; } -#ifdef LWS_LATENCY -void -lws_latency(struct lws_context *context, struct lws *wsi, const char *action, - int ret, int completed) -{ - unsigned long long u; - char buf[256]; - - u = lws_now_usecs(); - - if (!action) { - wsi->latency_start = u; - if (!wsi->action_start) - wsi->action_start = u; - return; - } - if (completed) { - if (wsi->action_start == wsi->latency_start) - sprintf(buf, - "Completion first try lat %lluus: %p: ret %d: %s\n", - u - wsi->latency_start, - (void *)wsi, ret, action); - else - sprintf(buf, - "Completion %lluus: lat %lluus: %p: ret %d: %s\n", - u - wsi->action_start, - u - wsi->latency_start, - (void *)wsi, ret, action); - wsi->action_start = 0; - } else - sprintf(buf, "lat %lluus: %p: ret %d: %s\n", - u - wsi->latency_start, (void *)wsi, ret, action); - - if (u - wsi->latency_start > context->worst_latency) { - context->worst_latency = u - wsi->latency_start; - strcpy(context->worst_latency_info, buf); - } - lwsl_latency("%s", buf); -} -#endif - LWS_VISIBLE int LWS_WARN_UNUSED_RESULT lws_raw_transaction_completed(struct lws *wsi) { diff --git a/lib/core/context.c b/lib/core/context.c index ec9ba4435..ca32249d1 100644 --- a/lib/core/context.c +++ b/lib/core/context.c @@ -144,6 +144,11 @@ lws_create_context(const struct lws_context_creation_info *info) context->pt_serv_buf_size = s1; #if defined(LWS_WITH_NETWORK) context->count_threads = count_threads; +#if defined(LWS_WITH_DETAILED_LATENCY) + context->detailed_latency_cb = info->detailed_latency_cb; + context->detailed_latency_filepath = info->detailed_latency_filepath; + context->latencies_fd = -1; +#endif #endif /* if he gave us names, set the uid / gid */ @@ -648,6 +653,11 @@ lws_context_destroy3(struct lws_context *context) #endif lws_context_deinit_ssl_library(context); +#if defined(LWS_WITH_DETAILED_LATENCIES) + if (context->latencies_fd != -1) + compatible_close(context->latencies_fd); +#endif + lws_free(context); lwsl_info("%s: ctx %p freed\n", __func__, context); @@ -764,7 +774,6 @@ lws_context_destroy(struct lws_context *context) volatile struct lws_foreign_thread_pollfd *ftp, *next; volatile struct lws_context_per_thread *vpt; struct lws_vhost *vh = NULL; - struct lws wsi; int n, m; #endif @@ -800,18 +809,11 @@ lws_context_destroy(struct lws_context *context) #if defined(LWS_WITH_NETWORK) m = context->count_threads; - memset(&wsi, 0, sizeof(wsi)); - wsi.context = context; - -#ifdef LWS_LATENCY - if (context->worst_latency_info[0]) - lwsl_notice("Worst latency: %s\n", context->worst_latency_info); -#endif while (m--) { struct lws_context_per_thread *pt = &context->pt[m]; - vpt = (volatile struct lws_context_per_thread *)pt; + vpt = (volatile struct lws_context_per_thread *)pt; ftp = vpt->foreign_pfd_list; while (ftp) { next = ftp->next; diff --git a/lib/core/private-lib-core.h b/lib/core/private-lib-core.h index 7c318c449..cf72f27eb 100644 --- a/lib/core/private-lib-core.h +++ b/lib/core/private-lib-core.h @@ -286,6 +286,7 @@ struct lws_context { struct lws_mutex_refcount mr; #endif +#if defined(LWS_WITH_NETWORK) #if defined(LWS_WITH_LIBEV) struct lws_context_eventlibs_libev ev; #endif @@ -309,22 +310,21 @@ struct lws_context { struct lws_vhost *vhost_list; struct lws_vhost *no_listener_vhost_list; struct lws_vhost *vhost_pending_destruction_list; - struct lws_context **pcontext_finalize; - const char *username, *groupname; #if defined(LWS_WITH_SERVER) const char *server_string; #endif struct lws_event_loop_ops *event_loop_ops; - -#if defined(LWS_WITH_FILE_OPS) - const struct lws_plat_file_ops *fops; #endif #if defined(LWS_WITH_TLS) const struct lws_tls_ops *tls_ops; #endif + +#if defined(LWS_WITH_DETAILED_LATENCY) + det_lat_buf_cb_t detailed_latency_cb; +#endif #if defined(LWS_WITH_PLUGINS) struct lws_plugin *plugin_list; #endif @@ -337,6 +337,16 @@ struct lws_context { #endif #endif /* NETWORK */ +#if defined(LWS_WITH_FILE_OPS) + const struct lws_plat_file_ops *fops; +#endif + + struct lws_context **pcontext_finalize; + const char *username, *groupname; +#if defined(LWS_WITH_DETAILED_LATENCY) + const char *detailed_latency_filepath; +#endif + #if defined(LWS_AMAZON_RTOS) mbedtls_entropy_context mec; mbedtls_ctr_drbg_context mcdc; @@ -364,14 +374,15 @@ struct lws_context { #endif void (*eventlib_signal_cb)(void *event_lib_handle, int signum); - time_t last_ws_ping_pong_check_s; - lws_usec_t time_up; /* monotonic */ - #if defined(LWS_HAVE_SYS_CAPABILITY_H) && defined(LWS_HAVE_LIBCAP) cap_value_t caps[4]; char count_caps; #endif + lws_usec_t time_up; /* monotonic */ + + time_t last_ws_ping_pong_check_s; + #if defined(LWS_PLAT_FREERTOS) unsigned long time_last_state_dump; uint32_t last_free_heap; @@ -385,7 +396,9 @@ struct lws_context { int uid, gid; int fd_random; - +#if defined(LWS_WITH_DETAILED_LATENCY) + int latencies_fd; +#endif int count_wsi_allocated; int count_cgi_spawned; unsigned int options; @@ -464,36 +477,10 @@ lws_strdup(const char *s); LWS_EXTERN int log_level; - - -#ifndef LWS_LATENCY -static LWS_INLINE void -lws_latency(struct lws_context *context, struct lws *wsi, const char *action, - int ret, int completion) { - do { - (void)context; (void)wsi; (void)action; (void)ret; - (void)completion; - } while (0); -} -static LWS_INLINE void -lws_latency_pre(struct lws_context *context, struct lws *wsi) { - do { (void)context; (void)wsi; } while (0); -} -#else -#define lws_latency_pre(_context, _wsi) lws_latency(_context, _wsi, NULL, 0, 0) -extern void -lws_latency(struct lws_context *context, struct lws *wsi, const char *action, - int ret, int completion); -#endif - - LWS_EXTERN int lws_b64_selftest(void); - - - #ifndef LWS_NO_DAEMONIZE LWS_EXTERN int get_daemonize_pid(); #else diff --git a/lib/plat/freertos/freertos-service.c b/lib/plat/freertos/freertos-service.c index 5fdd25669..5f8f7c622 100644 --- a/lib/plat/freertos/freertos-service.c +++ b/lib/plat/freertos/freertos-service.c @@ -145,6 +145,16 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi) n = select(max_fd + 1, &readfds, &writefds, &errfds, ptv); n = 0; + +#if defined(LWS_WITH_DETAILED_LATENCY) + /* + * so we can track how long it took before we actually read a POLLIN + * that was signalled when we last exited poll() + */ + if (context->detailed_latency_cb) + pt->ust_left_poll = lws_now_usecs(); +#endif + for (m = 0; m < (int)pt->fds_count; m++) { c = 0; if (FD_ISSET(pt->fds[m].fd, &readfds)) { diff --git a/lib/plat/unix/unix-service.c b/lib/plat/unix/unix-service.c index 6db663721..c158c664e 100644 --- a/lib/plat/unix/unix-service.c +++ b/lib/plat/unix/unix-service.c @@ -102,6 +102,15 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi) vpt->inside_poll = 0; lws_memory_barrier(); +#if defined(LWS_WITH_DETAILED_LATENCY) + /* + * so we can track how long it took before we actually read a POLLIN + * that was signalled when we last exited poll() + */ + if (context->detailed_latency_cb) + pt->ust_left_poll = lws_now_usecs(); +#endif + /* Collision will be rare and brief. Just spin until it completes */ while (vpt->foreign_spinlock) ; diff --git a/lib/roles/h2/http2.c b/lib/roles/h2/http2.c index 4923483b9..1c07c6c61 100644 --- a/lib/roles/h2/http2.c +++ b/lib/roles/h2/http2.c @@ -1912,14 +1912,15 @@ lws_h2_parser(struct lws *wsi, unsigned char *in, lws_filepos_t inlen, } #if defined(LWS_WITH_CLIENT) if (h2n->swsi->client_h2_substream) { - + if (h2n->swsi->protocol) { m = user_callback_handle_rxflow( h2n->swsi->protocol->callback, h2n->swsi, LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ, h2n->swsi->user_space, in - 1, n); - + } else + m = 1; in += n - 1; h2n->inside += n; h2n->count += n - 1; @@ -2241,8 +2242,10 @@ lws_h2_client_handshake(struct lws *wsi) if (lws_finalize_http_header(wsi, &p, end)) goto fail_length; - n = lws_write(wsi, start, p - start, - LWS_WRITE_HTTP_HEADERS); +#if defined(LWS_WITH_DETAILED_LATENCY) + wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); +#endif + n = lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS); if (n != (p - start)) { lwsl_err("_write returned %d from %ld\n", n, (long)(p - start)); diff --git a/lib/roles/http/client/client-handshake.c b/lib/roles/http/client/client-handshake.c index eb4d6dca0..326621596 100644 --- a/lib/roles/http/client/client-handshake.c +++ b/lib/roles/http/client/client-handshake.c @@ -154,6 +154,10 @@ send_hs: * wait in the queue until it's possible to send them. */ lws_callback_on_writable(wsi_piggyback); +#if defined(LWS_WITH_DETAILED_LATENCY) + wsi->detlat.earliest_write_req = + wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); +#endif lwsl_info("%s: wsi %p: waiting to send hdrs (par state 0x%x)\n", __func__, wsi, lwsi_state(wsi_piggyback)); } else { @@ -311,7 +315,7 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, } #if defined(LWS_WITH_UNIX_SOCK) - if (*ads == '+') { + if (ads && *ads == '+') { ads++; memset(&sau, 0, sizeof(sau)); sau.sun_family = AF_UNIX; @@ -552,10 +556,13 @@ ads_known: * The actual connection attempt */ +#if defined(LWS_WITH_DETAILED_LATENCY) + wsi->detlat.earliest_write_req = + wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); +#endif + m = connect(wsi->desc.sockfd, (const struct sockaddr *)psa, n); if (m == -1) { - - lwsl_debug("%s: connect says errno: %d\n", __func__, LWS_ERRNO); if (LWS_ERRNO != LWS_EALREADY && @@ -591,6 +598,22 @@ conn_good: lwsl_debug("%s: Connection started\n", __func__); + /* the tcp connection has happend */ + +#if defined(LWS_WITH_DETAILED_LATENCY) + if (wsi->context->detailed_latency_cb) { + wsi->detlat.type = LDLT_CONNECTION; + wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] = + lws_now_usecs() - + wsi->detlat.earliest_write_req_pre_write; + wsi->detlat.latencies[LAT_DUR_USERCB] = 0; + lws_det_lat_cb(wsi->context, &wsi->detlat); + wsi->detlat.earliest_write_req = + wsi->detlat.earliest_write_req_pre_write = + lws_now_usecs(); + } +#endif + lws_addrinfo_clean(wsi); if (wsi->protocol) @@ -664,6 +687,12 @@ lws_client_connect_2_dnsreq(struct lws *wsi) #endif int n, port = 0; + if (lwsi_state(wsi) == LRS_WAITING_DNS) { + lwsl_notice("%s: LRS_WAITING_DNS\n", __func__); + + return wsi; + } + #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) if (!wsi->http.ah && !wsi->stash) { cce = "ah was NULL at cc2"; @@ -836,6 +865,19 @@ create_new_conn: } #endif +#if defined(LWS_WITH_DETAILED_LATENCY) + if (lwsi_state(wsi) == LRS_WAITING_DNS && + wsi->context->detailed_latency_cb) { + wsi->detlat.type = LDLT_NAME_RESOLUTION; + wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] = + lws_now_usecs() - + wsi->detlat.earliest_write_req_pre_write; + wsi->detlat.latencies[LAT_DUR_USERCB] = 0; + lws_det_lat_cb(wsi->context, &wsi->detlat); + wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); + } +#endif + #if defined(LWS_CLIENT_HTTP_PROXYING) && \ (defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)) @@ -876,9 +918,13 @@ create_new_conn: lwsl_warn("%s: %p: lookup %s:%u\n", __func__, wsi, ads, port); (void)port; +#if defined(LWS_WITH_DETAILED_LATENCY) + wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); +#endif #if !defined(LWS_WITH_SYS_ASYNC_DNS) n = lws_getaddrinfo46(wsi, ads, &result); #else + lwsi_set_state(wsi, LRS_WAITING_DNS); /* this is either FAILED, CONTINUING, or already called connect_4 */ n = lws_async_dns_query(wsi->context, wsi->tsi, ads, LWS_ADNS_RECORD_A, diff --git a/lib/roles/http/client/client-http.c b/lib/roles/http/client/client-http.c index 1a4b66b5a..000c9a593 100644 --- a/lib/roles/http/client/client-http.c +++ b/lib/roles/http/client/client-http.c @@ -112,6 +112,9 @@ lws_client_socket_service(struct lws *wsi, struct lws_pollfd *pollfd, } lws_end_foreach_dll_safe(d, d1); if (wfound) { +#if defined(LWS_WITH_DETAILED_LATENCY) + wfound->detlat.earliest_write_req_pre_write = lws_now_usecs(); +#endif /* * pollfd has the master sockfd in it... we * need to use that in HANDSHAKE2 to understand @@ -152,6 +155,7 @@ lws_client_socket_service(struct lws *wsi, struct lws_pollfd *pollfd, /* either still pending connection, or changed mode */ return 0; + case LRS_WAITING_CONNECT: /* @@ -367,6 +371,16 @@ start_ws_handshake: } else wsi->tls.ssl = NULL; #endif +#if defined(LWS_WITH_DETAILED_LATENCY) + if (context->detailed_latency_cb) { + wsi->detlat.type = LDLT_TLS_NEG_CLIENT; + wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] = + lws_now_usecs() - + wsi->detlat.earliest_write_req_pre_write; + wsi->detlat.latencies[LAT_DUR_USERCB] = 0; + lws_det_lat_cb(wsi->context, &wsi->detlat); + } +#endif #if defined (LWS_WITH_HTTP2) if (wsi->client_h2_alpn) { /* @@ -409,7 +423,6 @@ start_ws_handshake: } /* send our request to the server */ - lws_latency_pre(context, wsi); w = _lws_client_wsi_master(wsi); lwsl_info("%s: HANDSHAKE2: %p: sending headers on %p " @@ -417,10 +430,10 @@ start_ws_handshake: __func__, wsi, w, (unsigned long)wsi->wsistate, (unsigned long)w->wsistate, w->desc.sockfd, wsi->desc.sockfd); - +#if defined(LWS_WITH_DETAILED_LATENCY) + wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); +#endif n = lws_ssl_capable_write(w, (unsigned char *)sb, (int)(p - sb)); - lws_latency(context, wsi, "send lws_issue_raw", n, - n == p - sb); switch (n) { case LWS_SSL_CAPABLE_ERROR: lwsl_debug("ERROR writing to client socket\n"); @@ -538,8 +551,6 @@ client_http_body_sent: int plen = 1; n = lws_ssl_capable_read(wsi, &c, 1); - lws_latency(context, wsi, "send lws_issue_raw", n, - n == 1); switch (n) { case 0: case LWS_SSL_CAPABLE_ERROR: diff --git a/lib/roles/http/header.c b/lib/roles/http/header.c index 2ea695317..007bb00ef 100644 --- a/lib/roles/http/header.c +++ b/lib/roles/http/header.c @@ -95,6 +95,9 @@ lws_finalize_write_http_header(struct lws *wsi, unsigned char *start, p = *pp; len = lws_ptr_diff(p, start); +#if defined(LWS_WITH_DETAILED_LATENCY) + wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); +#endif if (lws_write(wsi, start, len, LWS_WRITE_HTTP_HEADERS) != len) return 1; @@ -446,6 +449,9 @@ lws_return_http_status(struct lws *wsi, unsigned int code, * * Solve it by writing the headers now... */ +#if defined(LWS_WITH_DETAILED_LATENCY) + wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); +#endif m = lws_write(wsi, start, lws_ptr_diff(p, start), LWS_WRITE_HTTP_HEADERS); if (m != lws_ptr_diff(p, start)) diff --git a/lib/roles/listen/ops-listen.c b/lib/roles/listen/ops-listen.c index be5783313..670fdaa40 100644 --- a/lib/roles/listen/ops-listen.c +++ b/lib/roles/listen/ops-listen.c @@ -73,7 +73,6 @@ rops_handle_POLLIN_listen(struct lws_context_per_thread *pt, struct lws *wsi, /* listen socket got an unencrypted connection... */ clilen = sizeof(cli_addr); - lws_latency_pre(context, wsi); /* * We cannot identify the peer who is in the listen @@ -84,8 +83,6 @@ rops_handle_POLLIN_listen(struct lws_context_per_thread *pt, struct lws *wsi, accept_fd = accept((int)pollfd->fd, (struct sockaddr *)&cli_addr, &clilen); - lws_latency(context, wsi, "listener accept", - (int)accept_fd, accept_fd != LWS_SOCK_INVALID); if (accept_fd == LWS_SOCK_INVALID) { if (LWS_ERRNO == LWS_EAGAIN || LWS_ERRNO == LWS_EWOULDBLOCK) { diff --git a/lib/tls/mbedtls/mbedtls-client.c b/lib/tls/mbedtls/mbedtls-client.c index 88d7aa260..d50ee4d97 100644 --- a/lib/tls/mbedtls/mbedtls-client.c +++ b/lib/tls/mbedtls/mbedtls-client.c @@ -152,9 +152,6 @@ lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, int ebuf_len) lwsl_info("peer provided cert\n"); n = SSL_get_verify_result(wsi->tls.ssl); - lws_latency(wsi->context, wsi, - "SSL_get_verify_result LWS_CONNMODE..HANDSHAKE", n, n > 0); - lwsl_debug("get_verify says %d\n", n); if (n == X509_V_OK) diff --git a/lib/tls/mbedtls/mbedtls-ssl.c b/lib/tls/mbedtls/mbedtls-ssl.c index 65a47534e..d2e410a6f 100644 --- a/lib/tls/mbedtls/mbedtls-ssl.c +++ b/lib/tls/mbedtls/mbedtls-ssl.c @@ -113,7 +113,17 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, int len) if (wsi->vhost) wsi->vhost->conn_stats.rx += n; #endif - +#if defined(LWS_WITH_DETAILED_LATENCY) + if (context->detailed_latency_cb) { + wsi->detlat.req_size = len; + wsi->detlat.acc_size = n; + wsi->detlat.type = LDLT_READ; + wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = + lws_now_usecs() - pt->ust_left_poll; + wsi->detlat.latencies[LAT_DUR_USERCB] = 0; + lws_det_lat_cb(wsi->context, &wsi->detlat); + } +#endif /* * if it was our buffer that limited what we read, * check if SSL has additional data pending inside SSL buffers. diff --git a/lib/tls/openssl/openssl-client.c b/lib/tls/openssl/openssl-client.c index 180c00e6c..9628eb568 100644 --- a/lib/tls/openssl/openssl-client.c +++ b/lib/tls/openssl/openssl-client.c @@ -329,12 +329,9 @@ lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, int ebuf_len) char *sb = p; int n; - lws_latency_pre(wsi->context, wsi); errno = 0; ERR_clear_error(); n = SSL_get_verify_result(wsi->tls.ssl); - lws_latency(wsi->context, wsi, - "SSL_get_verify_result LWS_CONNMODE..HANDSHAKE", n, n > 0); lwsl_debug("get_verify says %d\n", n); diff --git a/lib/tls/openssl/openssl-ssl.c b/lib/tls/openssl/openssl-ssl.c index ee0bff852..cc5e93993 100644 --- a/lib/tls/openssl/openssl-ssl.c +++ b/lib/tls/openssl/openssl-ssl.c @@ -271,6 +271,18 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, int len) // lwsl_hexdump_err(buf, n); +#if defined(LWS_WITH_DETAILED_LATENCY) + if (context->detailed_latency_cb) { + wsi->detlat.req_size = len; + wsi->detlat.acc_size = n; + wsi->detlat.type = LDLT_READ; + wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = + lws_now_usecs() - pt->ust_left_poll; + wsi->detlat.latencies[LAT_DUR_USERCB] = 0; + lws_det_lat_cb(wsi->context, &wsi->detlat); + } +#endif + /* * if it was our buffer that limited what we read, * check if SSL has additional data pending inside SSL buffers. diff --git a/lib/tls/tls-client.c b/lib/tls/tls-client.c index 7bafffbb7..0a65e1197 100644 --- a/lib/tls/tls-client.c +++ b/lib/tls/tls-client.c @@ -27,13 +27,9 @@ int lws_ssl_client_connect1(struct lws *wsi) { - struct lws_context *context = wsi->context; - int n = 0; + int n; - lws_latency_pre(context, wsi); n = lws_tls_client_connect(wsi); - lws_latency(context, wsi, "SSL_connect hs", n, n > 0); - switch (n) { case LWS_SSL_CAPABLE_ERROR: return -1; @@ -58,12 +54,8 @@ lws_ssl_client_connect2(struct lws *wsi, char *errbuf, int len) int n = 0; if (lwsi_state(wsi) == LRS_WAITING_SSL) { - lws_latency_pre(wsi->context, wsi); - n = lws_tls_client_connect(wsi); lwsl_debug("%s: SSL_connect says %d\n", __func__, n); - lws_latency(wsi->context, wsi, - "SSL_connect LRS_WAITING_SSL", n, n > 0); switch (n) { case LWS_SSL_CAPABLE_ERROR: @@ -96,7 +88,7 @@ int lws_context_init_client_ssl(const struct lws_context_creation_info *info, const char *cert_filepath = info->ssl_cert_filepath; const char *ca_filepath = info->ssl_ca_filepath; const char *cipher_list = info->ssl_cipher_list; - struct lws wsi; + struct lws *wsi = vhost->context->pt[0].fake_wsi; if (vhost->options & LWS_SERVER_OPTION_ADOPT_APPLY_LISTEN_ACCEPT_CONFIG) return 0; @@ -152,11 +144,12 @@ int lws_context_init_client_ssl(const struct lws_context_creation_info *info, * give him a fake wsi with context set, so he can use * lws_get_context() in the callback */ - memset(&wsi, 0, sizeof(wsi)); - wsi.vhost = vhost; /* not a real bound wsi */ - wsi.context = vhost->context; - vhost->protocols[0].callback(&wsi, + wsi->vhost = vhost; /* not a real bound wsi */ + wsi->context = vhost->context; + wsi->protocol = NULL; + + vhost->protocols[0].callback(wsi, LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS, vhost->tls.ssl_client_ctx, NULL, 0); diff --git a/lib/tls/tls-server.c b/lib/tls/tls-server.c index 43f4c6d89..9a2291726 100644 --- a/lib/tls/tls-server.c +++ b/lib/tls/tls-server.c @@ -120,7 +120,7 @@ lws_context_init_server_ssl(const struct lws_context_creation_info *info, struct lws_vhost *vhost) { struct lws_context *context = vhost->context; - struct lws wsi; + struct lws *wsi = context->pt[0].fake_wsi; if (!lws_check_opt(info->options, LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT)) { @@ -159,9 +159,9 @@ lws_context_init_server_ssl(const struct lws_context_creation_info *info, * give him a fake wsi with context + vhost set, so he can use * lws_get_context() in the callback */ - memset(&wsi, 0, sizeof(wsi)); - wsi.vhost = vhost; /* not a real bound wsi */ - wsi.context = context; + wsi->vhost = vhost; /* not a real bound wsi */ + wsi->context = context; + wsi->protocol = NULL; /* * as a server, if we are requiring clients to identify themselves @@ -177,12 +177,12 @@ lws_context_init_server_ssl(const struct lws_context_creation_info *info, * allowing it to verify incoming client certs */ if (vhost->tls.use_ssl) { - if (lws_tls_server_vhost_backend_init(info, vhost, &wsi)) + if (lws_tls_server_vhost_backend_init(info, vhost, wsi)) return -1; lws_tls_server_client_cert_verify_config(vhost); - if (vhost->protocols[0].callback(&wsi, + if (vhost->protocols[0].callback(wsi, LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS, vhost->tls.ssl_ctx, vhost, 0)) return -1; @@ -272,8 +272,6 @@ lws_server_socket_service_ssl(struct lws *wsi, lws_sockfd_type accept_fd) goto fail; } - lws_latency_pre(context, wsi); - if (wsi->vhost->tls.allow_non_ssl_on_ssl_port) { n = recv(wsi->desc.sockfd, (char *)pt->serv_buf, @@ -390,8 +388,6 @@ lws_server_socket_service_ssl(struct lws *wsi, lws_sockfd_type accept_fd) errno = 0; lws_stats_bump(pt, LWSSTATS_C_SSL_ACCEPT_SPIN, 1); n = lws_tls_server_accept(wsi); - lws_latency(context, wsi, - "SSL_accept LRS_SSL_ACK_PENDING\n", n, n == 1); lwsl_info("SSL_accept says %d\n", n); switch (n) { case LWS_SSL_CAPABLE_DONE: @@ -416,6 +412,16 @@ lws_server_socket_service_ssl(struct lws *wsi, lws_sockfd_type accept_fd) wsi->accept_start_us); wsi->accept_start_us = lws_now_usecs(); #endif +#if defined(LWS_WITH_DETAILED_LATENCY) + if (context->detailed_latency_cb) { + wsi->detlat.type = LDLT_TLS_NEG_SERVER; + wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = + lws_now_usecs() - + wsi->detlat.earliest_write_req_pre_write; + wsi->detlat.latencies[LAT_DUR_USERCB] = 0; + lws_det_lat_cb(wsi->context, &wsi->detlat); + } +#endif /* adapt our vhost to match the SNI SSL_CTX that was chosen */ vh = context->vhost_list; diff --git a/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c b/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c index ffd1070fb..fdd4abdc5 100644 --- a/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c +++ b/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c @@ -278,6 +278,11 @@ int main(int argc, const char **argv) info.client_ssl_ca_filepath = "./warmcat.com.cer"; #endif +#if defined(LWS_WITH_DETAILED_LATENCY) + info.detailed_latency_cb = lws_det_lat_plot_cb; + info.detailed_latency_filepath = "/tmp/lws-latency-results"; +#endif + context = lws_create_context(&info); if (!context) { lwsl_err("lws init failed\n"); @@ -313,6 +318,9 @@ int main(int argc, const char **argv) if ((p = lws_cmdline_option(argc, argv, "--port"))) i.port = atoi(p); + if ((p = lws_cmdline_option(argc, argv, "--server"))) + i.address = p; + i.host = i.address; i.origin = i.address; i.method = "GET";