• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tstack / lnav / 25348852825-3024

04 May 2026 11:18PM UTC coverage: 69.963% (+0.7%) from 69.226%
25348852825-3024

push

github

tstack
[ui] horizontal scroll should work on columns

Related to #1685

7 of 141 new or added lines in 5 files covered. (4.96%)

7760 existing lines in 84 files now uncovered.

57014 of 81492 relevant lines covered (69.96%)

622491.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.86
/src/timeline_source.cc
1
/**
2
 * Copyright (c) 2023, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <algorithm>
31
#include <chrono>
32
#include <cmath>
33
#include <cstring>
34
#include <utility>
35
#include <vector>
36

37
#include "timeline_source.hh"
38

39
#include <time.h>
40

41
#include "base/auto_mem.hh"
42
#include "base/date_time_scanner.hh"
43
#include "base/humanize.hh"
44
#include "base/humanize.time.hh"
45
#include "base/itertools.enumerate.hh"
46
#include "base/itertools.hh"
47
#include "base/keycodes.hh"
48
#include "base/math_util.hh"
49
#include "command_executor.hh"
50
#include "lnav.hh"
51
#include "lnav_util.hh"
52
#include "logline_window.hh"
53
#include "md4cpp.hh"
54
#include "pcrepp/pcre2pp.hh"
55
#include "readline_highlighters.hh"
56
#include "sql_util.hh"
57
#include "sysclip.hh"
58
#include "tlx/container/btree_map.hpp"
59
#include "vtab_module.hh"
60

61
using namespace std::chrono_literals;
62
using namespace lnav::roles::literals;
63
using namespace md4cpp::literals;
64

65
static const std::vector<std::chrono::microseconds> TIME_SPANS = {
66
    500us, 1ms,   100ms, 500ms, 1s, 5s, 10s, 15s,     30s,      1min,
67
    5min,  15min, 1h,    2h,    4h, 8h, 24h, 7 * 24h, 30 * 24h, 365 * 24h,
68
};
69

70
static constexpr size_t MAX_OPID_WIDTH = 80;
71
static constexpr size_t MAX_DESC_WIDTH = 256;
72
static constexpr int CHART_INDENT = 24;
73

74
size_t
75
abbrev_ftime(char* datebuf, size_t db_size, const tm& lb_tm, const tm& dt)
×
76
{
77
    char lb_fmt[32] = " ";
×
78
    bool same = true;
×
79

80
    if (lb_tm.tm_year == dt.tm_year) {
×
81
        strcat(lb_fmt, "    ");
×
82
    } else {
83
        same = false;
×
84
        strcat(lb_fmt, "%Y");
×
85
    }
86
    if (same && lb_tm.tm_mon == dt.tm_mon) {
×
87
        strcat(lb_fmt, "   ");
×
88
    } else {
89
        if (!same) {
×
90
            strcat(lb_fmt, "-");
×
91
        }
92
        same = false;
×
93
        strcat(lb_fmt, "%m");
×
94
    }
95
    if (same && lb_tm.tm_mday == dt.tm_mday) {
×
96
        strcat(lb_fmt, "   ");
×
97
    } else {
98
        if (!same) {
×
99
            strcat(lb_fmt, "-");
×
100
        }
101
        same = false;
×
102
        strcat(lb_fmt, "%d");
×
103
    }
104
    if (same && lb_tm.tm_hour == dt.tm_hour) {
×
105
        strcat(lb_fmt, "   ");
×
106
    } else {
107
        if (!same) {
×
108
            strcat(lb_fmt, "T");
×
109
        }
110
        same = false;
×
111
        strcat(lb_fmt, "%H");
×
112
    }
113
    if (same && lb_tm.tm_min == dt.tm_min) {
×
114
        strcat(lb_fmt, "   ");
×
115
    } else {
116
        if (!same) {
×
UNCOV
117
            strcat(lb_fmt, ":");
×
118
        }
UNCOV
119
        same = false;
×
120
        strcat(lb_fmt, "%M");
×
121
    }
UNCOV
122
    return strftime(datebuf, db_size, lb_fmt, &dt);
×
123
}
124

125
std::vector<attr_line_t>
126
timeline_preview_overlay::list_overlay_menu(const listview_curses& lv,
×
127
                                            vis_line_t line)
128
{
129
    static constexpr auto MENU_WIDTH = 25;
130

UNCOV
131
    const auto* tc = dynamic_cast<const textview_curses*>(&lv);
×
132
    std::vector<attr_line_t> retval;
×
133

134
    if (tc->tc_text_selection_active || !tc->tc_selected_text) {
×
135
        return retval;
×
136
    }
137

138
    const auto& sti = tc->tc_selected_text.value();
×
139

140
    if (sti.sti_line != line) {
×
UNCOV
141
        return retval;
×
142
    }
143
    auto title = " Actions "_status_title;
×
UNCOV
144
    auto left = std::max(0, sti.sti_x - 2);
×
UNCOV
145
    auto dim = lv.get_dimensions();
×
146
    auto menu_line = vis_line_t{1};
×
147

148
    if (left + MENU_WIDTH >= dim.second) {
×
UNCOV
149
        left = dim.second - MENU_WIDTH;
×
150
    }
151

UNCOV
152
    this->los_menu_items.clear();
×
153

154
    retval.emplace_back(attr_line_t().pad_to(left).append(title));
×
155
    {
156
        auto start = left;
×
UNCOV
157
        attr_line_t al;
×
158

159
        al.append(":clipboard:"_emoji)
×
160
            .append(" Copy  ")
×
161
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
162
        this->los_menu_items.emplace_back(
×
163
            menu_line,
164
            line_range{start, start + (int) al.length()},
×
UNCOV
165
            [](const std::string& value) {
×
UNCOV
166
                auto clip_res = sysclip::open(sysclip::type_t::GENERAL);
×
167
                if (clip_res.isErr()) {
×
168
                    log_error("unable to open clipboard: %s",
×
169
                              clip_res.unwrapErr().c_str());
170
                    return;
×
171
                }
172

173
                auto clip_pipe = clip_res.unwrap();
×
174
                fwrite(value.c_str(), 1, value.length(), clip_pipe.in());
×
UNCOV
175
            });
×
UNCOV
176
        retval.emplace_back(attr_line_t().pad_to(left).append(al));
×
177
    }
178

UNCOV
179
    return retval;
×
UNCOV
180
}
×
181

182
timeline_header_overlay::timeline_header_overlay(
796✔
183
    const std::shared_ptr<timeline_source>& src)
796✔
184
    : gho_src(src)
796✔
185
{
186
}
796✔
187

188
bool
189
timeline_header_overlay::list_static_overlay(const listview_curses& lv,
112✔
190
                                             media_t media,
191
                                             int y,
192
                                             int bottom,
193
                                             attr_line_t& value_out)
194
{
195
    if (this->gho_src->ts_rebuild_in_progress) {
112✔
UNCOV
196
        return false;
×
197
    }
198

199
    if (this->gho_src->ts_time_order.empty()) {
112✔
200
        if (y == 0) {
×
201
            this->gho_static_lines.clear();
×
202

203
            if (this->gho_src->ts_filtered_count > 0) {
×
204
                auto um = lnav::console::user_message::warning(
205
                    attr_line_t()
×
206
                        .append(lnav::roles::number(
×
207
                            fmt::to_string(this->gho_src->ts_filtered_count)))
×
208
                        .append(" operations have been filtered out"));
×
UNCOV
209
                auto min_time = this->gho_src->get_min_row_time();
×
210
                if (min_time) {
×
211
                    um.with_note(attr_line_t("Operations before ")
×
212
                                     .append_quoted(lnav::to_rfc3339_string(
×
213
                                         min_time.value()))
×
214
                                     .append(" are not being shown"));
×
215
                }
UNCOV
216
                auto max_time = this->gho_src->get_max_row_time();
×
UNCOV
217
                if (max_time) {
×
218
                    um.with_note(attr_line_t("Operations after ")
×
219
                                     .append_quoted(lnav::to_rfc3339_string(
×
220
                                         max_time.value()))
×
UNCOV
221
                                     .append(" are not being shown"));
×
222
                }
223

UNCOV
224
                auto& fs = this->gho_src->ts_lss.get_filters();
×
225
                for (const auto& filt : fs) {
×
226
                    auto hits = this->gho_src->ts_lss.get_filtered_count_for(
×
227
                        filt->get_index());
228
                    if (filt->get_type() == text_filter::EXCLUDE && hits == 0) {
×
229
                        continue;
×
230
                    }
231
                    auto cmd = attr_line_t(":" + filt->to_command());
×
232
                    readline_command_highlighter(cmd, std::nullopt);
×
UNCOV
233
                    um.with_note(
×
234
                        attr_line_t("Filter ")
×
235
                            .append_quoted(cmd)
×
UNCOV
236
                            .append(" matched ")
×
237
                            .append(lnav::roles::number(fmt::to_string(hits)))
×
238
                            .append(" message(s) "));
×
239
                }
240
                this->gho_static_lines = um.to_attr_line().split_lines();
×
UNCOV
241
            } else {
×
242
                auto um
243
                    = lnav::console::user_message::error("No operations found");
×
244
                if (this->gho_src->ts_lss.size() > 0) {
×
UNCOV
245
                    um.with_note("The loaded logs do not define any OP IDs")
×
246
                        .with_help(attr_line_t("An OP ID can manually be set "
×
247
                                               "by performing an ")
UNCOV
248
                                       .append("UPDATE"_keyword)
×
UNCOV
249
                                       .append(" on a log vtable, such as ")
×
UNCOV
250
                                       .append("all_logs"_symbol));
×
251
                } else {
UNCOV
252
                    um.with_note(
×
253
                        "Operations are found in log files and none are loaded "
254
                        "right now");
255
                }
256

257
                this->gho_static_lines = um.to_attr_line().split_lines();
×
258
            }
259
        }
260

UNCOV
261
        if (y < this->gho_static_lines.size()) {
×
UNCOV
262
            value_out = this->gho_static_lines[y];
×
UNCOV
263
            return true;
×
264
        }
265

266
        return false;
×
267
    }
268

269
    auto [height, width] = lv.get_dimensions();
112✔
270
    if (width <= CHART_INDENT) {
112✔
UNCOV
271
        return false;
×
272
    }
273

274
    if (y == 0) {
112✔
275
        auto sel = lv.get_selection().value_or(0_vl);
60✔
276
        if (sel < this->gho_src->tss_view->get_top()) {
60✔
UNCOV
277
            return true;
×
278
        }
279
        const auto& row = *this->gho_src->ts_time_order[sel];
60✔
280
        auto tr = row.or_value.otr_range;
60✔
281
        auto [lb, ub] = this->gho_src->get_time_bounds_for(sel);
60✔
282
        auto sel_begin_us = tr.tr_begin - lb;
60✔
283
        auto sel_end_us = tr.tr_end - lb;
60✔
284

285
        require(sel_begin_us >= 0us);
60✔
286
        require(sel_end_us >= 0us);
60✔
287

288
        value_out.append("   Duration   "_h1)
60✔
289
            .append("|", VC_GRAPHIC.value(NCACS_VLINE))
60✔
290
            .append(" ")
60✔
291
            .append("\u2718"_error)
60✔
292
            .append("\u25b2"_warning)
60✔
293
            .append(" ")
60✔
294
            .append("|", VC_GRAPHIC.value(NCACS_VLINE))
120✔
295
            .append(" ")
60✔
296
            .append("Item"_h1);
60✔
297
        auto line_width = CHART_INDENT;
60✔
298
        auto mark_width = (double) (width - line_width);
60✔
299
        double span = (ub - lb).count();
60✔
300
        if (span < 1.0) {
60✔
UNCOV
301
            span = 1.0;
×
302
        }
303
        auto us_per_ch
304
            = std::chrono::microseconds{(int64_t) ceil(span / mark_width)};
60✔
305
        require(us_per_ch > 0us);
60✔
306
        auto us_per_inc = us_per_ch * 10;
60✔
307
        auto lr = line_range{
308
            static_cast<int>(CHART_INDENT + floor(sel_begin_us / us_per_ch)),
60✔
309
            static_cast<int>(CHART_INDENT + ceil(sel_end_us / us_per_ch)),
120✔
310
            line_range::unit::codepoint,
311
        };
60✔
312
        if (lr.lr_start == lr.lr_end) {
60✔
313
            lr.lr_end += 1;
48✔
314
        }
315
        if (lr.lr_end > width) {
60✔
316
            lr.lr_end = -1;
9✔
317
        }
318
        require(lr.lr_start >= 0);
60✔
319
        value_out.get_attrs().emplace_back(
120✔
320
            lr, VC_ROLE.value(role_t::VCR_CURSOR_LINE));
120✔
321
        auto total_us = std::chrono::microseconds{0};
60✔
322
        std::vector<std::string> durations;
60✔
323
        auto remaining_width = mark_width - 10;
60✔
324
        auto max_width = size_t{0};
60✔
325
        while (remaining_width > 0) {
279✔
326
            total_us += us_per_inc;
219✔
327
            auto dur = humanize::time::duration::from_tv(to_timeval(total_us));
219✔
328
            if (us_per_inc > 24 * 1h) {
219✔
329
                dur.with_resolution(24 * 1h);
20✔
330
            } else if (us_per_inc > 1h) {
199✔
UNCOV
331
                dur.with_resolution(1h);
×
332
            } else if (us_per_inc > 1min) {
199✔
333
                dur.with_resolution(1min);
22✔
334
            } else if (us_per_inc > 2s) {
177✔
335
                dur.with_resolution(1s);
83✔
336
            }
337
            durations.emplace_back(dur.to_string());
219✔
338
            max_width = std::max(durations.back().size(), max_width);
219✔
339
            remaining_width -= 10;
219✔
340
        }
341
        for (auto& label : durations) {
279✔
342
            line_width += 10;
219✔
343
            value_out.pad_to(line_width)
219✔
344
                .append("|", VC_GRAPHIC.value(NCACS_VLINE))
438✔
345
                .append(max_width - label.size(), ' ')
219✔
346
                .append(label);
219✔
347
        }
348

349
        auto hdr_attrs = text_attrs::with_underline();
60✔
350
        value_out.with_attr_for_all(VC_STYLE.value(hdr_attrs))
60✔
351
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO));
60✔
352

353
        return true;
60✔
354
    }
60✔
355

356
    const auto metric_count
357
        = static_cast<int>(this->gho_src->ts_metrics.size());
52✔
358
    if (y >= 1 && y <= metric_count) {
52✔
359
        const auto& ms = this->gho_src->ts_metrics[y - 1];
13✔
360
        auto sel = lv.get_selection().value_or(0_vl);
13✔
361
        auto [lb, ub] = this->gho_src->get_time_bounds_for(sel);
13✔
362
        auto mark_width = static_cast<int>(width) - CHART_INDENT;
13✔
363
        double span = (ub - lb).count();
13✔
364
        if (span < 1.0) {
13✔
UNCOV
365
            span = 1.0;
×
366
        }
367
        auto us_per_ch
368
            = std::chrono::microseconds{(int64_t) ceil(span / mark_width)};
13✔
369
        if (us_per_ch <= 0us) {
13✔
UNCOV
370
            us_per_ch = 1us;
×
371
        }
372

373
        attr_line_t row_line;
13✔
374
        row_line.append(" ").append(attr_line_t::from_table_cell_content(
26✔
375
            string_fragment::from_str(ms.ms_def.md_label), CHART_INDENT - 1));
13✔
376
        row_line.pad_to(CHART_INDENT);
13✔
377

378
        if (!ms.ms_matched) {
13✔
379
            // The requested shorthand name doesn't match any column
380
            // in any currently-loaded metric file.  Render an inline
381
            // error on the row rather than complaining at command
382
            // time — the expected file may not have been opened or
383
            // indexed yet.
384
            row_line.append(" no metric named ")
1✔
385
                .append(lnav::roles::identifier(ms.ms_def.md_label))
2✔
386
                .append(" in any loaded file")
1✔
387
                .with_attr_for_all(VC_ROLE.value(role_t::VCR_ERROR));
1✔
388
            value_out = std::move(row_line);
1✔
389
            return true;
1✔
390
        }
391
        if (!ms.ms_error.empty()) {
12✔
392
            // SQL metric failed at collect time — query prepared at
393
            // command time but something in the tables it touches has
394
            // since changed.  Put the sqlite error on the row so it's
395
            // visible without the user having to go look for it.
396
            row_line.append(" SQL error: ")
1✔
397
                .append(ms.ms_error)
1✔
398
                .with_attr_for_all(VC_ROLE.value(role_t::VCR_ERROR));
1✔
399
            value_out = std::move(row_line);
1✔
400
            return true;
1✔
401
        }
402

403
        // Bucket samples in [lb, ub] by us_per_ch, using max
404
        // aggregation.  Scale against the metric's global min/max
405
        // (populated from logfile_value_stats at collect time) so the
406
        // bar heights stay stable as the user scrolls — a sample
407
        // doesn't shift height just because other samples are in view.
408
        std::vector<double> buckets(mark_width,
409
                                    std::numeric_limits<double>::quiet_NaN());
11✔
410
        // Track the most recent sample before the window and whether
411
        // any sample exists after it, to bridge a zoom that falls
412
        // entirely between two sample points.  Samples arrive in
413
        // ORDER BY log_time, so the last pre-window value seen is the
414
        // most recent one.
415
        double pre_val = std::numeric_limits<double>::quiet_NaN();
11✔
416
        bool has_post = false;
11✔
417
        bool any_in_window = false;
11✔
418
        for (const auto& [ts, v] : ms.ms_samples) {
59✔
419
            if (ts < lb) {
59✔
420
                pre_val = v;
×
421
                continue;
×
422
            }
423
            if (ts >= ub) {
59✔
424
                has_post = true;
11✔
425
                break;
11✔
426
            }
427
            any_in_window = true;
48✔
428
            auto idx = static_cast<int>((ts - lb).count() / us_per_ch.count());
48✔
429
            if (idx < 0 || idx >= mark_width) {
48✔
430
                continue;
×
431
            }
432
            if (std::isnan(buckets[idx]) || v > buckets[idx]) {
48✔
433
                buckets[idx] = v;
47✔
434
            }
435
        }
436
        // If we have a pre-window sample and there's evidence the
437
        // signal continues through the window (in-window or post-window
438
        // sample), seed bucket 0 with the pre-window value so LOCF
439
        // fills from the start.
440
        if (!std::isnan(pre_val) && (any_in_window || has_post)
11✔
441
            && std::isnan(buckets[0]))
11✔
442
        {
UNCOV
443
            buckets[0] = pre_val;
×
444
        }
445
        // Last-value-carried-forward between adjacent samples, so
446
        // gauge-style metrics appear continuous.
447
        int last_idx = -1;
11✔
448
        double last_val = std::numeric_limits<double>::quiet_NaN();
11✔
449
        for (int i = 0; i < mark_width; i++) {
319✔
450
            if (!std::isnan(buckets[i])) {
308✔
451
                if (last_idx >= 0) {
47✔
452
                    for (int j = last_idx + 1; j < i; j++) {
194✔
453
                        buckets[j] = last_val;
158✔
454
                    }
455
                }
456
                last_idx = i;
47✔
457
                last_val = buckets[i];
47✔
458
            }
459
        }
460
        // If data continues past the window, fill trailing NaNs with
461
        // the last in-window value.
462
        if (has_post && last_idx >= 0) {
11✔
463
            for (int j = last_idx + 1; j < mark_width; j++) {
79✔
464
                buckets[j] = last_val;
68✔
465
            }
466
        }
467

468
        // Derive a stable color from the metric label so the sparkline
469
        // and the matching status-bar value share the same hue without
470
        // depending on insertion order.
471
        const auto metric_attrs
472
            = view_colors::singleton().attrs_for_ident(ms.ms_def.md_label);
11✔
473

474
        const bool have_range
475
            = !std::isnan(ms.ms_min) && !std::isnan(ms.ms_max);
11✔
476

477
        for (int x = 0; x < mark_width; x++) {
319✔
478
            const double v = buckets[x];
308✔
479
            if (std::isnan(v) || !have_range) {
308✔
480
                row_line.append(" ");
35✔
481
                continue;
35✔
482
            }
483
            // humanize::sparkline scales between the metric's global
484
            // min/max and preserves the 0=absent / at-min=▁ / at-max=█
485
            // convention.
486
            row_line.append(humanize::sparkline(v, ms.ms_max, ms.ms_min));
273✔
487
        }
488

489
        row_line.with_attr_for_all(VC_STYLE.value(metric_attrs));
11✔
490
        value_out = std::move(row_line);
11✔
491
        return true;
11✔
492
    }
13✔
493

494
    auto& tc = dynamic_cast<textview_curses&>(const_cast<listview_curses&>(lv));
39✔
495
    const auto& sticky_bv = tc.get_bookmarks()[&textview_curses::BM_STICKY];
39✔
496
    auto top = lv.get_top();
39✔
497
    auto sticky_range = sticky_bv.equal_range(0_vl, top);
39✔
498
    auto sticky_index = y - 1 - metric_count;
39✔
499
    if (sticky_index < static_cast<int>(
39✔
500
            std::distance(sticky_range.first, sticky_range.second)))
39✔
501
    {
502
        auto iter = std::next(sticky_range.first, sticky_index);
2✔
503
        tc.textview_value_for_row(*iter, value_out);
2✔
504
        value_out.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
2✔
505
        auto next_iter = std::next(iter);
2✔
506
        if (next_iter == sticky_range.second) {
2✔
507
            value_out.with_attr_for_all(
2✔
508
                VC_STYLE.value(text_attrs::with_underline()));
4✔
509
        }
510
        return true;
2✔
511
    }
512

513
    return false;
37✔
514
}
515
void
516
timeline_header_overlay::list_value_for_overlay(
646✔
517
    const listview_curses& lv,
518
    vis_line_t line,
519
    std::vector<attr_line_t>& value_out)
520
{
521
    if (!this->gho_show_details) {
646✔
522
        return;
646✔
523
    }
524

UNCOV
525
    if (lv.get_selection() != line) {
×
UNCOV
526
        return;
×
527
    }
528

UNCOV
529
    if (line >= this->gho_src->ts_time_order.size()) {
×
UNCOV
530
        return;
×
531
    }
532

UNCOV
533
    const auto& row = *this->gho_src->ts_time_order[line];
×
534

UNCOV
535
    if (row.or_value.otr_sub_ops.size() <= 1) {
×
UNCOV
536
        return;
×
537
    }
538

UNCOV
539
    auto width = lv.get_dimensions().second;
×
540

UNCOV
541
    if (width < 37) {
×
UNCOV
542
        return;
×
543
    }
544

UNCOV
545
    width -= 37;
×
546
    double span = row.or_value.otr_range.duration().count();
×
547
    double per_ch = span / (double) width;
×
548

549
    for (const auto& sub : row.or_value.otr_sub_ops) {
×
550
        value_out.resize(value_out.size() + 1);
×
551

552
        auto& al = value_out.back();
×
553
        auto& attrs = al.get_attrs();
×
554
        auto total_msgs = sub.ostr_level_stats.lls_total_count;
×
555
        auto duration = sub.ostr_range.tr_end - sub.ostr_range.tr_begin;
×
556
        auto duration_str = fmt::format(
557
            FMT_STRING(" {: >13}"),
×
UNCOV
558
            humanize::time::duration::from_tv(to_timeval(duration))
×
559
                .to_string());
×
UNCOV
560
        al.pad_to(14)
×
UNCOV
561
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
×
562
            .append(" ")
×
563
            .append(lnav::roles::error(humanize::sparkline(
×
564
                sub.ostr_level_stats.lls_error_count, total_msgs)))
×
565
            .append(lnav::roles::warning(humanize::sparkline(
×
566
                sub.ostr_level_stats.lls_warning_count, total_msgs)))
×
UNCOV
567
            .append(" ")
×
568
            .append(lnav::roles::identifier(sub.ostr_subid.to_string()))
×
569
            .append(row.or_max_subid_width
×
UNCOV
570
                        - sub.ostr_subid.utf8_length().unwrapOr(
×
UNCOV
571
                            row.or_max_subid_width),
×
572
                    ' ')
UNCOV
573
            .append(sub.ostr_description);
×
574
        al.with_attr_for_all(VC_ROLE.value(role_t::VCR_COMMENT));
×
575

UNCOV
576
        auto start_diff = (double) (sub.ostr_range.tr_begin
×
577
                                    - row.or_value.otr_range.tr_begin)
×
578
                              .count();
×
579
        auto end_diff
UNCOV
580
            = (double) (sub.ostr_range.tr_end - row.or_value.otr_range.tr_begin)
×
581
                  .count();
×
582

583
        auto lr = line_range{
UNCOV
584
            (int) (32 + (start_diff / per_ch)),
×
585
            (int) (32 + (end_diff / per_ch)),
×
586
            line_range::unit::codepoint,
587
        };
588

UNCOV
589
        if (lr.lr_start == lr.lr_end) {
×
590
            lr.lr_end += 1;
×
591
        }
592

593
        attrs.emplace_back(lr, VC_ROLE.value(role_t::VCR_TIMELINE_BAR));
×
594
    }
UNCOV
595
    if (!value_out.empty()) {
×
UNCOV
596
        value_out.back().get_attrs().emplace_back(
×
597
            line_range{0, -1}, VC_STYLE.value(text_attrs::with_underline()));
×
598
    }
599
}
600

601
std::optional<attr_line_t>
UNCOV
602
timeline_header_overlay::list_header_for_overlay(const listview_curses& lv,
×
603
                                                 media_t media,
604
                                                 vis_line_t line)
605
{
UNCOV
606
    if (lv.get_overlay_selection()) {
×
UNCOV
607
        return attr_line_t("\u258C Sub-operations: Press ")
×
UNCOV
608
            .append("Esc"_hotkey)
×
UNCOV
609
            .append(" to exit this panel");
×
610
    }
UNCOV
611
    return attr_line_t("\u258C Sub-operations: Press ")
×
UNCOV
612
        .append("CTRL-]"_hotkey)
×
UNCOV
613
        .append(" to focus on this panel");
×
614
}
615

616
timeline_source::timeline_source(textview_curses& log_view,
796✔
617
                                 logfile_sub_source& lss,
618
                                 textview_curses& preview_view,
619
                                 plain_text_source& preview_source,
620
                                 statusview_curses& preview_status_view,
621
                                 timeline_status_source& preview_status_source)
796✔
622
    : ts_log_view(log_view), ts_lss(lss), ts_preview_view(preview_view),
796✔
623
      ts_preview_source(preview_source),
796✔
624
      ts_preview_status_view(preview_status_view),
796✔
625
      ts_preview_status_source(preview_status_source)
796✔
626
{
627
    this->tss_supports_filtering = true;
796✔
628
    this->ts_preview_view.set_overlay_source(&this->ts_preview_overlay);
796✔
629
}
796✔
630

631
std::optional<timeline_source::row_type>
632
timeline_source::row_type_from_string(const std::string& str)
13✔
633
{
634
    if (str == "logfile") {
13✔
635
        return row_type::logfile;
2✔
636
    }
637
    if (str == "thread") {
11✔
638
        return row_type::thread;
2✔
639
    }
640
    if (str == "opid") {
9✔
641
        return row_type::opid;
8✔
642
    }
643
    if (str == "tag") {
1✔
UNCOV
644
        return row_type::tag;
×
645
    }
646
    if (str == "partition") {
1✔
UNCOV
647
        return row_type::partition;
×
648
    }
649
    return std::nullopt;
1✔
650
}
651

652
const char*
653
timeline_source::row_type_to_string(row_type rt)
2✔
654
{
655
    switch (rt) {
2✔
UNCOV
656
        case row_type::logfile:
×
UNCOV
657
            return "logfile";
×
658
        case row_type::thread:
2✔
659
            return "thread";
2✔
UNCOV
660
        case row_type::opid:
×
UNCOV
661
            return "opid";
×
UNCOV
662
        case row_type::tag:
×
UNCOV
663
            return "tag";
×
UNCOV
664
        case row_type::partition:
×
UNCOV
665
            return "partition";
×
666
    }
UNCOV
667
    return "unknown";
×
668
}
669

670
void
671
timeline_source::set_row_type_visibility(row_type rt, bool visible)
5✔
672
{
673
    if (visible) {
5✔
674
        this->ts_hidden_row_types.erase(rt);
1✔
675
    } else {
676
        this->ts_hidden_row_types.insert(rt);
4✔
677
    }
678
}
5✔
679

680
bool
681
timeline_source::is_row_type_visible(row_type rt) const
1,400✔
682
{
683
    return this->ts_hidden_row_types.find(rt)
1,400✔
684
        == this->ts_hidden_row_types.end();
2,800✔
685
}
686

687
bool
UNCOV
688
timeline_source::list_input_handle_key(listview_curses& lv, const ncinput& ch)
×
689
{
UNCOV
690
    switch (ch.eff_text[0]) {
×
UNCOV
691
        case 'q':
×
692
        case KEY_ESCAPE: {
UNCOV
693
            if (this->ts_preview_focused) {
×
UNCOV
694
                this->ts_preview_focused = false;
×
UNCOV
695
                this->ts_preview_view.set_height(5_vl);
×
UNCOV
696
                this->ts_preview_status_view.set_enabled(
×
UNCOV
697
                    this->ts_preview_focused);
×
UNCOV
698
                this->tss_view->set_enabled(!this->ts_preview_focused);
×
UNCOV
699
                return true;
×
700
            }
UNCOV
701
            break;
×
702
        }
UNCOV
703
        case '\n':
×
704
        case '\r':
705
        case NCKEY_ENTER: {
UNCOV
706
            this->ts_preview_focused = !this->ts_preview_focused;
×
UNCOV
707
            this->ts_preview_status_view.set_enabled(this->ts_preview_focused);
×
UNCOV
708
            this->tss_view->set_enabled(!this->ts_preview_focused);
×
UNCOV
709
            if (this->ts_preview_focused) {
×
UNCOV
710
                auto height = this->tss_view->get_dimensions().first;
×
711

UNCOV
712
                if (height > 5) {
×
UNCOV
713
                    this->ts_preview_view.set_height(height / 2_vl);
×
714
                }
715
            } else {
UNCOV
716
                this->ts_preview_view.set_height(5_vl);
×
717
            }
UNCOV
718
            return true;
×
719
        }
720
    }
UNCOV
721
    if (this->ts_preview_focused) {
×
UNCOV
722
        return this->ts_preview_view.handle_key(ch);
×
723
    }
724

UNCOV
725
    return false;
×
726
}
727

728
bool
UNCOV
729
timeline_source::text_handle_mouse(
×
730
    textview_curses& tc,
731
    const listview_curses::display_line_content_t&,
732
    mouse_event& me)
733
{
UNCOV
734
    auto nci = ncinput{};
×
UNCOV
735
    if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{0, -1})) {
×
UNCOV
736
        nci.id = '\r';
×
UNCOV
737
        nci.eff_text[0] = '\r';
×
UNCOV
738
        this->list_input_handle_key(tc, nci);
×
739
    }
740

UNCOV
741
    return false;
×
742
}
743

744
std::pair<std::chrono::microseconds, std::chrono::microseconds>
745
timeline_source::get_time_bounds_for(int line)
698✔
746
{
747
    const auto low_index = this->tss_view->get_top();
698✔
748
    auto high_index
749
        = std::min(this->tss_view->get_bottom(),
698✔
750
                   vis_line_t((int) this->ts_time_order.size() - 1));
698✔
751
    if (high_index == low_index) {
698✔
752
        high_index = vis_line_t(this->ts_time_order.size() - 1);
698✔
753
    }
754
    const auto& low_row = *this->ts_time_order[low_index];
698✔
755
    const auto& high_row = *this->ts_time_order[high_index];
698✔
756
    auto low_us = low_row.or_value.otr_range.tr_begin;
698✔
757
    auto high_us = high_row.or_value.otr_range.tr_begin;
698✔
758

759
    auto duration = high_us - low_us;
698✔
760
    auto span_iter
761
        = std::upper_bound(TIME_SPANS.begin(), TIME_SPANS.end(), duration);
698✔
762
    if (span_iter == TIME_SPANS.end()) {
698✔
763
        --span_iter;
7✔
764
    }
765
    auto span_portion = *span_iter / 8;
698✔
766
    auto lb = low_us;
698✔
767
    lb = rounddown(lb, span_portion);
698✔
768
    auto ub = high_us;
698✔
769
    ub = roundup(ub, span_portion);
698✔
770

771
    ensure(lb <= ub);
698✔
772
    return {lb, ub};
698✔
773
}
774

775
size_t
776
timeline_source::text_line_count()
12,002✔
777
{
778
    return this->ts_time_order.size();
12,002✔
779
}
780

781
line_info
782
timeline_source::text_value_for_line(textview_curses& tc,
625✔
783
                                     int line,
784
                                     std::string& value_out,
785
                                     line_flags_t flags)
786
{
787
    if (!this->ts_rebuild_in_progress
1,250✔
788
        && line < (ssize_t) this->ts_time_order.size())
625✔
789
    {
790
        const auto& row = *this->ts_time_order[line];
625✔
791
        auto duration
792
            = row.or_value.otr_range.tr_end - row.or_value.otr_range.tr_begin;
625✔
793
        auto duration_str = fmt::format(
794
            FMT_STRING("{: >13}"),
1,875✔
795
            humanize::time::duration::from_tv(to_timeval(duration))
625✔
796
                .to_string());
1,250✔
797

798
        this->ts_rendered_line.clear();
625✔
799

800
        auto total_msgs = row.or_value.otr_level_stats.lls_total_count;
625✔
801
        auto truncated_name
802
            = attr_line_t::from_table_cell_content(row.or_name, MAX_OPID_WIDTH);
625✔
803
        auto truncated_desc = attr_line_t::from_table_cell_content(
804
            row.or_description, MAX_DESC_WIDTH);
625✔
805
        std::optional<ui_icon_t> icon;
625✔
806
        auto padding = 1;
625✔
807
        switch (row.or_type) {
625✔
808
            case row_type::logfile:
47✔
809
                icon = ui_icon_t::file;
47✔
810
                break;
47✔
811
            case row_type::thread:
151✔
812
                icon = ui_icon_t::thread;
151✔
813
                break;
151✔
814
            case row_type::opid:
420✔
815
                padding = 3;
420✔
816
                break;
420✔
817
            case row_type::tag:
2✔
818
                icon = ui_icon_t::tag;
2✔
819
                break;
2✔
820
            case row_type::partition:
5✔
821
                icon = ui_icon_t::partition;
5✔
822
                break;
5✔
823
        }
824
        if (this->ts_preview_hidden_row_types.count(row.or_type) > 0) {
625✔
UNCOV
825
            this->ts_rendered_line.append(
×
826
                "-",
UNCOV
827
                VC_STYLE.value(text_attrs{
×
828
                    lnav::enums::to_underlying(text_attrs::style::blink),
UNCOV
829
                    styling::color_unit::from_palette(
×
UNCOV
830
                        lnav::enums::to_underlying(ansi_color::red)),
×
831
                }));
832
        } else {
833
            this->ts_rendered_line.append(" ");
625✔
834
        }
835

836
        this->ts_rendered_line
837
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
1,250✔
838
            .append("  ")
625✔
839
            .append(lnav::roles::error(humanize::sparkline(
1,875✔
840
                row.or_value.otr_level_stats.lls_error_count, total_msgs)))
625✔
841
            .append(lnav::roles::warning(humanize::sparkline(
1,875✔
842
                row.or_value.otr_level_stats.lls_warning_count, total_msgs)))
625✔
843
            .append("  ")
625✔
844
            .append(icon)
625✔
845
            .append(padding, ' ')
625✔
846
            .append(lnav::roles::identifier(truncated_name))
1,250✔
847
            .append(
1,250✔
848
                this->ts_opid_width - truncated_name.utf8_length_or_length(),
625✔
849
                ' ')
850
            .append(" ")
625✔
851
            .append(truncated_desc);
625✔
852
        this->ts_rendered_line.with_attr_for_all(
625✔
853
            VC_ROLE.value(role_t::VCR_COMMENT));
1,250✔
854

855
        value_out = this->ts_rendered_line.get_string();
625✔
856
    }
625✔
857

858
    return {};
625✔
859
}
860

861
void
862
timeline_source::text_attrs_for_line(textview_curses& tc,
625✔
863
                                     int line,
864
                                     string_attrs_t& value_out)
865
{
866
    if (!this->ts_rebuild_in_progress
1,250✔
867
        && line < (ssize_t) this->ts_time_order.size())
625✔
868
    {
869
        const auto& row = *this->ts_time_order[line];
625✔
870

871
        value_out = this->ts_rendered_line.get_attrs();
625✔
872

873
        auto lr = line_range{-1, -1, line_range::unit::codepoint};
625✔
874
        auto [sel_lb, sel_ub]
625✔
875
            = this->get_time_bounds_for(tc.get_selection().value_or(0_vl));
625✔
876

877
        if (row.or_value.otr_range.tr_begin <= sel_ub
625✔
878
            && sel_lb <= row.or_value.otr_range.tr_end)
625✔
879
        {
880
            auto width = tc.get_dimensions().second;
625✔
881

882
            if (width > CHART_INDENT) {
625✔
883
                width -= CHART_INDENT;
625✔
884
                const double span = (sel_ub - sel_lb).count();
625✔
885
                auto us_per_ch = std::chrono::microseconds{
886
                    static_cast<int64_t>(ceil(span / (double) width))};
625✔
887

888
                if (row.or_value.otr_range.tr_begin <= sel_lb) {
625✔
UNCOV
889
                    lr.lr_start = CHART_INDENT;
×
890
                } else {
891
                    auto start_diff
892
                        = (row.or_value.otr_range.tr_begin - sel_lb);
625✔
893

894
                    lr.lr_start = CHART_INDENT + floor(start_diff / us_per_ch);
625✔
895
                }
896

897
                if (sel_ub < row.or_value.otr_range.tr_end) {
625✔
898
                    lr.lr_end = -1;
27✔
899
                } else {
900
                    auto end_diff = (row.or_value.otr_range.tr_end - sel_lb);
598✔
901

902
                    lr.lr_end = CHART_INDENT + ceil(end_diff / us_per_ch);
598✔
903
                    if (lr.lr_start == lr.lr_end) {
598✔
904
                        lr.lr_end += 1;
450✔
905
                    }
906
                }
907

908
                require(lr.lr_start >= 0);
625✔
909
                value_out.emplace_back(lr,
625✔
910
                                       VC_ROLE.value(role_t::VCR_TIMELINE_BAR));
1,250✔
911
            }
912
        }
913
        if (row.or_is_context) {
625✔
UNCOV
914
            value_out.emplace_back(line_range{0, -1},
×
UNCOV
915
                                   VC_ROLE.value(role_t::VCR_CONTEXT_LINE));
×
916
        }
917
        auto alt_row_index = line % 4;
625✔
918
        if (alt_row_index == 2 || alt_row_index == 3) {
625✔
919
            value_out.emplace_back(line_range{0, -1},
293✔
920
                                   VC_ROLE.value(role_t::VCR_ALT_ROW));
586✔
921
        }
922
    }
923
}
625✔
924

925
size_t
UNCOV
926
timeline_source::text_size_for_line(textview_curses& tc,
×
927
                                    int line,
928
                                    text_sub_source::line_flags_t raw)
929
{
UNCOV
930
    return this->ts_total_width;
×
931
}
932

933
Result<std::string, lnav::console::user_message>
UNCOV
934
timeline_source::text_reload_data(exec_context& ec)
×
935
{
UNCOV
936
    if (!this->rebuild_indexes()) {
×
UNCOV
937
        return ec.make_error("timeline rebuild was interrupted");
×
938
    }
UNCOV
939
    this->apply_pending_bookmarks();
×
UNCOV
940
    if (this->tss_view != nullptr) {
×
UNCOV
941
        this->tss_view->reload_data();
×
UNCOV
942
        this->tss_view->redo_search();
×
943
    }
UNCOV
944
    return Ok(std::string());
×
945
}
946

947
bool
948
timeline_source::rebuild_indexes()
68✔
949
{
950
    static auto op = lnav_operation{"timeline_rebuild"};
68✔
951

952
    auto op_guard = lnav_opid_guard::internal(op);
68✔
953
    auto& bm = this->tss_view->get_bookmarks();
68✔
954
    auto& bm_files = bm[&logfile_sub_source::BM_FILES];
68✔
955
    auto& bm_errs = bm[&textview_curses::BM_ERRORS];
68✔
956
    auto& bm_warns = bm[&textview_curses::BM_WARNINGS];
68✔
957
    auto& bm_meta = bm[&textview_curses::BM_META];
68✔
958
    auto& bm_parts = bm[&textview_curses::BM_PARTITION];
68✔
959

960
    this->ts_rebuild_in_progress = true;
68✔
961

962
    static const bookmark_type_t* PRESERVE_TYPES[] = {
963
        &textview_curses::BM_USER,
964
        &textview_curses::BM_STICKY,
965
    };
966
    for (const auto* bm_type : PRESERVE_TYPES) {
204✔
967
        auto& bv = bm[bm_type];
136✔
968
        for (const auto& vl : bv.bv_tree) {
136✔
UNCOV
969
            auto line = static_cast<size_t>(vl);
×
UNCOV
970
            if (line < this->ts_time_order.size()) {
×
UNCOV
971
                const auto& row = *this->ts_time_order[line];
×
UNCOV
972
                this->ts_pending_bookmarks.emplace_back(pending_bookmark{
×
UNCOV
973
                    row.or_type,
×
974
                    row.or_name.to_string(),
975
                    bm_type,
976
                });
977
            }
978
        }
979
        bv.clear();
136✔
980
    }
981

982
    bm.clear();
68✔
983

984
    this->ts_lower_bound = {};
68✔
985
    this->ts_upper_bound = {};
68✔
986
    this->ts_opid_width = 0;
68✔
987
    this->ts_total_width = 0;
68✔
988
    this->ts_filtered_count = 0;
68✔
989
    this->ts_active_opids.clear();
68✔
990
    this->ts_descriptions.clear();
68✔
991
    this->ts_subid_map.clear();
68✔
992
    this->ts_allocator.reset();
68✔
993
    this->ts_preview_source.clear();
68✔
994
    this->ts_preview_rows.clear();
68✔
995
    this->ts_preview_status_source.get_description().clear();
68✔
996

997
    auto min_log_time_tv_opt = this->get_min_row_time();
68✔
998
    auto max_log_time_tv_opt = this->get_max_row_time();
68✔
999
    std::optional<std::chrono::microseconds> min_log_time_opt;
68✔
1000
    std::optional<std::chrono::microseconds> max_log_time_opt;
68✔
1001
    auto max_desc_width = size_t{0};
68✔
1002

1003
    if (min_log_time_tv_opt) {
68✔
1004
        min_log_time_opt = to_us(min_log_time_tv_opt.value());
1✔
1005
    }
1006
    if (max_log_time_tv_opt) {
68✔
1007
        max_log_time_opt = to_us(max_log_time_tv_opt.value());
1✔
1008
    }
1009

1010
    log_info("building opid table");
68✔
1011
    auto last_log_time = std::chrono::microseconds{};
68✔
1012
    tlx::btree_map<std::chrono::microseconds, std::string> part_map;
68✔
1013
    for (const auto& [index, ld] : lnav::itertools::enumerate(this->ts_lss)) {
151✔
1014
        if (ld->get_file_ptr() == nullptr) {
83✔
1015
            continue;
1✔
1016
        }
1017
        if (!ld->is_visible()) {
83✔
1018
            continue;
1✔
1019
        }
1020

1021
        auto* lf = ld->get_file_ptr();
82✔
1022
        lf->enable_cache();
82✔
1023

1024
        const auto& mark_meta = lf->get_bookmark_metadata();
82✔
1025
        {
1026
            for (const auto& [line_num, line_meta] : mark_meta) {
158✔
1027
                const auto ll = std::next(lf->begin(), line_num);
76✔
1028
                if (!line_meta.bm_name.empty()) {
76✔
1029
                    part_map.insert2(ll->get_time<>(), line_meta.bm_name);
14✔
1030
                }
1031
                for (const auto& entry : line_meta.bm_tags) {
80✔
1032
                    auto line_time = ll->get_time<>();
4✔
1033
                    auto tag_key = fmt::format(FMT_STRING("{}@{}:{}"),
8✔
1034
                                               entry.te_tag,
4✔
1035
                                               lf->get_unique_path(),
1036
                                               line_time.count());
4✔
1037
                    auto tag_key_sf
1038
                        = string_fragment::from_str(tag_key).to_owned(
8✔
1039
                            this->ts_allocator);
4✔
1040
                    auto tag_name_sf = string_fragment::from_str(entry.te_tag)
4✔
1041
                                           .to_owned(this->ts_allocator);
4✔
1042
                    auto tag_otr = opid_time_range{};
4✔
1043
                    tag_otr.otr_range.tr_begin = line_time;
4✔
1044
                    tag_otr.otr_range.tr_end = line_time;
4✔
1045
                    tag_otr.otr_level_stats.update_msg_count(
4✔
1046
                        ll->get_msg_level());
1047
                    this->ts_active_opids.emplace(
×
1048
                        tag_key_sf,
1049
                        opid_row{
4✔
1050
                            row_type::tag,
1051
                            tag_name_sf,
1052
                            tag_otr,
1053
                            string_fragment::invalid(),
1054
                        });
1055
                }
4✔
1056
            }
1057
        }
1058

1059
        auto path = string_fragment::from_str(lf->get_unique_path())
82✔
1060
                        .to_owned(this->ts_allocator);
82✔
1061
        auto lf_otr = opid_time_range{};
82✔
1062
        lf_otr.otr_range = lf->get_content_time_range();
82✔
1063
        lf_otr.otr_level_stats = lf->get_level_stats();
82✔
1064
        if (lf_otr.otr_range.tr_end > last_log_time) {
82✔
1065
            last_log_time = lf_otr.otr_range.tr_end;
78✔
1066
        }
1067
        auto lf_row = opid_row{
82✔
1068
            row_type::logfile,
1069
            path,
1070
            lf_otr,
1071
            string_fragment::invalid(),
1072
        };
82✔
1073
        lf_row.or_logfile = lf;
82✔
1074
        this->ts_active_opids.emplace(path, lf_row);
82✔
1075

1076
        {
1077
            auto r_tid_map = lf->get_thread_ids().readAccess();
82✔
1078

1079
            for (const auto& [tid_sf, tid_meta] : r_tid_map->ltis_tid_ranges) {
348✔
1080
                auto active_iter = this->ts_active_opids.find(tid_sf);
266✔
1081
                if (active_iter == this->ts_active_opids.end()) {
266✔
1082
                    auto tid = tid_sf.to_owned(this->ts_allocator);
257✔
1083
                    auto tid_otr = opid_time_range{};
257✔
1084
                    tid_otr.otr_range = tid_meta.titr_range;
257✔
1085
                    tid_otr.otr_level_stats = tid_meta.titr_level_stats;
257✔
UNCOV
1086
                    this->ts_active_opids.emplace(
×
1087
                        tid,
1088
                        opid_row{
257✔
1089
                            row_type::thread,
1090
                            tid,
1091
                            tid_otr,
1092
                            string_fragment::invalid(),
1093
                        });
1094
                } else {
257✔
1095
                    active_iter->second.or_value.otr_range
18✔
1096
                        |= tid_meta.titr_range;
9✔
1097
                }
1098
            }
1099
        }
82✔
1100

1101
        auto format = lf->get_format();
82✔
1102
        safe::ReadAccess<logfile::safe_opid_state> r_opid_map(
1103
            ld->get_file_ptr()->get_opids());
82✔
1104
        for (const auto& pair : r_opid_map->los_opid_ranges) {
1,141✔
1105
            const opid_time_range& otr = pair.second;
1,059✔
1106
            auto active_iter = this->ts_active_opids.find(pair.first);
1,059✔
1107
            if (active_iter == this->ts_active_opids.end()) {
1,059✔
1108
                auto opid = pair.first.to_owned(this->ts_allocator);
1,052✔
1109
                auto active_emp_res = this->ts_active_opids.emplace(
2,104✔
1110
                    opid,
1111
                    opid_row{
1,052✔
1112
                        row_type::opid,
1113
                        opid,
1114
                        otr,
1115
                        string_fragment::invalid(),
1116
                    });
1117
                active_iter = active_emp_res.first;
1,052✔
1118
            } else {
1119
                active_iter->second.or_value |= otr;
7✔
1120
            }
1121

1122
            opid_row& row = active_iter->second;
1,059✔
1123
            for (auto& sub : row.or_value.otr_sub_ops) {
1,067✔
1124
                auto subid_iter = this->ts_subid_map.find(sub.ostr_subid);
8✔
1125

1126
                if (subid_iter == this->ts_subid_map.end()) {
8✔
1127
                    subid_iter = this->ts_subid_map
8✔
1128
                                     .emplace(sub.ostr_subid.to_owned(
8✔
1129
                                                  this->ts_allocator),
8✔
1130
                                              true)
16✔
1131
                                     .first;
1132
                }
1133
                sub.ostr_subid = subid_iter->first;
8✔
1134
                if (sub.ostr_subid.length() > row.or_max_subid_width) {
8✔
1135
                    row.or_max_subid_width = sub.ostr_subid.length();
8✔
1136
                }
1137
            }
1138

1139
            if (otr.otr_description.lod_index) {
1,059✔
1140
                auto desc_id = otr.otr_description.lod_index.value();
1,020✔
1141
                auto desc_def_iter
1142
                    = format->lf_opid_description_def_vec->at(desc_id);
1,020✔
1143

1144
                auto desc_key
1145
                    = opid_description_def_key{format->get_name(), desc_id};
1,020✔
1146
                auto desc_defs_opt
1147
                    = row.or_description_defs.odd_defs.value_for(desc_key);
1,020✔
1148
                if (!desc_defs_opt) {
1,020✔
1149
                    row.or_description_defs.odd_defs.insert(desc_key,
1,020✔
1150
                                                            *desc_def_iter);
1151
                }
1152

1153
                if (!row.or_description_begin
1,020✔
1154
                    || otr.otr_range.tr_begin
1,020✔
UNCOV
1155
                        < row.or_description_begin.value())
×
1156
                {
1157
                    row.or_description_begin = otr.otr_range.tr_begin;
1,020✔
1158
                    row.or_description_def_key = desc_key;
1,020✔
1159
                    row.or_description_value = otr.otr_description.lod_elements;
1,020✔
1160
                }
1161
            } else if (!otr.otr_description.lod_elements.empty()) {
39✔
1162
                auto desc_sf = string_fragment::from_str(
4✔
1163
                    otr.otr_description.lod_elements.values().front());
4✔
1164
                row.or_description = desc_sf.to_owned(this->ts_allocator);
4✔
1165
            }
1166
            row.or_value.otr_description.lod_elements.clear();
1,059✔
1167
        }
1168

1169
        if (this->ts_index_progress) {
82✔
1170
            switch (this->ts_index_progress(
×
UNCOV
1171
                progress_t{index, this->ts_lss.file_count()}))
×
1172
            {
UNCOV
1173
                case lnav::progress_result_t::ok:
×
UNCOV
1174
                    break;
×
UNCOV
1175
                case lnav::progress_result_t::interrupt:
×
UNCOV
1176
                    log_debug("timeline rebuild interrupted");
×
UNCOV
1177
                    this->ts_rebuild_in_progress = false;
×
UNCOV
1178
                    return false;
×
1179
            }
1180
        }
1181
    }
82✔
1182
    if (this->ts_index_progress) {
68✔
UNCOV
1183
        this->ts_index_progress(std::nullopt);
×
1184
    }
1185

1186
    std::set<string_fragment> consumed_tag_keys;
136✔
1187
    {
1188
        static const auto START_PREFIX_RE = lnav::pcre2pp::code::from_const(
1189
            R"(^#(?:start(?:ed)?|begin)[-_:.]?(.*)$)", PCRE2_CASELESS);
68✔
1190
        static const auto START_SUFFIX_RE = lnav::pcre2pp::code::from_const(
1191
            R"(^(.*?)(?:[-_:.])?\b(?:start(?:ed)?|begin)$)", PCRE2_CASELESS);
68✔
1192
        static const auto STOP_PREFIX_RE = lnav::pcre2pp::code::from_const(
1193
            R"(^#(?:stop(?:ped)?|end(?:ed)?|finish(?:ed)?)[-_:.]?(.*)$)",
1194
            PCRE2_CASELESS);
68✔
1195
        static const auto STOP_SUFFIX_RE = lnav::pcre2pp::code::from_const(
1196
            R"(^(.*?)(?:[-_:.])?\b(?:stop(?:ped)?|end(?:ed)?|finish(?:ed)?)$)",
1197
            PCRE2_CASELESS);
68✔
1198

1199
        struct span_event {
1200
            bool se_is_start;
1201
            std::chrono::microseconds se_time;
1202
            opid_row* se_row;
1203
            string_fragment se_key;
1204
        };
1205
        std::map<string_fragment, std::vector<span_event>> events_by_base;
68✔
1206
        thread_local auto md = lnav::pcre2pp::match_data::unitialized();
68✔
1207
        for (auto& pair : this->ts_active_opids) {
1,463✔
1208
            if (pair.second.or_type != row_type::tag) {
1,395✔
1209
                continue;
1,391✔
1210
            }
1211
            auto tag = pair.second.or_name;
4✔
1212
            std::optional<bool> is_start;
4✔
1213
            std::optional<string_fragment> base;
4✔
1214
            if (START_PREFIX_RE.capture_from(tag).into(md).found_p()) {
4✔
1215
                is_start = true;
2✔
1216
                base = md[1];
2✔
1217
            } else if (START_SUFFIX_RE.capture_from(tag).into(md).found_p()) {
2✔
UNCOV
1218
                is_start = true;
×
1219
                base = md[1];
×
1220
            } else if (STOP_PREFIX_RE.capture_from(tag).into(md).found_p()) {
2✔
1221
                is_start = false;
2✔
1222
                base = md[1];
2✔
1223
            } else if (STOP_SUFFIX_RE.capture_from(tag).into(md).found_p()) {
×
1224
                is_start = false;
×
1225
                base = md[1];
×
1226
            }
1227
            if (!is_start.has_value()) {
4✔
UNCOV
1228
                continue;
×
1229
            }
1230
            if (base->empty()) {
4✔
UNCOV
1231
                continue;
×
1232
            }
1233
            events_by_base[base.value()].push_back({
8✔
1234
                is_start.value(),
4✔
1235
                pair.second.or_value.otr_range.tr_begin,
1236
                &pair.second,
4✔
1237
                pair.first,
1238
            });
1239
        }
1240

1241
        for (auto& base_pair : events_by_base) {
70✔
1242
            auto& events = base_pair.second;
2✔
1243
            std::stable_sort(
2✔
1244
                events.begin(), events.end(), [](const auto& l, const auto& r) {
2✔
1245
                    if (l.se_time != r.se_time) {
2✔
1246
                        return l.se_time < r.se_time;
2✔
1247
                    }
UNCOV
1248
                    return l.se_is_start && !r.se_is_start;
×
1249
                });
1250
            opid_row* current_start = nullptr;
2✔
1251
            for (const auto& evt : events) {
6✔
1252
                if (evt.se_is_start) {
4✔
1253
                    if (current_start != nullptr) {
2✔
1254
                        current_start->or_value.otr_range.tr_end
UNCOV
1255
                            = evt.se_time - 1us;
×
1256
                    }
1257
                    current_start = evt.se_row;
2✔
1258
                } else if (current_start != nullptr) {
2✔
1259
                    current_start->or_value.otr_range.tr_end = evt.se_time;
2✔
1260
                    consumed_tag_keys.insert(evt.se_key);
2✔
1261
                    current_start = nullptr;
2✔
1262
                }
1263
            }
1264
            if (current_start != nullptr) {
2✔
UNCOV
1265
                current_start->or_value.otr_range.tr_end = last_log_time;
×
1266
            }
1267
        }
1268
    }
68✔
1269

1270
    for (auto part_iter = part_map.begin(); part_iter != part_map.end();) {
75✔
1271
        const auto begin_time = part_iter->first;
7✔
1272
        const auto& part_name = part_iter->second;
7✔
1273

1274
        auto next_iter = std::next(part_iter);
7✔
1275
        while (next_iter != part_map.end() && next_iter->second == part_name) {
14✔
1276
            next_iter = std::next(next_iter);
7✔
1277
        }
1278

1279
        auto part_key
1280
            = fmt::format(FMT_STRING("{}@{}"), part_name, begin_time.count());
21✔
1281
        auto part_key_sf
1282
            = string_fragment::from_str(part_key).to_owned(this->ts_allocator);
7✔
1283
        auto part_name_sf
1284
            = string_fragment::from_str(part_name).to_owned(this->ts_allocator);
7✔
1285
        auto part_otr = opid_time_range{};
7✔
1286
        part_otr.otr_range.tr_begin = begin_time;
7✔
1287
        if (next_iter != part_map.end()) {
7✔
1288
            part_otr.otr_range.tr_end = next_iter->first;
4✔
1289
        } else {
1290
            part_otr.otr_range.tr_end = last_log_time;
3✔
1291
        }
UNCOV
1292
        this->ts_active_opids.emplace(part_key_sf,
×
1293
                                      opid_row{
7✔
1294
                                          row_type::partition,
1295
                                          part_name_sf,
1296
                                          part_otr,
1297
                                          string_fragment::invalid(),
1298
                                      });
1299

1300
        part_iter = next_iter;
7✔
1301
    }
7✔
1302

1303
    log_info("active opids: %zu", this->ts_active_opids.size());
68✔
1304

1305
    size_t filtered_in_count = 0;
68✔
1306
    for (const auto& filt : this->tss_filters) {
70✔
1307
        if (!filt->is_enabled()) {
2✔
UNCOV
1308
            continue;
×
1309
        }
1310
        if (filt->get_type() == text_filter::INCLUDE) {
2✔
1311
            filtered_in_count += 1;
1✔
1312
        }
1313
    }
1314
    this->ts_filter_hits = {};
68✔
1315

1316
    // Collect all candidates with their filter status, then sort,
1317
    // then apply context expansion.
1318
    struct row_candidate {
1319
        opid_row* rc_row;
1320
        std::string rc_full_desc;
1321
        bool rc_matched;
1322
        bool rc_is_context{false};
1323
    };
1324
    std::vector<row_candidate> candidates;
68✔
1325
    candidates.reserve(this->ts_active_opids.size());
68✔
1326

1327
    for (auto& pair : this->ts_active_opids) {
1,470✔
1328
        if (consumed_tag_keys.count(pair.first) > 0) {
1,402✔
1329
            continue;
6✔
1330
        }
1331
        opid_row& row = pair.second;
1,400✔
1332
        opid_time_range& otr = pair.second.or_value;
1,400✔
1333
        std::string full_desc;
1,400✔
1334
        if (row.or_description.empty()) {
1,400✔
1335
            const auto& desc_defs = row.or_description_defs.odd_defs;
1,396✔
1336
            if (row.or_description_begin) {
1,396✔
1337
                auto desc_def_opt
1338
                    = desc_defs.value_for(row.or_description_def_key);
1,020✔
1339
                if (desc_def_opt) {
1,020✔
1340
                    full_desc = desc_def_opt.value()->to_string(
2,040✔
1341
                        row.or_description_value);
1,020✔
1342
                }
1343
            }
1344
            row.or_description_begin = std::nullopt;
1,396✔
1345
            auto full_desc_sf = string_fragment::from_str(full_desc);
1,396✔
1346
            auto desc_sf_iter = this->ts_descriptions.find(full_desc_sf);
1,396✔
1347
            if (desc_sf_iter == this->ts_descriptions.end()) {
1,396✔
1348
                full_desc_sf = string_fragment::from_str(full_desc).to_owned(
266✔
1349
                    this->ts_allocator);
133✔
1350
                this->ts_descriptions.insert(full_desc_sf);
133✔
1351
            } else {
1352
                full_desc_sf = *desc_sf_iter;
1,263✔
1353
            }
1354
            pair.second.or_description = full_desc_sf;
1,396✔
1355
        } else {
1356
            full_desc += pair.second.or_description;
4✔
1357
        }
1358

1359
        if (!this->is_row_type_visible(row.or_type)) {
1,400✔
1360
            this->ts_filtered_count += 1;
4✔
1361
            continue;
4✔
1362
        }
1363

1364
        auto matched = true;
1,396✔
1365
        shared_buffer sb_opid;
1,396✔
1366
        shared_buffer_ref sbr_opid;
1,396✔
1367
        sbr_opid.share(
1,396✔
1368
            sb_opid, pair.second.or_name.data(), pair.second.or_name.length());
1,396✔
1369
        shared_buffer sb_desc;
1,396✔
1370
        shared_buffer_ref sbr_desc;
1,396✔
1371
        sbr_desc.share(sb_desc, full_desc.c_str(), full_desc.length());
1,396✔
1372
        if (this->tss_apply_filters) {
1,396✔
1373
            auto filtered_in = false;
1,396✔
1374
            auto filtered_out = false;
1,396✔
1375
            for (const auto& filt : this->tss_filters) {
1,564✔
1376
                if (!filt->is_enabled()) {
168✔
1377
                    continue;
×
1378
                }
1379
                for (const auto sbr : {&sbr_opid, &sbr_desc}) {
504✔
1380
                    if (filt->matches(std::nullopt, *sbr)) {
336✔
1381
                        this->ts_filter_hits[filt->get_index()] += 1;
2✔
1382
                        switch (filt->get_type()) {
2✔
1383
                            case text_filter::INCLUDE:
1✔
1384
                                filtered_in = true;
1✔
1385
                                break;
1✔
1386
                            case text_filter::EXCLUDE:
1✔
1387
                                filtered_out = true;
1✔
1388
                                break;
1✔
UNCOV
1389
                            default:
×
UNCOV
1390
                                break;
×
1391
                        }
1392
                    }
1393
                }
1394
            }
1395

1396
            if (min_log_time_opt
1,396✔
1397
                && otr.otr_range.tr_end < min_log_time_opt.value())
1,396✔
1398
            {
1399
                filtered_out = true;
16✔
1400
            }
1401
            if (max_log_time_opt
1,396✔
1402
                && max_log_time_opt.value() < otr.otr_range.tr_begin)
1,396✔
1403
            {
1404
                filtered_out = true;
16✔
1405
            }
1406

1407
            if ((filtered_in_count > 0 && !filtered_in) || filtered_out) {
1,396✔
1408
                matched = false;
116✔
1409
            }
1410
        }
1411

1412
        candidates.push_back({&pair.second, std::move(full_desc), matched});
1,396✔
1413
    }
1,400✔
1414

1415
    // Sort candidates by time before applying context expansion
1416
    std::stable_sort(candidates.begin(),
68✔
1417
                     candidates.end(),
1418
                     [](const auto& lhs, const auto& rhs) {
6,732✔
1419
                         return *lhs.rc_row < *rhs.rc_row;
6,732✔
1420
                     });
1421

1422
    // Apply context expansion on sorted candidates
1423
    auto context_before = this->tss_context_before;
68✔
1424
    auto context_after = this->tss_context_after;
68✔
1425
    if (context_before > 0 || context_after > 0) {
68✔
1426
        // Forward pass: mark after-context rows
UNCOV
1427
        size_t after_remaining = 0;
×
UNCOV
1428
        for (auto& cand : candidates) {
×
UNCOV
1429
            if (cand.rc_matched) {
×
1430
                after_remaining = context_after;
×
1431
            } else if (after_remaining > 0) {
×
1432
                cand.rc_matched = true;
×
1433
                cand.rc_is_context = true;
×
1434
                after_remaining -= 1;
×
1435
            }
1436
        }
1437
        // Reverse pass: mark before-context rows
UNCOV
1438
        size_t before_remaining = 0;
×
UNCOV
1439
        for (auto it = candidates.rbegin(); it != candidates.rend(); ++it) {
×
UNCOV
1440
            if (it->rc_matched) {
×
UNCOV
1441
                before_remaining = context_before;
×
1442
            } else if (before_remaining > 0) {
×
UNCOV
1443
                it->rc_matched = true;
×
UNCOV
1444
                it->rc_is_context = true;
×
UNCOV
1445
                before_remaining -= 1;
×
1446
            }
1447
        }
1448
    }
1449

1450
    // Emit matched rows
1451
    this->ts_time_order.clear();
68✔
1452
    this->ts_time_order.reserve(candidates.size());
68✔
1453
    for (auto& cand : candidates) {
1,464✔
1454
        if (!cand.rc_matched) {
1,396✔
1455
            this->ts_filtered_count += 1;
116✔
1456
            continue;
116✔
1457
        }
1458

1459
        auto* row_ptr = cand.rc_row;
1,280✔
1460
        row_ptr->or_is_context = cand.rc_is_context;
1,280✔
1461
        if (row_ptr->or_name.column_width() > this->ts_opid_width) {
1,280✔
1462
            this->ts_opid_width = row_ptr->or_name.column_width();
132✔
1463
        }
1464
        if (cand.rc_full_desc.size() > max_desc_width) {
1,280✔
1465
            max_desc_width = cand.rc_full_desc.size();
32✔
1466
        }
1467

1468
        if (this->ts_lower_bound == 0us
1,280✔
1469
            || row_ptr->or_value.otr_range.tr_begin < this->ts_lower_bound)
1,280✔
1470
        {
1471
            this->ts_lower_bound = row_ptr->or_value.otr_range.tr_begin;
66✔
1472
        }
1473
        if (this->ts_upper_bound == 0us
1,280✔
1474
            || this->ts_upper_bound < row_ptr->or_value.otr_range.tr_end)
1,280✔
1475
        {
1476
            this->ts_upper_bound = row_ptr->or_value.otr_range.tr_end;
130✔
1477
        }
1478

1479
        this->ts_time_order.emplace_back(row_ptr);
1,280✔
1480
    }
1481
    for (size_t lpc = 0; lpc < this->ts_time_order.size(); lpc++) {
1,348✔
1482
        const auto& row = *this->ts_time_order[lpc];
1,280✔
1483
        if (row.or_type == row_type::logfile) {
1,280✔
1484
            bm_files.insert_once(vis_line_t(lpc));
80✔
1485
        } else if (row.or_type == row_type::tag) {
1,200✔
1486
            bm_meta.insert_once(vis_line_t(lpc));
2✔
1487
        } else if (row.or_type == row_type::partition) {
1,198✔
1488
            bm_parts.insert_once(vis_line_t(lpc));
7✔
1489
        }
1490
        if (row.or_value.otr_level_stats.lls_error_count > 0) {
1,280✔
1491
            bm_errs.insert_once(vis_line_t(lpc));
149✔
1492
        }
1493
        if (row.or_value.otr_level_stats.lls_warning_count > 0) {
1,280✔
1494
            bm_warns.insert_once(vis_line_t(lpc));
92✔
1495
        }
1496
    }
1497

1498
    this->ts_opid_width = std::min(this->ts_opid_width, MAX_OPID_WIDTH);
68✔
1499
    this->ts_total_width
1500
        = std::max<size_t>(22 + this->ts_opid_width + max_desc_width,
136✔
1501
                           1 + 16 + 5 + 8 + 5 + 16 + 1 /* header */);
