blob: 0e6f8b0d6e7eb9a1fba6bc77558d2d497740928d [file] [log] [blame]
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "views.hpp"
#include "bargraph.hpp"
#include "histogram.hpp"
#include "menu.hpp"
#include <string.h>
#include <algorithm>
extern SensorSnapshot* g_sensor_snapshot;
extern BarGraph<float>* g_bargraph;
extern DBusTopStatistics* g_dbus_statistics;
extern Histogram<float>* g_histogram;
extern DBusTopWindow* g_current_active_view;
extern const std::string FieldNames[];
extern const int FieldPreferredWidths[];
namespace dbus_top_analyzer
{
extern DBusTopStatistics g_dbus_statistics;
}
// Linear interpolation
float Lerp(float a, float b, float t)
{
return a + t * (b - a);
}
// Linear map
float Map(float value, float start1, float stop1, float start2, float stop2,
bool within_bounds)
{
float t = (value - start1) / (stop1 - start1);
float ret = Lerp(start2, stop2, t);
if (within_bounds)
{
if (ret < start2)
ret = start2;
if (ret > stop2)
ret = stop2;
}
return ret;
}
template <typename T>
void HistoryBarGraph(WINDOW* win, const Rect& rect, BarGraph<T>* bargraph)
{
const int RIGHT_MARGIN = 5;
const int x0 = rect.x, y0 = 2;
const int w = rect.w - 2 - RIGHT_MARGIN;
const int h = rect.h - 3; // height of content
wattrset(win, 0);
wattron(win, A_BOLD | A_UNDERLINE);
mvwaddstr(win, 1, x0, "History (Total msg/s)");
wattrset(win, 0);
// 1. Obtain data, determine Y range
std::vector<float> data = bargraph->GetLastNValues(w - RIGHT_MARGIN - 1);
float ymax = -1e20, ymin = 1e20;
if (data.empty())
{
data.push_back(0);
ymin = 0;
ymax = 10;
}
else
{
for (const float x : data)
{
ymax = std::max(ymax, x);
ymin = std::min(ymin, x);
}
}
// Fix edge case for both == 0
float diff = ymax - ymin;
if (diff < 0)
{
diff = -diff;
}
const float EPS = 1e-4;
if (diff < EPS)
{
ymax += 10;
ymin -= 10;
}
// Choose a suitable round-up unit to snap the grid labels to
int snap = 1;
if (ymax < 100)
{
snap = 10;
}
else if (ymax < 10000)
{
snap = 100;
}
else
{
snap = 1000;
}
const float eps = snap / 100.0f;
int label_ymax =
(static_cast<int>((ymax - eps) / snap) + 1) * snap; // round up
int label_ymin = static_cast<int>(ymin / snap) * snap; // round down
float y_per_row = (label_ymax - label_ymin) * 1.0f / (h - 1);
int actual_ymax = label_ymax + static_cast<int>(y_per_row / 2);
int actual_ymin = label_ymin - static_cast<int>(y_per_row / 2);
// 2. Print Y axis ticks
for (int i = 0; i < h; i++)
{
char buf[10];
snprintf(
buf, sizeof(buf), "%-6d",
static_cast<int>(Lerp(label_ymax, label_ymin, i * 1.0f / (h - 1))));
mvwaddstr(win, i + y0, x0 + w - RIGHT_MARGIN + 1, buf);
mvwaddch(win, i + y0, x0, '-');
mvwaddch(win, i + y0, x0 + w - RIGHT_MARGIN, '-');
}
// 3. Go through the historical data and draw on the canvas
for (int i = 0;
i < std::min(static_cast<int>(data.size()), w - RIGHT_MARGIN - 1); i++)
{
float value = data[i];
// antialiasing: todo for now
// float value1 = value; // value1 is 1 column to the right
// if (i > 0) value1 = data[i-1];
int x = x0 + w - i - RIGHT_MARGIN - 1;
float t = Map(value, actual_ymin, actual_ymax, 0, h, true);
int row = static_cast<int>(t);
float remaining = t - row;
char ch; // Last filling character
if (remaining >= 0.66f)
{
ch = ':';
}
else if (remaining >= 0.33f)
{
ch = '.';
}
else
{
ch = ' ';
}
int y = y0 + h - row - 1;
mvwaddch(win, y, x, ch);
for (int j = 0; j < row; j++)
{
mvwaddch(win, y + j + 1, x, ':');
}
}
}
template <typename T>
void DrawHistogram(WINDOW* win, const Rect& rect, Histogram<T>* histogram)
{
// const int MARGIN = 7; // 5 digits margin
const int LEFT_MARGIN = 7;
// const int max_bucket_h = histogram->MaxBucketHeight();
const int H_PAD = 0, V_PAD = 1;
// x0, x1, y0 and y1 are the bounding box of the contents to be printed
const int x0 = rect.x + H_PAD;
const int x1 = rect.x + rect.w - H_PAD;
const int y0 = rect.y + V_PAD;
const int y1 = rect.y + rect.h - 1 - V_PAD;
// Title
wattron(win, A_BOLD | A_UNDERLINE);
mvwaddstr(win, y0, x0, "Method Call Time (us) Histogram");
wattrset(win, 0);
// x2 is the beginning X of the histogram itself (not containing the margin)
const int x2 = x0 + LEFT_MARGIN;
if (histogram->Empty())
{
mvwaddstr(win, (y1 + y0) / 2, (x0 + x1) / 2, "(Empty)");
return;
}
histogram->SetBucketCount(x1 - x2 + 1);
histogram->ComputeHistogram();
// Draw X axis labels
char buf[22];
snprintf(buf, sizeof(buf), "%.2f",
static_cast<float>(histogram->LowPercentile()));
mvwaddstr(win, y1, x0 + LEFT_MARGIN, buf);
snprintf(buf, sizeof(buf), "%.2f",
static_cast<float>(histogram->HighPercentile()));
mvwaddstr(win, y1, x1 + 1 - strlen(buf), buf);
snprintf(buf, sizeof(buf), "%d%%-%d%%",
static_cast<int>(histogram->LowCumDensity() * 100),
static_cast<int>(histogram->HighCumDensity() * 100));
mvwaddstr(win, y1, x0, buf);
// Draw Y axis labels
const float hist_ymax = y1 - 1;
const float hist_ymin = y0 + 1;
const int max_histogram_h = histogram->MaxBucketHeight();
if (hist_ymax <= hist_ymin)
return; // Not enough space for rendering
if (max_histogram_h <= 0)
return;
bool LOG_TRANSFORM = true;
float lg_maxh = 0;
if (LOG_TRANSFORM)
{
lg_maxh = log(max_histogram_h);
}
for (int y = hist_ymin; y <= hist_ymax; y++)
{
// There are (hist_ymax - hist_ymin + 1) divisions
float fullness;
fullness = (hist_ymax - y + 1) * 1.0f / (hist_ymax - hist_ymin + 1);
int h;
if (!LOG_TRANSFORM)
{
h = static_cast<int>(max_histogram_h * fullness);
}
else
{
h = static_cast<int>(exp(fullness * lg_maxh));
}
snprintf(buf, sizeof(buf), "%6d-", h);
mvwaddstr(win, y, x0 + LEFT_MARGIN - strlen(buf), buf);
}
const int bar_height = hist_ymax - hist_ymin + 1; // Height of a full bar
for (int x = x2, bidx = 0; x <= x1; x++, bidx++)
{
int h = histogram->BucketHeight(bidx);
float lines_visible;
if (!LOG_TRANSFORM)
{
lines_visible = h * 1.0f / max_histogram_h * bar_height;
}
else
{
if (h <= 0)
lines_visible = 0;
else
lines_visible = log(h) * 1.0f / lg_maxh * bar_height;
}
// The histogram's top shall start from this line
int y = hist_ymax - static_cast<int>(lines_visible);
float y_frac = lines_visible - static_cast<int>(lines_visible);
char ch; // Last filling character
if (y >= hist_ymin)
{ // At the maximum bucket the Y overflows, so skip
if (y_frac >= 0.66f)
{
ch = ':';
}
else if (y_frac >= 0.33f)
{
ch = '.';
}
else
{
if (y < hist_ymax)
{
ch = ' ';
}
else
{
if (y_frac > 0)
{
ch =
'.'; // Makes long-tailed distribution easier to see
}
}
}
mvwaddch(win, y, x, ch);
}
y++;
for (; y <= hist_ymax; y++)
{
mvwaddch(win, y, x, ':');
}
}
}
void SummaryView::UpdateDBusTopStatistics(DBusTopStatistics* stat)
{
if (!stat)
return;
float interval_secs = stat->seconds_since_last_sample_;
if (interval_secs == 0)
{
interval_secs = GetSummaryIntervalInMillises() / 1000.0f;
}
// Per-second
method_call_ = stat->num_mc_ / interval_secs;
method_return_ = stat->num_mr_ / interval_secs;
signal_ = stat->num_sig_ / interval_secs;
error_ = stat->num_error_ / interval_secs;
total_ = stat->num_messages_ / interval_secs;
g_bargraph->AddValue(total_);
}
std::string Ellipsize(const std::string& s, int len_limit)
{
if (len_limit <= 3)
return s.substr(0, len_limit);
if (static_cast<int>(s.size()) < len_limit)
{
return s;
}
else
{
return s.substr(0, len_limit - 3) + "...";
}
}
void SummaryView::Render()
{
// Draw text
werase(win);
if (!visible_)
return;
wattron(win, A_BOLD | A_UNDERLINE);
mvwaddstr(win, 1, 1, "Message Type | msg/s");
wattrset(win, 0);
const int xend = 30;
std::string s;
s = FloatToString(method_call_);
mvwaddstr(win, 2, 1, "Method Call");
mvwaddstr(win, 2, xend - s.size(), s.c_str());
s = FloatToString(method_return_);
mvwaddstr(win, 3, 1, "Method Return ");
mvwaddstr(win, 3, xend - s.size(), s.c_str());
s = FloatToString(signal_);
mvwaddstr(win, 4, 1, "Signal");
mvwaddstr(win, 4, xend - s.size(), s.c_str());
s = FloatToString(error_);
mvwaddstr(win, 5, 1, "Error ");
mvwaddstr(win, 5, xend - s.size(), s.c_str());
wattron(win, A_UNDERLINE);
s = FloatToString(total_);
mvwaddstr(win, 6, 1, "Total");
mvwaddstr(win, 6, xend - s.size(), s.c_str());
wattroff(win, A_UNDERLINE);
wattrset(win, 0);
// Draw history bar graph
Rect bargraph_rect = rect;
const int bargraph_x = 64;
bargraph_rect.x += bargraph_x;
bargraph_rect.w -= bargraph_x;
HistoryBarGraph(win, bargraph_rect, g_bargraph);
// Draw histogram
Rect histogram_rect = rect;
histogram_rect.x += 32;
histogram_rect.w = bargraph_rect.x - histogram_rect.x - 3;
DrawHistogram(win, histogram_rect, g_histogram);
// Draw border between summary and histogram
for (int y = bargraph_rect.y; y <= bargraph_rect.y + bargraph_rect.h; y++)
{
mvwaddch(win, y, histogram_rect.x - 1, '|');
mvwaddch(win, y, bargraph_rect.x - 1, '|');
}
DrawBorderIfNeeded();
wrefresh(win);
}
void SensorDetailView::Render()
{
werase(win);
if (!visible_)
return;
// If some sensor is focused, show details regarding that sensor
if (state == SensorList)
{ // Otherwise show the complete list
const int ncols = DispSensorsPerRow(); // Number of columns in viewport
const int nrows = DispSensorsPerColumn(); // # rows in viewport
int sensors_per_page = nrows * ncols;
// Just in case the window gets invisibly small
if (sensors_per_page < 1)
return;
int num_sensors = sensor_ids_.size();
int total_num_columns = (num_sensors - 1) / nrows + 1;
bool is_cursor_out_of_view = false;
if (idx0 > choice_ || idx1 <= choice_)
{
is_cursor_out_of_view = true;
}
if (idx0 == INVALID || idx1 == INVALID)
{
is_cursor_out_of_view = true;
}
if (is_cursor_out_of_view)
{
idx0 = 0, idx1 = sensors_per_page;
}
while (idx1 <= choice_)
{
idx0 += nrows;
idx1 += nrows;
}
const int y0 = 2; // to account for the border and info line
const int x0 = 4; // to account for the left overflow marks
int y = y0, x = x0;
for (int i = 0; i < sensors_per_page; i++)
{
int idx = idx0 + i;
if (idx < static_cast<int>(sensor_ids_.size()))
{
if (idx == choice_)
{
wattrset(win, A_REVERSE);
}
std::string s = sensor_ids_[idx];
if (static_cast<int>(s.size()) > col_width) {
s = s.substr(0, col_width - 2) + "..";
} else {
while (static_cast<int>(s.size()) < col_width)
{
s.push_back(' ');
}
}
mvwprintw(win, y, x, s.c_str());
wattrset(win, 0);
}
else
break;
y++;
if (i % nrows == nrows - 1)
{
y = y0;
x += col_width + h_spacing;
}
}
// Print overflow marks to the right of the screen
for (int i = 0; i < nrows; i++)
{
int idx = idx0 + sensors_per_page + i;
if (idx < num_sensors)
{
mvwaddch(win, y0 + i, x, '>');
}
}
// Print overflow marks to the left of the screen
for (int i = 0; i < nrows; i++)
{
int idx = idx0 - nrows + i;
if (idx >= 0)
{
mvwaddch(win, y0 + i, 2, '<');
}
}
// idx1 is one past the visible range, so no need to +1
const int col0 = idx0 / nrows + 1, col1 = idx1 / nrows;
mvwprintw(win, 1, 2, "Columns %d-%d of %d", col0, col1,
total_num_columns);
mvwprintw(win, 1, rect.w - 15, "%d sensors", sensor_ids_.size());
}
else if (state == SensorDetail)
{
// sensor_ids_ is the cached list of sensors, it should be the same size
// as the actual number of sensors in the snapshot
mvwprintw(win, 1, 2, "Details of sensor %s", curr_sensor_id_.c_str());
mvwprintw(win, 1, rect.w - 15, "Sensor %d/%u", choice_ + 1,
sensor_ids_.size()); // 1-based
std::vector<Sensor*> sensors =
g_sensor_snapshot->FindSensorsBySensorID(curr_sensor_id_);
const int N = static_cast<int>(sensors.size());
const int w = rect.w - 5;
mvwprintw(win, 3, 2, "There are %d sensors with the name %s", N,
curr_sensor_id_.c_str());
int y = 5;
int x = 2;
if (N > 0)
{
for (int j = 0; j < N; j++)
{
Sensor* sensor = sensors[j];
mvwprintw(win, y, x, "%d/%d", j + 1, N);
char buf[200];
snprintf(buf, sizeof(buf), "DBus Service : %s",
sensor->ServiceName().c_str());
y += DrawTextWithWidthLimit(win, buf, y, x, w, "/");
snprintf(buf, sizeof(buf), "DBus Connection : %s",
sensor->ConnectionName().c_str());
y += DrawTextWithWidthLimit(win, buf, y, x, w, "/");
snprintf(buf, sizeof(buf), "DBus Object Path: %s",
sensor->ObjectPath().c_str());
y += DrawTextWithWidthLimit(win, buf, y, x, w, "/");
y++;
}
}
else
{
mvwprintw(win, y, x, "Sensor details not found");
}
}
DrawBorderIfNeeded();
wrefresh(win);
}
std::string SensorDetailView::GetStatusString()
{
if (state == SensorList)
{
return "[Arrow Keys]=Move Cursor [Q]=Deselect [Enter]=Show Sensor "
"Detail";
}
else
{
return "[Arrow Keys]=Cycle Through Sensors [Esc/Q]=Exit";
}
}
DBusStatListView::DBusStatListView() : DBusTopWindow()
{
highlight_col_idx_ = 0;
sort_col_idx_ = 0;
sort_order_ = SortOrder::Ascending;
horizontal_pan_ = 0;
row_idx_ = INVALID;
disp_row_idx_ = 0;
horizontal_pan_ = 0;
menu1_ = new ArrowKeyNavigationMenu(this);
menu2_ = new ArrowKeyNavigationMenu(this);
// Load all available field names
std::set<std::string> inactive_fields;
std::set<std::string> active_fields;
// Default choice of field names
const int N = static_cast<int>(sizeof(FieldNames) / sizeof(FieldNames[0]));
for (int i = 0; i < N; i++)
{
inactive_fields.insert(FieldNames[i]);
}
for (const std::string& s :
dbus_top_analyzer::g_dbus_statistics.GetFieldNames())
{
inactive_fields.erase(s);
active_fields.insert(s);
}
for (int i = 0; i < N; i++)
{
const std::string s = FieldNames[i];
if (inactive_fields.count(s) > 0)
{
menu1_->AddItem(s);
}
else
{
menu2_->AddItem(s);
}
}
curr_menu_state_ = LeftSide;
menu_h_ = 5;
menu_w_ = 24; // Need at least 2*padding + 15 for enough space, see menu.hpp
menu_margin_ = 6;
// Populate preferred column widths
for (int i = 0; i < N; i++)
{
column_widths_[FieldNames[i]] = FieldPreferredWidths[i];
}
}
std::pair<int, int> DBusStatListView::GetXSpanForColumn(const int col_idx)
{
std::vector<int> cw = ColumnWidths();
if (col_idx < 0 || col_idx >= static_cast<int>(cw.size()))
{
return std::make_pair(INVALID, INVALID);
}
int x0 = 0, x1 = 0;
for (int i = 0; i < col_idx; i++)
{
if (i > 0)
{
x0 += cw[i];
}
}
x1 = x0 + cw[col_idx] - 1;
return std::make_pair(x0, x1);
}
// If tolerance > 0, consider overlap before 2 intervals intersect
// If tolerance ==0, consider overlap if 2 intervals exactly intersect
// If tolerance < 0, consider overlap if Minimal Translate Distance is >=
// -threshold
bool IsSpansOverlap(const std::pair<int, int>& s0,
const std::pair<int, int>& s1, int tolerance)
{
if (tolerance >= 0)
{
if (s0.second < s1.first - tolerance)
return false;
else if (s1.second < s0.first - tolerance)
return false;
else
return true;
}
else
{
// Compute overlapping distance
std::vector<std::pair<int, int>> tmp(
4); // [x, 1] means the start of interval
// [x,-1] means the end of interval
tmp[0] = std::make_pair(s0.first, 1);
tmp[1] = std::make_pair(s0.second, -1);
tmp[2] = std::make_pair(s1.first, 1);
tmp[3] = std::make_pair(s1.second, -1);
std::sort(tmp.begin(), tmp.end());
int overlap_x0 = -INVALID, overlap_x1 = -INVALID;
int idx = 0;
const int N = static_cast<int>(tmp.size());
int level = 0;
while (idx < N)
{
const int x = tmp[idx].first;
while (idx < N && x == tmp[idx].first)
{
level += tmp[idx].second;
idx++;
}
// The starting position of the overlap
if (level == 2)
{
overlap_x0 = idx - 1;
}
// The ending position of the overlap
if (overlap_x0 != -INVALID && level < 2 && overlap_x1 == -INVALID)
{
overlap_x1 = idx - 1;
}
}
const int overlap_length = overlap_x1 - overlap_x0 + 1;
if (overlap_length >= -tolerance)
return true;
else
return false;
}
}
bool DBusStatListView::IsXSpanVisible(const std::pair<int, int>& xs,
int tolerance)
{
const std::pair<int, int> vxs = {horizontal_pan_, horizontal_pan_ + rect.w};
return IsSpansOverlap(xs, vxs, tolerance);
}
std::vector<std::string> DBusStatListView::ColumnHeaders()
{
return visible_columns_;
}
std::vector<int> DBusStatListView::ColumnWidths()
{
std::vector<int> widths = {8}; // for "Msg/s"
std::vector<std::string> agg_headers = visible_columns_;
std::vector<int> agg_widths(agg_headers.size(), 0);
for (int i = 0; i < static_cast<int>(agg_headers.size()); i++)
{
agg_widths[i] = column_widths_[agg_headers[i]];
}
widths.insert(widths.end(), agg_widths.begin(), agg_widths.end());
return widths;
}
// Coordinate systems are in world space, +x faces to the right
// Viewport: [horizontal_pan_, horizontal_pan_ + rect.w]
// Contents: [ column_width[0] ][ column_width[1] ][ column_width[2] ]
void DBusStatListView::PanViewportOrMoveHighlightedColumn(const int delta_x)
{
// If the column to the left is visible, highlight it
const int N = static_cast<int>(ColumnHeaders().size());
bool col_idx_changed = false;
if (delta_x < 0)
{ // Pan left
if (highlight_col_idx_ > 0)
{
std::pair<int, int> xs_left =
GetXSpanForColumn(highlight_col_idx_ - 1);
if (IsXSpanVisible(xs_left, -1))
{
highlight_col_idx_--;
col_idx_changed = true;
}
}
if (!col_idx_changed)
{
horizontal_pan_ += delta_x;
}
}
else if (delta_x > 0)
{ // Pan right
if (highlight_col_idx_ < N - 1)
{
std::pair<int, int> xs_right =
GetXSpanForColumn(highlight_col_idx_ + 1);
if (IsXSpanVisible(xs_right, -1))
{
highlight_col_idx_++;
col_idx_changed = true;
}
}
if (!col_idx_changed)
{
horizontal_pan_ += delta_x;
}
}
}
void DBusStatListView::OnKeyDown(const std::string& key)
{
{
switch (curr_menu_state_)
{
case LeftSide:
{
if (key == "up")
{
menu1_->OnKeyDown("up");
}
else if (key == "down")
{
menu1_->OnKeyDown("down");
}
else if (key == "right")
{
SetMenuState(RightSide);
}
else if (key == "enter")
{
SetMenuState(Hidden);
}
else if (key == "space")
{
std::string ch;
if (menu1_->RemoveHighlightedItem(&ch))
{
menu2_->AddItem(ch);
}
}
break;
}
case RightSide:
{
if (key == "up")
{
menu2_->OnKeyDown("up");
}
else if (key == "down")
{
menu2_->OnKeyDown("down");
}
else if (key == "left")
{
SetMenuState(LeftSide);
}
else if (key == "enter")
{
SetMenuState(Hidden);
}
else if (key == "space")
{
std::string ch;
if (menu2_->RemoveHighlightedItem(&ch))
{
menu1_->AddItem(ch);
}
}
break;
}
case Hidden:
{
if (key == "enter")
{
switch (last_menu_state_)
{
case LeftSide:
case RightSide:
SetMenuState(last_menu_state_);
break;
default:
SetMenuState(LeftSide);
}
}
else if (key == "left")
{
PanViewportOrMoveHighlightedColumn(-2);
}
else if (key == "right")
{
PanViewportOrMoveHighlightedColumn(2);
}
else if (key == "up")
{
disp_row_idx_--;
if (disp_row_idx_ < 0)
{
disp_row_idx_ = 0;
}
}
else if (key == "down")
{
disp_row_idx_++;
const int N = static_cast<int>(stats_snapshot_.size());
if (disp_row_idx_ >= N)
{
disp_row_idx_ = N - 1;
}
}
else if (key == "a")
{
sort_order_ = SortOrder::Ascending;
sort_col_idx_ = highlight_col_idx_;
break;
}
else if (key == "d")
{
sort_order_ = SortOrder::Descending;
sort_col_idx_ = highlight_col_idx_;
break;
}
break;
}
}
}
Render();
}
// Window C
void DBusStatListView::Render()
{
werase(win);
if (!visible_)
return;
int num_lines_shown = rect.h - 3;
if (curr_menu_state_ == LeftSide || curr_menu_state_ == RightSide)
{
menu1_->Render();
menu2_->Render();
num_lines_shown -= (menu_h_ + 3);
// Draw the arrow
const int x1 = menu1_->rect_.x;
const int h1 = menu1_->rect_.h;
const int x2 = menu2_->rect_.x;
const int w2 = menu2_->rect_.w;
const int y1 = menu1_->rect_.y;
const int arrow_x = (x1 + x2 + w2) / 2 - 2;
const int arrow_y = y1 + 2;
const int caption_x = x1;
const int caption_y = y1 + h1;
for (int x = 1; x < rect.w - 1; x++)
{
mvwaddch(win, y1 - 3, x, '-');
}
mvwprintw(win, y1 - 3, arrow_x - 8, "Press [Enter] to show/hide");
mvwprintw(win, y1 - 2, caption_x - 5,
"DBus fields for aggregating and sorting results:");
if (curr_menu_state_ == LeftSide)
{
mvwprintw(win, y1 - 1, x1 - 4, "--[ Available Fields ]--");
mvwprintw(win, y1 - 1, x2 - 4, "--- Active Fields ---");
}
else
{
mvwprintw(win, y1 - 1, x1 - 4, "--- Available Fields ---");
mvwprintw(win, y1 - 1, x2 - 4, "--[ Active Fields ]--");
}
if (curr_menu_state_ == LeftSide)
{
mvwprintw(win, arrow_y, arrow_x, "-->");
mvwprintw(win, caption_y, caption_x,
"Press [Space] to move to the right");
}
else
{
mvwprintw(win, arrow_y, arrow_x, "<--");
mvwprintw(win, caption_y, caption_x,
"Press [Space] to move to the left");
}
}
std::vector<std::string> headers;
std::vector<int> widths;
visible_columns_ = g_dbus_statistics->GetFieldNames();
std::vector<std::string> agg_headers = visible_columns_;
std::vector<int> agg_widths(agg_headers.size(), 0);
for (int i = 0; i < static_cast<int>(agg_headers.size()); i++)
{
agg_widths[i] = column_widths_[agg_headers[i]];
}
headers.insert(headers.end(), agg_headers.begin(), agg_headers.end());
widths.insert(widths.end(), agg_widths.begin(), agg_widths.end());
std::vector<int> xs;
int curr_x = 2 - horizontal_pan_;
for (const int w : widths)
{
xs.push_back(curr_x);
curr_x += w;
}
const int N = headers.size();
// Bound col_idx_
if (highlight_col_idx_ >= N)
{
highlight_col_idx_ = N - 1;
}
// Render column headers
for (int i = 0; i < N; i++)
{
std::string s = headers[i];
// 1 char outside boundary = start printing from the second character,
// etc
// Print "<" for Ascending order (meaning: row 0 < row 1 < row 2 ... )
// Print ">" for Descending order (meaning: row 0 > row 1 > row 2 ... )
if (sort_col_idx_ == i)
{
if (sort_order_ == SortOrder::Ascending)
{
s.push_back('<');
}
else
{
s.push_back('>');
}
}
// Highlight the "currently-selected column"
if (highlight_col_idx_ == i)
{
wattrset(win, 0);
wattron(win, A_REVERSE);
}
else
{
wattrset(win, 0);
wattron(win, A_UNDERLINE);
}
int x = xs[i];
if (x < 0)
{
if (-x < static_cast<int>(s.size()))
{
s = s.substr(-x);
}
else
s = "";
x = 0;
}
mvwaddstr(win, 1, x, s.c_str());
}
wattrset(win, 0);
// Time since the last update of Window C
float interval_secs = g_dbus_statistics->seconds_since_last_sample_;
if (interval_secs == 0)
{
interval_secs = GetSummaryIntervalInMillises() / 1000.0f;
}
stats_snapshot_ = g_dbus_statistics->StatsSnapshot();
const int nrows = static_cast<int>(stats_snapshot_.size());
const std::vector<DBusTopSortField> fields = g_dbus_statistics->GetFields();
const int ncols = static_cast<int>(fields.size());
// Merge the list of DBus Message properties & computed metrics together
std::map<std::vector<std::string>, DBusTopComputedMetrics>::iterator itr =
stats_snapshot_.begin();
struct StringOrFloat
{ // Cannot use union so using struct
std::string s;
float f;
};
// "Stage" the snapshot for displaying in the form of a spreadsheet
std::vector<std::pair<StringOrFloat, std::vector<std::string>>>
stats_snapshot_staged;
const DBusTopSortField sort_field = fields[sort_col_idx_];
const bool is_sort_key_numeric = DBusTopSortFieldIsNumeric(sort_field);
for (int i = 0; i < nrows; i++) // One row of cells
{
int idx0 = 0; // indexing into the std::vector<string> of each row
std::vector<std::string> row;
StringOrFloat sort_key; // The key used for sorting
for (int j = 0; j < ncols; j++) // one column in the row
{
DBusTopSortField field = fields[j];
// Populate the content of stats_snapshot_staged
StringOrFloat sof; // Represents this column
// When we haven't used up all
if (idx0 < static_cast<int>(itr->first.size()))
{
sof.s = itr->first[idx0];
}
switch (field)
{
case kSender: // string
case kDestination: // string
case kInterface: // string
case kPath: // string
case kMember: // string
case kSenderPID: // numeric
case kSenderCMD: // string
row.push_back(itr->first[idx0]);
idx0++;
if (field == kSenderPID)
{
// Note: attempting to std::atof("(unknown)") on the BMC
// will cause hang. And GDB won't show backtrace.
if (sof.s == "(unknown)")
{
if (sort_order_ == Ascending)
{
sof.f = -1;
}
else
{
sof.f = 1e20;
}
}
else
{
sof.f = std::atof(sof.s.c_str());
}
}
break;
case kMsgPerSec: // Compute "messages per second"
{
int numbers[] = {
itr->second.num_method_calls,
itr->second.num_method_returns,
itr->second.num_signals,
itr->second.num_errors,
};
int the_sum = 0; // For sorting
std::string s; // String representation in the form or
// "1.00/2.00/3.00/4.00"
for (int i = 0; i < 4; i++)
{
the_sum += numbers[i];
if (i > 0)
s += "/";
float per_sec = numbers[i] / interval_secs;
s += FloatToString(per_sec);
}
row.push_back(s);
sof.f = the_sum;
break;
}
case kAverageLatency: // Compute "average Method Call latency"
const DBusTopComputedMetrics& m = itr->second;
if (m.num_method_calls == 0)
{
row.push_back("n/a");
if (sort_order_ == Ascending)
{
sof.f = -1; // Put to the top
}
else
{
sof.f = 1e20; // Put to the top
}
}
else
{
float avg_latency_usec =
m.total_latency_usec / m.num_method_calls;
row.push_back(FloatToString(avg_latency_usec));
sof.f = avg_latency_usec;
}
break;
}
if (j == sort_col_idx_)
{
sort_key = sof;
}
}
stats_snapshot_staged.push_back(std::make_pair(sort_key, row));
itr++;
}
// Sort the "staged snapshot" using the sort_key, using different functions
// depending on whether sort key is numeric or string
if (is_sort_key_numeric)
{
std::sort(
stats_snapshot_staged.begin(), stats_snapshot_staged.end(),
[](const std::pair<StringOrFloat, std::vector<std::string>>& a,
const std::pair<StringOrFloat, std::vector<std::string>>& b) {
return a.first.f < b.first.f;
});
}
else
{
std::sort(
stats_snapshot_staged.begin(), stats_snapshot_staged.end(),
[](const std::pair<StringOrFloat, std::vector<std::string>>& a,
const std::pair<StringOrFloat, std::vector<std::string>>& b) {
return a.first.s < b.first.s;
});
}
if (sort_order_ == Descending)
{
std::reverse(stats_snapshot_staged.begin(),
stats_snapshot_staged.end());
}
// Minus 2 because of "msgs/s" and "+"
const int num_fields = N;
// The Y span of the area for rendering the "spreadsheet"
const int y0 = 2, y1 = y0 + num_lines_shown - 1;
// Key is sender, destination, interface, path, etc
for (int i = 0, shown = 0;
i + disp_row_idx_ < static_cast<int>(stats_snapshot_staged.size()) &&
shown < num_lines_shown;
i++, shown++)
{
std::string s;
int x = 0;
const std::vector<std::string> key =
stats_snapshot_staged[i + disp_row_idx_].second;
for (int j = 0; j < num_fields; j++)
{
x = xs[j];
s = key[j];
// Determine column width limit for this particular column
int col_width = 100;
if (j < num_fields - 1)
{
col_width = xs[j + 1] - xs[j] - 1;
}
s = Ellipsize(s, col_width);
if (x < 0)
{
if (-x < static_cast<int>(s.size()))
s = s.substr(-x);
else
s = "";
x = 0;
}
// Trim if string overflows to the right
if (x + static_cast<int>(s.size()) > rect.w)
{
s = s.substr(0, rect.w - x);
}
mvwaddstr(win, 2 + i, x, s.c_str());
}
}
// Overflows past the top ...
if (disp_row_idx_ > 0)
{
std::string x = " [+" + std::to_string(disp_row_idx_) + " rows above]";
mvwaddstr(win, y0, rect.w - static_cast<int>(x.size()) - 1, x.c_str());
}
// Overflows past the bottom ...
const int last_disp_row_idx = disp_row_idx_ + num_lines_shown - 1;
if (last_disp_row_idx < nrows - 1)
{
std::string x = " [+" +
std::to_string((nrows - 1) - last_disp_row_idx) +
" rows below]";
mvwaddstr(win, y1, rect.w - static_cast<int>(x.size()) - 1, x.c_str());
}
DrawBorderIfNeeded();
wrefresh(win);
}
void DBusStatListView::OnResize(int win_w, int win_h)
{
rect.y = 8 - MARGIN_BOTTOM;
rect.w = win_w - (win_w / 2) + 1; // Perfectly overlap on the vertical edge
rect.x = win_w - rect.w;
rect.h = win_h - rect.y - MARGIN_BOTTOM;
const int x0 = rect.w / 2 - menu_w_ - menu_margin_ / 2;
const int x1 = x0 + menu_margin_ + menu_w_;
const int menu_y = rect.h - menu_h_;
menu1_->SetRect(Rect(x0, menu_y, menu_w_, menu_h_)); // Local coordinates
menu1_->SetOrder(ArrowKeyNavigationMenu::Order::ColumnMajor);
menu2_->SetRect(Rect(x1, menu_y, menu_w_, menu_h_));
menu2_->SetOrder(ArrowKeyNavigationMenu::Order::ColumnMajor);
UpdateWindowSizeAndPosition();
}
std::vector<DBusTopSortField> DBusStatListView::GetSortFields()
{
std::vector<DBusTopSortField> ret;
const int N = sizeof(FieldNames) / sizeof(FieldNames[0]);
for (const std::string& s : menu2_->Items())
{
for (int i = 0; i < N; i++)
{
if (FieldNames[i] == s)
{
ret.push_back(static_cast<DBusTopSortField>(i));
break;
}
}
}
return ret;
}
std::string DBusStatListView::GetStatusString()
{
if (curr_menu_state_ == LeftSide || curr_menu_state_ == RightSide)
{
return "[Enter]=Hide Panel [Space]=Choose Entry [Arrow Keys]=Move "
"Cursor";
}
else
{
return "[Enter]=Show Column Select Panel [Arrow Keys]=Pan View";
}
}
void FooterView::Render()
{
werase(win);
const time_t now = time(nullptr);
const char* date_time = ctime(&now);
wattrset(win, 0);
std::string help_info;
if (g_current_active_view == nullptr)
{
help_info = "Press [Tab] to cycle through views";
}
else
{
help_info = g_current_active_view->GetStatusString();
}
mvwaddstr(win, 0, 1, date_time);
mvwaddstr(win, 0, rect.w - int(help_info.size()) - 1, help_info.c_str());
wrefresh(win);
}