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

tstack / lnav / 22752382176-2819

06 Mar 2026 06:42AM UTC coverage: 68.915% (-0.01%) from 68.925%
22752382176-2819

push

github

tstack
[timeline] some tweaks for recent change

0 of 1 new or added line in 1 file covered. (0.0%)

22 existing lines in 2 files now uncovered.

52047 of 75524 relevant lines covered (68.91%)

443776.81 hits per line

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

63.31
/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(
646✔
178
    const std::shared_ptr<timeline_source>& src)
646✔
179
    : gho_src(src)
646✔
180
{
181
}
646✔
182

183
bool
184
timeline_header_overlay::list_static_overlay(const listview_curses& lv,
36✔
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) {
36✔
191
        return false;
×
192
    }
193

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

198
            if (this->gho_src->gs_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->gs_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->gs_lss.get_filters();
×
220
                for (const auto& filt : fs) {
×
221
                    auto hits = this->gho_src->gs_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->gs_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) {
36✔
265
        return false;
14✔
266
    }
267

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

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

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

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

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

348
    return true;
22✔
349
}
22✔
350
void
351
timeline_header_overlay::list_value_for_overlay(
420✔
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) {
420✔
357
        return;
420✔
358
    }
359

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

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

368
    const auto& row = *this->gho_src->gs_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,
646✔
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)
646✔
458
    : gs_log_view(log_view), gs_lss(lss), gs_preview_view(preview_view),
646✔
459
      gs_preview_source(preview_source),
646✔
460
      gs_preview_status_view(preview_status_view),
646✔
461
      gs_preview_status_source(preview_status_source)
646✔
462
{
463
    this->tss_supports_filtering = true;
646✔
464
    this->gs_preview_view.set_overlay_source(&this->gs_preview_overlay);
646✔
465
}
646✔
466

467
bool
468
timeline_source::list_input_handle_key(listview_curses& lv, const ncinput& ch)
×
469
{
470
    switch (ch.eff_text[0]) {
×
471
        case 'q':
×
472
        case KEY_ESCAPE: {
473
            if (this->gs_preview_focused) {
×
474
                this->gs_preview_focused = false;
×
475
                this->gs_preview_view.set_height(5_vl);
×
476
                this->gs_preview_status_view.set_enabled(
×
477
                    this->gs_preview_focused);
×
478
                this->tss_view->set_enabled(!this->gs_preview_focused);
×
479
                return true;
×
480
            }
481
            break;
×
482
        }
483
        case '\n':
×
484
        case '\r':
485
        case NCKEY_ENTER: {
486
            this->gs_preview_focused = !this->gs_preview_focused;
×
487
            this->gs_preview_status_view.set_enabled(this->gs_preview_focused);
×
488
            this->tss_view->set_enabled(!this->gs_preview_focused);
×
489
            if (this->gs_preview_focused) {
×
490
                auto height = this->tss_view->get_dimensions().first;
×
491

492
                if (height > 5) {
×
493
                    this->gs_preview_view.set_height(height / 2_vl);
×
494
                }
495
            } else {
496
                this->gs_preview_view.set_height(5_vl);
×
497
            }
498
            return true;
×
499
        }
500
    }
501
    if (this->gs_preview_focused) {
×
502
        return this->gs_preview_view.handle_key(ch);
×
503
    }
504

505
    return false;
×
506
}
507

508
bool
509
timeline_source::text_handle_mouse(
×
510
    textview_curses& tc,
511
    const listview_curses::display_line_content_t&,
512
    mouse_event& me)
513
{
514
    auto nci = ncinput{};
×
515
    if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{0, -1})) {
×
516
        nci.id = '\r';
×
517
        nci.eff_text[0] = '\r';
×
518
        this->list_input_handle_key(tc, nci);
×
519
    }
520

521
    return false;
×
522
}
523

524
std::pair<std::chrono::microseconds, std::chrono::microseconds>
525
timeline_source::get_time_bounds_for(int line)
434✔
526
{
527
    const auto low_index = this->tss_view->get_top();
434✔
528
    auto high_index
529
        = std::min(this->tss_view->get_bottom(),
434✔
530
                   vis_line_t((int) this->gs_time_order.size() - 1));
434✔
531
    if (high_index == low_index) {
434✔
532
        high_index = vis_line_t(this->gs_time_order.size() - 1);
434✔
533
    }
534
    const auto& low_row = *this->gs_time_order[low_index];
434✔
535
    const auto& high_row = *this->gs_time_order[high_index];
434✔
536
    auto low_us = low_row.or_value.otr_range.tr_begin;
434✔
537
    auto high_us = high_row.or_value.otr_range.tr_begin;
434✔
538

539
    auto duration = high_us - low_us;
434✔
540
    auto span_iter
541
        = std::upper_bound(TIME_SPANS.begin(), TIME_SPANS.end(), duration);
434✔
542
    if (span_iter == TIME_SPANS.end()) {
434✔
543
        --span_iter;
7✔
544
    }
545
    auto span_portion = *span_iter / 8;
434✔
546
    auto lb = low_us;
434✔
547
    lb = rounddown(lb, span_portion);
434✔
548
    auto ub = high_us;
434✔
549
    ub = roundup(ub, span_portion);
434✔
550

551
    ensure(lb <= ub);
434✔
552
    return {lb, ub};
434✔
553
}
554

555
size_t
556
timeline_source::text_line_count()
9,052✔
557
{
558
    return this->gs_time_order.size();
9,052✔
559
}
560

561
line_info
562
timeline_source::text_value_for_line(textview_curses& tc,
412✔
563
                                     int line,
564
                                     std::string& value_out,
565
                                     line_flags_t flags)