68✔
1502

1503
    this->collect_metric_samples();
68✔
1504

1505
    this->apply_pending_bookmarks();
68✔
1506

1507
    this->tss_view->set_needs_update();
68✔
1508
    this->ts_rebuild_in_progress = false;
68✔
1509

1510
    ensure(this->ts_time_order.empty() || this->ts_opid_width > 0);
68✔
1511

1512
    return true;
68✔
1513
}
1,464✔
1514

1515
std::optional<vis_line_t>
1516
timeline_source::row_for_time(timeval time_bucket)
38✔
1517
{
1518
    auto time_bucket_us = to_us(time_bucket);
38✔
1519
    auto iter = this->ts_time_order.begin();
38✔
1520
    while (true) {
1521
        if (iter == this->ts_time_order.end()) {
41✔
1522
            return std::nullopt;
1✔
1523
        }
1524

1525
        if ((*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
40✔
1526
            break;
37✔
1527
        }
1528
        ++iter;
3✔
1529
    }
1530

1531
    auto closest_iter = iter;
37✔
1532
    auto closest_diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
37✔
1533
    for (; iter != this->ts_time_order.end(); ++iter) {
139✔
1534
        if (time_bucket_us < (*iter)->or_value.otr_range.tr_begin) {
138✔
1535
            break;
36✔
1536
        }
1537
        if (!(*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
102✔
1538
            continue;
9✔
1539
        }
1540

1541
        auto diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
93✔
1542
        if (diff < closest_diff) {
93✔
1543
            closest_iter = iter;
4✔
1544
            closest_diff = diff;
4✔
1545
        }
1546

1547
        for (const auto& sub : (*iter)->or_value.otr_sub_ops) {
93✔
UNCOV
1548
            if (!sub.ostr_range.contains_inclusive(time_bucket_us)) {
×
UNCOV
1549
                continue;
×
1550
            }
1551

UNCOV
1552
            diff = time_bucket_us - sub.ostr_range.tr_begin;
×
UNCOV
1553
            if (diff < closest_diff) {
×
UNCOV
1554
                closest_iter = iter;
×
UNCOV
1555
                closest_diff = diff;
×
1556
            }
1557
        }
1558
    }
1559

1560
    return vis_line_t(std::distance(this->ts_time_order.begin(), closest_iter));
74✔
1561
}
1562

1563
std::optional<vis_line_t>
1564
timeline_source::row_for(const row_info& ri)
58✔
1565
{
1566
    auto vl_opt = this->ts_lss.row_for(ri);
58✔
1567
    if (!vl_opt) {
58✔
UNCOV
1568
        return this->row_for_time(ri.ri_time);
×
1569
    }
1570

1571
    auto vl = vl_opt.value();
58✔
1572
    auto win = this->ts_lss.window_at(vl);
58✔
1573
    for (const auto& msg_line : *win) {
134✔
1574
        const auto& lvv = msg_line.get_values();
58✔
1575

1576
        if (lvv.lvv_opid_value) {
58✔
1577
            auto opid_iter
1578
                = this->ts_active_opids.find(lvv.lvv_opid_value.value());
23✔
1579
            if (opid_iter != this->ts_active_opids.end()) {
23✔
1580
                for (const auto& [index, oprow] :
187✔
1581
                     lnav::itertools::enumerate(this->ts_time_order))
190✔
1582
                {
1583
                    if (oprow == &opid_iter->second) {
164✔
1584
                        return vis_line_t(index);
20✔
1585
                    }
1586
                }
1587
            }
1588
        }
1589
    }
78✔
1590

1591
    return this->row_for_time(ri.ri_time);
38✔
1592
}
58✔
1593

1594
std::optional<text_time_translator::row_info>
1595
timeline_source::time_for_row(vis_line_t row)
86✔
1596
{
1597
    if (row >= this->ts_time_order.size()) {
86✔
UNCOV
1598
        return std::nullopt;
×
1599
    }
1600

1601
    const auto& otr = this->ts_time_order[row]->or_value;
86✔
1602

1603
    if (this->tss_view->get_selection() == row) {
86✔
1604
        auto ov_sel = this->tss_view->get_overlay_selection();
86✔
1605

1606
        if (ov_sel && ov_sel.value() < otr.otr_sub_ops.size()) {
86✔
UNCOV
1607
            return row_info{
×
UNCOV
1608
                to_timeval(otr.otr_sub_ops[ov_sel.value()].ostr_range.tr_begin),
×
1609
                row,
1610
            };
1611
        }
1612
    }
1613

1614
    auto preview_selection = this->ts_preview_view.get_selection();
86✔
1615
    if (preview_selection && preview_selection < this->ts_preview_rows.size()) {
86✔
1616
        return this->ts_preview_rows[preview_selection.value()];
83✔
1617
    }
1618

1619
    return row_info{
6✔
1620
        to_timeval(otr.otr_range.tr_begin),
3✔
1621
        row,
1622
    };
6✔
1623
}
1624

1625
size_t
1626
timeline_source::text_line_width(textview_curses& curses)
8,847✔
1627
{
1628
    return this->ts_total_width;
8,847✔
1629
}
1630

1631
void
1632
timeline_source::text_selection_changed(textview_curses& tc)
138✔
1633
{
1634
    static const size_t MAX_PREVIEW_LINES = 200;
1635

1636
    auto sel = tc.get_selection();
138✔
1637

1638
    this->ts_preview_source.clear();
138✔
1639
    this->ts_preview_rows.clear();
138✔
1640
    if (!sel || sel.value() >= this->ts_time_order.size()) {
138✔
1641
        return;
73✔
1642
    }
1643

1644
    const auto& row = *this->ts_time_order[sel.value()];
65✔
1645
    auto low_us = row.or_value.otr_range.tr_begin;
65✔
1646
    auto high_us = row.or_value.otr_range.tr_end;
65✔
1647
    auto id_sf = row.or_name;
65✔
1648
    auto level_stats = row.or_value.otr_level_stats;
65✔
1649
    auto ov_sel = tc.get_overlay_selection();
65✔
1650
    if (ov_sel) {
65✔
1651
        const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
UNCOV
1652
        id_sf = sub.ostr_subid;
×
UNCOV
1653
        low_us = sub.ostr_range.tr_begin;
×
1654
        high_us = sub.ostr_range.tr_end;
×
UNCOV
1655
        level_stats = sub.ostr_level_stats;
×
1656
    }
1657
    high_us += 1s;
65✔
1658
    auto low_vl = this->ts_lss.row_for_time(to_timeval(low_us));
65✔
1659
    auto high_vl = this->ts_lss.row_for_time(to_timeval(high_us))
65✔
1660
                       .value_or(vis_line_t(this->ts_lss.text_line_count()));
65✔
1661

1662
    if (!low_vl) {
65✔
1663
        return;
×
1664
    }
1665

1666
    auto preview_content = attr_line_t();
65✔
1667
    auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
65✔
1668
    auto win = this->ts_lss.window_at(low_vl.value(), high_vl);
65✔
1669
    auto id_bloom_bits = row.or_name.bloom_bits();
65✔
1670
    auto msg_count = 0;
65✔
1671
    for (const auto& msg_line : *win) {
2,083✔
1672
        switch (row.or_type) {
1,009✔
1673
            case row_type::logfile:
397✔
1674
                if (msg_line.get_file_ptr() != row.or_logfile) {
397✔
UNCOV
1675
                    continue;
×
1676
                }
1677
                break;
397✔
1678
            case row_type::thread: {
337✔
1679
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
337✔
1680
                    continue;
296✔
1681
                }
1682
                const auto& lvv = msg_line.get_values();
42✔
1683
                if (!lvv.lvv_thread_id_value) {
42✔
UNCOV
1684
                    continue;
×
1685
                }
1686
                auto tid_sf = lvv.lvv_thread_id_value.value();
42✔
1687
                if (!(tid_sf == row.or_name)) {
42✔
1688
                    continue;
1✔
1689
                }
1690
                break;
41✔
1691
            }
1692
            case row_type::opid: {
275✔
1693
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
275✔
1694
                    continue;
233✔
1695
                }
1696

1697
                const auto& lvv = msg_line.get_values();
42✔
1698
                if (!lvv.lvv_opid_value) {
42✔
UNCOV
1699
                    continue;
×
1700
                }
1701
                auto opid_sf = lvv.lvv_opid_value.value();
42✔
1702

1703
                if (!(opid_sf == row.or_name)) {
42✔
UNCOV
1704
                    continue;
×
1705
                }
1706
                break;
42✔
1707
            }
42✔
UNCOV
1708
            case row_type::tag: {
×
1709
                const auto& bm
UNCOV
1710
                    = msg_line.get_file_ptr()->get_bookmark_metadata();
×
UNCOV
1711
                auto bm_iter = bm.find(msg_line.get_file_line_number());
×
UNCOV
1712
                if (bm_iter == bm.end()) {
×
UNCOV
1713
                    continue;
×
1714
                }
UNCOV
1715
                auto tag_name = row.or_name.to_string();
×
UNCOV
1716
                if (!(bm_iter->second.bm_tags
×
UNCOV
1717
                      | lnav::itertools::find(tag_name)))
×
1718
                {
UNCOV
1719
                    continue;
×
1720
                }
UNCOV
1721
                break;
×
1722
            }
UNCOV
1723
            case row_type::partition:
×
UNCOV
1724
                break;
×
1725
        }
1726

1727
        for (size_t lpc = 0; lpc < msg_line.get_line_count(); lpc++) {
963✔
1728
            auto vl = msg_line.get_vis_line() + vis_line_t(lpc);
483✔
1729
            auto cl = this->ts_lss.at(vl);
483✔
1730
            auto row_al = attr_line_t();
483✔
1731
            this->ts_log_view.textview_value_for_row(vl, row_al);
483✔
1732
            preview_content.append(row_al).append("\n");
483✔
1733
            this->ts_preview_rows.emplace_back(
483✔
1734
                msg_line.get_logline().get_timeval(), cl);
483✔
1735
            ++cl;
483✔
1736
        }
483✔
1737
        msg_count += 1;
480✔
1738
        msgs_remaining -= 1;
480✔
1739
        if (msgs_remaining == 0) {
480✔
UNCOV
1740
            break;
×
1741
        }
1742
    }
65✔
1743

1744
    this->ts_preview_source.replace_with(preview_content);
65✔
1745
    this->ts_preview_view.set_selection(0_vl);
65✔
1746
    this->ts_preview_status_source.get_description().set_value(
65✔
1747
        " ID %.*s", id_sf.length(), id_sf.data());
1748
    auto err_count = level_stats.lls_error_count;
65✔
1749
    if (err_count == 0) {
65✔
1750
        this->ts_preview_status_source
29✔
1751
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
29✔
1752
            .set_value("");
29✔
1753
    } else {
1754
        this->ts_preview_status_source
36✔
1755
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
36✔
1756
            .set_value("\u2022 %'d", err_count);
36✔
1757
    }
1758
    auto warn_count = level_stats.lls_warning_count;
65✔
1759
    if (warn_count == 0) {
65✔
1760
        this->ts_preview_status_source
60✔
1761
            .statusview_value_for_field(timeline_status_source::TSF_WARNINGS)
60✔
1762
            .set_value("");
60✔
1763
    } else {
1764
        this->ts_preview_status_source
5✔
1765
            .statusview_value_for_field(timeline_status_source::TSF_WARNINGS)
5✔
1766
            .set_value("\u2022 %'d", warn_count);
5✔
1767
    }
1768
    if (msg_count < level_stats.lls_total_count) {
65✔
1769
        this->ts_preview_status_source
62✔
1770
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
62✔
1771
            .set_value(
62✔
1772
                "%'d of %'d messages ", msg_count, level_stats.lls_total_count);
1773
    } else {
1774
        this->ts_preview_status_source
3✔
1775
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
3✔
1776
            .set_value("%'d messages ", level_stats.lls_total_count);
3✔
1777
    }
1778
    this->ts_preview_status_view.set_needs_update();
65✔
1779
    this->update_metric_status();
65✔
1780
}
65✔
1781

