| // 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); |
| } |