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

tstack / lnav / 23066365063-2835

13 Mar 2026 06:12PM UTC coverage: 68.989% (+0.01%) from 68.979%
23066365063-2835

push

github

tstack
[cmds] add sticky headers

Related to #1385

114 of 146 new or added lines in 6 files covered. (78.08%)

13 existing lines in 4 files now uncovered.

52488 of 76082 relevant lines covered (68.99%)

520548.33 hits per line

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

62.79
/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 <utility>
33
#include <vector>
34

35
#include "timeline_source.hh"
36

37
#include <time.h>
38

39
#include "base/humanize.hh"
40
#include "base/humanize.time.hh"
41
#include "base/itertools.enumerate.hh"
42
#include "base/itertools.hh"
43
#include "base/keycodes.hh"
44
#include "base/math_util.hh"
45
#include "command_executor.hh"
46
#include "crashd.client.hh"
47
#include "lnav_util.hh"
48
#include "logline_window.hh"
49
#include "md4cpp.hh"
50
#include "pcrepp/pcre2pp.hh"
51
#include "readline_highlighters.hh"
52
#include "sql_util.hh"
53
#include "sysclip.hh"
54
#include "tlx/container/btree_map.hpp"
55

56
using namespace std::chrono_literals;
57
using namespace lnav::roles::literals;
58
using namespace md4cpp::literals;
59

60
static const std::vector<std::chrono::microseconds> TIME_SPANS = {
61
    500us, 1ms,   100ms, 500ms, 1s, 5s, 10s, 15s,     30s,      1min,
62
    5min,  15min, 1h,    2h,    4h, 8h, 24h, 7 * 24h, 30 * 24h, 365 * 24h,
63
};
64

65
static constexpr size_t MAX_OPID_WIDTH = 80;
66
static constexpr size_t MAX_DESC_WIDTH = 256;
67
static constexpr int CHART_INDENT = 24;
68

69
size_t
70
abbrev_ftime(char* datebuf, size_t db_size, const tm& lb_tm, const tm& dt)
×
71
{
72
    char lb_fmt[32] = " ";
×
73
    bool same = true;
×
74

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

120
std::vector<attr_line_t>
121
timeline_preview_overlay::list_overlay_menu(const listview_curses& lv,
×
122
                                            vis_line_t line)
123
{
124
    static constexpr auto MENU_WIDTH = 25;
125

126
    const auto* tc = dynamic_cast<const textview_curses*>(&lv);
×
127
    std::vector<attr_line_t> retval;
×
128

129
    if (tc->tc_text_selection_active || !tc->tc_selected_text) {
×
130
        return retval;
×
131
    }
132

133
    const auto& sti = tc->tc_selected_text.value();
×
134

135
    if (sti.sti_line != line) {
×
136
        return retval;
×
137
    }
138
    auto title = " Actions "_status_title;
×
139
    auto left = std::max(0, sti.sti_x - 2);
×
140
    auto dim = lv.get_dimensions();
×
141
    auto menu_line = vis_line_t{1};
×
142

143
    if (left + MENU_WIDTH >= dim.second) {
×
144
        left = dim.second - MENU_WIDTH;
×
145
    }
146

147
    this->los_menu_items.clear();
×
148

149
    retval.emplace_back(attr_line_t().pad_to(left).append(title));
×
150
    {
151
        auto start = left;
×
152
        attr_line_t al;
×
153

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

168
                auto clip_pipe = clip_res.unwrap();
×
169
                fwrite(value.c_str(), 1, value.length(), clip_pipe.in());
×
170
            });
×
171
        retval.emplace_back(attr_line_t().pad_to(left).append(al));
×
172
    }
173

174
    return retval;
×
175
}
×
176

177
timeline_header_overlay::timeline_header_overlay(
669✔
178
    const std::shared_ptr<timeline_source>& src)
669✔
179
    : gho_src(src)
669✔
180
{
181
}
669✔
182

183
bool
184
timeline_header_overlay::list_static_overlay(const listview_curses& lv,
42✔
185
                                             media_t media,
186
                                             int y,
187
                                             int bottom,
188
                                             attr_line_t& value_out)
189
{
190
    if (this->gho_src->ts_rebuild_in_progress) {
42✔
191
        return false;
×
192
    }
193

194
    if (this->gho_src->ts_time_order.empty()) {
42✔
195
        if (y == 0) {
×
196
            this->gho_static_lines.clear();
×
197

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

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

252
                this->gho_static_lines = um.to_attr_line().split_lines();
×
253
            }
254
        }
255

256
        if (y < this->gho_static_lines.size()) {
×
257
            value_out = this->gho_static_lines[y];
×
258
            return true;
×
259
        }
260

261
        return false;
×
262
    }
263

264
    if (y > 0) {
42✔
265
        return false;
17✔
266
    }
267

268
    auto sel = lv.get_selection().value_or(0_vl);
25✔
269
    if (sel < this->gho_src->tss_view->get_top()) {
25✔
270
        return true;
×
271
    }
272
    const auto& row = *this->gho_src->ts_time_order[sel];
25✔
273
    auto tr = row.or_value.otr_range;
25✔
274
    auto [lb, ub] = this->gho_src->get_time_bounds_for(sel);
25✔
275
    auto sel_begin_us = tr.tr_begin - lb;
25✔
276
    auto sel_end_us = tr.tr_end - lb;
25✔
277

278
    require(sel_begin_us >= 0us);
25✔
279
    require(sel_end_us >= 0us);
25✔
280

281
    auto [height, width] = lv.get_dimensions();
25✔
282
    if (width <= CHART_INDENT) {
25✔
283
        return true;
×
284
    }
285

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

344
    auto hdr_attrs = text_attrs::with_underline();
25✔
345
    value_out.with_attr_for_all(VC_STYLE.value(hdr_attrs))
25✔
346
        .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO));
25✔
347

348
    return true;
25✔
349
}
25✔
350
void
351
timeline_header_overlay::list_value_for_overlay(
444✔
352
    const listview_curses& lv,
353
    vis_line_t line,
354
    std::vector<attr_line_t>& value_out)