1782
void
1783
timeline_source::text_filters_changed()
21✔
1784
{
1785
    this->rebuild_indexes();
21✔
1786
    this->tss_view->reload_data();
21✔
1787
    this->tss_view->redo_search();
21✔
1788
}
21✔
1789

1790
void
UNCOV
1791
timeline_source::clear_preview()
×
1792
{
UNCOV
1793
    text_sub_source::clear_preview();
×
UNCOV
1794
    this->ts_preview_hidden_row_types.clear();
×
1795
}
1796

1797
void
1798
timeline_source::add_commands_for_session(
80✔
1799
    const std::function<void(const std::string&)>& receiver)
1800
{
1801
    text_sub_source::add_commands_for_session(receiver);
80✔
1802

1803
    for (const auto& rt : this->ts_hidden_row_types) {
80✔
UNCOV
1804
        receiver(fmt::format(FMT_STRING("hide-in-timeline {}"),
×
UNCOV
1805
                             row_type_to_string(rt)));
×
1806
    }
1807
    for (const auto& m : this->ts_metrics) {
80✔
UNCOV
1808
        if (m.ms_def.md_kind == metric_kind::sql) {
×
UNCOV
1809
            receiver(fmt::format(FMT_STRING("timeline-metric-sql {} {}"),
×
UNCOV
1810
                                 m.ms_def.md_label,
×
UNCOV
1811
                                 m.ms_def.md_query));
×
1812
        } else {
UNCOV
1813
            receiver(fmt::format(FMT_STRING("timeline-metric {}"),
×
UNCOV
1814
                                 m.ms_def.md_label));
×
1815
        }
1816
    }
1817
}
80✔
1818