566
{
567
    if (!this->ts_rebuild_in_progress
824✔
568
        && line < (ssize_t) this->gs_time_order.size())
412✔
569
    {
570
        const auto& row = *this->gs_time_order[line];
412✔
571
        auto duration
572
            = row.or_value.otr_range.tr_end - row.or_value.otr_range.tr_begin;
412✔
573
        auto duration_str = fmt::format(
574
            FMT_STRING(" {: >13}"),
1,236✔
575
            humanize::time::duration::from_tv(to_timeval(duration))
412✔
576
                .to_string());
824✔
577

578
        this->gs_rendered_line.clear();
412✔
579

580
        auto total_msgs = row.or_value.otr_level_stats.lls_total_count;
412✔
581
        auto truncated_name
582
            = attr_line_t::from_table_cell_content(row.or_name, MAX_OPID_WIDTH);
412✔
583
        auto truncated_desc = attr_line_t::from_table_cell_content(
584
            row.or_description, MAX_DESC_WIDTH);
412✔
585
        std::optional<ui_icon_t> icon;
412✔
586
        auto padding = 1;
412✔
587
        switch (row.or_type) {
412✔
588
            case row_type::logfile:
14✔
589
                icon = ui_icon_t::file;
14✔
590
                break;
14✔
591
            case row_type::thread:
30✔
592
                icon = ui_icon_t::thread;
30✔
593
                break;
30✔
594
            case row_type::opid:
366✔
595
                padding = 3;
366✔
596
                break;
366✔
597
            case row_type::tag:
×
598
                icon = ui_icon_t::tag;
×
599
                break;
×
600
            case row_type::partition:
2✔
601
                icon = ui_icon_t::partition;
2✔
602
                break;
2✔
603
        }
604
        this->gs_rendered_line
605
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
824✔
606
            .append("  ")
412✔
607
            .append(lnav::roles::error(humanize::sparkline(
1,236✔
608
                row.or_value.otr_level_stats.lls_error_count, total_msgs)))
412✔
609
            .append(lnav::roles::warning(humanize::sparkline(
1,236✔
610
                row.or_value.otr_level_stats.lls_warning_count, total_msgs)))
412✔
611
            .append("  ")
412✔
612
            .append(icon)
412✔
613
            .append(padding, ' ')
412✔
614
            .append(lnav::roles::identifier(truncated_name))
824✔
615
            .append(
824✔
616
                this->gs_opid_width - truncated_name.utf8_length_or_length(),
412✔
617
                ' ')
618
            .append(truncated_desc);
412✔
619
        this->gs_rendered_line.with_attr_for_all(
412✔
620
            VC_ROLE.value(role_t::VCR_COMMENT));
824✔
621

622
        value_out = this->gs_rendered_line.get_string();
412✔
623
    }
412✔
624

625
    return {};
412✔
626
}
627

628
void
629
timeline_source::text_attrs_for_line(textview_curses& tc,
412✔
630
                                     int line,
631
                                     string_attrs_t& value_out)
632
{
633
    if (!this->ts_rebuild_in_progress
824✔
634
        && line < (ssize_t) this->gs_time_order.size())
412✔
635
    {
636
        const auto& row = *this->gs_time_order[line];
412✔
637

638
        value_out = this->gs_rendered_line.get_attrs();
412✔
639

640
        auto lr = line_range{-1, -1, line_range::unit::codepoint};
412✔
641
        auto [sel_lb, sel_ub]
412✔
642
            = this->get_time_bounds_for(tc.get_selection().value_or(0_vl));
412✔
643

644
        if (row.or_value.otr_range.tr_begin <= sel_ub
412✔
645
            && sel_lb <= row.or_value.otr_range.tr_end)
412✔
646
        {
647
            auto width = tc.get_dimensions().second;
412✔
648

649
            if (width > CHART_INDENT) {
412✔
650
                width -= CHART_INDENT;
412✔
651
                const double span = (sel_ub - sel_lb).count();
412✔
652
                auto us_per_ch = std::chrono::microseconds{
653
                    static_cast<int64_t>(ceil(span / (double) width))};
412✔
654

655
                if (row.or_value.otr_range.tr_begin <= sel_lb) {
412✔
656
                    lr.lr_start = CHART_INDENT;
×
657
                } else {
658
                    auto start_diff
659
                        = (row.or_value.otr_range.tr_begin - sel_lb);
412✔
660

661
                    lr.lr_start = CHART_INDENT + floor(start_diff / us_per_ch);
412✔
662
                }
663

664
                if (sel_ub < row.or_value.otr_range.tr_end) {
412✔
665
                    lr.lr_end = -1;
13✔
666
                } else {
667
                    auto end_diff = (row.or_value.otr_range.tr_end - sel_lb);
399✔
668

669
                    lr.lr_end = CHART_INDENT + ceil(end_diff / us_per_ch);
399✔
670
                    if (lr.lr_start == lr.lr_end) {
399✔
671
                        lr.lr_end += 1;
304✔
672
                    }
673
                }
674

675
                auto block_attrs = text_attrs::with_reverse();
412✔
676
                require(lr.lr_start >= 0);
412✔
677
                value_out.emplace_back(lr, VC_STYLE.value(block_attrs));
412✔
678
            }
679
        }
680
        auto alt_row_index = line % 4;
412✔
681
        if (alt_row_index == 2 || alt_row_index == 3) {
412✔
682
            value_out.emplace_back(line_range{0, -1},
200✔
683
                                   VC_ROLE.value(role_t::VCR_ALT_ROW));
400✔
684
        }
685
    }
686
}
412✔
687

688
size_t
689
timeline_source::text_size_for_line(textview_curses& tc,
×
690
                                    int line,
691
                                    text_sub_source::line_flags_t raw)
