From 57d5744f2c064ccb7bf8dd3dca2a24f6755297ac Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Fri, 28 Jul 2017 14:07:25 -0700 Subject: MAINT-7634: Move StatsAccumulator into llcommon, collect data sent and error codes from core. --- indra/llcorehttp/CMakeLists.txt | 2 + indra/llcorehttp/_httpoprequest.cpp | 6 ++- indra/llcorehttp/_httppolicy.cpp | 3 ++ indra/llcorehttp/_httpservice.cpp | 49 +++++++++++++++------- indra/llcorehttp/bufferarray.cpp | 22 ++++++++-- indra/llcorehttp/httprequest.cpp | 12 +++++- indra/llcorehttp/httprequest.h | 15 ++++++- indra/llcorehttp/httpstats.cpp | 82 +++++++++++++++++++++++++++++++++++++ indra/llcorehttp/httpstats.h | 70 +++++++++++++++++++++++++++++++ 9 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 indra/llcorehttp/httpstats.cpp create mode 100644 indra/llcorehttp/httpstats.h (limited to 'indra/llcorehttp') diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt index 7482fc577f..435fb09aa4 100644 --- a/indra/llcorehttp/CMakeLists.txt +++ b/indra/llcorehttp/CMakeLists.txt @@ -30,6 +30,7 @@ set(llcorehttp_SOURCE_FILES httpoptions.cpp httprequest.cpp httpresponse.cpp + httpstats.cpp _httplibcurl.cpp _httpopcancel.cpp _httpoperation.cpp @@ -57,6 +58,7 @@ set(llcorehttp_HEADER_FILES httpoptions.h httprequest.h httpresponse.h + httpstats.h _httpinternal.h _httplibcurl.h _httpopcancel.h diff --git a/indra/llcorehttp/_httpoprequest.cpp b/indra/llcorehttp/_httpoprequest.cpp index 07cc0e4625..f35f9848ff 100644 --- a/indra/llcorehttp/_httpoprequest.cpp +++ b/indra/llcorehttp/_httpoprequest.cpp @@ -47,6 +47,8 @@ #include "llhttpconstants.h" #include "llproxy.h" +#include "httpstats.h" + // *DEBUG: "[curl:bugs] #1420" problem and testing. // // A pipelining problem, https://sourceforge.net/p/curl/bugs/1420/, @@ -805,6 +807,7 @@ size_t HttpOpRequest::writeCallback(void * data, size_t size, size_t nmemb, void } const size_t req_size(size * nmemb); const size_t write_size(op->mReplyBody->append(static_cast(data), req_size)); + HTTPStats::instance().recordDataDown(write_size); return write_size; } @@ -833,7 +836,8 @@ size_t HttpOpRequest::readCallback(void * data, size_t size, size_t nmemb, void const size_t do_size((std::min)(req_size, body_size - op->mCurlBodyPos)); const size_t read_size(op->mReqBody->read(op->mCurlBodyPos, static_cast(data), do_size)); - op->mCurlBodyPos += read_size; + HTTPStats::instance().recordDataUp(read_size); + op->mCurlBodyPos += read_size; return read_size; } diff --git a/indra/llcorehttp/_httppolicy.cpp b/indra/llcorehttp/_httppolicy.cpp index b2709b53ec..628a5c79e1 100644 --- a/indra/llcorehttp/_httppolicy.cpp +++ b/indra/llcorehttp/_httppolicy.cpp @@ -34,6 +34,7 @@ #include "_httppolicyclass.h" #include "lltimer.h" +#include "httpstats.h" namespace { @@ -448,6 +449,8 @@ bool HttpPolicy::stageAfterCompletion(const HttpOpRequest::ptr_t &op) } op->stageFromActive(mService); + + HTTPStats::instance().recordResultCode(op->mStatus.getStatus()); return false; // not active } diff --git a/indra/llcorehttp/_httpservice.cpp b/indra/llcorehttp/_httpservice.cpp index 6c39fdc61b..49d865cbfa 100644 --- a/indra/llcorehttp/_httpservice.cpp +++ b/indra/llcorehttp/_httpservice.cpp @@ -38,7 +38,8 @@ #include "lltimer.h" #include "llthread.h" - +#include "llexception.h" +#include "llmemory.h" namespace { @@ -293,22 +294,42 @@ void HttpService::threadRun(LLCoreInt::HttpThread * thread) ELoopSpeed loop(REQUEST_SLEEP); while (! mExitRequested) { - loop = processRequestQueue(loop); + try + { + loop = processRequestQueue(loop); - // Process ready queue issuing new requests as needed - ELoopSpeed new_loop = mPolicy->processReadyQueue(); - loop = (std::min)(loop, new_loop); + // Process ready queue issuing new requests as needed + ELoopSpeed new_loop = mPolicy->processReadyQueue(); + loop = (std::min)(loop, new_loop); - // Give libcurl some cycles - new_loop = mTransport->processTransport(); - loop = (std::min)(loop, new_loop); + // Give libcurl some cycles + new_loop = mTransport->processTransport(); + loop = (std::min)(loop, new_loop); - // Determine whether to spin, sleep briefly or sleep for next request - if (REQUEST_SLEEP != loop) - { - ms_sleep(HTTP_SERVICE_LOOP_SLEEP_NORMAL_MS); - } - } + // Determine whether to spin, sleep briefly or sleep for next request + if (REQUEST_SLEEP != loop) + { + ms_sleep(HTTP_SERVICE_LOOP_SLEEP_NORMAL_MS); + } + } + catch (const LLContinueError&) + { + LOG_UNHANDLED_EXCEPTION(""); + } + catch (std::bad_alloc) + { + LLMemory::logMemoryInfo(TRUE); + + //output possible call stacks to log file. + LLError::LLCallStacks::print(); + + LL_ERRS() << "Bad memory allocation in HttpService::threadRun()!" << LL_ENDL; + } + catch (...) + { + CRASH_ON_UNHANDLED_EXCEPTION(""); + } + } shutdown(); sState = STOPPED; diff --git a/indra/llcorehttp/bufferarray.cpp b/indra/llcorehttp/bufferarray.cpp index 8eaaeed710..be534b3ce4 100644 --- a/indra/llcorehttp/bufferarray.cpp +++ b/indra/llcorehttp/bufferarray.cpp @@ -25,6 +25,8 @@ */ #include "bufferarray.h" +#include "llexception.h" +#include "llmemory.h" // BufferArray is a list of chunks, each a BufferArray::Block, of contiguous @@ -140,8 +142,22 @@ size_t BufferArray::append(const void * src, size_t len) { mBlocks.reserve(mBlocks.size() + 5); } - Block * block = Block::alloc(BLOCK_ALLOC_SIZE); - memcpy(block->mData, c_src, copy_len); + Block * block; + try + { + block = Block::alloc(BLOCK_ALLOC_SIZE); + } + catch (std::bad_alloc) + { + LLMemory::logMemoryInfo(TRUE); + + //output possible call stacks to log file. + LLError::LLCallStacks::print(); + + LL_WARNS() << "Bad memory allocation in thrown by Block::alloc in read!" << LL_ENDL; + break; + } + memcpy(block->mData, c_src, copy_len); block->mUsed = copy_len; llassert_always(block->mUsed <= block->mAlloced); mBlocks.push_back(block); @@ -149,7 +165,7 @@ size_t BufferArray::append(const void * src, size_t len) c_src += copy_len; len -= copy_len; } - return ret; + return ret - len; } diff --git a/indra/llcorehttp/httprequest.cpp b/indra/llcorehttp/httprequest.cpp index e09f0c3b18..d9662c1232 100644 --- a/indra/llcorehttp/httprequest.cpp +++ b/indra/llcorehttp/httprequest.cpp @@ -37,7 +37,7 @@ #include "_httpopsetget.h" #include "lltimer.h" - +#include "httpstats.h" namespace { @@ -52,6 +52,7 @@ namespace LLCore // ==================================== // HttpRequest Implementation // ==================================== +HttpRequest::Statistics HttpRequest::mStatistics; HttpRequest::HttpRequest() @@ -62,6 +63,12 @@ HttpRequest::HttpRequest() mRequestQueue->addRef(); mReplyQueue.reset( new HttpReplyQueue() ); + + ++mStatistics.mCurrentRequests; + ++mStatistics.mTotalRequests; + + + LL_WARNS("HTTPRequest") << "New HttpRequest created (outstanding: " << mStatistics.mCurrentRequests << " total: " << mStatistics.mTotalRequests << ")" << LL_ENDL; } @@ -74,6 +81,9 @@ HttpRequest::~HttpRequest() } mReplyQueue.reset(); + --mStatistics.mCurrentRequests; + + LL_WARNS("HTTPRequest") << "HttpRequest destroyed (outstanding: " << mStatistics.mCurrentRequests << " total: " << mStatistics.mTotalRequests << ")" << LL_ENDL; } diff --git a/indra/llcorehttp/httprequest.h b/indra/llcorehttp/httprequest.h index 17cfdcd7b6..c958132ae2 100644 --- a/indra/llcorehttp/httprequest.h +++ b/indra/llcorehttp/httprequest.h @@ -680,7 +680,20 @@ private: /// @} // End Global State // ==================================== - + + struct Statistics + { + Statistics(): + mTotalRequests(0), + mCurrentRequests(0) + {} + + S32 mTotalRequests; + S32 mCurrentRequests; + }; + + static Statistics mStatistics; + }; // end class HttpRequest diff --git a/indra/llcorehttp/httpstats.cpp b/indra/llcorehttp/httpstats.cpp new file mode 100644 index 0000000000..467d364885 --- /dev/null +++ b/indra/llcorehttp/httpstats.cpp @@ -0,0 +1,82 @@ +/** + * @file llviewerstats.cpp + * @brief LLViewerStats class implementation + * + * $LicenseInfo:firstyear=2002&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "httpstats.h" +#include "llerror.h" + +namespace LLCore +{ +HTTPStats::HTTPStats() +{ + resetStats(); +} + + +HTTPStats::~HTTPStats() +{ +} + +void HTTPStats::resetStats() +{ + mResutCodes.clear(); + mDataDown.reset(); + mDataUp.reset(); +} + + +void HTTPStats::recordResultCode(S32 code) +{ + std::map::iterator it; + + it = mResutCodes.find(code); + + if (it == mResutCodes.end()) + mResutCodes[code] = 1; + else + (*it).second = (*it).second + 1; + +} + +void HTTPStats::dumpStats() +{ + std::stringstream out; + + out << "HTTPCore Stats" << std::endl; + out << "Bytes Sent: " << mDataUp.getSum() << std::endl; + out << "Bytes Recv: " << mDataDown.getSum() << std::endl; + out << "Result Codes:" << std::endl << "--- -----" << std::endl; + + + for (std::map::iterator it = mResutCodes.begin(); it != mResutCodes.end(); ++it) + { + out << (*it).first << " " << (*it).second << std::endl; + } + + LL_WARNS("HTTP Core") << out.str() << LL_ENDL; +} + + +} diff --git a/indra/llcorehttp/httpstats.h b/indra/llcorehttp/httpstats.h new file mode 100644 index 0000000000..d4460bcfae --- /dev/null +++ b/indra/llcorehttp/httpstats.h @@ -0,0 +1,70 @@ +/** + * @file llviewerim_peningtats.h + * @brief LLViewerStats class header file + * + * $LicenseInfo:firstyear=2002&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLVIEWERSTATS_H +#define LL_LLVIEWERSTATS_H + +#include "lltracerecording.h" +#include "lltrace.h" +#include "llstatsaccumulator.h" +#include "llsingleton.h" +#include "llsd.h" + +namespace LLCore +{ + class HTTPStats : public LLSingleton + { + LLSINGLETON(HTTPStats); + virtual ~HTTPStats(); + + public: + void resetStats(); + + typedef LLStatsAccumulator StatsAccumulator; + + void recordDataDown(size_t bytes) + { + mDataDown.push(bytes); + } + + void recordDataUp(size_t bytes) + { + mDataUp.push(bytes); + } + + void recordResultCode(S32 code); + + void dumpStats(); + private: + StatsAccumulator mDataDown; + StatsAccumulator mDataUp; + + std::map mResutCodes; + }; + + +} +#endif // LL_LLVIEWERSTATS_H -- cgit v1.2.3 From 1038633526330cf931ba097dbafdd270b5bb56e3 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 8 Aug 2017 09:04:32 -0700 Subject: MAINT-7634: Logging and instrumentation canges to narrow down viewer crashes. --- indra/llcorehttp/_httpoprequest.cpp | 19 +++++++++++++++++++ indra/llcorehttp/_httppolicy.cpp | 2 +- indra/llcorehttp/httprequest.cpp | 10 +--------- indra/llcorehttp/httprequest.h | 13 ------------- indra/llcorehttp/httpresponse.h | 10 ++++++++++ indra/llcorehttp/httpstats.cpp | 34 ++++++++++++++++++++++++++++++---- indra/llcorehttp/httpstats.h | 4 ++++ 7 files changed, 65 insertions(+), 27 deletions(-) (limited to 'indra/llcorehttp') diff --git a/indra/llcorehttp/_httpoprequest.cpp b/indra/llcorehttp/_httpoprequest.cpp index f35f9848ff..36955f3797 100644 --- a/indra/llcorehttp/_httpoprequest.cpp +++ b/indra/llcorehttp/_httpoprequest.cpp @@ -247,6 +247,25 @@ void HttpOpRequest::visitNotifier(HttpRequest * request) response->setHeaders(mReplyHeaders); response->setRequestURL(mReqURL); + std::string method("UNKNOWN"); + + if (mReqMethod == HOR_COPY) + method = "COPY"; + else if (mReqMethod == HOR_DELETE) + method = "DELETE"; + else if (mReqMethod == HOR_GET) + method = "GET"; + else if (mReqMethod == HOR_MOVE) + method = "MOVE"; + else if (mReqMethod == HOR_PATCH) + method = "PATCH"; + else if (mReqMethod == HOR_POST) + method = "POST"; + else if (mReqMethod == HOR_PUT) + method = "PUT"; + + response->setRequestMethod(method); + if (mReplyOffset || mReplyLength) { // Got an explicit offset/length in response diff --git a/indra/llcorehttp/_httppolicy.cpp b/indra/llcorehttp/_httppolicy.cpp index 628a5c79e1..a302db8b46 100644 --- a/indra/llcorehttp/_httppolicy.cpp +++ b/indra/llcorehttp/_httppolicy.cpp @@ -450,7 +450,7 @@ bool HttpPolicy::stageAfterCompletion(const HttpOpRequest::ptr_t &op) op->stageFromActive(mService); - HTTPStats::instance().recordResultCode(op->mStatus.getStatus()); + HTTPStats::instance().recordResultCode(op->mStatus.getType()); return false; // not active } diff --git a/indra/llcorehttp/httprequest.cpp b/indra/llcorehttp/httprequest.cpp index d9662c1232..2687f77217 100644 --- a/indra/llcorehttp/httprequest.cpp +++ b/indra/llcorehttp/httprequest.cpp @@ -52,7 +52,6 @@ namespace LLCore // ==================================== // HttpRequest Implementation // ==================================== -HttpRequest::Statistics HttpRequest::mStatistics; HttpRequest::HttpRequest() @@ -64,11 +63,7 @@ HttpRequest::HttpRequest() mReplyQueue.reset( new HttpReplyQueue() ); - ++mStatistics.mCurrentRequests; - ++mStatistics.mTotalRequests; - - - LL_WARNS("HTTPRequest") << "New HttpRequest created (outstanding: " << mStatistics.mCurrentRequests << " total: " << mStatistics.mTotalRequests << ")" << LL_ENDL; + HTTPStats::instance().recordHTTPRequest(); } @@ -81,9 +76,6 @@ HttpRequest::~HttpRequest() } mReplyQueue.reset(); - --mStatistics.mCurrentRequests; - - LL_WARNS("HTTPRequest") << "HttpRequest destroyed (outstanding: " << mStatistics.mCurrentRequests << " total: " << mStatistics.mTotalRequests << ")" << LL_ENDL; } diff --git a/indra/llcorehttp/httprequest.h b/indra/llcorehttp/httprequest.h index c958132ae2..a418eb6a7a 100644 --- a/indra/llcorehttp/httprequest.h +++ b/indra/llcorehttp/httprequest.h @@ -681,19 +681,6 @@ private: // End Global State // ==================================== - struct Statistics - { - Statistics(): - mTotalRequests(0), - mCurrentRequests(0) - {} - - S32 mTotalRequests; - S32 mCurrentRequests; - }; - - static Statistics mStatistics; - }; // end class HttpRequest diff --git a/indra/llcorehttp/httpresponse.h b/indra/llcorehttp/httpresponse.h index 0bfa4585c7..b834085e5c 100644 --- a/indra/llcorehttp/httpresponse.h +++ b/indra/llcorehttp/httpresponse.h @@ -204,6 +204,15 @@ public: return mRequestUrl; } + void setRequestMethod(const std::string &method) + { + mRequestMethod = method; + } + + const std::string &getRequestMethod() const + { + return mRequestMethod; + } protected: // Response data here @@ -217,6 +226,7 @@ protected: unsigned int mRetries; unsigned int m503Retries; std::string mRequestUrl; + std::string mRequestMethod; TransferStats::ptr_t mStats; }; diff --git a/indra/llcorehttp/httpstats.cpp b/indra/llcorehttp/httpstats.cpp index 467d364885..b2de7f51ff 100644 --- a/indra/llcorehttp/httpstats.cpp +++ b/indra/llcorehttp/httpstats.cpp @@ -44,6 +44,7 @@ void HTTPStats::resetStats() mResutCodes.clear(); mDataDown.reset(); mDataUp.reset(); + mRequests = 0; } @@ -60,16 +61,41 @@ void HTTPStats::recordResultCode(S32 code) } +namespace +{ + std::string byte_count_converter(F32 bytes) + { + static const char unit_suffix[] = { 'B', 'K', 'M', 'G' }; + + F32 value = bytes; + int suffix = 0; + + while ((value > 1024.0) && (suffix < 3)) + { + value /= 1024.0; + ++suffix; + } + + std::stringstream out; + + out << std::setprecision(4) << value << unit_suffix[suffix]; + + return out.str(); + } +} + void HTTPStats::dumpStats() { std::stringstream out; - out << "HTTPCore Stats" << std::endl; - out << "Bytes Sent: " << mDataUp.getSum() << std::endl; - out << "Bytes Recv: " << mDataDown.getSum() << std::endl; + out << "HTTP DATA SUMMARY" << std::endl; + out << "HTTP Transfer counts:" << std::endl; + out << "Data Sent: " << byte_count_converter(mDataUp.getSum()) << " (" << mDataUp.getSum() << ")" << std::endl; + out << "Data Recv: " << byte_count_converter(mDataDown.getSum()) << " (" << mDataDown.getSum() << ")" << std::endl; + out << "Total requests: " << mRequests << "(request objects created)" << std::endl; + out << std::endl; out << "Result Codes:" << std::endl << "--- -----" << std::endl; - for (std::map::iterator it = mResutCodes.begin(); it != mResutCodes.end(); ++it) { out << (*it).first << " " << (*it).second << std::endl; diff --git a/indra/llcorehttp/httpstats.h b/indra/llcorehttp/httpstats.h index d4460bcfae..2c713cb548 100644 --- a/indra/llcorehttp/httpstats.h +++ b/indra/llcorehttp/httpstats.h @@ -55,6 +55,8 @@ namespace LLCore mDataUp.push(bytes); } + void recordHTTPRequest() { ++mRequests; } + void recordResultCode(S32 code); void dumpStats(); @@ -62,6 +64,8 @@ namespace LLCore StatsAccumulator mDataDown; StatsAccumulator mDataUp; + S32 mRequests; + std::map mResutCodes; }; -- cgit v1.2.3 From 79856e655432a30f5bba2e8d7adecdc3626ce94f Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Mon, 14 Aug 2017 14:54:58 -0700 Subject: MAINT-7634: Feedback from code review, move enum to string converter to own function. --- indra/llcorehttp/_httpoprequest.cpp | 38 +++++++++++++++++++------------------ indra/llcorehttp/_httpoprequest.h | 4 +++- 2 files changed, 23 insertions(+), 19 deletions(-) (limited to 'indra/llcorehttp') diff --git a/indra/llcorehttp/_httpoprequest.cpp b/indra/llcorehttp/_httpoprequest.cpp index 36955f3797..f526af37b5 100644 --- a/indra/llcorehttp/_httpoprequest.cpp +++ b/indra/llcorehttp/_httpoprequest.cpp @@ -247,24 +247,7 @@ void HttpOpRequest::visitNotifier(HttpRequest * request) response->setHeaders(mReplyHeaders); response->setRequestURL(mReqURL); - std::string method("UNKNOWN"); - - if (mReqMethod == HOR_COPY) - method = "COPY"; - else if (mReqMethod == HOR_DELETE) - method = "DELETE"; - else if (mReqMethod == HOR_GET) - method = "GET"; - else if (mReqMethod == HOR_MOVE) - method = "MOVE"; - else if (mReqMethod == HOR_PATCH) - method = "PATCH"; - else if (mReqMethod == HOR_POST) - method = "POST"; - else if (mReqMethod == HOR_PUT) - method = "PUT"; - - response->setRequestMethod(method); + response->setRequestMethod(methodToString(mReqMethod)); if (mReplyOffset || mReplyLength) { @@ -1161,6 +1144,25 @@ int HttpOpRequest::debugCallback(CURL * handle, curl_infotype info, char * buffe return 0; } +std::string HttpOpRequest::methodToString(const HttpOpRequest::EMethod &e) +{ + if (e == HOR_COPY) + return "COPY"; + else if (e == HOR_DELETE) + return "DELETE"; + else if (e == HOR_GET) + return "GET"; + else if (e == HOR_MOVE) + return "MOVE"; + else if (e == HOR_PATCH) + return "PATCH"; + else if (e == HOR_POST) + return "POST"; + else if (e == HOR_PUT) + return "PUT"; + + return "UNKNOWN"; +} } // end namespace LLCore diff --git a/indra/llcorehttp/_httpoprequest.h b/indra/llcorehttp/_httpoprequest.h index dbcc57d0fd..201c37d5c3 100644 --- a/indra/llcorehttp/_httpoprequest.h +++ b/indra/llcorehttp/_httpoprequest.h @@ -87,7 +87,8 @@ public: HOR_COPY, HOR_MOVE }; - + static std::string methodToString(const EMethod &); + virtual void stageFromRequest(HttpService *); virtual void stageFromReady(HttpService *); virtual void stageFromActive(HttpService *); @@ -235,6 +236,7 @@ public: }; // end class HttpOpRequest + /// HttpOpRequestCompare isn't an operation but a uniform comparison /// functor for STL containers that order by priority. Mainly /// used for the ready queue container but defined here. -- cgit v1.2.3