1819
Result<void, lnav::console::user_message>
1820
timeline_source::add_metric(const metric_def& md)
14✔
1821
{
1822
    // Replace on duplicate label.
1823
    for (auto& m : this->ts_metrics) {
19✔
1824
        if (m.ms_def.md_label == md.md_label) {
5✔
UNCOV
1825
            m.ms_def = md;
×
UNCOV
1826
            this->collect_metric_samples_for(m);
×
UNCOV
1827
            this->update_metric_status();
×
UNCOV
1828
            this->tss_view->set_needs_update();
×
UNCOV
1829
            return Ok();
×
1830
        }
1831
    }
1832
    if (this->ts_metrics.size() >= MAX_METRICS) {
14✔
UNCOV
1833
        attr_line_t active;
×
UNCOV
1834
        for (const auto& ms : this->ts_metrics) {
×
UNCOV
1835
            if (!active.empty()) {
×
UNCOV
1836
                active.append(", ");
×
1837
            }
UNCOV
1838
            active.append(lnav::roles::identifier(ms.ms_def.md_label));
×
1839
        }
UNCOV
1840
        return Err(
×
UNCOV
1841
            lnav::console::user_message::error(
×
UNCOV
1842
                attr_line_t("too many metrics are being tracked"))
×
UNCOV
1843
                .with_reason(fmt::format(
×
UNCOV
1844
                    FMT_STRING("at most {} metrics can be shown at once"),
×
1845
                    MAX_METRICS))
UNCOV
1846
                .with_note(attr_line_t("currently tracking: ").append(active))
×
UNCOV
1847
                .with_help(attr_line_t("use ")
×
UNCOV
1848
                               .append(":clear-timeline-metric"_keyword)
×
UNCOV
1849
                               .append(" <label> to remove a metric before "
×
UNCOV
1850
                                       "adding a new one")));
×
1851
    }
1852
    metric_state ms;
14✔
1853
    ms.ms_def = md;
14✔
1854
    this->ts_metrics.emplace_back(std::move(ms));
14✔
1855
    this->collect_metric_samples_for(this->ts_metrics.back());
14✔
1856
    this->update_metric_status();
14✔
1857
    this->tss_view->set_needs_update();
14✔
1858
    return Ok();
14✔
1859
}
14✔
1860