355
{
356
    if (!this->gho_show_details) {
444✔
357
        return;
444✔
358
    }
359

360
    if (lv.get_selection() != line) {
×
361
        return;
×
362
    }
363

364
    if (line >= this->gho_src->ts_time_order.size()) {
×
365
        return;
×
366
    }
367

368
    const auto& row = *this->gho_src->ts_time_order[line];
×
369

370
    if (row.or_value.otr_sub_ops.size() <= 1) {
×
371
        return;
×
372
    }
373

374
    auto width = lv.get_dimensions().second;
×
375

376
    if (width < 37) {
×
377
        return;
×
378
    }
379

380
    width -= 37;
×
381
    double span = row.or_value.otr_range.duration().count();
×
382
    double per_ch = span / (double) width;
×
383

384
    for (const auto& sub : row.or_value.otr_sub_ops) {
×
385
        value_out.resize(value_out.size() + 1);
×
386

387
        auto& al = value_out.back();
×
388
        auto& attrs = al.get_attrs();
×
389
        auto total_msgs = sub.ostr_level_stats.lls_total_count;
×
390
        auto duration = sub.ostr_range.tr_end - sub.ostr_range.tr_begin;
×
391
        auto duration_str = fmt::format(
392
            FMT_STRING(" {: >13}"),
×
393
            humanize::time::duration::from_tv(to_timeval(duration))
×
394
                .to_string());
×
395
        al.pad_to(14)
×
396
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
×
397
            .append(" ")
×
398
            .append(lnav::roles::error(humanize::sparkline(
×
399
                sub.ostr_level_stats.lls_error_count, total_msgs)))
×
400
            .append(lnav::roles::warning(humanize::sparkline(
×
401
                sub.ostr_level_stats.lls_warning_count, total_msgs)))
×
402
            .append(" ")
×
403
            .append(lnav::roles::identifier(sub.ostr_subid.to_string()))
×
404
            .append(row.or_max_subid_width
×
405
                        - sub.ostr_subid.utf8_length().unwrapOr(
×
406
                            row.or_max_subid_width),
×
407
                    ' ')
408
            .append(sub.ostr_description);
×
409
        al.with_attr_for_all(VC_ROLE.value(role_t::VCR_COMMENT));
×
410

411
        auto start_diff = (double) (sub.ostr_range.tr_begin
×
412
                                    - row.or_value.otr_range.tr_begin)
×
413
                              .count();
×
414
        auto end_diff
415
            = (double) (sub.ostr_range.tr_end - row.or_value.otr_range.tr_begin)
×
416
                  .count();
×
417

418
        auto lr = line_range{
419
            (int) (32 + (start_diff / per_ch)),
×
420
            (int) (32 + (end_diff / per_ch)),
×
421
            line_range::unit::codepoint,
422
        };
423

424
        if (lr.lr_start == lr.lr_end) {
×
425
            lr.lr_end += 1;
×
426
        }
427

428
        auto block_attrs = text_attrs::with_reverse();
×
429
        attrs.emplace_back(lr, VC_STYLE.value(block_attrs));
×
430
    }
431
    if (!value_out.empty()) {
×
432
        value_out.back().get_attrs().emplace_back(
×
433
            line_range{0, -1}, VC_STYLE.value(text_attrs::with_underline()));
×
434
    }
435
}
436

437
std::optional<attr_line_t>
438
timeline_header_overlay::list_header_for_overlay(const listview_curses& lv,
×
439
                                                 media_t media,
440
                                                 vis_line_t line)
441
{
442
    if (lv.get_overlay_selection()) {
×
443
        return attr_line_t("\u258C Sub-operations: Press ")
×
444
            .append("Esc"_hotkey)
×
445
            .append(" to exit this panel");
×
446
    }
447
    return attr_line_t("\u258C Sub-operations: Press ")
×
448
        .append("CTRL-]"_hotkey)
×
449
        .append(" to focus on this panel");
×
450
}
451

452
timeline_source::timeline_source(textview_curses& log_view,
669✔
453
                                 logfile_sub_source& lss,
454
                                 textview_curses& preview_view,
455
                                 plain_text_source& preview_source,
456
                                 statusview_curses& preview_status_view,
457
                                 timeline_status_source& preview_status_source)
669✔
458
    : ts_log_view(log_view), ts_lss(lss), ts_preview_view(preview_view),
669✔
459
      ts_preview_source(preview_source),
669✔
460
      ts_preview_status_view(preview_status_view),
669✔
461
      ts_preview_status_source(preview_status_source)
669✔
462
{
463
    this->tss_supports_filtering = true;
669✔
464
    this->ts_preview_view.set_overlay_source(&this->ts_preview_overlay);
669✔
465
}
669✔
466

467
std::optional<timeline_source::row_type>
468
timeline_source::row_type_from_string(const std::string& str)
11✔
469
{
470
    if (str == "logfile") {
11✔
471
        return row_type::logfile;
2✔
472
    }
473
    if (str == "thread") {
9✔
474
        return row_type::thread;
×
475
    }
476
    if (str == "opid") {
9✔
477
        return row_type::opid;
8✔
478
    }
479
    if (str == "tag") {
1✔
480
        return row_type::tag;
×
481
    }
482
    if (str == "partition") {
1✔
483
        return row_type::partition;
×
484
    }
485
    return std::nullopt;
1✔
486
}
487

488
const char*
489
timeline_source::row_type_to_string(row_type rt)
×
490
{
491
    switch (rt) {
×
492
        case row_type::logfile:
×
493
            return "logfile";
×
494
        case row_type::thread:
×
495
            return "thread";
×
496
        case row_type::opid:
×
497
            return "opid";
×
498
        case row_type::tag:
×
499
            return "tag";
×
500
        case row_type::partition:
×
501
            return "partition";
×
502
    }
503
    return "unknown";
×
504
}
505

506
void
507
timeline_source::set_row_type_visibility(row_type rt, bool visible)
5✔
508
{
509
    if (visible) {
5✔
510
        this->ts_hidden_row_types.erase(rt);
1✔
511
    } else {
512
        this->ts_hidden_row_types.insert(rt);
4✔
513
    }
514
}
5✔
515

516
bool
517
timeline_source::is_row_type_visible(row_type rt) const
1,140✔
518
{
519
    return this->ts_hidden_row_types.find(rt)
1,140✔
520
        == this->ts_hidden_row_types.end();
2,280✔
521
}
522

523
bool
524
timeline_source::list_input_handle_key(listview_curses& lv, const ncinput& ch)
×
525
{
526
    switch (ch.eff_text[0]) {
×
527
        case 'q':
×
528
        case KEY_ESCAPE: {
529
            if (this->ts_preview_focused) {
×
530
                this->ts_preview_focused = false;
×
531
                this->ts_preview_view.set_height(5_vl);
×
532
                this->ts_preview_status_view.set_enabled(
×
533
                    this->ts_preview_focused);
×
534
                this->tss_view->set_enabled(!this->ts_preview_focused);
×
535
                return true;
×
536
            }
537
            break;
×
538
        }
539
        case '\n':
×
540
        case '\r':
541
        case NCKEY_ENTER: {
542
            this->ts_preview_focused = !this->ts_preview_focused;
×
543
            this->ts_preview_status_view.set_enabled(this->ts_preview_focused);
×
544
            this->tss_view->set_enabled(!this->ts_preview_focused);
×
545
            if (this->ts_preview_focused) {
×
546
                auto height = this->tss_view->get_dimensions().first;
×
547

548
                if (height > 5) {
×
549
                    this->ts_preview_view.set_height(height / 2_vl);
×
550
                }
551
            } else {
552
                this->ts_preview_view.set_height(5_vl);
×
553
            }
554
            return true;
×
555
        }
556
    }
557
    if (this->ts_preview_focused) {
×
558
        return this->ts_preview_view.handle_key(ch);
×
559
    }
560

561
    return false;
×
562
}
563

564
bool
565
timeline_source::text_handle_mouse(
×
566
    textview_curses& tc,
567
    const listview_curses::display_line_content_t&,
568
    mouse_event& me)
569
{
570
    auto nci = ncinput{};
×
571
    if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{0, -1})) {
×
572
        nci.id = '\r';
×
573
        nci.eff_text[0] = '\r';
×
574
        this->list_input_handle_key(tc, nci);
×
575
    }
576

577
    return false;
×
578
}
579