692
{
693
    return this->gs_total_width;
×
694
}
695

696
bool
697
timeline_source::rebuild_indexes()
28✔
698
{
699
    static auto op = lnav_operation{"timeline_rebuild"};
28✔
700

701
    auto op_guard = lnav_opid_guard::internal(op);
28✔
702
    auto& bm = this->tss_view->get_bookmarks();
28✔
703
    auto& bm_files = bm[&logfile_sub_source::BM_FILES];
28✔
704
    auto& bm_errs = bm[&textview_curses::BM_ERRORS];
28✔
705
    auto& bm_warns = bm[&textview_curses::BM_WARNINGS];
28✔
706
    auto& bm_meta = bm[&textview_curses::BM_META];
28✔
707
    auto& bm_parts = bm[&textview_curses::BM_PARTITION];
28✔
708

709
    this->ts_rebuild_in_progress = true;
28✔
710
    bm_errs.clear();
28✔
711
    bm_warns.clear();
28✔
712
    bm_meta.clear();
28✔
713
    bm_parts.clear();
28✔
714

715
    this->gs_lower_bound = {};
28✔
716
    this->gs_upper_bound = {};
28✔
717
    this->gs_opid_width = 0;
28✔
718
    this->gs_total_width = 0;
28✔
719
    this->gs_filtered_count = 0;
28✔
720
    this->gs_active_opids.clear();
28✔
721
    this->gs_descriptions.clear();
28✔
722
    this->gs_subid_map.clear();
28✔
723
    this->gs_allocator.reset();
28✔
724
    this->gs_preview_source.clear();
28✔
725
    this->gs_preview_rows.clear();
28✔
726
    this->gs_preview_status_source.get_description().clear();
28✔
727

728
    auto min_log_time_tv_opt = this->get_min_row_time();
28✔
729
    auto max_log_time_tv_opt = this->get_max_row_time();
28✔
730
    std::optional<std::chrono::microseconds> min_log_time_opt;
28✔
731
    std::optional<std::chrono::microseconds> max_log_time_opt;
28✔
732
    auto max_desc_width = size_t{0};
28✔
733

734
    if (min_log_time_tv_opt) {
28✔
735
        min_log_time_opt = to_us(min_log_time_tv_opt.value());
1✔
736
    }
737
    if (max_log_time_tv_opt) {
28✔
738
        max_log_time_opt = to_us(max_log_time_tv_opt.value());
1✔
739
    }
740

741
    log_info("building opid table");
28✔
742
    auto last_log_time = std::chrono::microseconds{};
28✔
743
    tlx::btree_map<std::chrono::microseconds, std::string> part_map;
28✔
744
    for (const auto& [index, ld] : lnav::itertools::enumerate(this->gs_lss)) {
58✔
745
        if (ld->get_file_ptr() == nullptr) {
30✔
746
            continue;
1✔
747
        }
748
        if (!ld->is_visible()) {
30✔
749
            continue;
1✔
750
        }
751

752
        auto* lf = ld->get_file_ptr();
29✔
753
        lf->enable_cache();
29✔
754

755
        const auto& mark_meta = lf->get_bookmark_metadata();
29✔
756
        {
757
            for (const auto& [line_num, line_meta] : mark_meta) {
38✔
758
                const auto ll = std::next(lf->begin(), line_num);
9✔
759
                if (!line_meta.bm_name.empty()) {
9✔
760
                    part_map.insert2(ll->get_time<std::chrono::microseconds>(),
2✔
761
                                     line_meta.bm_name);
2✔
762
                }
763
                for (const auto& tag : line_meta.bm_tags) {
9✔
764
                    auto line_time = ll->get_time<std::chrono::microseconds>();
×
765
                    auto tag_key = fmt::format(FMT_STRING("{}@{}:{}"),
×
766
                                               tag,
767
                                               lf->get_unique_path(),
768
                                               line_time.count());
×
769
                    auto tag_key_sf
770
                        = string_fragment::from_str(tag_key).to_owned(
×
771
                            this->gs_allocator);
×
772
                    auto tag_name_sf = string_fragment::from_str(tag).to_owned(
×
773
                        this->gs_allocator);
×
774
                    auto tag_otr = opid_time_range{};
×
775
                    tag_otr.otr_range.tr_begin = line_time;
×
776
                    tag_otr.otr_range.tr_end = line_time;
×
777
                    tag_otr.otr_level_stats.update_msg_count(
×
778
                        ll->get_msg_level());
779
                    this->gs_active_opids.emplace(
×
780
                        tag_key_sf,
781
                        opid_row{
×
782
                            row_type::tag,
783
                            tag_name_sf,
784
                            tag_otr,
785
                            string_fragment::invalid(),
786
                        });
787
                }
788
            }
789
        }
790

791
        auto path = string_fragment::from_str(lf->get_unique_path())
29✔
792
                        .to_owned(this->gs_allocator);
29✔
793
        auto lf_otr = opid_time_range{};
29✔
794
        lf_otr.otr_range = lf->get_content_time_range();
29✔
795
        lf_otr.otr_level_stats = lf->get_level_stats();
29✔
796
        if (lf_otr.otr_range.tr_end > last_log_time) {
29✔
797
            last_log_time = lf_otr.otr_range.tr_end;
29✔
798
        }
799
        auto lf_row = opid_row{
29✔
800
            row_type::logfile,
801
            path,
802
            lf_otr,
803
            string_fragment::invalid(),
804
        };
29✔
805
        lf_row.or_logfile = lf;
29✔
806
        this->gs_active_opids.emplace(path, lf_row);
29✔
807

808
        {
809
            auto r_tid_map = lf->get_thread_ids().readAccess();
29✔
810

811
            for (const auto& [tid_sf, tid_meta] : r_tid_map->ltis_tid_ranges) {
82✔
812
                auto active_iter = this->gs_active_opids.find(tid_sf);
53✔
813
                if (active_iter == this->gs_active_opids.end()) {
53✔
814
                    auto tid = tid_sf.to_owned(this->gs_allocator);
44✔
815
                    auto tid_otr = opid_time_range{};
44✔
816
                    tid_otr.otr_range = tid_meta.titr_range;
44✔
817
                    tid_otr.otr_level_stats = tid_meta.titr_level_stats;
44✔
818
                    this->gs_active_opids.emplace(
×
819
                        tid,
820
                        opid_row{
44✔
821
                            row_type::thread,
822
                            tid,
823
                            tid_otr,
824
                            string_fragment::invalid(),
825
                        });
826
                } else {
44✔
827
                    active_iter->second.or_value.otr_range
18✔
828
                        |= tid_meta.titr_range;
9✔
829
                }
830
            }
831
        }
29✔
832

833
        auto format = lf->get_format();
29✔
834
        safe::ReadAccess<logfile::safe_opid_state> r_opid_map(
835
            ld->get_file_ptr()->get_opids());
29✔
836
        for (const auto& pair : r_opid_map->los_opid_ranges) {
1,015✔
837
            const opid_time_range& otr = pair.second;
986✔
838
            auto active_iter = this->gs_active_opids.find(pair.first);
986✔
839
            if (active_iter == this->gs_active_opids.end()) {
986✔
840
                auto opid = pair.first.to_owned(this->gs_allocator);
986✔
841
                auto active_emp_res = this->gs_active_opids.emplace(
1,972✔
842
                    opid,
843
                    opid_row{
986✔
844
                        row_type::opid,
845
                        opid,
846
                        otr,
847
                        string_fragment::invalid(),
848
                    });
849
                active_iter = active_emp_res.first;
986✔
850
            } else {
851
                active_iter->second.or_value |= otr;
×
852
            }
853

854
            opid_row& row = active_iter->second;
986✔
855
            for (auto& sub : row.or_value.otr_sub_ops) {
986✔
856
                auto subid_iter = this->gs_subid_map.find(sub.ostr_subid);
×
857

858
                if (subid_iter == this->gs_subid_map.end()) {
×
859
                    subid_iter = this->gs_subid_map
×
860
                                     .emplace(sub.ostr_subid.to_owned(
×
861
                                                  this->gs_allocator),
×
862
                                              true)
×
863
                                     .first;
864
                }
865
                sub.ostr_subid = subid_iter->first;
×
866
                if (sub.ostr_subid.length() > row.or_max_subid_width) {
×
867
                    row.or_max_subid_width = sub.ostr_subid.length();
×
868
                }
869
            }
870

871
            if (otr.otr_description.lod_index) {
986✔
872
                auto desc_id = otr.otr_description.lod_index.value();
981✔
873
                auto desc_def_iter
874
                    = format->lf_opid_description_def_vec->at(desc_id);
981✔
875

876
                auto desc_key
877
                    = opid_description_def_key{format->get_name(), desc_id};
981✔
878
                auto desc_defs_opt
879
                    = row.or_description_defs.odd_defs.value_for(desc_key);
981✔
880
                if (!desc_defs_opt) {
981✔
881
                    row.or_description_defs.odd_defs.insert(desc_key,
981✔
882
                                                            *desc_def_iter);
883
                }
884

885
                auto& all_descs = row.or_descriptions;
981✔
886
                const auto& new_desc_v = otr.otr_description.lod_elements;
981✔
887
                all_descs.insert(desc_key, new_desc_v);
981✔
888
            } else if (!otr.otr_description.lod_elements.empty()) {
5✔
889
                auto desc_sf = string_fragment::from_str(
4✔
890
                    otr.otr_description.lod_elements.values().front());
4✔
891
                row.or_description = desc_sf.to_owned(this->gs_allocator);
4✔
892
            }
893
            row.or_value.otr_description.lod_elements.clear();
986✔
894
        }
895

896
        if (this->gs_index_progress) {
29✔
897
            switch (this->gs_index_progress(
×
898
                progress_t{index, this->gs_lss.file_count()}))
×
899
            {
900
                case lnav::progress_result_t::ok:
×
901
                    break;
×
902
                case lnav::progress_result_t::interrupt:
×
903
                    log_debug("timeline rebuild interrupted");
×
904
                    this->ts_rebuild_in_progress = false;
×
905
                    return false;
×
906
            }
907
        }
908
    }
29✔
909
    if (this->gs_index_progress) {
28✔
910
        this->gs_index_progress(std::nullopt);
×
911
    }
912

913
    {
914
        static const auto START_RE = lnav::pcre2pp::code::from_const(
915
            R"(^(?:start(?:ed)?|begin)|\b(?:start(?:ed)?|begin)$)",
916
            PCRE2_CASELESS);
28✔
917

918
        std::vector<opid_row*> start_tags;
28✔
919
        for (auto& pair : this->gs_active_opids) {
1,087✔
920
            if (pair.second.or_type != row_type::tag) {
1,059✔
921
                continue;
1,059✔
922
            }
923
            if (START_RE.find_in(pair.second.or_name).ignore_error()) {
×
924
                start_tags.emplace_back(&pair.second);
×
925
            }
926
        }
927
        std::stable_sort(start_tags.begin(),
28✔
928
                         start_tags.end(),
929
                         [](const auto* lhs, const auto* rhs) {
×
930
                             if (lhs->or_name == rhs->or_name) {
×
931
                                 return lhs->or_value.otr_range.tr_begin
×
932
                                     < rhs->or_value.otr_range.tr_begin;
×
933
                             }
934
                             return lhs->or_name < rhs->or_name;
×
935
                         });
936
        for (size_t i = 0; i < start_tags.size(); i++) {
28✔
937
            if (i + 1 < start_tags.size()
×
938
                && start_tags[i]->or_name == start_tags[i + 1]->or_name)
×
939
            {
940
                start_tags[i]->or_value.otr_range.tr_end
×
NEW
941
                    = start_tags[i + 1]->or_value.otr_range.tr_begin - 1us;
×
942
            } else {
943
                start_tags[i]->or_value.otr_range.tr_end = last_log_time;
×
944
            }
945
        }
946
    }
28✔
947

948
    for (auto part_iter = part_map.begin(); part_iter != part_map.end();
30✔
949
         ++part_iter)
2✔
950
    {
951
        auto next_iter = std::next(part_iter);
2✔
952
        auto part_name_sf = string_fragment::from_str(part_iter->second)
2✔
953
                                .to_owned(this->gs_allocator);
2✔
954
        auto part_otr = opid_time_range{};
2✔
955
        part_otr.otr_range.tr_begin = part_iter->first;
2✔
956
        if (next_iter != part_map.end()) {
2✔
957
            part_otr.otr_range.tr_end = next_iter->first;
1✔
958
        } else {
959
            part_otr.otr_range.tr_end = last_log_time;
1✔
960
        }
961
        this->gs_active_opids.emplace(part_name_sf,
×
962
                                      opid_row{
2✔
963
                                          row_type::partition,
964
                                          part_name_sf,
965
                                          part_otr,
966
                                          string_fragment::invalid(),
967
                                      });
968
    }
2✔
969

970
    log_info("active opids: %zu", this->gs_active_opids.size());
28✔
971

972
    size_t filtered_in_count = 0;
28✔
973
    for (const auto& filt : this->tss_filters) {
30✔
974
        if (!filt->is_enabled()) {
2✔
975
            continue;
×
976
        }
977
        if (filt->get_type() == text_filter::INCLUDE) {
2✔
978
            filtered_in_count += 1;
1✔
979
        }
980
    }
981
    this->gs_filter_hits = {};
28✔
982
    this->gs_time_order.clear();
28✔
983
    this->gs_time_order.reserve(this->gs_active_opids.size());
28✔
984
    for (auto& pair : this->gs_active_opids) {
1,089✔
985
        opid_row& row = pair.second;
1,061✔
986
        opid_time_range& otr = pair.second.or_value;
1,061✔
987
        std::string full_desc;
1,061✔
988
        if (row.or_description.empty()) {
1,061✔
989
            const auto& desc_defs = row.or_description_defs.odd_defs;
1,057✔
990
            if (!row.or_descriptions.empty()) {
1,057✔
991
                auto desc_def_opt
992
                    = desc_defs.value_for(row.or_descriptions.keys().front());
981✔
993
                if (desc_def_opt) {
981✔
994
                    full_desc = desc_def_opt.value()->to_string(
2,943✔
995
                        row.or_descriptions.values().front());
1,962✔
996
                }
997
            }
998
            row.or_descriptions.clear();
1,057✔
999
            auto full_desc_sf = string_fragment::from_str(full_desc);
1,057✔
1000
            auto desc_sf_iter = this->gs_descriptions.find(full_desc_sf);
1,057✔
1001
            if (desc_sf_iter == this->gs_descriptions.end()) {
1,057✔
1002
                full_desc_sf = string_fragment::from_str(full_desc).to_owned(
2,114✔
1003
                    this->gs_allocator);
1,057✔
1004
            }
1005
            pair.second.or_description = full_desc_sf;
1,057✔
1006
        } else {
1007
            full_desc += pair.second.or_description;
4✔
1008
        }
1009

1010
        shared_buffer sb_opid;
1,061✔
1011
        shared_buffer_ref sbr_opid;
1,061✔
1012
        sbr_opid.share(
1,061✔
1013
            sb_opid, pair.second.or_name.data(), pair.second.or_name.length());
1,061✔
1014
        shared_buffer sb_desc;
1,061✔
1015
        shared_buffer_ref sbr_desc;
1,061✔
1016
        sbr_desc.share(sb_desc, full_desc.c_str(), full_desc.length());
1,061✔
1017
        if (this->tss_apply_filters) {
1,061✔
1018
            auto filtered_in = false;
1,061✔
1019
            auto filtered_out = false;
1,061✔
1020
            for (const auto& filt : this->tss_filters) {
1,229✔
1021
                if (!filt->is_enabled()) {
168✔
1022
                    continue;
×
1023
                }
1024
                for (const auto sbr : {&sbr_opid, &sbr_desc}) {
504✔
1025
                    if (filt->matches(std::nullopt, *sbr)) {
336✔
1026
                        this->gs_filter_hits[filt->get_index()] += 1;
2✔
1027
                        switch (filt->get_type()) {
2✔
1028
                            case text_filter::INCLUDE:
1✔
1029
                                filtered_in = true;
1✔
1030
                                break;
1✔
1031
                            case text_filter::EXCLUDE:
1✔
1032
                                filtered_out = true;
1✔
1033
                                break;
1✔
1034
                            default:
×
1035
                                break;
×
1036
                        }
1037
                    }
1038
                }
1039
            }
1040

1041
            if (min_log_time_opt
1,061✔
1042
                && otr.otr_range.tr_end < min_log_time_opt.value())
1,061✔
1043
            {
1044
                filtered_out = true;
16✔
1045
            }
1046
            if (max_log_time_opt
1,061✔
1047
                && max_log_time_opt.value() < otr.otr_range.tr_begin)
1,061✔
1048
            {
1049
                filtered_out = true;
16✔
1050
            }
1051

1052
            if ((filtered_in_count > 0 && !filtered_in) || filtered_out) {
1,061✔
1053
                this->gs_filtered_count += 1;
116✔
1054
                continue;
116✔
1055
            }
1056
        }
1057

1058
        if (pair.second.or_name.length() > this->gs_opid_width) {
945✔
1059
            this->gs_opid_width = pair.second.or_name.length();
54✔
1060
        }
1061
        if (full_desc.size() > max_desc_width) {
945✔
1062
            max_desc_width = full_desc.size();
28✔
1063
        }
1064

1065
        if (this->gs_lower_bound == 0us
945✔
1066
            || pair.second.or_value.otr_range.tr_begin < this->gs_lower_bound)
945✔
1067
        {
1068
            this->gs_lower_bound = pair.second.or_value.otr_range.tr_begin;
72✔
1069
        }
1070
        if (this->gs_upper_bound == 0us
945✔
1071
            || this->gs_upper_bound < pair.second.or_value.otr_range.tr_end)
945✔
1072
        {
1073
            this->gs_upper_bound = pair.second.or_value.otr_range.tr_end;
73✔
1074
        }
1075

1076
        this->gs_time_order.emplace_back(&pair.second);
945✔
1077
    }
1,525✔
1078
    std::stable_sort(
28✔
1079
        this->gs_time_order.begin(),
1080
        this->gs_time_order.end(),
1081
        [](const auto* lhs, const auto* rhs) { return *lhs < *rhs; });
5,203✔
1082
    for (size_t lpc = 0; lpc < this->gs_time_order.size(); lpc++) {
973✔
1083
        const auto& row = *this->gs_time_order[lpc];
945✔
1084
        if (row.or_type == row_type::logfile) {
945✔
1085
            bm_files.insert_once(vis_line_t(lpc));
28✔
1086
        } else if (row.or_type == row_type::tag) {
917✔
1087
            bm_meta.insert_once(vis_line_t(lpc));
×
1088
        } else if (row.or_type == row_type::partition) {
917✔
1089
            bm_parts.insert_once(vis_line_t(lpc));
2✔
1090
        }
1091
        if (row.or_value.otr_level_stats.lls_error_count > 0) {
945✔
1092
            bm_errs.insert_once(vis_line_t(lpc));
61✔
1093
        } else if (row.or_value.otr_level_stats.lls_warning_count > 0) {
884✔
1094
            bm_warns.insert_once(vis_line_t(lpc));
7✔
1095
        }
1096
    }
1097

1098
    this->gs_opid_width = std::min(this->gs_opid_width, MAX_OPID_WIDTH);
28✔
1099
    this->gs_total_width
1100
        = std::max<size_t>(22 + this->gs_opid_width + max_desc_width,
56✔
1101
                           1 + 16 + 5 + 8 + 5 + 16 + 1 /* header */);
28✔
1102

1103
    this->tss_view->set_needs_update();
28✔
1104
    this->ts_rebuild_in_progress = false;
28✔
1105

1106
    ensure(this->gs_time_order.empty() || this->gs_opid_width > 0);
28✔
1107

1108
    return true;
28✔
1109
}
28✔
1110