1861
bool
1862
timeline_source::remove_metric(const std::string& label)
1✔
1863
{
1864
    auto iter = std::find_if(
1✔
1865
        this->ts_metrics.begin(),
1866
        this->ts_metrics.end(),
1867
        [&](const metric_state& m) { return m.ms_def.md_label == label; });
1✔
1868
    if (iter == this->ts_metrics.end()) {
1✔
UNCOV
1869
        return false;
×
1870
    }
1871
    this->ts_metrics.erase(iter);
1✔
1872
    this->update_metric_status();
1✔
1873
    this->tss_view->set_needs_update();
1✔
1874
    return true;
1✔
1875
}
1876

1877
void
1878
timeline_source::clear_metrics()
11✔
1879
{
1880
    this->ts_metrics.clear();
11✔
1881
    this->update_metric_status();
11✔
1882
    this->tss_view->set_needs_update();
11✔
1883
}
11✔
1884

1885
void
1886
timeline_source::collect_metric_samples()
68✔
1887
{
1888
    // Full rebuild path: called from rebuild_indexes when the set of
1889
    // loaded files or the time window may have changed.  Individual
1890
    // add/replace paths use collect_metric_samples_for to avoid
1891
    // re-walking files for metrics that haven't changed.
1892
    for (auto& ms : this->ts_metrics) {
68✔
UNCOV
1893
        this->collect_metric_samples_for(ms);
×
1894
    }
1895
}
68✔
1896