580
std::pair<std::chrono::microseconds, std::chrono::microseconds>
581
timeline_source::get_time_bounds_for(int line)
461✔
582
{
583
    const auto low_index = this->tss_view->get_top();
461✔
584
    auto high_index
585
        = std::min(this->tss_view->get_bottom(),
461✔
586
                   vis_line_t((int) this->ts_time_order.size() - 1));
461✔
587
    if (high_index == low_index) {
461✔
588
        high_index = vis_line_t(this->ts_time_order.size() - 1);
461✔
589
    }
590
    const auto& low_row = *this->ts_time_order[low_index];
461✔
591
    const auto& high_row = *this->ts_time_order[high_index];
461✔
592
    auto low_us = low_row.or_value.otr_range.tr_begin;
461✔
593
    auto high_us = high_row.or_value.otr_range.tr_begin;
461✔
594

595
    auto duration = high_us - low_us;
461✔
596
    auto span_iter
597
        = std::upper_bound(TIME_SPANS.begin(), TIME_SPANS.end(), duration);
461✔
598
    if (span_iter == TIME_SPANS.end()) {
461✔
599
        --span_iter;
7✔
600
    }
601
    auto span_portion = *span_iter / 8;
461✔
602
    auto lb = low_us;
461✔
603
    lb = rounddown(lb, span_portion);
461✔
604
    auto ub = high_us;
461✔
605
    ub = roundup(ub, span_portion);
461✔
606

607
    ensure(lb <= ub);
461✔
608
    return {lb, ub};
461✔
609
}
610

611
size_t
612
timeline_source::text_line_count()
9,480✔
613
{
614
    return this->ts_time_order.size();
9,480✔
615
}
616

617
line_info
618
timeline_source::text_value_for_line(textview_curses& tc,
436✔
619
                                     int line,
620
                                     std::string& value_out,
621
                                     line_flags_t flags)
622
{
623
    if (!this->ts_rebuild_in_progress
872✔
624
        && line < (ssize_t) this->ts_time_order.size())
436✔
625
    {
626
        const auto& row = *this->ts_time_order[line];
436✔
627
        auto duration
628
            = row.or_value.otr_range.tr_end - row.or_value.otr_range.tr_begin;
436✔
629
        auto duration_str = fmt::format(
630
            FMT_STRING("{: >13}"),
1,308✔
631
            humanize::time::duration::from_tv(to_timeval(duration))
436✔
632
                .to_string());
872✔
633

634
        this->ts_rendered_line.clear();
436✔
635

636
        auto total_msgs = row.or_value.otr_level_stats.lls_total_count;
436✔
637
        auto truncated_name
638
            = attr_line_t::from_table_cell_content(row.or_name, MAX_OPID_WIDTH);
436✔
639
        auto truncated_desc = attr_line_t::from_table_cell_content(
640
            row.or_description, MAX_DESC_WIDTH);
436✔
641
        std::optional<ui_icon_t> icon;
436✔
642
        auto padding = 1;
436✔
643
        switch (row.or_type) {
436✔
644
            case row_type::logfile:
16✔
645
                icon = ui_icon_t::file;
16✔
646
                break;
16✔
647
            case row_type::thread:
51✔
648
                icon = ui_icon_t::thread;
51✔
649
                break;
51✔
650
            case row_type::opid:
367✔
651
                padding = 3;
367✔
652
                break;
367✔
653
            case row_type::tag:
×
654
                icon = ui_icon_t::tag;
×
655
                break;
×
656
            case row_type::partition:
2✔
657
                icon = ui_icon_t::partition;
2✔
658
                break;
2✔
659
        }
660
        if (this->ts_preview_hidden_row_types.count(row.or_type) > 0) {
436✔
661
            this->ts_rendered_line.append(
×
662
                "-",
663
                VC_STYLE.value(text_attrs{
×
664
                    lnav::enums::to_underlying(text_attrs::style::blink),
665
                    styling::color_unit::from_palette(
×
666
                        lnav::enums::to_underlying(ansi_color::red)),
×
667
                }));
668
        } else {
669
            this->ts_rendered_line.append(" ");
436✔
670
        }
671

672
        this->ts_rendered_line
673
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
872✔
674
            .append("  ")
436✔
675
            .append(lnav::roles::error(humanize::sparkline(
1,308✔
676
                row.or_value.otr_level_stats.lls_error_count, total_msgs)))
436✔
677
            .append(lnav::roles::warning(humanize::sparkline(
1,308✔
678
                row.or_value.otr_level_stats.lls_warning_count, total_msgs)))
436✔
679
            .append("  ")
436✔
680
            .append(icon)
436✔
681
            .append(padding, ' ')
436✔
682
            .append(lnav::roles::identifier(truncated_name))
872✔
683
            .append(
872✔
684
                this->ts_opid_width - truncated_name.utf8_length_or_length(),
436✔
685
                ' ')
686
            .append(truncated_desc);
436✔
687
        this->ts_rendered_line.with_attr_for_all(
436✔
688
            VC_ROLE.value(role_t::VCR_COMMENT));
872✔
689

690
        value_out = this->ts_rendered_line.get_string();
436✔
691
    }
436✔
692

693
    return {};
436✔
694
}
695

696
void
697
timeline_source::text_attrs_for_line(textview_curses& tc,
436✔
698
                                     int line,
699
                                     string_attrs_t& value_out)
700
{
701
    if (!this->ts_rebuild_in_progress
872✔
702
        && line < (ssize_t) this->ts_time_order.size())
436✔
703
    {
704
        const auto& row = *this->ts_time_order[line];
436✔
705

706
        value_out = this->ts_rendered_line.get_attrs();
436✔
707

708
        auto lr = line_range{-1, -1, line_range::unit::codepoint};
436✔
709
        auto [sel_lb, sel_ub]
436✔
710
            = this->get_time_bounds_for(tc.get_selection().value_or(0_vl));
436✔
711

712
        if (row.or_value.otr_range.tr_begin <= sel_ub
436✔
713
            && sel_lb <= row.or_value.otr_range.tr_end)
436✔
714
        {
715
            auto width = tc.get_dimensions().second;
436✔
716

717
            if (width > CHART_INDENT) {
436✔
718
                width -= CHART_INDENT;
436✔
719
                const double span = (sel_ub - sel_lb).count();
436✔
720
                auto us_per_ch = std::chrono::microseconds{
721
                    static_cast<int64_t>(ceil(span / (double) width))};
436✔
722

723
                if (row.or_value.otr_range.tr_begin <= sel_lb) {
436✔
724
                    lr.lr_start = CHART_INDENT;
×
725
                } else {
726
                    auto start_diff
727
                        = (row.or_value.otr_range.tr_begin - sel_lb);
436✔
728

729
                    lr.lr_start = CHART_INDENT + floor(start_diff / us_per_ch);
436✔
730
                }
731

732
                if (sel_ub < row.or_value.otr_range.tr_end) {
436✔
733
                    lr.lr_end = -1;
13✔
734
                } else {
735
                    auto end_diff = (row.or_value.otr_range.tr_end - sel_lb);
423✔
736

737
                    lr.lr_end = CHART_INDENT + ceil(end_diff / us_per_ch);
423✔
738
                    if (lr.lr_start == lr.lr_end) {
423✔
739
                        lr.lr_end += 1;
325✔
740
                    }
741
                }
742

743
                auto block_attrs = text_attrs::with_reverse();
436✔
744
                require(lr.lr_start >= 0);
436✔
745
                value_out.emplace_back(lr, VC_STYLE.value(block_attrs));
436✔
746
            }
747
        }
748
        auto alt_row_index = line % 4;
436✔
749
        if (alt_row_index == 2 || alt_row_index == 3) {
436✔
750
            value_out.emplace_back(line_range{0, -1},
211✔
751
                                   VC_ROLE.value(role_t::VCR_ALT_ROW));
422✔
752
        }
753
    }
754
}
436✔
755

