open_toontown_panda3d/pandatool/src/pstatserver/pStatTimeline.cxx

916 lines
28 KiB
C++

/**
* PANDA 3D SOFTWARE
* Copyright (c) Carnegie Mellon University. All rights reserved.
*
* All use of this software is subject to the terms of the revised BSD
* license. You should have received a copy of this license along
* with this source code in a file named "LICENSE."
*
* @file pStatTimeline.cxx
* @author rdb
* @date 2022-02-11
*/
#include "pStatTimeline.h"
#include "pStatFrameData.h"
#include "pStatCollectorDef.h"
#include "string_utils.h"
#include "config_pstatclient.h"
#include <algorithm>
/**
*
*/
PStatTimeline::
PStatTimeline(PStatMonitor *monitor, int xsize, int ysize) :
PStatGraph(monitor, xsize, ysize)
{
// Default to 1 millisecond per 10 pixels.
_time_scale = 1 / 10000.0;
_target_time_scale = _time_scale;
_guide_bar_units = GBU_ms | GBU_show_units;
// Load in the initial data, so that the user can see everything back to the
// beginning (or as far as pstats-history goes back to).
const PStatClientData *client_data = monitor->get_client_data();
if (client_data != nullptr) {
size_t row_offset = 0;
// Ignore this nasty "inverted" collector, which messes up everything.
_app_collector_index = client_data->find_collector("App:Show code:General");
for (int thread_index = 0; thread_index < client_data->get_num_threads(); ++thread_index) {
_threads.emplace_back();
ThreadRow &thread_row = _threads.back();
thread_row._row_offset = row_offset;
if (!client_data->has_thread(thread_index)) {
continue;
}
thread_row._visible = true;
const PStatThreadData *thread_data = client_data->get_thread_data(thread_index);
if (thread_data != nullptr) {
_threads_changed = true;
if (!thread_data->is_empty()) {
int oldest_frame = thread_data->get_oldest_frame_number();
int latest_frame = thread_data->get_latest_frame_number();
double oldest_start_time = thread_data->get_frame(oldest_frame).get_start();
double latest_end_time = thread_data->get_frame(latest_frame).get_end();
if (!_have_start_time) {
_have_start_time = true;
_lowest_start_time = oldest_start_time;
}
else {
_lowest_start_time = std::min(_lowest_start_time, oldest_start_time);
}
_highest_end_time = std::max(_highest_end_time, latest_end_time + _clock_skew);
for (int frame = oldest_frame; frame <= latest_frame; ++frame) {
if (thread_data->has_frame(frame)) {
update_bars(thread_index, frame);
}
}
}
}
row_offset += thread_row._rows.size() + 1;
}
}
_start_time = _lowest_start_time;
_target_start_time = _start_time;
monitor->_timelines.insert(this);
}
/**
*
*/
PStatTimeline::
~PStatTimeline() {
_monitor->_timelines.erase(this);
}
/**
* Called as each frame's data is made available. There is no guarantee the
* frames will arrive in order, or that all of them will arrive at all. The
* monitor should be prepared to accept frames received out-of-order or
* missing.
*/
void PStatTimeline::
new_data(int thread_index, int frame_number) {
const PStatClientData *client_data = _monitor->get_client_data();
if (client_data != nullptr) {
const PStatThreadData *thread_data =
client_data->get_thread_data(thread_index);
if (thread_data != nullptr && thread_data->has_frame(frame_number)) {
const PStatFrameData &frame_data = thread_data->get_frame(frame_number);
double frame_start = frame_data.get_start() + _clock_skew;
double frame_end = frame_data.get_end() + _clock_skew;
if (thread_index == 0 && _app_collector_index == -1) {
_app_collector_index = client_data->find_collector("App:Show code:General");
}
if (!_have_start_time) {
_start_time = frame_start;
_have_start_time = true;
_lowest_start_time = _start_time;
}
else if (_start_time < _lowest_start_time) {
_lowest_start_time = _start_time;
}
if (frame_end > _highest_end_time) {
_highest_end_time = frame_end;
}
while (thread_index >= (int)_threads.size()) {
_threads_changed = true;
if (_threads.size() == 0) {
_threads.resize(1);
} else {
_threads.resize(_threads.size() + 1);
_threads[_threads.size() - 1]._row_offset =
_threads[_threads.size() - 2]._row_offset;
if (_threads[_threads.size() - 2]._visible) {
_threads[_threads.size() - 1]._row_offset +=
_threads[_threads.size() - 2]._rows.size() + 1;
}
}
}
if (update_bars(thread_index, frame_number)) {
// The number of rows was changed.
// Change the offset of all subsequent ThreadRows.
ThreadRow &thread_row = _threads[thread_index];
size_t offset = thread_row._row_offset + thread_row._rows.size() + 1;
for (size_t ti = (size_t)(thread_index + 1); ti < _threads.size(); ++ti) {
_threads[ti]._row_offset = offset;
if (_threads[ti]._visible) {
offset += _threads[ti]._rows.size() + 1;
}
}
_threads_changed = true;
normal_guide_bars();
force_redraw();
}
else if (frame_end >= _start_time || frame_start <= _start_time + get_horizontal_scale()) {
normal_guide_bars();
begin_draw();
draw_thread(thread_index, frame_start, frame_end);
end_draw();
}
}
}
idle();
}
/**
* Called by new_data(). Updates the bars without doing any drawing. Returns
* true if the number of rows was changed (forcing a full redraw), false if
* only new bars were added on the right side.
*/
bool PStatTimeline::
update_bars(int thread_index, int frame_number) {
const PStatClientData *client_data = _monitor->get_client_data();
const PStatThreadData *thread_data = client_data->get_thread_data(thread_index);
const PStatFrameData &frame_data = thread_data->get_frame(frame_number);
ThreadRow &thread_row = _threads[thread_index];
thread_row._label = client_data->get_thread_name(thread_index);
bool changed_num_rows = false;
if (!thread_row._visible) {
thread_row._visible = true;
changed_num_rows = true;
}
// pair<int collector_index, double start_time>
pvector<std::pair<int, double> > stack;
bool had_children = false;
double frame_start = frame_data.get_start() + _clock_skew;
double prev = frame_start;
// There may still be open collectors from the previous frame. Rebuild the
// stack based on that so we can close them properly.
for (size_t i = 0; i < thread_row._rows.size(); ++i) {
Row &row = thread_row._rows[i];
// Keep iterating from the back until we get to the previous frame.
for (size_t j = 0; j < row.size(); ++j) {
ColorBar &bar = row[row.size() - j - 1];
if (bar._frame_number < frame_number) {
// Was this collector left unstopped at the end of the last frame?
if (bar._open_end && bar._frame_number == frame_number - 1) {
if (i >= stack.size()) {
stack.resize(i + 1, std::make_pair(-1, 0.0));
}
stack[i] = std::make_pair(bar._collector_index, bar._start);
// Remove this bar for now, we'll recreate it when we close it.
row.erase(row.begin() + (row.size() - j - 1));
}
break;
}
}
}
size_t num_events = frame_data.get_num_events();
for (size_t i = 0; i < num_events; ++i) {
int collector_index = frame_data.get_time_collector(i);
double time = frame_data.get_time(i) + _clock_skew;
if (time < prev) {
// Apparently, it is too hard to ask for monotonically increasing time
// values! Don't be tempted to sort the values, or think this is a bug,
// the client can arbitrarily move back the clock if it feels like it!
// I had some fancy mechanism that handled clock skew properly for
// multiple out-of-order frames, but I gave up when I realised it's not
// possible to solve this problem for multiple threads, so we just assume
// this is done just once at startup before the other threads exist.
double delta = prev - time;
_clock_skew += delta;
_highest_end_time += delta;
time = prev;
nout << "Correcting for clock skew of "
<< format_number(delta, GBU_show_units | GBU_ms)
<< " in frame " << frame_number << " of thread "
<< thread_index << "\n";
// Move all bars after this frame to the right by this amount.
for (ThreadRow &thread_row : _threads) {
for (Row &row : thread_row._rows) {
for (ColorBar &bar : row) {
if (bar._frame_number > frame_number) {
bar._start += delta;
bar._end += delta;
}
}
}
}
}
prev = time;
if (collector_index == _app_collector_index) {
continue;
}
if (frame_data.is_start(i)) {
if (collector_index == 0 && !stack.empty()) {
// Collector 0 always goes at the top, no matter what.
stack[0].first = 0;
stack[0].second = std::max(time, _start_time);
} else {
stack.push_back(std::make_pair(collector_index, std::max(time, _start_time)));
if (stack.size() > thread_row._rows.size()) {
thread_row._rows.resize(stack.size());
changed_num_rows = true;
}
had_children = false;
}
}
else if (!stack.empty() && stack.back().first == collector_index) {
// Most likely case, ending the most recent collector that is still open.
double start_time = stack.back().second;
stack.pop_back();
thread_row._rows[stack.size()].push_back({
start_time, time, collector_index, thread_index, frame_number, false, false});
// Pop off stack levels for prematurely ended collectors (see below).
while (!stack.empty() && stack.back().first < 0) {
stack.pop_back();
}
had_children = true;
}
else {
// Unlikely case: ending a collector before a "child" has ended.
// Go back and clear the row where this collector started.
// Don't decrement the row index.
size_t i;
for (i = 0; i < stack.size(); ++i) {
auto &item = stack[stack.size() - 1 - i];
if (item.first == collector_index) {
thread_row._rows[stack.size() - 1 - i].push_back({
item.second, time, collector_index, thread_index, frame_number, false, false});
item.first = -1;
break;
}
}
if (i == stack.size()) {
// We stopped a collector that wasn't started at all. That means it
// must have already been running when this frame started, which we
// normally handle by pre-populating the stack with the open collectors
// from last frame, but maybe we didn't get the last frame (yet).
// Unfortunately we need to add it above any bars we've already started
// and stopped, which means we need to shift everything down belonging
// to this frame, except for the top-level Frame collector. This is
// probably pretty inefficient, but it should be pretty rare for frames
// to arrive out-of-order in this manner.
nassertd(!stack.empty()) continue;
stack.insert(stack.begin() + 1, std::make_pair(-1, 0.0));
const size_t num_rows = thread_row._rows.size();
for (size_t j = num_rows - 1; j >= 1; --j) {
size_t row_size = thread_row._rows[j].size();
for (size_t k = row_size; k > 0; --k) {
Row &row = thread_row._rows[j];
ColorBar bar = row[k - 1];
if (bar._frame_number == frame_number) {
row.erase(row.begin() + (k - 1));
if (j + 1 >= thread_row._rows.size()) {
thread_row._rows.resize(j + 2);
changed_num_rows = true;
}
// Insert it into the row below while retaining sorting.
Row &row2 = thread_row._rows[j + 1];
row2.insert(std::upper_bound(row2.begin(), row2.end(), bar), bar);
}
else if (bar._frame_number < frame_number) {
break;
}
}
}
// Now insert the bar, just below Frame level.
if (thread_row._rows.size() < 2) {
thread_row._rows.resize(2);
changed_num_rows = true;
}
thread_row._rows[1].push_back({
frame_start, time, collector_index, thread_index,
frame_number, true, false});
}
else if (i > 0 && !had_children) {
// Figure out if the currently active collector could actually be
// slotted higher. This prevents the staircase effect where
// overlapping collectors will cause the number of rows to continue
// to grow at every overlap. We only do this if the current
// collector has not had children yet, that'd be too confusing.
int j = stack.size() - 3;
while (j > 0) {
auto &item = stack[j];
if (item.first >= 0) {
// Nope, can't do.
break;
}
// Yes, does this row have enough space?
Row &row = thread_row._rows[j];
if (row.empty() || row.back()._end < stack.back().second) {
// It does.
item = std::move(stack.back());
stack.pop_back();
while (!stack.empty() && stack.back().first < 0) {
stack.pop_back();
}
break;
}
--j;
}
}
}
}
// Add all unclosed bars, marking them as open-ended, for later gluing.
while (!stack.empty()) {
int collector_index = stack.back().first;
if (collector_index >= 0) {
double start_time = stack.back().second;
thread_row._rows[stack.size() - 1].push_back({
start_time, frame_data.get_end() + _clock_skew,
collector_index, thread_index, frame_number, false, true,
});
}
stack.pop_back();
}
if (thread_row._last_frame >= 0 && frame_number < thread_row._last_frame) {
// Added a frame out of order.
for (Row &row : thread_row._rows) {
// Sort by end time.
std::sort(row.begin(), row.end());
// Glue together open ends and beginnings that match up.
size_t end = row.size() - 1;
for (size_t i = 0; i < end;) {
ColorBar &left = row[i];
ColorBar &right = row[i + 1];
if (left._collector_index == right._collector_index &&
left._frame_number + 1 == right._frame_number &&
left._open_end && right._open_begin) {
// Erase the left one, to maintain the sorting by end time.
right._open_begin = false;
right._start = left._start;
row.erase(row.begin() + i);
--end;
} else {
++i;
}
}
}
} else {
thread_row._last_frame = frame_number;
}
return changed_num_rows;
}
/**
* Called when the mouse hovers over the graph, and should return the text that
* should appear on the tooltip.
*/
std::string PStatTimeline::
get_bar_tooltip(int row, int x) const {
ColorBar bar;
if (find_bar(row, x, bar)) {
const PStatClientData *client_data = _monitor->get_client_data();
if (client_data != nullptr && client_data->has_collector(bar._collector_index)) {
std::ostringstream text;
text << client_data->get_collector_fullname(bar._collector_index);
text << " (";
if (bar._open_begin || bar._open_end) {
text << "at least ";
}
text << format_number(bar._end - bar._start, GBU_show_units | GBU_ms) << ")";
return text.str();
}
}
return std::string();
}
/**
* Writes the graph state to a datagram.
*/
void PStatTimeline::
write_datagram(Datagram &dg) const {
dg.add_float64(_time_scale);
dg.add_float64(_start_time);
dg.add_float64(_lowest_start_time);
dg.add_float64(_highest_end_time);
PStatGraph::write_datagram(dg);
}
/**
* Restores the graph state from a datagram.
*/
void PStatTimeline::
read_datagram(DatagramIterator &scan) {
_time_scale = scan.get_float64();
_start_time = scan.get_float64();
_lowest_start_time = scan.get_float64();
_highest_end_time = scan.get_float64();
_scroll_speed = 0.0;
_zoom_speed = 0.0;
_have_start_time = true;
_target_start_time = _start_time;
_target_time_scale = _time_scale;
PStatGraph::read_datagram(scan);
normal_guide_bars();
force_redraw();
}
/**
* To be called by the user class when the widget size has changed. This
* updates the chart's internal data and causes it to issue redraw commands to
* reflect the new size.
*/
void PStatTimeline::
changed_size(int xsize, int ysize) {
if (xsize != _xsize || ysize != _ysize) {
_xsize = xsize;
_ysize = ysize;
normal_guide_bars();
force_redraw();
}
}
/**
* To be called by the user class when the whole thing needs to be redrawn for
* some reason.
*/
void PStatTimeline::
force_redraw() {
clear_region();
begin_draw();
for (const GuideBar &bar : _guide_bars) {
int x = timestamp_to_pixel(bar._height);
if (x > 0 && x < get_xsize() - 1) {
draw_guide_bar(x, bar._style);
}
}
double start_time = _start_time;
double end_time = start_time + get_horizontal_scale();
int num_rows = 0;
for (size_t ti = 0; ti < _threads.size(); ++ti) {
ThreadRow &thread_row = _threads[ti];
if (thread_row._visible) {
for (size_t ri = 0; ri < thread_row._rows.size(); ++ri) {
draw_row((int)ti, (int)ri, start_time, end_time);
++num_rows;
}
draw_separator(num_rows++);
}
}
end_draw();
}
/**
* To be called by the user class when the whole thing needs to be redrawn for
* some reason.
*/
void PStatTimeline::
force_redraw(int row, int from_x, int to_x) {
double start_time = std::max(_start_time, pixel_to_timestamp(from_x));
double end_time = std::min(_start_time + get_horizontal_scale(), pixel_to_timestamp(to_x));
begin_draw();
for (size_t ti = 0; ti < _threads.size(); ++ti) {
ThreadRow &thread_row = _threads[ti];
if (!thread_row._visible || (int)thread_row._row_offset > row) {
break;
}
int row_index = row - (int)thread_row._row_offset;
if (row_index < (int)thread_row._rows.size()) {
draw_row((int)ti, row_index, start_time, end_time);
}
}
end_draw();
}
/**
* Calls update_guide_bars with parameters suitable to this kind of graph.
*/
void PStatTimeline::
normal_guide_bars() {
double start_time = get_horizontal_scroll();
double time_width = get_horizontal_scale();
double end_time = start_time + time_width;
// We want vaguely 150 pixels between guide bars.
int max_frames = get_xsize() / 100;
int l = (int)std::floor(3.0 * log10(pixel_to_height(150)) + 0.5);
double interval = pow(10.0, std::ceil(l / 3.0));
if ((l + 3000) % 3 == 1) {
interval /= 5;
}
else if ((l + 3000) % 3 == 2) {
interval /= 2;
}
_guide_bars.clear();
// Rather than getting the client data, we look in the color bar data for
// the first row, because the client data gets wiped after a while.
if (!_threads.empty() && !_threads[0]._rows.empty()) {
const Row &row = _threads[0]._rows[0];
// Look for the last Frame bar with end time lower than our start time.
Row::const_iterator it = std::lower_bound(row.begin(), row.end(), ColorBar {0.0, start_time});
while (it != row.end() && it->_collector_index != 0) {
++it;
}
int num_frames = 0;
while (it != row.end() && it->_start <= end_time) {
double frame_start = it->_start;
double frame_end = it->_end;
int frame_number = it->_frame_number;
if (frame_start > start_time) {
if (!_guide_bars.empty() && height_to_pixel(frame_start - _guide_bars.back()._height) < 30) {
// Get rid of last label, it is in the way.
_guide_bars.back()._label.clear();
}
std::string label = "#";
label += format_string(frame_number);
_guide_bars.push_back(GuideBar(frame_start, label, GBS_frame));
if (++num_frames > max_frames) {
// Forget it, this is becoming too many lines.
_guide_bars.clear();
break;
}
}
do {
++it;
}
while (it != row.end() && it->_collector_index != 0);
double frame_width;
if (it != row.end()) {
// Only go up to the start of the next frame, limiting to however much
// fits in the graph.
frame_width = std::min(frame_end - frame_start, end_time - frame_start);
} else {
// Reached the end; just continue to the end of the graph.
frame_width = end_time - frame_start;
}
if (interval > 0.0) {
int first_bar = std::max((int)((start_time - frame_start) / interval), 1);
int num_bars = (int)std::round(frame_width / interval);
for (int i = first_bar; i < num_bars; ++i) {
double offset = i * interval;
std::string label = "+";
label += format_number(offset, GBU_show_units | GBU_ms);
_guide_bars.push_back(GuideBar(frame_start + offset, label, GBS_normal));
}
}
// If there's a gap between frames, add another line.
if (it->_start > frame_end && it->_frame_number > frame_number + 1) {
std::string label;
if (it->_start - frame_end >= interval) {
label = "#" + format_string(frame_number + 1);
if (it->_frame_number > frame_number + 2) {
label += "-" + format_string(it->_frame_number - 1);
}
label += " (dropped)";
}
_guide_bars.push_back(GuideBar(frame_end, label, GBS_frame));
}
}
}
if (_guide_bars.empty() && interval > 0.0) {
int first_bar = std::max((int)(start_time / interval), 1);
int num_bars = (int)std::round(end_time / interval);
for (int i = first_bar; i < num_bars; ++i) {
double time = i * interval;
std::string label = format_number(time, GBU_show_units | GBU_ms);
_guide_bars.push_back(GuideBar(time, label, GBS_frame));
}
}
_guide_bars_changed = true;
}
/**
* Should be overridden by the user class to wipe out the entire strip chart
* region.
*/
void PStatTimeline::
clear_region() {
}
/**
* Should be overridden by the user class. This hook will be called before
* drawing any bars in the chart.
*/
void PStatTimeline::
begin_draw() {
}
/**
*
*/
void PStatTimeline::
draw_thread(int thread_index, double start_time, double end_time) {
if (thread_index < 0 || (size_t)thread_index > _threads.size()) {
return;
}
ThreadRow &thread_row = _threads[(size_t)thread_index];
if (thread_row._visible) {
for (size_t ri = 0; ri < thread_row._rows.size(); ++ri) {
draw_row(thread_index, (int)ri, start_time, end_time);
}
}
}
/**
*
*/
void PStatTimeline::
draw_row(int thread_index, int row_index, double start_time, double end_time) {
ThreadRow &thread_row = _threads[thread_index];
Row &row = thread_row._rows[row_index];
const PStatClientData *client_data = _monitor->get_client_data();
// Find the first element whose end time is larger than our start time.
// Then iterate until at least the end of the frame.
Row::iterator it = std::lower_bound(row.begin(), row.end(), ColorBar {0.0, start_time});
if (it == row.end()) {
return;
}
int frame_number = it->_frame_number;
do {
ColorBar &bar = *it;
int from_x = timestamp_to_pixel(bar._start);
int to_x = timestamp_to_pixel(bar._end);
if (to_x >= 0 && to_x > from_x && from_x < get_xsize()) {
if (bar._collector_index != 0) {
const PStatCollectorDef &def = client_data->get_collector_def(bar._collector_index);
if (to_x - from_x >= 32 && def._parent_index > 0) {
// Try including the parent name.
const PStatCollectorDef &parent_def = client_data->get_collector_def(def._parent_index);
std::string long_name = parent_def._name + ":" + def._name;
draw_bar(thread_row._row_offset + row_index, from_x, to_x,
bar._collector_index, long_name);
} else {
draw_bar(thread_row._row_offset + row_index, from_x, to_x,
bar._collector_index, def._name);
}
} else {
draw_bar(thread_row._row_offset + row_index, from_x, to_x,
bar._collector_index,
std::string("Frame ") + format_string(bar._frame_number));
}
}
++it;
}
while (it != row.end() && (it->_start <= end_time || it->_frame_number == frame_number));
}
/**
* Draws a horizontal separator.
*/
void PStatTimeline::
draw_separator(int) {
}
/**
* Draws a vertical guide bar. If the row is -1, draws it in all rows.
*/
void PStatTimeline::
draw_guide_bar(int x, GuideBarStyle style) {
}
/**
* Draws a single bar in the chart for the indicated row, in the color for the
* given collector, for the indicated horizontal pixel range.
*/
void PStatTimeline::
draw_bar(int, int, int, int, const std::string &) {
}
/**
* Should be overridden by the user class. This hook will be called after
* drawing a series of color bars in the chart.
*/
void PStatTimeline::
end_draw() {
}
/**
* Should be overridden by the user class to perform any other updates might
* be necessary after the bars have been redrawn.
*/
void PStatTimeline::
idle() {
}
/**
* Should be called periodically to update any animated values. Returns false
* to indicate that the animation is done and no longer needs to be called.
*/
bool PStatTimeline::
animate(double time, double dt) {
int hmove = ((_keys_held & (F_right | F_d)) != 0)
- ((_keys_held & (F_left | F_a)) != 0);
int vmove = ((_keys_held & F_w) != 0)
- ((_keys_held & F_s) != 0);
if (hmove > 0) {
if (_scroll_speed < 0) {
_scroll_speed = 1.0;
}
_scroll_speed += 1.0;
}
else if (hmove < 0) {
if (_scroll_speed > 0) {
_scroll_speed = -1.0;
}
_scroll_speed -= 1.0;
}
else if (_scroll_speed != 0.0) {
_scroll_speed *= std::exp(-12.0 * dt);
if (std::abs(_scroll_speed) < 0.2) {
_scroll_speed = 0.0;
}
}
if (vmove > 0) {
if (_zoom_speed < 0) {
_zoom_speed = 1.0;
}
_zoom_speed += 1.0;
}
else if (vmove < 0) {
if (_zoom_speed > 0) {
_zoom_speed = -1.0;
}
_zoom_speed -= 1.0;
}
else if (_zoom_speed != 0.0) {
_zoom_speed *= std::exp(-12.0 * dt);
if (std::abs(_zoom_speed) < 0.2) {
_zoom_speed = 0.0;
}
}
if (_zoom_speed != 0.0) {
zoom_to(get_horizontal_scale() * pow(0.5, _zoom_speed * dt), _zoom_center);
}
if (_scroll_speed != 0.0) {
scroll_by(_scroll_speed * 300 * _time_scale * dt);
}
if (_target_start_time != _start_time) {
double dist = _target_start_time - _start_time;
// When the difference is less than 2 pixels, snap to target position.
if (std::abs(dist) < _time_scale * 2) {
_start_time = _target_start_time;
} else {
dist *= 1.0 - std::exp(-12.0 * dt);
_start_time += dist;
}
}
if (_target_time_scale != _time_scale) {
//double dist = std::log(_target_time_scale) - std::log(_time_scale);
double dist = _target_time_scale - _time_scale;
if (_target_start_time == _start_time && std::abs(dist) < 0.01) {
_time_scale = _target_time_scale;
} else {
dist *= 1.0 - std::exp(-12.0 * dt);
//_time_scale *= std::exp(dist);
_time_scale += dist;
}
}
normal_guide_bars();
force_redraw();
// Stop the animation when the speed is 0 and no key is still held.
return _keys_held != 0
|| _scroll_speed != 0
|| _zoom_speed != 0
|| _target_start_time != _start_time
|| _target_time_scale != _time_scale;
}
/**
* Return the ColorBar at the indicated position.
*/
bool PStatTimeline::
find_bar(int row, int x, ColorBar &bar) const {
double time = pixel_to_timestamp(x);
for (size_t ti = 0; ti < _threads.size(); ++ti) {
const ThreadRow &thread_row = _threads[ti];
if ((int)thread_row._row_offset > row) {
break;
}
int row_index = row - (int)thread_row._row_offset;
if (row_index < (int)thread_row._rows.size()) {
// Find the first element whose end time is larger than the given time.
const Row &bars = thread_row._rows[row_index];
Row::const_iterator it = std::lower_bound(bars.begin(), bars.end(), ColorBar {time, time});
if (it != bars.end() && it->_start <= time) {
bar = *it;
return true;
}
}
}
return false;
}