1111
std::optional<vis_line_t>
1112
timeline_source::row_for_time(timeval time_bucket)
9✔
1113
{
1114
    auto time_bucket_us = to_us(time_bucket);
9✔
1115
    auto iter = this->gs_time_order.begin();
9✔
1116
    while (true) {
1117
        if (iter == this->gs_time_order.end()) {
11✔
1118
            return std::nullopt;
1✔
1119
        }
1120

1121
        if ((*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
10✔
1122
            break;
8✔
1123
        }
1124
        ++iter;
2✔
1125
    }
1126

1127
    auto closest_iter = iter;
8✔
1128
    auto closest_diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
8✔
1129
    for (; iter != this->gs_time_order.end(); ++iter) {
28✔
1130
        if (time_bucket_us < (*iter)->or_value.otr_range.tr_begin) {
27✔
1131
            break;
7✔
1132
        }
1133
        if (!(*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
20✔
1134
            continue;
4✔
1135
        }
1136

1137
        auto diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
16✔
1138
        if (diff < closest_diff) {
16✔
1139
            closest_iter = iter;
2✔
1140
            closest_diff = diff;
2✔
1141
        }
1142

1143
        for (const auto& sub : (*iter)->or_value.otr_sub_ops) {
16✔
1144
            if (!sub.ostr_range.contains_inclusive(time_bucket_us)) {
×
1145
                continue;
×
1146
            }
1147

1148
            diff = time_bucket_us - sub.ostr_range.tr_begin;
×
1149
            if (diff < closest_diff) {
×
1150
                closest_iter = iter;
×
1151
                closest_diff = diff;
×
1152
            }
1153
        }
1154
    }
1155

1156
    return vis_line_t(std::distance(this->gs_time_order.begin(), closest_iter));
16✔
1157
}
1158

1159
std::optional<vis_line_t>
1160
timeline_source::row_for(const row_info& ri)
22✔
1161
{
1162
    auto vl_opt = this->gs_lss.row_for(ri);
22✔
1163
    if (!vl_opt) {
22✔
1164
        return this->row_for_time(ri.ri_time);
×
1165
    }
1166

1167
    auto vl = vl_opt.value();
22✔
1168
    auto win = this->gs_lss.window_at(vl);
22✔
1169
    for (const auto& msg_line : *win) {
40✔
1170
        const auto& lvv = msg_line.get_values();
22✔
1171

1172
        if (lvv.lvv_opid_value) {
22✔
1173
            auto opid_iter
1174
                = this->gs_active_opids.find(lvv.lvv_opid_value.value());
16✔
1175
            if (opid_iter != this->gs_active_opids.end()) {
16✔
1176
                for (const auto& [index, oprow] :
168✔
1177
                     lnav::itertools::enumerate(this->gs_time_order))
171✔
1178
                {
1179
                    if (oprow == &opid_iter->second) {
152✔
1180
                        return vis_line_t(index);
13✔
1181
                    }
1182
                }
1183
            }
1184
        }
1185
    }
35✔
1186

1187
    return this->row_for_time(ri.ri_time);
9✔
1188
}
22✔
1189

1190
std::optional<text_time_translator::row_info>
1191
timeline_source::time_for_row(vis_line_t row)
30✔
1192
{
1193
    if (row >= this->gs_time_order.size()) {
30✔
1194
        return std::nullopt;
×
1195
    }
1196

1197
    const auto& otr = this->gs_time_order[row]->or_value;
30✔
1198

1199
    if (this->tss_view->get_selection() == row) {
30✔
1200
        auto ov_sel = this->tss_view->get_overlay_selection();
30✔
1201

1202
        if (ov_sel && ov_sel.value() < otr.otr_sub_ops.size()) {
30✔
1203
            return row_info{
×
1204
                to_timeval(otr.otr_sub_ops[ov_sel.value()].ostr_range.tr_begin),
×
1205
                row,
1206
            };
1207
        }
1208
    }
1209

1210
    auto preview_selection = this->gs_preview_view.get_selection();
30✔
1211
    if (!preview_selection) {
30✔
1212
        return std::nullopt;
×
1213
    }
1214
    if (preview_selection < this->gs_preview_rows.size()) {
30✔
1215
        return this->gs_preview_rows[preview_selection.value()];
28✔
1216
    }
1217

1218
    return row_info{
4✔
1219
        to_timeval(otr.otr_range.tr_begin),
2✔
1220
        row,
1221
    };
4✔
1222
}
1223

1224
size_t
1225
timeline_source::text_line_width(textview_curses& curses)
6,813✔
1226
{
1227
    return this->gs_total_width;
6,813✔
1228
}
1229

1230
void
1231
timeline_source::text_selection_changed(textview_curses& tc)
56✔
1232
{
1233
    static const size_t MAX_PREVIEW_LINES = 200;
1234

1235
    auto sel = tc.get_selection();
56✔
1236

1237
    this->gs_preview_source.clear();
56✔
1238
    this->gs_preview_rows.clear();
56✔
1239
    if (!sel || sel.value() >= this->gs_time_order.size()) {
56✔
1240
        return;
32✔
1241
    }
1242

1243
    const auto& row = *this->gs_time_order[sel.value()];
24✔
1244
    auto low_us = row.or_value.otr_range.tr_begin;
24✔
1245
    auto high_us = row.or_value.otr_range.tr_end;
24✔
1246
    auto id_sf = row.or_name;
24✔
1247
    auto level_stats = row.or_value.otr_level_stats;
24✔
1248
    auto ov_sel = tc.get_overlay_selection();
24✔
1249
    if (ov_sel) {
24✔
1250
        const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
1251
        id_sf = sub.ostr_subid;
×
1252
        low_us = sub.ostr_range.tr_begin;
×
1253
        high_us = sub.ostr_range.tr_end;
×
1254
        level_stats = sub.ostr_level_stats;
×
1255
    }
1256
    high_us += 1s;
24✔
1257
    auto low_vl = this->gs_lss.row_for_time(to_timeval(low_us));
24✔
1258
    auto high_vl = this->gs_lss.row_for_time(to_timeval(high_us))
24✔
1259
                       .value_or(vis_line_t(this->gs_lss.text_line_count()));
24✔
1260

1261
    if (!low_vl) {
24✔
1262
        return;
×
1263
    }
1264

1265
    auto preview_content = attr_line_t();
24✔
1266
    auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
24✔
1267
    auto win = this->gs_lss.window_at(low_vl.value(), high_vl);
24✔
1268
    auto id_bloom_bits = row.or_name.bloom_bits();
24✔
1269
    auto msg_count = 0;
24✔
1270
    for (const auto& msg_line : *win) {
1,474✔
1271
        switch (row.or_type) {
725✔
1272
            case row_type::logfile:
397✔
1273
                if (msg_line.get_file_ptr() != row.or_logfile) {
397✔
1274
                    continue;
×
1275
                }
1276
                break;
397✔
1277
            case row_type::thread: {
69✔
1278
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
69✔
1279
                    continue;
61✔
1280
                }
1281
                const auto& lvv = msg_line.get_values();
8✔
1282
                if (!lvv.lvv_thread_id_value) {
8✔
1283
                    continue;
×
1284
                }
1285
                auto tid_sf = lvv.lvv_thread_id_value.value();
8✔
1286
                if (!(tid_sf == row.or_name)) {
8✔
1287
                    continue;
×
1288
                }
1289
                break;
8✔
1290
            }
8✔
1291
            case row_type::opid: {
259✔
1292
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
259✔
1293
                    continue;
233✔
1294
                }
1295

1296
                const auto& lvv = msg_line.get_values();
26✔
1297
                if (!lvv.lvv_opid_value) {
26✔
1298
                    continue;
×
1299
                }
1300
                auto opid_sf = lvv.lvv_opid_value.value();
26✔
1301

1302
                if (!(opid_sf == row.or_name)) {
26✔
1303
                    continue;
×
1304
                }
1305
                break;
26✔
1306
            }
26✔
1307
            case row_type::tag: {
×
1308
                const auto& bm
1309
                    = msg_line.get_file_ptr()->get_bookmark_metadata();
×
1310
                auto bm_iter = bm.find(msg_line.get_file_line_number());
×
1311
                if (bm_iter == bm.end()) {
×
1312
                    continue;
×
1313
                }
1314
                auto tag_name = row.or_name.to_string();
×
1315
                if (!(bm_iter->second.bm_tags
×
1316
                      | lnav::itertools::find(tag_name)))
×
1317
                {
1318
                    continue;
×
1319
                }
1320
                break;
×
1321
            }
1322
            case row_type::partition:
×
1323
                break;
×
1324
        }
1325

1326
        for (size_t lpc = 0; lpc < msg_line.get_line_count(); lpc++) {
865✔
1327
            auto vl = msg_line.get_vis_line() + vis_line_t(lpc);
434✔
1328
            auto cl = this->gs_lss.at(vl);
434✔
1329
            auto row_al = attr_line_t();
434✔
1330
            this->gs_log_view.textview_value_for_row(vl, row_al);
434✔
1331
            preview_content.append(row_al).append("\n");
434✔
1332
            this->gs_preview_rows.emplace_back(
434✔
1333
                msg_line.get_logline().get_timeval(), cl);
434✔
1334
            ++cl;
434✔
1335
        }
434✔
1336
        msg_count += 1;
431✔
1337
        msgs_remaining -= 1;
431✔
1338
        if (msgs_remaining == 0) {
431✔
1339
            break;
×
1340
        }
1341
    }
24✔
1342

1343
    this->gs_preview_source.replace_with(preview_content);
24✔
1344
    this->gs_preview_view.set_selection(0_vl);
24✔
1345
    this->gs_preview_status_source.get_description().set_value(
24✔
1346
        " ID %.*s", id_sf.length(), id_sf.data());
1347
    auto err_count = level_stats.lls_error_count;
24✔
1348
    if (err_count == 0) {
24✔
1349
        this->gs_preview_status_source
16✔
1350
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
16✔
1351
            .set_value("");
16✔
1352
    } else if (err_count > 1) {
8✔
1353
        this->gs_preview_status_source
3✔
1354
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
3✔
1355
            .set_value("%'d errors", err_count);
3✔
1356
    } else {
1357
        this->gs_preview_status_source
5✔
1358
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
5✔
1359
            .set_value("%'d error", err_count);
5✔
1360
    }
1361
    if (msg_count < level_stats.lls_total_count) {
24✔
1362
        this->gs_preview_status_source
22✔
1363
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
22✔
1364
            .set_value(
22✔
1365
                "%'d of %'d messages ", msg_count, level_stats.lls_total_count);
1366
    } else {
1367
        this->gs_preview_status_source
2✔
1368
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
2✔
1369
            .set_value("%'d messages ", level_stats.lls_total_count);
2✔
1370
    }
1371
    this->gs_preview_status_view.set_needs_update();
24✔
1372
}
24✔
1373