1897
void
1898
timeline_source::collect_metric_samples_for(metric_state& ms)
14✔
1899
{
1900
    // Reset the per-metric state up front so replace-on-duplicate and
1901
    // full-rebuild share the same clean-slate behavior.  In particular
1902
    // ms_unit_suffix/ms_unit_divisor must be cleared — a replacing
1903
    // definition could target a different column whose unit differs
1904
    // from the previous one.
1905
    ms.ms_samples.clear();
14✔
1906
    ms.ms_min = std::numeric_limits<double>::quiet_NaN();
14✔
1907
    ms.ms_max = std::numeric_limits<double>::quiet_NaN();
14✔
1908
    ms.ms_unit_suffix.clear();
14✔
1909
    ms.ms_unit_divisor = 1.0;
14✔
1910
    ms.ms_matched = false;
14✔
1911
    ms.ms_error.clear();
14✔
1912

1913
    if (this->ts_time_order.empty()) {
14✔
UNCOV
1914
        return;
×
1915
    }
1916

1917
    if (ms.ms_def.md_kind == metric_kind::sql) {
14✔
1918
        // User query validated at command time, so the columns exist.
1919
        // "matched" here means: we won't render an "unknown metric"
1920
        // error banner for SQL metrics — an empty sample set for SQL
1921
        // is a legitimate "query returned 0 rows" situation, not a
1922
        // typo.
1923
        ms.ms_matched = true;
4✔
1924
        this->collect_metric_samples_via_sql(ms);
4✔
1925
    } else {
1926
        this->collect_metric_samples_from_files(ms);
10✔
1927
    }
1928
}
1929

