blob: 359874fcba995323ca57c2a73018886e05db13d4 [file]
#pragma once
#include <common/utils.hpp>
#include <chrono>
#include <deque>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <unordered_map>
#include <vector>
namespace pldm
{
namespace flightrecorder
{
using TxOrRx = bool;
using FlightRecorderData = std::vector<uint8_t>;
using FlightRecorderTimeStamp =
std::chrono::time_point<std::chrono::system_clock>;
using FlightRecorderRecord =
std::tuple<FlightRecorderTimeStamp, TxOrRx, FlightRecorderData>;
static constexpr auto flightRecorderDumpPath = "/tmp/pldm_flight_recorder";
/** @class FlightRecorder
*
* The class for implementing the PLDM flight recorder logic. This class
* handles the insertion of the data into the recorder and also provides
* API's to dump the flight recorder into a file.
*/
class FlightRecorder
{
private:
FlightRecorder()
{
flightRecorderPolicy = (FLIGHT_RECORDER_MAX_ENTRIES > 0) ? true : false;
}
enum LatencyRangeEnum
{
MS1_10 = 0,
MS10_100,
MS100_1000,
S1_5,
S5_MAX,
};
struct Metrics
{
uint64_t txPackets;
uint64_t txBytes;
uint64_t txRetryPackets;
uint64_t rxPackets;
uint64_t rxBytes;
uint64_t latencyRange[S5_MAX + 1];
uint64_t retries;
// In-kernel MCTP socket acts like UDP. Tx error is uncommon.
// Any Tx error will be refelcted as Rx timeout.
uint64_t errors;
};
LatencyRangeEnum mapLatencyRange(int ms)
{
if (ms <= 10)
return MS1_10;
if (ms <= 100)
return MS10_100;
if (ms <= 1000)
return MS100_1000;
if (ms <= 5000)
return S1_5;
return S5_MAX;
}
void saveMetric(uint8_t eid, size_t size, TxOrRx isTx, bool isRetry)
{
if (METRICS_DUMP_PERIOD == 0)
return;
auto emplaceResult = metricsMap.try_emplace(eid, Metrics{});
Metrics& metrics = emplaceResult.first->second;
if (isTx)
{
metrics.txBytes += size;
metrics.txPackets++;
lastTxTimestamp = std::chrono::steady_clock::now();
isLastTxTimestampValid = true;
if (isRetry)
{
metrics.retries++;
}
}
else
{
metrics.rxBytes += size;
metrics.rxPackets++;
if (isLastTxTimestampValid)
{
int elapsedMs = static_cast<int>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - lastTxTimestamp)
.count());
metrics.latencyRange[mapLatencyRange(elapsedMs)]++;
isLastTxTimestampValid = false;
}
}
}
protected:
bool flightRecorderPolicy;
std::unordered_map<uint8_t, std::deque<FlightRecorderRecord>> recordMap;
bool isLastTxTimestampValid;
std::chrono::time_point<std::chrono::steady_clock> lastTxTimestamp;
std::chrono::time_point<std::chrono::steady_clock> lastMetricsDumpTime;
std::unordered_map<uint8_t, Metrics> metricsMap;
std::unordered_map<uint8_t, Metrics> lasMetricsSnapshot;
public:
FlightRecorder(const FlightRecorder&) = delete;
FlightRecorder(FlightRecorder&&) = delete;
FlightRecorder& operator=(const FlightRecorder&) = delete;
FlightRecorder& operator=(FlightRecorder&&) = delete;
~FlightRecorder() = default;
static FlightRecorder& GetInstance()
{
static FlightRecorder flightRecorder;
return flightRecorder;
}
void increaseError(uint8_t eid)
{
if (METRICS_DUMP_PERIOD == 0)
return;
auto iter = metricsMap.find(eid);
if (iter != metricsMap.end())
{
iter->second.errors++;
}
}
/** @brief Add records to the flightRecorder
* @param[in] eid - The eid of the message
* @param[in] buffer - The request/respose byte buffer
* @param[in] isTx - Whether the message comes from TX or RX
*
* @return void
*/
void saveRecord(uint8_t eid, const FlightRecorderData& buffer, TxOrRx isTx,
bool isRetry = false)
{
saveMetric(eid, buffer.size(), isTx, isRetry);
// if the flight recorder policy is enabled, then only insert the
// messages into the flight recorder, if not this function will be just
// a no-op
if (flightRecorderPolicy)
{
auto emplaceResult =
recordMap.try_emplace(eid, std::deque<FlightRecorderRecord>());
std::deque<FlightRecorderRecord>& records =
emplaceResult.first->second;
auto entry = std::make_tuple(std::chrono::system_clock::now(), isTx,
buffer);
records.push_back(std::move(entry));
if (records.size() > FLIGHT_RECORDER_MAX_ENTRIES)
{
records.pop_front();
}
}
}
/** @brief play flight recorder
*
* @return void
*/
void playRecorder()
{
if (flightRecorderPolicy)
{
std::ofstream recorderOutputFile(flightRecorderDumpPath);
std::cerr << "Dumping the flight recorder into "
<< flightRecorderDumpPath;
for (const auto& [eid, records] : recordMap)
{
recorderOutputFile << "EID: " << (unsigned)eid << "\n";
for (const auto& [timestamp, isTx, message] : records)
{
recorderOutputFile << timestamp << " : ";
recorderOutputFile << (isTx ? "Tx : " : "Rx : ");
for (int byte : message)
{
recorderOutputFile << std::format("{:02x} ", byte);
}
recorderOutputFile << std::endl;
}
}
recorderOutputFile.close();
}
else
{
std::cerr << "Fight recorder policy is disabled";
}
}
void printEidHistory(uint8_t eid)
{
if (!flightRecorderPolicy)
return;
if (!recordMap.contains(eid))
return;
std::cerr << "EID: " << (unsigned)eid << " command history:\n";
for (const auto& [timestamp, isTx, message] : recordMap[eid])
{
std::cerr << timestamp << " : ";
std::cerr << (isTx ? "Tx : " : "Rx : ");
for (int byte : message)
{
std::cerr << std::format("{:02x} ", byte);
}
std::cerr << std::endl;
}
}
void dumpIncrementalMetrics()
{
if (METRICS_DUMP_PERIOD == 0)
return;
const auto now = std::chrono::steady_clock::now();
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
now - lastMetricsDumpTime)
.count();
if (elapsedMs == 0)
{
// Unlikely, just avoid devide by zero
elapsedMs = 1;
}
for (const auto& [eid, current] : metricsMap)
{
auto emplaceResult = lasMetricsSnapshot.try_emplace(eid, Metrics{});
const Metrics& last = emplaceResult.first->second;
std::string output = std::format(
"Incremental metrics: EID: {:2}, TxPackets:{}, TxBytes:{},"
" RxPackets:{}, RxBytes:{}, Retries:{}, Errors:{}, TxRate:{} B/s,"
" RxRate:{} B/s",
eid, current.txPackets - last.txPackets,
current.txBytes - last.txBytes,
current.rxPackets - last.rxPackets,
current.rxBytes - last.rxBytes, current.retries - last.retries,
current.errors - last.errors,
(current.txBytes - last.txBytes) * 1000 / elapsedMs,
(current.rxBytes - last.rxBytes) * 1000 / elapsedMs);
std::cerr << output << std::endl;
}
for (const auto& [eid, current] : metricsMap)
{
auto iter = lasMetricsSnapshot.find(eid);
if (iter == lasMetricsSnapshot.end())
{
continue;
}
const Metrics& last = iter->second;
std::string output = std::format(
"Latency histogram: EID: {:2}, 1~10ms:{}, 10~100ms:{},"
" 100~1000ms:{}, 1~5s:{}, 5~s:{}",
eid, current.latencyRange[MS1_10] - last.latencyRange[MS1_10],
current.latencyRange[MS10_100] - last.latencyRange[MS10_100],
current.latencyRange[MS100_1000] -
last.latencyRange[MS100_1000],
current.latencyRange[S1_5] - last.latencyRange[S1_5],
current.latencyRange[S5_MAX] - last.latencyRange[S5_MAX]);
std::cerr << output << std::endl;
}
lastMetricsDumpTime = now;
lasMetricsSnapshot = metricsMap;
}
};
} // namespace flightrecorder
} // namespace pldm