1374
void
1375
timeline_source::text_filters_changed()
12✔
1376
{
1377
    this->rebuild_indexes();
12✔
1378
    this->tss_view->reload_data();
12✔
1379
    this->tss_view->redo_search();
12✔
1380
}
12✔
1381

1382
int
1383
timeline_source::get_filtered_count() const
49✔
1384
{
1385
    return this->gs_filtered_count;
49✔
1386
}
1387

1388
int
1389
timeline_source::get_filtered_count_for(size_t filter_index) const
×
1390
{
1391
    return this->gs_filter_hits[filter_index];
×
1392
}
1393

1394
static const std::vector<breadcrumb::possibility>&
1395
timestamp_poss()
×
1396
{
1397
    const static std::vector<breadcrumb::possibility> retval = {
1398
        breadcrumb::possibility{"-1 day"},
1399
        breadcrumb::possibility{"-1h"},
1400
        breadcrumb::possibility{"-30m"},
1401
        breadcrumb::possibility{"-15m"},
1402
        breadcrumb::possibility{"-5m"},
1403
        breadcrumb::possibility{"-1m"},
1404
        breadcrumb::possibility{"+1m"},
1405
        breadcrumb::possibility{"+5m"},
1406
        breadcrumb::possibility{"+15m"},
1407
        breadcrumb::possibility{"+30m"},
1408
        breadcrumb::possibility{"+1h"},
1409
        breadcrumb::possibility{"+1 day"},
1410
    };
1411

1412
    return retval;
×
1413
}
1414

1415
void
1416
timeline_source::text_crumbs_for_line(int line,
×
1417
                                      std::vector<breadcrumb::crumb>& crumbs)
1418
{
1419
    text_sub_source::text_crumbs_for_line(line, crumbs);
×
1420

1421
    if (line >= this->gs_time_order.size()) {
×
1422
        return;
×
1423
    }
1424

1425
    const auto& row = *this->gs_time_order[line];
×
1426
    char ts[64];
1427

1428
    sql_strftime(ts, sizeof(ts), row.or_value.otr_range.tr_begin, 'T');
×
1429

1430
    crumbs.emplace_back(std::string(ts),
×
1431
                        timestamp_poss,
1432
                        [ec = this->gs_exec_context](const auto& ts) {
×
1433
                            auto cmd
×
1434
                                = fmt::format(FMT_STRING(":goto {}"),
×
1435
                                              ts.template get<std::string>());
1436
                            ec->execute(INTERNAL_SRC_LOC, cmd);
×
1437
                        });
×
1438
    crumbs.back().c_expected_input
×
1439
        = breadcrumb::crumb::expected_input_t::anything;
×
1440
    crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
×
1441
}
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