1930
void
1931
timeline_source::collect_metric_samples_from_files(metric_state& ms)
10✔
1932
{
1933
    // Shorthand "source.metric" — walk the loaded log files directly,
1934
    // picking out matching metric files and reading just the one
1935
    // column per line.  Skips the SQL cursor overhead that the
1936
    // `all_metrics` vtable would incur, and avoids re-parsing every
1937
    // non-matching cell.
1938
    const auto dot = ms.ms_def.md_label.find('.');
10✔
1939
    std::string source;
10✔
1940
    std::string metric_name;
10✔
1941
    if (dot == std::string::npos) {
10✔
1942
        metric_name = ms.ms_def.md_label;
8✔
1943
    } else {
1944
        source = ms.ms_def.md_label.substr(0, dot);
2✔
1945
        metric_name = ms.ms_def.md_label.substr(dot + 1);
2✔
1946
    }
1947

1948
    for (const auto& ld : lnav_data.ld_log_source) {
34✔
1949
        auto* lf = ld->get_file_ptr();
24✔
1950
        if (lf == nullptr) {
24✔
1951
            continue;
14✔
1952
        }
1953
        auto format = lf->get_format();
24✔
1954
        if (!format || !format->lf_is_metric) {
24✔
1955
            continue;
10✔
1956
        }
1957
        if (!source.empty() && lf->get_unique_path().stem().string() != source)
14✔
1958
        {
1959
            continue;
2✔
1960
        }
1961

1962
        // Find the column index for this metric in this file's schema.
1963
        // Different files can ship different column orders, so the
1964
        // lookup is per-file.
1965
        const auto& meta_vec = format->get_value_metadata();
12✔
1966
        size_t col_idx = meta_vec.size();
12✔
1967
        for (size_t i = 0; i < meta_vec.size(); i++) {
19✔
1968
            if (meta_vec[i].lvm_name.to_string() == metric_name) {
17✔
1969
                col_idx = i;
10✔
1970
                break;
10✔
1971
            }
1972
        }
1973
        if (col_idx >= meta_vec.size()) {
12✔
1974
            continue;
2✔
1975
        }
1976
        ms.ms_matched = true;
10✔
1977
        // Seed the unit hint from the format-level column meta if it
1978
        // has one.  metrics_log_format doesn't populate this at
1979
        // format time — the suffix is only known after annotating a
1980
        // row that uses a humanized cell — so the per-sample loop
1981
        // below also looks at each parsed value's meta and fills in
1982
        // ms_unit_suffix the first time it sees one.
1983
        if (ms.ms_unit_suffix.empty() && ms.ms_unit_divisor == 1.0) {
10✔
1984
            ms.ms_unit_suffix = meta_vec[col_idx].lvm_unit_suffix.to_string();
10✔
1985
            if (meta_vec[col_idx].lvm_unit_divisor != 0.0) {
10✔
1986
                ms.ms_unit_divisor = meta_vec[col_idx].lvm_unit_divisor;
10✔
1987
            }
1988
        }
1989
        // Pull the column's global min/max from the file's value
1990
        // stats (already computed during indexing) so the sparkline
1991
        // scale stays stable as the user scrolls — a sample doesn't
1992
        // change bar height based on what else is in view.  Stats
1993
        // are in base units, same as the samples we store.
1994
        if (const auto* file_stats
10✔
1995
            = lf->stats_for_value(meta_vec[col_idx].lvm_name);
10✔
1996
            file_stats != nullptr && file_stats->lvs_count > 0)
10✔
1997
        {
1998
            double lo = file_stats->lvs_min_value;
10✔
1999
            double hi = file_stats->lvs_max_value;
10✔
2000
            if (ms.ms_unit_divisor != 0.0 && ms.ms_unit_divisor != 1.0) {
10✔
UNCOV
2001
                lo /= ms.ms_unit_divisor;
×
UNCOV
2002
                hi /= ms.ms_unit_divisor;
×
2003
            }
2004
            if (std::isnan(ms.ms_min) || lo < ms.ms_min) {
10✔
2005
                ms.ms_min = lo;
9✔
2006
            }
2007
            if (std::isnan(ms.ms_max) || hi > ms.ms_max) {
10✔
2008
                ms.ms_max = hi;
9✔
2009
            }
2010
        }
2011

2012
        string_attrs_t sa;
10✔
2013
        logline_value_vector values;
10✔
2014
        for (auto line_iter = lf->begin(); line_iter != lf->end(); ++line_iter)
77✔
2015
        {
2016
            if (line_iter->is_continued() || line_iter->is_ignored()) {
67✔
2017
                continue;
10✔
2018
            }
2019
            sa.clear();
57✔
2020
            values.lvv_values.clear();
57✔
2021
            auto read_res = lf->read_line(line_iter);
57✔
2022
            if (read_res.isErr()) {
57✔
UNCOV
2023
                continue;
×
2024
            }
2025
            values.lvv_sbr = read_res.unwrap();
57✔
2026
            auto line_number = std::distance(lf->begin(), line_iter);
57✔
2027
            format->annotate(lf, line_number, sa, values);
57✔
2028
            if (col_idx >= values.lvv_values.size()) {
57✔
UNCOV
2029
                continue;
×
2030
            }
2031
            const auto& lv = values.lvv_values[col_idx];
57✔
2032
            double v;
2033
            if (lv.lv_meta.lvm_kind == value_kind_t::VALUE_FLOAT) {
57✔
2034
                v = lv.lv_value.d;
40✔
2035
            } else if (lv.lv_meta.lvm_kind == value_kind_t::VALUE_INTEGER) {
17✔
2036
                v = static_cast<double>(lv.lv_value.i);
17✔
2037
            } else {
UNCOV
2038
                continue;
×
2039
            }
2040
            // First value that carries a unit wins.  Humanized cells
2041
            // ("1.5KB", "20ms") populate lvm_unit_suffix during
2042
            // annotate(); plain numeric cells leave it empty.
2043
            if (ms.ms_unit_suffix.empty()
57✔
2044
                && !lv.lv_meta.lvm_unit_suffix.empty())
57✔
2045
            {
UNCOV
2046
                ms.ms_unit_suffix = lv.lv_meta.lvm_unit_suffix.to_string();
×
UNCOV
2047
                if (lv.lv_meta.lvm_unit_divisor != 0.0) {
×
UNCOV
2048
                    ms.ms_unit_divisor = lv.lv_meta.lvm_unit_divisor;
×
2049
                }
2050
            }
2051
            if (ms.ms_unit_divisor != 0.0 && ms.ms_unit_divisor != 1.0) {
57✔
UNCOV
2052
                v /= ms.ms_unit_divisor;
×
2053
            }
2054
            ms.ms_samples.emplace_back(line_iter->get_time<>(), v);
57✔
2055
            // ms_min/ms_max are seeded from the file's value stats
2056
            // above, which already cover every sample we're about to
2057
            // emit — no need to re-track here.
2058
        }
57✔
2059
    }
26✔
2060
    // Samples may arrive out of order when the same metric name lives
2061
    // in multiple files with overlapping timestamps.  Downstream
2062
    // rendering assumes ascending time.
2063
    std::sort(ms.ms_samples.begin(),
10✔
2064
              ms.ms_samples.end(),
2065
              [](const auto& a, const auto& b) { return a.first < b.first; });
111✔
2066
}
10✔
2067