756
size_t
757
timeline_source::text_size_for_line(textview_curses& tc,
×
758
                                    int line,
759
                                    text_sub_source::line_flags_t raw)
760
{
761
    return this->ts_total_width;
×
762
}
763

764
bool
765
timeline_source::rebuild_indexes()
37✔
766
{
767
    static auto op = lnav_operation{"timeline_rebuild"};
37✔
768

769
    auto op_guard = lnav_opid_guard::internal(op);
37✔
770
    auto& bm = this->tss_view->get_bookmarks();
37✔
771
    auto& bm_files = bm[&logfile_sub_source::BM_FILES];
37✔
772
    auto& bm_errs = bm[&textview_curses::BM_ERRORS];
37✔
773
    auto& bm_warns = bm[&textview_curses::BM_WARNINGS];
37✔
774
    auto& bm_meta = bm[&textview_curses::BM_META];
37✔
775
    auto& bm_parts = bm[&textview_curses::BM_PARTITION];
37✔
776

777
    this->ts_rebuild_in_progress = true;
37✔
778
    bm_errs.clear();
37✔
779
    bm_warns.clear();
37✔
780
    bm_meta.clear();
37✔
781
    bm_parts.clear();
37✔
782

783
    this->ts_lower_bound = {};
37✔
784
    this->ts_upper_bound = {};
37✔
785
    this->ts_opid_width = 0;
37✔
786
    this->ts_total_width = 0;
37✔
787
    this->ts_filtered_count = 0;
37✔
788
    this->ts_active_opids.clear();
37✔
789
    this->ts_descriptions.clear();
37✔
790
    this->ts_subid_map.clear();
37✔
791
    this->ts_allocator.reset();
37✔
792
    this->ts_preview_source.clear();
37✔
793
    this->ts_preview_rows.clear();
37✔
794
    this->ts_preview_status_source.get_description().clear();
37✔
795

796
    auto min_log_time_tv_opt = this->get_min_row_time();
37✔
797
    auto max_log_time_tv_opt = this->get_max_row_time();
37✔
798
    std::optional<std::chrono::microseconds> min_log_time_opt;
37✔
799
    std::optional<std::chrono::microseconds> max_log_time_opt;
37✔
800
    auto max_desc_width = size_t{0};
37✔
801

802
    if (min_log_time_tv_opt) {
37✔
803
        min_log_time_opt = to_us(min_log_time_tv_opt.value());
1✔
804
    }
805
    if (max_log_time_tv_opt) {
37✔
806
        max_log_time_opt = to_us(max_log_time_tv_opt.value());
1✔
807
    }
808

809
    log_info("building opid table");
37✔
810
    auto last_log_time = std::chrono::microseconds{};
37✔
811
    tlx::btree_map<std::chrono::microseconds, std::string> part_map;
37✔
812
    for (const auto& [index, ld] : lnav::itertools::enumerate(this->ts_lss)) {
76✔
813
        if (ld->get_file_ptr() == nullptr) {
39✔
814
            continue;
1✔
815
        }
816
        if (!ld->is_visible()) {
39✔
817
            continue;
1✔
818
        }
819

820
        auto* lf = ld->get_file_ptr();
38✔
821
        lf->enable_cache();
38✔
822

823
        const auto& mark_meta = lf->get_bookmark_metadata();
38✔
824
        {
825
            for (const auto& [line_num, line_meta] : mark_meta) {
68✔
826
                const auto ll = std::next(lf->begin(), line_num);
30✔
827
                if (!line_meta.bm_name.empty()) {
30✔
828
                    part_map.insert2(ll->get_time<std::chrono::microseconds>(),
2✔
829
                                     line_meta.bm_name);
2✔
830
                }
831
                for (const auto& tag : line_meta.bm_tags) {
30✔
832
                    auto line_time = ll->get_time<std::chrono::microseconds>();
×
833
                    auto tag_key = fmt::format(FMT_STRING("{}@{}:{}"),
×
834
                                               tag,
835
                                               lf->get_unique_path(),
836
                                               line_time.count());
×
837
                    auto tag_key_sf
838
                        = string_fragment::from_str(tag_key).to_owned(
×
839
                            this->ts_allocator);
×
840
                    auto tag_name_sf = string_fragment::from_str(tag).to_owned(
×
841
                        this->ts_allocator);
×
842
                    auto tag_otr = opid_time_range{};
×
843
                    tag_otr.otr_range.tr_begin = line_time;
×
844
                    tag_otr.otr_range.tr_end = line_time;
×
845
                    tag_otr.otr_level_stats.update_msg_count(
×
846
                        ll->get_msg_level());
847
                    this->ts_active_opids.emplace(
×
848
                        tag_key_sf,
849
                        opid_row{
×
850
                            row_type::tag,
851
                            tag_name_sf,
852
                            tag_otr,
853
                            string_fragment::invalid(),
854
                        });
855
                }
856
            }
857
        }
858

859
        auto path = string_fragment::from_str(lf->get_unique_path())
38✔
860
                        .to_owned(this->ts_allocator);
38✔
861
        auto lf_otr = opid_time_range{};
38✔
862
        lf_otr.otr_range = lf->get_content_time_range();
38✔
863
        lf_otr.otr_level_stats = lf->get_level_stats();
38✔
864
        if (lf_otr.otr_range.tr_end > last_log_time) {
38✔
865
            last_log_time = lf_otr.otr_range.tr_end;
38✔
866
        }
867
        auto lf_row = opid_row{
38✔
868
            row_type::logfile,
869
            path,
870
            lf_otr,
871
            string_fragment::invalid(),
872
        };
38✔
873
        lf_row.or_logfile = lf;
38✔
874
        this->ts_active_opids.emplace(path, lf_row);
38✔
875

876
        {
877
            auto r_tid_map = lf->get_thread_ids().readAccess();
38✔
878

879
            for (const auto& [tid_sf, tid_meta] : r_tid_map->ltis_tid_ranges) {
154✔
880
                auto active_iter = this->ts_active_opids.find(tid_sf);
116✔
881
                if (active_iter == this->ts_active_opids.end()) {
116✔
882
                    auto tid = tid_sf.to_owned(this->ts_allocator);
107✔
883
                    auto tid_otr = opid_time_range{};
107✔
884
                    tid_otr.otr_range = tid_meta.titr_range;
107✔
885
                    tid_otr.otr_level_stats = tid_meta.titr_level_stats;
107✔
886
                    this->ts_active_opids.emplace(
×
887
                        tid,
888
                        opid_row{
107✔
889
                            row_type::thread,
890
                            tid,
891
                            tid_otr,
892
                            string_fragment::invalid(),
893
                        });
894
                } else {
107✔
895
                    active_iter->second.or_value.otr_range
18✔
896
                        |= tid_meta.titr_range;
9✔
897
                }
898
            }
899
        }
38✔
900

901
        auto format = lf->get_format();
38✔
902
        safe::ReadAccess<logfile::safe_opid_state> r_opid_map(
903
            ld->get_file_ptr()->get_opids());
38✔
904
        for (const auto& pair : r_opid_map->los_opid_ranges) {
1,031✔
905
            const opid_time_range& otr = pair.second;
993✔
906
            auto active_iter = this->ts_active_opids.find(pair.first);
993✔
907
            if (active_iter == this->ts_active_opids.end()) {
993✔
908
                auto opid = pair.first.to_owned(this->ts_allocator);
993✔
909
                auto active_emp_res = this->ts_active_opids.emplace(
1,986✔
910
                    opid,
911
                    opid_row{
993✔
912
                        row_type::opid,
913
                        opid,
914
                        otr,
915
                        string_fragment::invalid(),
916
                    });
917
                active_iter = active_emp_res.first;
993✔
918
            } else {
919
                active_iter->second.or_value |= otr;
×
920
            }
921

922
            opid_row& row = active_iter->second;
993✔
923
            for (auto& sub : row.or_value.otr_sub_ops) {
993✔
924
                auto subid_iter = this->ts_subid_map.find(sub.ostr_subid);
×
925

926
                if (subid_iter == this->ts_subid_map.end()) {
×
927
                    subid_iter = this->ts_subid_map
×
928
                                     .emplace(sub.ostr_subid.to_owned(
×
929
                                                  this->ts_allocator),
×
930
                                              true)
×
931
                                     .first;
932
                }
933
                sub.ostr_subid = subid_iter->first;
×
934
                if (sub.ostr_subid.length() > row.or_max_subid_width) {
×
935
                    row.or_max_subid_width = sub.ostr_subid.length();
×
936
                }
937
            }
938

939
            if (otr.otr_description.lod_index) {
993✔
940
                auto desc_id = otr.otr_description.lod_index.value();
981✔
941
                auto desc_def_iter
942
                    = format->lf_opid_description_def_vec->at(desc_id);
981✔
943

944
                auto desc_key
945
                    = opid_description_def_key{format->get_name(), desc_id};
981✔
946
                auto desc_defs_opt
947
                    = row.or_description_defs.odd_defs.value_for(desc_key);
981✔
948
                if (!desc_defs_opt) {
981✔
949
                    row.or_description_defs.odd_defs.insert(desc_key,
981✔
950
                                                            *desc_def_iter);
951
                }
952

953
                auto& all_descs = row.or_descriptions;
981✔
954
                const auto& new_desc_v = otr.otr_description.lod_elements;
981✔
955
                all_descs.insert(desc_key, new_desc_v);
981✔
956
            } else if (!otr.otr_description.lod_elements.empty()) {
12✔
957
                auto desc_sf = string_fragment::from_str(
4✔
958
                    otr.otr_description.lod_elements.values().front());
4✔
959
                row.or_description = desc_sf.to_owned(this->ts_allocator);
4✔
960
            }
961
            row.or_value.otr_description.lod_elements.clear();
993✔
962
        }
963

964
        if (this->ts_index_progress) {
38✔
965
            switch (this->ts_index_progress(
×
966
                progress_t{index, this->ts_lss.file_count()}))
×
967
            {
968
                case lnav::progress_result_t::ok:
×
969
                    break;
×
970
                case lnav::progress_result_t::interrupt:
×
971
                    log_debug("timeline rebuild interrupted");
×
972
                    this->ts_rebuild_in_progress = false;
×
973
                    return false;
×
974
            }
975
        }
976
    }
38✔
977
    if (this->ts_index_progress) {
37✔
978
        this->ts_index_progress(std::nullopt);
×
979
    }
980

981
    {
982
        static const auto START_RE = lnav::pcre2pp::code::from_const(
983
            R"(^(?:start(?:ed)?|begin)|\b(?:start(?:ed)?|begin)$)",
984
            PCRE2_CASELESS);
37✔
985

986
        std::vector<opid_row*> start_tags;
37✔
987
        for (auto& pair : this->ts_active_opids) {
1,175✔
988
            if (pair.second.or_type != row_type::tag) {
1,138✔
989
                continue;
1,138✔
990
            }
991
            if (START_RE.find_in(pair.second.or_name).ignore_error()) {
×
992
                start_tags.emplace_back(&pair.second);
×
993
            }
994
        }
995
        std::stable_sort(start_tags.begin(),
37✔
996
                         start_tags.end(),
997
                         [](const auto* lhs, const auto* rhs) {
×
998
                             if (lhs->or_name == rhs->or_name) {
×
999
                                 return lhs->or_value.otr_range.tr_begin
×
1000
                                     < rhs->or_value.otr_range.tr_begin;
×
1001
                             }
1002
                             return lhs->or_name < rhs->or_name;
×
1003
                         });
1004
        for (size_t i = 0; i < start_tags.size(); i++) {
37✔
1005
            if (i + 1 < start_tags.size()
×
1006
                && start_tags[i]->or_name == start_tags[i + 1]->or_name)
×
1007
            {
1008
                start_tags[i]->or_value.otr_range.tr_end
×
1009
                    = start_tags[i + 1]->or_value.otr_range.tr_begin - 1us;
×
1010
            } else {
1011
                start_tags[i]->or_value.otr_range.tr_end = last_log_time;
×
1012
            }
1013
        }
1014
    }
37✔
1015

1016
    for (auto part_iter = part_map.begin(); part_iter != part_map.end();
39✔
1017
         ++part_iter)
2✔
1018
    {
1019
        auto next_iter = std::next(part_iter);
2✔
1020
        auto part_name_sf = string_fragment::from_str(part_iter->second)
2✔
1021
                                .to_owned(this->ts_allocator);
2✔
1022
        auto part_otr = opid_time_range{};
2✔
1023
        part_otr.otr_range.tr_begin = part_iter->first;
2✔
1024
        if (next_iter != part_map.end()) {
2✔
1025
            part_otr.otr_range.tr_end = next_iter->first;
1✔
1026
        } else {
1027
            part_otr.otr_range.tr_end = last_log_time;
1✔
1028
        }
1029
        this->ts_active_opids.emplace(part_name_sf,
×
1030
                                      opid_row{
2✔
1031
                                          row_type::partition,
1032
                                          part_name_sf,
1033
                                          part_otr,
1034
                                          string_fragment::invalid(),
1035
                                      });
1036
    }
2✔
1037

1038
    log_info("active opids: %zu", this->ts_active_opids.size());
37✔
1039

1040
    size_t filtered_in_count = 0;
37✔
1041
    for (const auto& filt : this->tss_filters) {
39✔
1042
        if (!filt->is_enabled()) {
2✔
1043
            continue;
×
1044
        }
1045
        if (filt->get_type() == text_filter::INCLUDE) {
2✔
1046
            filtered_in_count += 1;
1✔
1047
        }
1048
    }
1049
    this->ts_filter_hits = {};
37✔
1050
    this->ts_time_order.clear();
37✔
1051
    this->ts_time_order.reserve(this->ts_active_opids.size());
37✔
1052
    for (auto& pair : this->ts_active_opids) {
1,177✔
1053
        opid_row& row = pair.second;
1,140✔
1054
        opid_time_range& otr = pair.second.or_value;
1,140✔
1055
        std::string full_desc;
1,140✔
1056
        if (row.or_description.empty()) {
1,140✔
1057
            const auto& desc_defs = row.or_description_defs.odd_defs;
1,136✔
1058
            if (!row.or_descriptions.empty()) {
1,136✔
1059
                auto desc_def_opt
1060
                    = desc_defs.value_for(row.or_descriptions.keys().front());
981✔
1061
                if (desc_def_opt) {
981✔
1062
                    full_desc = desc_def_opt.value()->to_string(
2,943✔
1063
                        row.or_descriptions.values().front());
1,962✔
1064
                }
1065
            }
1066
            row.or_descriptions.clear();
1,136✔
1067
            auto full_desc_sf = string_fragment::from_str(full_desc);
1,136✔
1068
            auto desc_sf_iter = this->ts_descriptions.find(full_desc_sf);
1,136✔
1069
            if (desc_sf_iter == this->ts_descriptions.end()) {
1,136✔
1070
                full_desc_sf = string_fragment::from_str(full_desc).to_owned(
2,272✔
1071
                    this->ts_allocator);
1,136✔
1072
            }
1073
            pair.second.or_description = full_desc_sf;
1,136✔
1074
        } else {
1075
            full_desc += pair.second.or_description;
4✔
1076
        }
1077

1078
        if (!this->is_row_type_visible(row.or_type)) {
1,140✔
1079
            this->ts_filtered_count += 1;
4✔
1080
            continue;
4✔
1081
        }
1082

1083
        shared_buffer sb_opid;
1,136✔
1084
        shared_buffer_ref sbr_opid;
1,136✔
1085
        sbr_opid.share(
1,136✔
1086
            sb_opid, pair.second.or_name.data(), pair.second.or_name.length());
1,136✔
1087
        shared_buffer sb_desc;
1,136✔
1088
        shared_buffer_ref sbr_desc;
1,136✔
1089
        sbr_desc.share(sb_desc, full_desc.c_str(), full_desc.length());
1,136✔
1090
        if (this->tss_apply_filters) {
1,136✔
1091
            auto filtered_in = false;
1,136✔
1092
            auto filtered_out = false;
1,136✔
1093
            for (const auto& filt : this->tss_filters) {
1,304✔
1094
                if (!filt->is_enabled()) {
168✔
1095
                    continue;
×
1096
                }
1097
                for (const auto sbr : {&sbr_opid, &sbr_desc}) {
504✔
1098
                    if (filt->matches(std::nullopt, *sbr)) {
336✔
1099
                        this->ts_filter_hits[filt->get_index()] += 1;
2✔
1100
                        switch (filt->get_type()) {
2✔
1101
                            case text_filter::INCLUDE:
1✔
1102
                                filtered_in = true;
1✔
1103
                                break;
1✔
1104
                            case text_filter::EXCLUDE:
1✔
1105
                                filtered_out = true;
1✔
1106
                                break;
1✔
1107
                            default:
×
1108
                                break;
×
1109
                        }
1110
                    }
1111
                }
1112
            }
1113

1114
            if (min_log_time_opt
1,136✔
1115
                && otr.otr_range.tr_end < min_log_time_opt.value())
1,136✔
1116
            {
1117
                filtered_out = true;
16✔
1118
            }
1119
            if (max_log_time_opt
1,136✔
1120
                && max_log_time_opt.value() < otr.otr_range.tr_begin)
1,136✔
1121
            {
1122
                filtered_out = true;
16✔
1123
            }
1124

1125
            if ((filtered_in_count > 0 && !filtered_in) || filtered_out) {
1,136✔
1126
                this->ts_filtered_count += 1;
116✔
1127
                continue;
116✔
1128
            }
1129
        }
1130

1131
        if (pair.second.or_name.length() > this->ts_opid_width) {
1,020✔
1132
            this->ts_opid_width = pair.second.or_name.length();
98✔
1133
        }
1134
        if (full_desc.size() > max_desc_width) {
1,020✔
1135
            max_desc_width = full_desc.size();
27✔
1136
        }
1137

1138
        if (this->ts_lower_bound == 0us
1,020✔
1139
            || pair.second.or_value.otr_range.tr_begin < this->ts_lower_bound)
1,020✔
1140
        {
1141
            this->ts_lower_bound = pair.second.or_value.otr_range.tr_begin;
112✔
1142
        }
1143
        if (this->ts_upper_bound == 0us
1,020✔
1144
            || this->ts_upper_bound < pair.second.or_value.otr_range.tr_end)
1,020✔
1145
        {
1146
            this->ts_upper_bound = pair.second.or_value.otr_range.tr_end;
91✔
1147
        }
1148

1149
        this->ts_time_order.emplace_back(&pair.second);
1,020✔
1150
    }
1,604✔
1151
    std::stable_sort(
37✔
1152
        this->ts_time_order.begin(),
1153
        this->ts_time_order.end(),
1154
        [](const auto* lhs, const auto* rhs) { return *lhs < *rhs; });
5,369✔
1155
    for (size_t lpc = 0; lpc < this->ts_time_order.size(); lpc++) {
1,057✔
1156
        const auto& row = *this->ts_time_order[lpc];
1,020✔
1157
        if (row.or_type == row_type::logfile) {
1,020✔
1158
            bm_files.insert_once(vis_line_t(lpc));
36✔
1159
        } else if (row.or_type == row_type::tag) {
984✔
1160
            bm_meta.insert_once(vis_line_t(lpc));
×
1161
        } else if (row.or_type == row_type::partition) {
984✔
1162
            bm_parts.insert_once(vis_line_t(lpc));
2✔
1163
        }
1164
        if (row.or_value.otr_level_stats.lls_error_count > 0) {
1,020✔
1165
            bm_errs.insert_once(vis_line_t(lpc));
91✔
1166
        }
1167
        if (row.or_value.otr_level_stats.lls_warning_count > 0) {
1,020✔
1168
            bm_warns.insert_once(vis_line_t(lpc));
36✔
1169
        }
1170
    }
1171

1172
    this->ts_opid_width = std::min(this->ts_opid_width, MAX_OPID_WIDTH);
37✔
1173
    this->ts_total_width
1174
        = std::max<size_t>(22 + this->ts_opid_width + max_desc_width,
74✔
1175
                           1 + 16 + 5 + 8 + 5 + 16 + 1 /* header */);
37✔
1176

1177
    this->tss_view->set_needs_update();
37✔
1178
    this->ts_rebuild_in_progress = false;
37✔
1179

1180
    ensure(this->ts_time_order.empty() || this->ts_opid_width > 0);
37✔
1181

1182
    return true;
37✔
1183
}
37✔
1184

1185
std::optional<vis_line_t>
1186
timeline_source::row_for_time(timeval time_bucket)
18✔
1187
{
1188
    auto time_bucket_us = to_us(time_bucket);
18✔
1189
    auto iter = this->ts_time_order.begin();
18✔
1190
    while (true) {
1191
        if (iter == this->ts_time_order.end()) {
20✔
1192
            return std::nullopt;
1✔
1193
        }
1194

1195
        if ((*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
19✔
1196
            break;
17✔
1197
        }
1198
        ++iter;
2✔
1199
    }
1200

1201
    auto closest_iter = iter;
17✔
1202
    auto closest_diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
17✔
1203
    for (; iter != this->ts_time_order.end(); ++iter) {
54✔
1204
        if (time_bucket_us < (*iter)->or_value.otr_range.tr_begin) {
53✔
1205
            break;
16✔
1206
        }
1207
        if (!(*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
37✔
1208
            continue;
4✔
1209
        }
1210

1211
        auto diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
33✔
1212
        if (diff < closest_diff) {
33✔
1213
            closest_iter = iter;
2✔
1214
            closest_diff = diff;
2✔
1215
        }
1216

1217
        for (const auto& sub : (*iter)->or_value.otr_sub_ops) {
33✔
1218
            if (!sub.ostr_range.contains_inclusive(time_bucket_us)) {
×
1219
                continue;
×
1220
            }
1221

1222
            diff = time_bucket_us - sub.ostr_range.tr_begin;
×
1223
            if (diff < closest_diff) {
×
1224
                closest_iter = iter;
×
1225
                closest_diff = diff;
×
1226
            }
1227
        }
1228
    }
1229

1230
    return vis_line_t(std::distance(this->ts_time_order.begin(), closest_iter));
34✔
1231
}
1232

1233
std::optional<vis_line_t>
1234
timeline_source::row_for(const row_info& ri)
31✔
1235
{
1236
    auto vl_opt = this->ts_lss.row_for(ri);
31✔
1237
    if (!vl_opt) {
31✔
1238
        return this->row_for_time(ri.ri_time);
×
1239
    }
1240

1241
    auto vl = vl_opt.value();
31✔
1242
    auto win = this->ts_lss.window_at(vl);
31✔
1243
    for (const auto& msg_line : *win) {
67✔
1244
        const auto& lvv = msg_line.get_values();
31✔
1245

1246
        if (lvv.lvv_opid_value) {
31✔
1247
            auto opid_iter
1248
                = this->ts_active_opids.find(lvv.lvv_opid_value.value());
16✔
1249
            if (opid_iter != this->ts_active_opids.end()) {
16✔
1250
                for (const auto& [index, oprow] :
168✔
1251
                     lnav::itertools::enumerate(this->ts_time_order))
171✔
1252
                {
1253
                    if (oprow == &opid_iter->second) {
152✔
1254
                        return vis_line_t(index);
13✔
1255
                    }
1256
                }
1257
            }
1258
        }
1259
    }
44✔
1260

1261
    return this->row_for_time(ri.ri_time);
18✔
1262
}
31✔
1263

1264
std::optional<text_time_translator::row_info>
1265
timeline_source::time_for_row(vis_line_t row)
39✔
1266
{
1267
    if (row >= this->ts_time_order.size()) {
39✔
1268
        return std::nullopt;
×
1269
    }
1270

1271
    const auto& otr = this->ts_time_order[row]->or_value;
39✔
1272

1273
    if (this->tss_view->get_selection() == row) {
39✔
1274
        auto ov_sel = this->tss_view->get_overlay_selection();
39✔
1275

1276
        if (ov_sel && ov_sel.value() < otr.otr_sub_ops.size()) {
39✔
1277
            return row_info{
×
1278
                to_timeval(otr.otr_sub_ops[ov_sel.value()].ostr_range.tr_begin),
×
1279
                row,
1280
            };
1281
        }
1282
    }
1283

1284
    auto preview_selection = this->ts_preview_view.get_selection();
39✔
1285
    if (!preview_selection) {
39✔
1286
        return std::nullopt;
×
1287
    }
1288
    if (preview_selection < this->ts_preview_rows.size()) {
39✔
1289
        return this->ts_preview_rows[preview_selection.value()];
37✔
1290
    }
1291

1292
    return row_info{
4✔
1293
        to_timeval(otr.otr_range.tr_begin),
2✔
1294
        row,
1295
    };
4✔
1296
}
1297

1298
size_t
1299
timeline_source::text_line_width(textview_curses& curses)
7,092✔
1300
{
1301
    return this->ts_total_width;
7,092✔
1302
}
1303

1304
void
1305
timeline_source::text_selection_changed(textview_curses& tc)
70✔
1306
{
1307
    static const size_t MAX_PREVIEW_LINES = 200;
1308

1309
    auto sel = tc.get_selection();
70✔
1310

1311
    this->ts_preview_source.clear();
70✔
1312
    this->ts_preview_rows.clear();
70✔
1313
    if (!sel || sel.value() >= this->ts_time_order.size()) {
70✔
1314
        return;
37✔
1315
    }
1316

1317
    const auto& row = *this->ts_time_order[sel.value()];
33✔
1318
    auto low_us = row.or_value.otr_range.tr_begin;
33✔
1319
    auto high_us = row.or_value.otr_range.tr_end;
33✔
1320
    auto id_sf = row.or_name;
33✔
1321
    auto level_stats = row.or_value.otr_level_stats;
33✔
1322
    auto ov_sel = tc.get_overlay_selection();
33✔
1323
    if (ov_sel) {
33✔
1324
        const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
1325
        id_sf = sub.ostr_subid;
×
1326
        low_us = sub.ostr_range.tr_begin;
×
1327
        high_us = sub.ostr_range.tr_end;
×
1328
        level_stats = sub.ostr_level_stats;
×
1329
    }
1330
    high_us += 1s;
33✔
1331
    auto low_vl = this->ts_lss.row_for_time(to_timeval(low_us));
33✔
1332
    auto high_vl = this->ts_lss.row_for_time(to_timeval(high_us))
33✔
1333
                       .value_or(vis_line_t(this->ts_lss.text_line_count()));
33✔
1334

1335
    if (!low_vl) {
33✔
1336
        return;
×
1337
    }
1338

1339
    auto preview_content = attr_line_t();
33✔
1340
    auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
33✔
1341
    auto win = this->ts_lss.window_at(low_vl.value(), high_vl);
33✔
1342
    auto id_bloom_bits = row.or_name.bloom_bits();
33✔
1343
    auto msg_count = 0;
33✔
1344
    for (const auto& msg_line : *win) {
1,609✔
1345
        switch (row.or_type) {
788✔
1346
            case row_type::logfile:
397✔
1347
                if (msg_line.get_file_ptr() != row.or_logfile) {
397✔
1348
                    continue;
×
1349
                }
1350
                break;
397✔
1351
            case row_type::thread: {
132✔
1352
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
132✔
1353
                    continue;
115✔
1354
                }
1355
                const auto& lvv = msg_line.get_values();
17✔
1356
                if (!lvv.lvv_thread_id_value) {
17✔
UNCOV
1357
                    continue;
×
1358
                }
1359
                auto tid_sf = lvv.lvv_thread_id_value.value();
17✔
1360
                if (!(tid_sf == row.or_name)) {
17✔
1361
                    continue;
×
1362
                }
1363
                break;
17✔
1364
            }
17✔
1365
            case row_type::opid: {
259✔
1366
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
259✔
1367
                    continue;
233✔
1368
                }
1369

1370
                const auto& lvv = msg_line.get_values();
26✔
1371
                if (!lvv.lvv_opid_value) {
26✔
1372
                    continue;
×
1373
                }
1374
                auto opid_sf = lvv.lvv_opid_value.value();
26✔
1375

1376
                if (!(opid_sf == row.or_name)) {
26✔
1377
                    continue;
×
1378
                }
1379
                break;
26✔
1380
            }
26✔
1381
            case row_type::tag: {
×
1382
                const auto& bm
1383
                    = msg_line.get_file_ptr()->get_bookmark_metadata();
×
1384
                auto bm_iter = bm.find(msg_line.get_file_line_number());
×
1385
                if (bm_iter == bm.end()) {
×
1386
                    continue;
×
1387
                }
1388
                auto tag_name = row.or_name.to_string();
×
1389
                if (!(bm_iter->second.bm_tags
×
1390
                      | lnav::itertools::find(tag_name)))
×
1391
                {
1392
                    continue;
×
1393
                }
1394
                break;
×
1395
            }
1396
            case row_type::partition:
×
1397
                break;
×
1398
        }
1399

1400
        for (size_t lpc = 0; lpc < msg_line.get_line_count(); lpc++) {
883✔
1401
            auto vl = msg_line.get_vis_line() + vis_line_t(lpc);
443✔
1402
            auto cl = this->ts_lss.at(vl);
443✔
1403
            auto row_al = attr_line_t();
443✔
1404
            this->ts_log_view.textview_value_for_row(vl, row_al);
443✔
1405
            preview_content.append(row_al).append("\n");
443✔
1406
            this->ts_preview_rows.emplace_back(
443✔
1407
                msg_line.get_logline().get_timeval(), cl);
443✔
1408
            ++cl;
443✔
1409
        }
443✔
1410
        msg_count += 1;
440✔
1411
        msgs_remaining -= 1;
440✔
1412
        if (msgs_remaining == 0) {
440✔
1413
            break;
×
1414
        }
1415
    }
33✔
1416

1417
    this->ts_preview_source.replace_with(preview_content);
33✔
1418
    this->ts_preview_view.set_selection(0_vl);
33✔
1419
    this->ts_preview_status_source.get_description().set_value(
33✔
1420
        " ID %.*s", id_sf.length(), id_sf.data());
1421
    auto err_count = level_stats.lls_error_count;
33✔
1422
    if (err_count == 0) {
33✔
1423
        this->ts_preview_status_source
16✔
1424
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
16✔
1425
            .set_value("");
16✔
1426
    } else {
1427
        this->ts_preview_status_source
17✔
1428
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
17✔
1429
            .set_value("\u2022 %'d", err_count);
17✔
1430
    }
1431
    auto warn_count = level_stats.lls_warning_count;
33✔
1432
    if (warn_count == 0) {
33✔
1433
        this->ts_preview_status_source
30✔
1434
            .statusview_value_for_field(timeline_status_source::TSF_WARNINGS)
30✔
1435
            .set_value("");
30✔
1436
    } else {
1437
        this->ts_preview_status_source
3✔
1438
            .statusview_value_for_field(timeline_status_source::TSF_WARNINGS)
3✔
1439
            .set_value("\u2022 %'d", warn_count);
3✔
1440
    }
1441
    if (msg_count < level_stats.lls_total_count) {
33✔
1442
        this->ts_preview_status_source
31✔
1443
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
31✔
1444
            .set_value(
31✔
1445
                "%'d of %'d messages ", msg_count, level_stats.lls_total_count);
1446
    } else {
1447
        this->ts_preview_status_source
2✔
1448
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
2✔
1449
            .set_value("%'d messages ", level_stats.lls_total_count);
2✔
1450
    }
1451
    this->ts_preview_status_view.set_needs_update();
33✔
1452
}
33✔
1453

1454
void
1455
timeline_source::text_filters_changed()
16✔
1456
{
1457
    this->rebuild_indexes();
16✔
1458
    this->tss_view->reload_data();
16✔
1459
    this->tss_view->redo_search();
16✔
1460
}
16✔
1461

1462
void
1463
timeline_source::clear_preview()
×
1464
{
1465
    text_sub_source::clear_preview();
×
1466
    this->ts_preview_hidden_row_types.clear();
×
1467
}
1468

1469
void
1470
timeline_source::add_commands_for_session(
47✔
1471
    const std::function<void(const std::string&)>& receiver)
1472
{
1473
    text_sub_source::add_commands_for_session(receiver);
47✔
1474

1475
    for (const auto& rt : this->ts_hidden_row_types) {
47✔
1476
        receiver(fmt::format(FMT_STRING("hide-in-timeline {}"),
×
1477
                             row_type_to_string(rt)));
×
1478
    }
1479
}
47✔
1480

1481
int
1482
timeline_source::get_filtered_count() const
71✔
1483
{
1484
    return this->ts_filtered_count;
71✔
1485
}
1486

1487
int
1488
timeline_source::get_filtered_count_for(size_t filter_index) const
×
1489
{
1490
    return this->ts_filter_hits[filter_index];
×
1491
}
1492

1493
static const std::vector<breadcrumb::possibility>&
1494
timestamp_poss()
×
1495
{
1496
    const static std::vector<breadcrumb::possibility> retval = {
1497
        breadcrumb::possibility{"-1 day"},
1498
        breadcrumb::possibility{"-1h"},
1499
        breadcrumb::possibility{"-30m"},
1500
        breadcrumb::possibility{"-15m"},
1501
        breadcrumb::possibility{"-5m"},
1502
        breadcrumb::possibility{"-1m"},
1503
        breadcrumb::possibility{"+1m"},
1504
        breadcrumb::possibility{"+5m"},
1505
        breadcrumb::possibility{"+15m"},
1506
        breadcrumb::possibility{"+30m"},
1507
        breadcrumb::possibility{"+1h"},
1508
        breadcrumb::possibility{"+1 day"},
1509
    };
1510

1511
    return retval;
×
1512
}
1513

1514
void
1515
timeline_source::text_crumbs_for_line(int line,
×
1516
                                      std::vector<breadcrumb::crumb>& crumbs)
1517
{
1518
    text_sub_source::text_crumbs_for_line(line, crumbs);
×
1519

1520
    if (line >= this->ts_time_order.size()) {
×
1521
        return;
×
1522
    }
1523

1524
    const auto& row = *this->ts_time_order[line];
×
1525
    char ts[64];
1526

1527
    sql_strftime(ts, sizeof(ts), row.or_value.otr_range.tr_begin, 'T');
×
1528

1529
    crumbs.emplace_back(std::string(ts),
×
1530
                        timestamp_poss,
1531
                        [ec = this->ts_exec_context](const auto& ts) {
×
1532
                            auto cmd
×
1533
                                = fmt::format(FMT_STRING(":goto {}"),
×
1534
                                              ts.template get<std::string>());
1535
                            ec->execute(INTERNAL_SRC_LOC, cmd);
×
1536
                        });
×
1537
    crumbs.back().c_expected_input
×
1538
        = breadcrumb::crumb::expected_input_t::anything;
×
1539
    crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
×
1540
}
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