2068
void
2069
timeline_source::collect_metric_samples_via_sql(metric_state& ms)
4✔
2070
{
2071
    // The query returns (log_time TEXT, value REAL).  Timestamps
2072
    // get parsed via date_time_scanner below — this avoids the
2073
    // SQL-text/UTC-offset mismatch between how all_metrics stores
2074
    // log_time and what SQLite's datetime() produces.  Windowing
2075
    // happens at render time against ts_lower_bound/ts_upper_bound.
2076
    const auto full_sql
2077
        = fmt::format(FMT_STRING("SELECT log_time, value FROM ({}) "
8✔
2078
                                 "ORDER BY log_time"),
2079
                      ms.ms_def.md_query);
4✔
2080

2081
    // Route through sqlite3_error_to_user_message so raise_error()
2082
    // JSON payloads get unwrapped to the human-readable message
2083
    // instead of showing up as `lnav-error:{…}` in the banner.  The
2084
    // query validated at command time, so any error here means
2085
    // something about the underlying tables has since changed.
2086
    auto capture_err = [&ms] {
1✔
2087
        ms.ms_error = sqlite3_error_to_user_message(lnav_data.ld_db.in())
2✔
2088
                          .um_message.get_string();
1✔
2089
    };
1✔
2090

2091
    auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
4✔
2092
    auto rc = sqlite3_prepare_v2(lnav_data.ld_db.in(),
8✔
2093
                                 full_sql.c_str(),
2094
                                 full_sql.size(),
4✔
2095
                                 stmt.out(),
2096
                                 nullptr);
2097
    if (rc != SQLITE_OK) {
4✔
UNCOV
2098
        capture_err();
×
UNCOV
2099
        return;
×
2100
    }
2101

2102
    while (true) {
2103
        auto step_rc = sqlite3_step(stmt.in());
20✔
2104
        if (step_rc == SQLITE_DONE) {
20✔
2105
            break;
3✔
2106
        }
2107
        if (step_rc != SQLITE_ROW) {
17✔
2108
            capture_err();
1✔
2109
            return;
1✔
2110
        }
2111
        double unix_s = sqlite3_column_double(stmt.in(), 0);
16✔
2112
        std::chrono::microseconds ts_us{};
16✔
2113
        if (sqlite3_column_type(stmt.in(), 0) == SQLITE_TEXT) {
16✔
2114
            const auto* txt = reinterpret_cast<const char*>(
2115
                sqlite3_column_text(stmt.in(), 0));
16✔
2116
            exttm tm;
16✔
2117
            timeval tv;
2118
            if (txt != nullptr
16✔
2119
                && date_time_scanner().scan(txt, strlen(txt), nullptr, &tm, tv)
16✔
2120
                    != nullptr)
2121
            {
2122
                ts_us = to_us(tv);
16✔
2123
            }
2124
        } else {
UNCOV
2125
            ts_us = std::chrono::microseconds{
×
2126
                static_cast<int64_t>(unix_s * 1000000.0)};
2127
        }
2128
        const auto val_type = sqlite3_column_type(stmt.in(), 1);
16✔
2129
        if (val_type == SQLITE_NULL) {
16✔
UNCOV
2130
            continue;
×
2131
        }
2132
        double v;
2133
        if (val_type == SQLITE_TEXT) {
16✔
2134
            // The SQL expression may return humanized strings like
2135
            // "20ms" or "1.5KB"; hand them through try_from so the
2136
            // sparkline draws against the real (base-unit) value and
2137
            // ms_unit_suffix gets populated from the first recognized
2138
            // unit.  If the text isn't parseable, skip the row.
2139
            const auto txt_sf = string_fragment::from_bytes(
10✔
2140
                sqlite3_column_text(stmt.in(), 1),
2141
                sqlite3_column_bytes(stmt.in(), 1));
10✔
2142
            auto try_res = humanize::try_from<double>(txt_sf);
10✔
2143
            if (!try_res) {
10✔
UNCOV
2144
                continue;
×
2145
            }
2146
            v = try_res->value;
10✔
2147
            if (ms.ms_unit_suffix.empty() && !try_res->unit_suffix.empty()) {
10✔
2148
                ms.ms_unit_suffix = try_res->unit_suffix.to_string();
2✔
2149
            }
2150
        } else {
2151
            v = sqlite3_column_double(stmt.in(), 1);
6✔
2152
        }
2153
        ms.ms_samples.emplace_back(ts_us, v);
16✔
2154
        if (std::isnan(ms.ms_min) || v < ms.ms_min) {
16✔
2155
            ms.ms_min = v;
5✔
2156
        }
2157
        if (std::isnan(ms.ms_max) || v > ms.ms_max) {
16✔
2158
            ms.ms_max = v;
10✔
2159
        }
2160
    }
16✔
2161
}
5✔
2162

2163
void
2164
timeline_source::update_metric_status()
91✔
2165
{
2166
    auto& field = this->ts_preview_status_source.statusview_value_for_field(
91✔
2167
        timeline_status_source::TSF_METRICS);
2168
    if (this->ts_metrics.empty()) {
91✔
2169
        field.clear();
76✔
2170
        this->ts_preview_status_view.set_needs_update();
76✔
2171
        return;
76✔
2172
    }
2173

2174
    auto sel = this->tss_view ? this->tss_view->get_selection() : std::nullopt;
15✔
2175
    auto ov_sel = this->tss_view ? this->tss_view->get_overlay_selection()
15✔
UNCOV
2176
                                 : std::nullopt;
×
2177
    // The focused op (or hovered sub-op) spans a range, not a single
2178
    // moment, so report both the min and max the metric hit during
2179
    // that window instead of just the as-of-end value.  When nothing
2180
    // is selected we fall back to the first sample so at least one
2181
    // value shows up.
2182
    std::optional<
2183
        std::pair<std::chrono::microseconds, std::chrono::microseconds>>
2184
        focus_range;
15✔
2185
    if (sel && sel.value() < static_cast<ssize_t>(this->ts_time_order.size())) {
15✔
2186
        const auto& row = *this->ts_time_order[sel.value()];
15✔
2187
        focus_range
2188
            = {row.or_value.otr_range.tr_begin, row.or_value.otr_range.tr_end};
15✔
2189
        if (ov_sel && ov_sel.value() < row.or_value.otr_sub_ops.size()) {
15✔
UNCOV
2190
            const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
UNCOV
2191
            focus_range = {sub.ostr_range.tr_begin, sub.ostr_range.tr_end};
×
2192
        }
2193
    }
2194

2195
    attr_line_t al;
15✔
2196
    auto& vc = view_colors::singleton();
15✔
2197
    for (const auto& m : this->ts_metrics) {
35✔
2198
        if (m.ms_samples.empty()) {
20✔
2199
            continue;
4✔
2200
        }
2201
        std::optional<double> range_min;
18✔
2202
        std::optional<double> range_max;
18✔
2203
        if (!focus_range) {
18✔
2204
            // No selection — fall back to the first sample so the
2205
            // status bar has something to show when the view first
2206
            // opens.
UNCOV
2207
            range_min = range_max = m.ms_samples.front().second;
×
2208
        } else {
2209
            const auto [lb, ub] = *focus_range;
18✔
2210
            // Samples are sorted ascending by timestamp; binary-search
2211
            // for the first sample at-or-after lb and the first one
2212
            // strictly after ub, then min/max across the slice.
2213
            auto lo = std::lower_bound(m.ms_samples.begin(),
18✔
2214
                                       m.ms_samples.end(),
2215
                                       lb,
2216
                                       [](const auto& sample, const auto& ts) {
56✔
2217
                                           return sample.first < ts;
56✔
2218
                                       });
2219
            auto hi = std::upper_bound(lo,
18✔
2220
                                       m.ms_samples.end(),
2221
                                       ub,
2222
                                       [](const auto& ts, const auto& sample) {
56✔
2223
                                           return ts < sample.first;
56✔
2224
                                       });
2225
            for (auto it = lo; it != hi; ++it) {
34✔
2226
                if (!range_min || it->second < *range_min) {
16✔
2227
                    range_min = it->second;
16✔
2228
                }
2229
                if (!range_max || it->second > *range_max) {
16✔
2230
                    range_max = it->second;
16✔
2231
                }
2232
            }
2233
            if (!range_min && lo != m.ms_samples.begin()) {
18✔
2234
                // No samples fall inside the op window.  Show the
2235
                // last value observed before the window so the
2236
                // reader still gets a snapshot of the metric's
2237
                // state at op start (LOCF).
UNCOV
2238
                const auto held = std::prev(lo)->second;
×
UNCOV
2239
                range_min = range_max = held;
×
2240
            }
2241
        }
2242
        if (!range_min) {
18✔
2243
            continue;
2✔
2244
        }
2245
        if (!al.empty()) {
16✔
2246
            al.append(" ");
3✔
2247
        }
2248
        const auto suffix = string_fragment::from_str(m.ms_unit_suffix);
16✔
2249
        std::string chunk;
16✔
2250
        if (*range_min == *range_max) {
16✔
2251
            chunk = fmt::format(FMT_STRING(" {}={} "),
48✔
2252
                                m.ms_def.md_label,
16✔
2253
                                humanize::format(*range_min, suffix));
48✔
2254
        } else {
UNCOV
2255
            chunk = fmt::format(FMT_STRING(" {}={}..{} "),
×
UNCOV
2256
                                m.ms_def.md_label,
×
UNCOV
2257
                                humanize::format(*range_min, suffix),
×
UNCOV
2258
                                humanize::format(*range_max, suffix));
×
2259
        }
2260
        al.append(chunk, VC_STYLE.value(vc.attrs_for_ident(m.ms_def.md_label)));
16✔
2261
    }
16✔
2262
    field.set_value(al);
15✔
2263
    this->ts_preview_status_view.set_needs_update();
15✔
2264
}
15✔
2265

2266
void
2267
timeline_source::apply_pending_bookmarks()
115✔
2268
{
2269
    if (this->ts_pending_bookmarks.empty()) {
115✔
2270
        return;
113✔
2271
    }
2272

2273
    auto* tc = this->tss_view;
2✔
2274
    for (const auto& pb : this->ts_pending_bookmarks) {
4✔
2275
        auto row_name_sf = string_fragment::from_str(pb.pb_row_name);
2✔
2276
        for (size_t lpc = 0; lpc < this->ts_time_order.size(); lpc++) {
2✔
2277
            const auto& row = *this->ts_time_order[lpc];
2✔
2278
            if (row.or_name == row_name_sf && row.or_type == pb.pb_row_type) {
2✔
2279
                tc->set_user_mark(pb.pb_mark_type, vis_line_t(lpc), true);
2✔
2280
                break;
2✔
2281
            }
2282
        }
2283
    }
2284
    this->ts_pending_bookmarks.clear();
2✔
2285
}
2286

2287
int
2288
timeline_source::get_filtered_count() const
153✔
2289
{
2290
    return this->ts_filtered_count;
153✔
2291
}
2292

2293
int
UNCOV
2294
timeline_source::get_filtered_count_for(size_t filter_index) const
×
2295
{
UNCOV
2296
    return this->ts_filter_hits[filter_index];
×
2297
}
2298

2299
static const std::vector<breadcrumb::possibility>&
UNCOV
2300
timestamp_poss()
×
2301
{
2302
    const static std::vector<breadcrumb::possibility> retval = {
2303
        breadcrumb::possibility{"-1 day"},
2304
        breadcrumb::possibility{"-1h"},
2305
        breadcrumb::possibility{"-30m"},
2306
        breadcrumb::possibility{"-15m"},
2307
        breadcrumb::possibility{"-5m"},
2308
        breadcrumb::possibility{"-1m"},
2309
        breadcrumb::possibility{"+1m"},
2310
        breadcrumb::possibility{"+5m"},
2311
        breadcrumb::possibility{"+15m"},
2312
        breadcrumb::possibility{"+30m"},
2313
        breadcrumb::possibility{"+1h"},
2314
        breadcrumb::possibility{"+1 day"},
2315
    };
2316

UNCOV
2317
    return retval;
×
2318
}
2319

2320
void
UNCOV
2321
timeline_source::text_crumbs_for_line(int line,
×
2322
                                      std::vector<breadcrumb::crumb>& crumbs)
2323
{
UNCOV
2324
    text_sub_source::text_crumbs_for_line(line, crumbs);
×
2325

UNCOV
2326
    if (line >= this->ts_time_order.size()) {
×
UNCOV
2327
        return;
×
2328
    }
2329

UNCOV
2330
    const auto& row = *this->ts_time_order[line];
×
2331
    char ts[64];
2332

UNCOV
2333
    sql_strftime(ts, sizeof(ts), row.or_value.otr_range.tr_begin, 'T');
×
2334

UNCOV
2335
    crumbs.emplace_back(std::string(ts),
×
2336
                        timestamp_poss,
UNCOV
2337
                        [ec = this->ts_exec_context](const auto& ts) {
×
UNCOV
2338
                            auto cmd
×
UNCOV
2339
                                = fmt::format(FMT_STRING(":goto {}"),
×
2340
                                              ts.template get<std::string>());
UNCOV
2341
                            ec->execute(INTERNAL_SRC_LOC, cmd);
×
UNCOV
2342
                        });
×
UNCOV
2343
    crumbs.back().c_expected_input
×
UNCOV
2344
        = breadcrumb::crumb::expected_input_t::anything;
×
UNCOV
2345
    crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
×
2346
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc