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

tstack / lnav / 23836075406-2909

01 Apr 2026 06:51AM UTC coverage: 69.084% (+0.007%) from 69.077%
23836075406-2909

push

github

tstack
[tags] only save user-provided tags in the session, not format-provided

60 of 76 new or added lines in 14 files covered. (78.95%)

2 existing lines in 2 files now uncovered.

53287 of 77134 relevant lines covered (69.08%)

535481.86 hits per line

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

64.96
/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 "lnav_util.hh"
47
#include "logline_window.hh"
48
#include "md4cpp.hh"
49
#include "pcrepp/pcre2pp.hh"
50
#include "readline_highlighters.hh"
51
#include "sql_util.hh"
52
#include "sysclip.hh"
53
#include "tlx/container/btree_map.hpp"
54

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

173
    return retval;
×
174
}
×
175

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

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

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

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

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

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

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

260
        return false;
×
261
    }
262

263
    if (y == 0) {
62✔
264
        auto sel = lv.get_selection().value_or(0_vl);
39✔
265
        if (sel < this->gho_src->tss_view->get_top()) {
39✔
266
            return true;
×
267
        }
268
        const auto& row = *this->gho_src->ts_time_order[sel];
39✔
269
        auto tr = row.or_value.otr_range;
39✔
270
        auto [lb, ub] = this->gho_src->get_time_bounds_for(sel);
39✔
271
        auto sel_begin_us = tr.tr_begin - lb;
39✔
272
        auto sel_end_us = tr.tr_end - lb;
39✔
273

274
        require(sel_begin_us >= 0us);
39✔
275
        require(sel_end_us >= 0us);
39✔
276

277
        auto [height, width] = lv.get_dimensions();
39✔
278
        if (width <= CHART_INDENT) {
39✔
279
            return true;
×
280
        }
281

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

340
        auto hdr_attrs = text_attrs::with_underline();
39✔
341
        value_out.with_attr_for_all(VC_STYLE.value(hdr_attrs))
39✔
342
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO));
39✔
343

344
        return true;
39✔
345
    }
39✔
346

347
    auto& tc = dynamic_cast<textview_curses&>(const_cast<listview_curses&>(lv));
23✔
348
    const auto& sticky_bv = tc.get_bookmarks()[&textview_curses::BM_STICKY];
23✔
349
    auto top = lv.get_top();
23✔
350
    auto sticky_range = sticky_bv.equal_range(0_vl, top);
23✔
351
    auto sticky_index = y - 1;
23✔
352
    if (sticky_index < static_cast<int>(
23✔
353
            std::distance(sticky_range.first, sticky_range.second)))
23✔
354
    {
355
        auto iter = std::next(sticky_range.first, sticky_index);
2✔
356
        tc.textview_value_for_row(*iter, value_out);
2✔
357
        value_out.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
2✔
358
        auto next_iter = std::next(iter);
2✔
359
        if (next_iter == sticky_range.second) {
2✔
360
            value_out.with_attr_for_all(
2✔
361
                VC_STYLE.value(text_attrs::with_underline()));
4✔
362
        }
363
        return true;
2✔
364
    }
365

366
    return false;
21✔
367
}
368
void
369
timeline_header_overlay::list_value_for_overlay(
470✔
370
    const listview_curses& lv,
371
    vis_line_t line,
372
    std::vector<attr_line_t>& value_out)
373
{
374
    if (!this->gho_show_details) {
470✔
375
        return;
470✔
376
    }
377

378
    if (lv.get_selection() != line) {
×
379
        return;
×
380
    }
381

382
    if (line >= this->gho_src->ts_time_order.size()) {
×
383
        return;
×
384
    }
385

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

388
    if (row.or_value.otr_sub_ops.size() <= 1) {
×
389
        return;
×
390
    }
391

392
    auto width = lv.get_dimensions().second;
×
393

394
    if (width < 37) {
×
395
        return;
×
396
    }
397

398
    width -= 37;
×
399
    double span = row.or_value.otr_range.duration().count();
×
400
    double per_ch = span / (double) width;
×
401

402
    for (const auto& sub : row.or_value.otr_sub_ops) {
×
403
        value_out.resize(value_out.size() + 1);
×
404

405
        auto& al = value_out.back();
×
406
        auto& attrs = al.get_attrs();
×
407
        auto total_msgs = sub.ostr_level_stats.lls_total_count;
×
408
        auto duration = sub.ostr_range.tr_end - sub.ostr_range.tr_begin;
×
409
        auto duration_str = fmt::format(
410
            FMT_STRING(" {: >13}"),
×
411
            humanize::time::duration::from_tv(to_timeval(duration))
×
412
                .to_string());
×
413
        al.pad_to(14)
×
414
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
×
415
            .append(" ")
×
416
            .append(lnav::roles::error(humanize::sparkline(
×
417
                sub.ostr_level_stats.lls_error_count, total_msgs)))
×
418
            .append(lnav::roles::warning(humanize::sparkline(
×
419
                sub.ostr_level_stats.lls_warning_count, total_msgs)))
×
420
            .append(" ")
×
421
            .append(lnav::roles::identifier(sub.ostr_subid.to_string()))
×
422
            .append(row.or_max_subid_width
×
423
                        - sub.ostr_subid.utf8_length().unwrapOr(
×
424
                            row.or_max_subid_width),
×
425
                    ' ')
426
            .append(sub.ostr_description);
×
427
        al.with_attr_for_all(VC_ROLE.value(role_t::VCR_COMMENT));
×
428

429
        auto start_diff = (double) (sub.ostr_range.tr_begin
×
430
                                    - row.or_value.otr_range.tr_begin)
×
431
                              .count();
×
432
        auto end_diff
433
            = (double) (sub.ostr_range.tr_end - row.or_value.otr_range.tr_begin)
×
434
                  .count();
×
435

436
        auto lr = line_range{
437
            (int) (32 + (start_diff / per_ch)),
×
438
            (int) (32 + (end_diff / per_ch)),
×
439
            line_range::unit::codepoint,
440
        };
441

442
        if (lr.lr_start == lr.lr_end) {
×
443
            lr.lr_end += 1;
×
444
        }
445

446
        auto block_attrs = text_attrs::with_reverse();
×
447
        attrs.emplace_back(lr, VC_STYLE.value(block_attrs));
×
448
    }
449
    if (!value_out.empty()) {
×
450
        value_out.back().get_attrs().emplace_back(
×
451
            line_range{0, -1}, VC_STYLE.value(text_attrs::with_underline()));
×
452
    }
453
}
454

455
std::optional<attr_line_t>
456
timeline_header_overlay::list_header_for_overlay(const listview_curses& lv,
×
457
                                                 media_t media,
458
                                                 vis_line_t line)
459
{
460
    if (lv.get_overlay_selection()) {
×
461
        return attr_line_t("\u258C Sub-operations: Press ")
×
462
            .append("Esc"_hotkey)
×
463
            .append(" to exit this panel");
×
464
    }
465
    return attr_line_t("\u258C Sub-operations: Press ")
×
466
        .append("CTRL-]"_hotkey)
×
467
        .append(" to focus on this panel");
×
468
}
469

470
timeline_source::timeline_source(textview_curses& log_view,
696✔
471
                                 logfile_sub_source& lss,
472
                                 textview_curses& preview_view,
473
                                 plain_text_source& preview_source,
474
                                 statusview_curses& preview_status_view,
475
                                 timeline_status_source& preview_status_source)
696✔
476
    : ts_log_view(log_view), ts_lss(lss), ts_preview_view(preview_view),
696✔
477
      ts_preview_source(preview_source),
696✔
478
      ts_preview_status_view(preview_status_view),
696✔
479
      ts_preview_status_source(preview_status_source)
696✔
480
{
481
    this->tss_supports_filtering = true;
696✔
482
    this->ts_preview_view.set_overlay_source(&this->ts_preview_overlay);
696✔
483
}
696✔
484

485
std::optional<timeline_source::row_type>
486
timeline_source::row_type_from_string(const std::string& str)
13✔
487
{
488
    if (str == "logfile") {
13✔
489
        return row_type::logfile;
2✔
490
    }
491
    if (str == "thread") {
11✔
492
        return row_type::thread;
2✔
493
    }
494
    if (str == "opid") {
9✔
495
        return row_type::opid;
8✔
496
    }
497
    if (str == "tag") {
1✔
498
        return row_type::tag;
×
499
    }
500
    if (str == "partition") {
1✔
501
        return row_type::partition;
×
502
    }
503
    return std::nullopt;
1✔
504
}
505

506
const char*
507
timeline_source::row_type_to_string(row_type rt)
2✔
508
{
509
    switch (rt) {
2✔
510
        case row_type::logfile:
×
511
            return "logfile";
×
512
        case row_type::thread:
2✔
513
            return "thread";
2✔
514
        case row_type::opid:
×
515
            return "opid";
×
516
        case row_type::tag:
×
517
            return "tag";
×
518
        case row_type::partition:
×
519
            return "partition";
×
520
    }
521
    return "unknown";
×
522
}
523

524
void
525
timeline_source::set_row_type_visibility(row_type rt, bool visible)
5✔
526
{
527
    if (visible) {
5✔
528
        this->ts_hidden_row_types.erase(rt);
1✔
529
    } else {
530
        this->ts_hidden_row_types.insert(rt);
4✔
531
    }
532
}
5✔
533

534
bool
535
timeline_source::is_row_type_visible(row_type rt) const
1,160✔
536
{
537
    return this->ts_hidden_row_types.find(rt)
1,160✔
538
        == this->ts_hidden_row_types.end();
2,320✔
539
}
540

541
bool
542
timeline_source::list_input_handle_key(listview_curses& lv, const ncinput& ch)
×
543
{
544
    switch (ch.eff_text[0]) {
×
545
        case 'q':
×
546
        case KEY_ESCAPE: {
547
            if (this->ts_preview_focused) {
×
548
                this->ts_preview_focused = false;
×
549
                this->ts_preview_view.set_height(5_vl);
×
550
                this->ts_preview_status_view.set_enabled(
×
551
                    this->ts_preview_focused);
×
552
                this->tss_view->set_enabled(!this->ts_preview_focused);
×
553
                return true;
×
554
            }
555
            break;
×
556
        }
557
        case '\n':
×
558
        case '\r':
559
        case NCKEY_ENTER: {
560
            this->ts_preview_focused = !this->ts_preview_focused;
×
561
            this->ts_preview_status_view.set_enabled(this->ts_preview_focused);
×
562
            this->tss_view->set_enabled(!this->ts_preview_focused);
×
563
            if (this->ts_preview_focused) {
×
564
                auto height = this->tss_view->get_dimensions().first;
×
565

566
                if (height > 5) {
×
567
                    this->ts_preview_view.set_height(height / 2_vl);
×
568
                }
569
            } else {
570
                this->ts_preview_view.set_height(5_vl);
×
571
            }
572
            return true;
×
573
        }
574
    }
575
    if (this->ts_preview_focused) {
×
576
        return this->ts_preview_view.handle_key(ch);
×
577
    }
578

579
    return false;
×
580
}
581

582
bool
583
timeline_source::text_handle_mouse(
×
584
    textview_curses& tc,
585
    const listview_curses::display_line_content_t&,
586
    mouse_event& me)
587
{
588
    auto nci = ncinput{};
×
589
    if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{0, -1})) {
×
590
        nci.id = '\r';
×
591
        nci.eff_text[0] = '\r';
×
592
        this->list_input_handle_key(tc, nci);
×
593
    }
594

595
    return false;
×
596
}
597

598
std::pair<std::chrono::microseconds, std::chrono::microseconds>
599
timeline_source::get_time_bounds_for(int line)
493✔
600
{
601
    const auto low_index = this->tss_view->get_top();
493✔
602
    auto high_index
603
        = std::min(this->tss_view->get_bottom(),
493✔
604
                   vis_line_t((int) this->ts_time_order.size() - 1));
493✔
605
    if (high_index == low_index) {
493✔
606
        high_index = vis_line_t(this->ts_time_order.size() - 1);
493✔
607
    }
608
    const auto& low_row = *this->ts_time_order[low_index];
493✔
609
    const auto& high_row = *this->ts_time_order[high_index];
493✔
610
    auto low_us = low_row.or_value.otr_range.tr_begin;
493✔
611
    auto high_us = high_row.or_value.otr_range.tr_begin;
493✔
612

613
    auto duration = high_us - low_us;
493✔
614
    auto span_iter
615
        = std::upper_bound(TIME_SPANS.begin(), TIME_SPANS.end(), duration);
493✔
616
    if (span_iter == TIME_SPANS.end()) {
493✔
617
        --span_iter;
7✔
618
    }
619
    auto span_portion = *span_iter / 8;
493✔
620
    auto lb = low_us;
493✔
621
    lb = rounddown(lb, span_portion);
493✔
622
    auto ub = high_us;
493✔
623
    ub = roundup(ub, span_portion);
493✔
624

625
    ensure(lb <= ub);
493✔
626
    return {lb, ub};
493✔
627
}
628

629
size_t
630
timeline_source::text_line_count()
9,996✔
631
{
632
    return this->ts_time_order.size();
9,996✔
633
}
634

635
line_info
636
timeline_source::text_value_for_line(textview_curses& tc,
454✔
637
                                     int line,
638
                                     std::string& value_out,
639
                                     line_flags_t flags)
640
{
641
    if (!this->ts_rebuild_in_progress
908✔
642
        && line < (ssize_t) this->ts_time_order.size())
454✔
643
    {
644
        const auto& row = *this->ts_time_order[line];
454✔
645
        auto duration
646
            = row.or_value.otr_range.tr_end - row.or_value.otr_range.tr_begin;
454✔
647
        auto duration_str = fmt::format(
648
            FMT_STRING("{: >13}"),
1,362✔
649
            humanize::time::duration::from_tv(to_timeval(duration))
454✔
650
                .to_string());
908✔
651

652
        this->ts_rendered_line.clear();
454✔
653

654
        auto total_msgs = row.or_value.otr_level_stats.lls_total_count;
454✔
655
        auto truncated_name
656
            = attr_line_t::from_table_cell_content(row.or_name, MAX_OPID_WIDTH);
454✔
657
        auto truncated_desc = attr_line_t::from_table_cell_content(
658
            row.or_description, MAX_DESC_WIDTH);
454✔
659
        std::optional<ui_icon_t> icon;
454✔
660
        auto padding = 1;
454✔
661
        switch (row.or_type) {
454✔
662
            case row_type::logfile:
20✔
663
                icon = ui_icon_t::file;
20✔
664
                break;
20✔
665
            case row_type::thread:
57✔
666
                icon = ui_icon_t::thread;
57✔
667
                break;
57✔
668
            case row_type::opid:
375✔
669
                padding = 3;
375✔
670
                break;
375✔
671
            case row_type::tag:
×
672
                icon = ui_icon_t::tag;
×
673
                break;
×
674
            case row_type::partition:
2✔
675
                icon = ui_icon_t::partition;
2✔
676
                break;
2✔
677
        }
678
        if (this->ts_preview_hidden_row_types.count(row.or_type) > 0) {
454✔
679
            this->ts_rendered_line.append(
×
680
                "-",
681
                VC_STYLE.value(text_attrs{
×
682
                    lnav::enums::to_underlying(text_attrs::style::blink),
683
                    styling::color_unit::from_palette(
×
684
                        lnav::enums::to_underlying(ansi_color::red)),
×
685
                }));
686
        } else {
687
            this->ts_rendered_line.append(" ");
454✔
688
        }
689

690
        this->ts_rendered_line
691
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
908✔
692
            .append("  ")
454✔
693
            .append(lnav::roles::error(humanize::sparkline(
1,362✔
694
                row.or_value.otr_level_stats.lls_error_count, total_msgs)))
454✔
695
            .append(lnav::roles::warning(humanize::sparkline(
1,362✔
696
                row.or_value.otr_level_stats.lls_warning_count, total_msgs)))
454✔
697
            .append("  ")
454✔
698
            .append(icon)
454✔
699
            .append(padding, ' ')
454✔
700
            .append(lnav::roles::identifier(truncated_name))
908✔
701
            .append(
908✔
702
                this->ts_opid_width - truncated_name.utf8_length_or_length(),
454✔
703
                ' ')
704
            .append(truncated_desc);
454✔
705
        this->ts_rendered_line.with_attr_for_all(
454✔
706
            VC_ROLE.value(role_t::VCR_COMMENT));
908✔
707

708
        value_out = this->ts_rendered_line.get_string();
454✔
709
    }
454✔
710

711
    return {};
454✔
712
}
713

714
void
715
timeline_source::text_attrs_for_line(textview_curses& tc,
454✔
716
                                     int line,
717
                                     string_attrs_t& value_out)
718
{
719
    if (!this->ts_rebuild_in_progress
908✔
720
        && line < (ssize_t) this->ts_time_order.size())
454✔
721
    {
722
        const auto& row = *this->ts_time_order[line];
454✔
723

724
        value_out = this->ts_rendered_line.get_attrs();
454✔
725

726
        auto lr = line_range{-1, -1, line_range::unit::codepoint};
454✔
727
        auto [sel_lb, sel_ub]
454✔
728
            = this->get_time_bounds_for(tc.get_selection().value_or(0_vl));
454✔
729

730
        if (row.or_value.otr_range.tr_begin <= sel_ub
454✔
731
            && sel_lb <= row.or_value.otr_range.tr_end)
454✔
732
        {
733
            auto width = tc.get_dimensions().second;
454✔
734

735
            if (width > CHART_INDENT) {
454✔
736
                width -= CHART_INDENT;
454✔
737
                const double span = (sel_ub - sel_lb).count();
454✔
738
                auto us_per_ch = std::chrono::microseconds{
739
                    static_cast<int64_t>(ceil(span / (double) width))};
454✔
740

741
                if (row.or_value.otr_range.tr_begin <= sel_lb) {
454✔
742
                    lr.lr_start = CHART_INDENT;
×
743
                } else {
744
                    auto start_diff
745
                        = (row.or_value.otr_range.tr_begin - sel_lb);
454✔
746

747
                    lr.lr_start = CHART_INDENT + floor(start_diff / us_per_ch);
454✔
748
                }
749

750
                if (sel_ub < row.or_value.otr_range.tr_end) {
454✔
751
                    lr.lr_end = -1;
13✔
752
                } else {
753
                    auto end_diff = (row.or_value.otr_range.tr_end - sel_lb);
441✔
754

755
                    lr.lr_end = CHART_INDENT + ceil(end_diff / us_per_ch);
441✔
756
                    if (lr.lr_start == lr.lr_end) {
441✔
757
                        lr.lr_end += 1;
339✔
758
                    }
759
                }
760

761
                auto block_attrs = text_attrs::with_reverse();
454✔
762
                require(lr.lr_start >= 0);
454✔
763
                value_out.emplace_back(lr, VC_STYLE.value(block_attrs));
454✔
764
            }
765
        }
766
        auto alt_row_index = line % 4;
454✔
767
        if (alt_row_index == 2 || alt_row_index == 3) {
454✔
768
            value_out.emplace_back(line_range{0, -1},
219✔
769
                                   VC_ROLE.value(role_t::VCR_ALT_ROW));
438✔
770
        }
771
    }
772
}
454✔
773

774
size_t
775
timeline_source::text_size_for_line(textview_curses& tc,
×
776
                                    int line,
777
                                    text_sub_source::line_flags_t raw)
778
{
779
    return this->ts_total_width;
×
780
}
781

782
bool
783
timeline_source::rebuild_indexes()
41✔
784
{
785
    static auto op = lnav_operation{"timeline_rebuild"};
41✔
786

787
    auto op_guard = lnav_opid_guard::internal(op);
41✔
788
    auto& bm = this->tss_view->get_bookmarks();
41✔
789
    auto& bm_files = bm[&logfile_sub_source::BM_FILES];
41✔
790
    auto& bm_errs = bm[&textview_curses::BM_ERRORS];
41✔
791
    auto& bm_warns = bm[&textview_curses::BM_WARNINGS];
41✔
792
    auto& bm_meta = bm[&textview_curses::BM_META];
41✔
793
    auto& bm_parts = bm[&textview_curses::BM_PARTITION];
41✔
794

795
    this->ts_rebuild_in_progress = true;
41✔
796

797
    static const bookmark_type_t* PRESERVE_TYPES[] = {
798
        &textview_curses::BM_USER,
799
        &textview_curses::BM_STICKY,
800
    };
801
    for (const auto* bm_type : PRESERVE_TYPES) {
123✔
802
        auto& bv = bm[bm_type];
82✔
803
        for (const auto& vl : bv.bv_tree) {
82✔
804
            auto line = static_cast<size_t>(vl);
×
805
            if (line < this->ts_time_order.size()) {
×
806
                const auto& row = *this->ts_time_order[line];
×
807
                this->ts_pending_bookmarks.emplace_back(pending_bookmark{
×
808
                    row.or_type,
×
809
                    row.or_name.to_string(),
810
                    bm_type,
811
                });
812
            }
813
        }
814
        bv.clear();
82✔
815
    }
816

817
    bm.clear();
41✔
818

819
    this->ts_lower_bound = {};
41✔
820
    this->ts_upper_bound = {};
41✔
821
    this->ts_opid_width = 0;
41✔
822
    this->ts_total_width = 0;
41✔
823
    this->ts_filtered_count = 0;
41✔
824
    this->ts_active_opids.clear();
41✔
825
    this->ts_descriptions.clear();
41✔
826
    this->ts_subid_map.clear();
41✔
827
    this->ts_allocator.reset();
41✔
828
    this->ts_preview_source.clear();
41✔
829
    this->ts_preview_rows.clear();
41✔
830
    this->ts_preview_status_source.get_description().clear();
41✔
831

832
    auto min_log_time_tv_opt = this->get_min_row_time();
41✔
833
    auto max_log_time_tv_opt = this->get_max_row_time();
41✔
834
    std::optional<std::chrono::microseconds> min_log_time_opt;
41✔
835
    std::optional<std::chrono::microseconds> max_log_time_opt;
41✔
836
    auto max_desc_width = size_t{0};
41✔
837

838
    if (min_log_time_tv_opt) {
41✔
839
        min_log_time_opt = to_us(min_log_time_tv_opt.value());
1✔
840
    }
841
    if (max_log_time_tv_opt) {
41✔
842
        max_log_time_opt = to_us(max_log_time_tv_opt.value());
1✔
843
    }
844

845
    log_info("building opid table");
41✔
846
    auto last_log_time = std::chrono::microseconds{};
41✔
847
    tlx::btree_map<std::chrono::microseconds, std::string> part_map;
41✔
848
    for (const auto& [index, ld] : lnav::itertools::enumerate(this->ts_lss)) {
84✔
849
        if (ld->get_file_ptr() == nullptr) {
43✔
850
            continue;
1✔
851
        }
852
        if (!ld->is_visible()) {
43✔
853
            continue;
1✔
854
        }
855

856
        auto* lf = ld->get_file_ptr();
42✔
857
        lf->enable_cache();
42✔
858

859
        const auto& mark_meta = lf->get_bookmark_metadata();
42✔
860
        {
861
            for (const auto& [line_num, line_meta] : mark_meta) {
72✔
862
                const auto ll = std::next(lf->begin(), line_num);
30✔
863
                if (!line_meta.bm_name.empty()) {
30✔
864
                    part_map.insert2(ll->get_time<std::chrono::microseconds>(),
2✔
865
                                     line_meta.bm_name);
2✔
866
                }
867
                for (const auto& entry : line_meta.bm_tags) {
30✔
868
                    auto line_time = ll->get_time<std::chrono::microseconds>();
×
869
                    auto tag_key = fmt::format(FMT_STRING("{}@{}:{}"),
×
NEW
870
                                               entry.te_tag,
×
871
                                               lf->get_unique_path(),
872
                                               line_time.count());
×
873
                    auto tag_key_sf
874
                        = string_fragment::from_str(tag_key).to_owned(
×
875
                            this->ts_allocator);
×
NEW
876
                    auto tag_name_sf = string_fragment::from_str(entry.te_tag)
×
NEW
877
                                           .to_owned(this->ts_allocator);
×
878
                    auto tag_otr = opid_time_range{};
×
879
                    tag_otr.otr_range.tr_begin = line_time;
×
880
                    tag_otr.otr_range.tr_end = line_time;
×
881
                    tag_otr.otr_level_stats.update_msg_count(
×
882
                        ll->get_msg_level());
883
                    this->ts_active_opids.emplace(
×
884
                        tag_key_sf,
885
                        opid_row{
×
886
                            row_type::tag,
887
                            tag_name_sf,
888
                            tag_otr,
889
                            string_fragment::invalid(),
890
                        });
891
                }
892
            }
893
        }
894

895
        auto path = string_fragment::from_str(lf->get_unique_path())
42✔
896
                        .to_owned(this->ts_allocator);
42✔
897
        auto lf_otr = opid_time_range{};
42✔
898
        lf_otr.otr_range = lf->get_content_time_range();
42✔
899
        lf_otr.otr_level_stats = lf->get_level_stats();
42✔
900
        if (lf_otr.otr_range.tr_end > last_log_time) {
42✔
901
            last_log_time = lf_otr.otr_range.tr_end;
42✔
902
        }
903
        auto lf_row = opid_row{
42✔
904
            row_type::logfile,
905
            path,
906
            lf_otr,
907
            string_fragment::invalid(),
908
        };
42✔
909
        lf_row.or_logfile = lf;
42✔
910
        this->ts_active_opids.emplace(path, lf_row);
42✔
911

912
        {
913
            auto r_tid_map = lf->get_thread_ids().readAccess();
42✔
914

915
            for (const auto& [tid_sf, tid_meta] : r_tid_map->ltis_tid_ranges) {
166✔
916
                auto active_iter = this->ts_active_opids.find(tid_sf);
124✔
917
                if (active_iter == this->ts_active_opids.end()) {
124✔
918
                    auto tid = tid_sf.to_owned(this->ts_allocator);
115✔
919
                    auto tid_otr = opid_time_range{};
115✔
920
                    tid_otr.otr_range = tid_meta.titr_range;
115✔
921
                    tid_otr.otr_level_stats = tid_meta.titr_level_stats;
115✔
922
                    this->ts_active_opids.emplace(
×
923
                        tid,
924
                        opid_row{
115✔
925
                            row_type::thread,
926
                            tid,
927
                            tid_otr,
928
                            string_fragment::invalid(),
929
                        });
930
                } else {
115✔
931
                    active_iter->second.or_value.otr_range
18✔
932
                        |= tid_meta.titr_range;
9✔
933
                }
934
            }
935
        }
42✔
936

937
        auto format = lf->get_format();
42✔
938
        safe::ReadAccess<logfile::safe_opid_state> r_opid_map(
939
            ld->get_file_ptr()->get_opids());
42✔
940
        for (const auto& pair : r_opid_map->los_opid_ranges) {
1,043✔
941
            const opid_time_range& otr = pair.second;
1,001✔
942
            auto active_iter = this->ts_active_opids.find(pair.first);
1,001✔
943
            if (active_iter == this->ts_active_opids.end()) {
1,001✔
944
                auto opid = pair.first.to_owned(this->ts_allocator);
1,001✔
945
                auto active_emp_res = this->ts_active_opids.emplace(
2,002✔
946
                    opid,
947
                    opid_row{
1,001✔
948
                        row_type::opid,
949
                        opid,
950
                        otr,
951
                        string_fragment::invalid(),
952
                    });
953
                active_iter = active_emp_res.first;
1,001✔
954
            } else {
955
                active_iter->second.or_value |= otr;
×
956
            }
957

958
            opid_row& row = active_iter->second;
1,001✔
959
            for (auto& sub : row.or_value.otr_sub_ops) {
1,009✔
960
                auto subid_iter = this->ts_subid_map.find(sub.ostr_subid);
8✔
961

962
                if (subid_iter == this->ts_subid_map.end()) {
8✔
963
                    subid_iter = this->ts_subid_map
8✔
964
                                     .emplace(sub.ostr_subid.to_owned(
8✔
965
                                                  this->ts_allocator),
8✔
966
                                              true)
16✔
967
                                     .first;
968
                }
969
                sub.ostr_subid = subid_iter->first;
8✔
970
                if (sub.ostr_subid.length() > row.or_max_subid_width) {
8✔
971
                    row.or_max_subid_width = sub.ostr_subid.length();
8✔
972
                }
973
            }
974

975
            if (otr.otr_description.lod_index) {
1,001✔
976
                auto desc_id = otr.otr_description.lod_index.value();
985✔
977
                auto desc_def_iter
978
                    = format->lf_opid_description_def_vec->at(desc_id);
985✔
979

980
                auto desc_key
981
                    = opid_description_def_key{format->get_name(), desc_id};
985✔
982
                auto desc_defs_opt
983
                    = row.or_description_defs.odd_defs.value_for(desc_key);
985✔
984
                if (!desc_defs_opt) {
985✔
985
                    row.or_description_defs.odd_defs.insert(desc_key,
985✔
986
                                                            *desc_def_iter);
987
                }
988

989
                if (!row.or_description_begin
985✔
990
                    || otr.otr_range.tr_begin
985✔
NEW
991
                        < row.or_description_begin.value())
×
992
                {
993
                    row.or_description_begin = otr.otr_range.tr_begin;
985✔
994
                    row.or_description_def_key = desc_key;
985✔
995
                    row.or_description_value = otr.otr_description.lod_elements;
985✔
996
                }
997
            } else if (!otr.otr_description.lod_elements.empty()) {
16✔
998
                auto desc_sf = string_fragment::from_str(
4✔
999
                    otr.otr_description.lod_elements.values().front());
4✔
1000
                row.or_description = desc_sf.to_owned(this->ts_allocator);
4✔
1001
            }
1002
            row.or_value.otr_description.lod_elements.clear();
1,001✔
1003
        }
1004

1005
        if (this->ts_index_progress) {
42✔
1006
            switch (this->ts_index_progress(
×
1007
                progress_t{index, this->ts_lss.file_count()}))
×
1008
            {
1009
                case lnav::progress_result_t::ok:
×
1010
                    break;
×
1011
                case lnav::progress_result_t::interrupt:
×
1012
                    log_debug("timeline rebuild interrupted");
×
1013
                    this->ts_rebuild_in_progress = false;
×
1014
                    return false;
×
1015
            }
1016
        }
1017
    }
42✔
1018
    if (this->ts_index_progress) {
41✔
1019
        this->ts_index_progress(std::nullopt);
×
1020
    }
1021

1022
    {
1023
        static const auto START_RE = lnav::pcre2pp::code::from_const(
1024
            R"(^(?:start(?:ed)?|begin)|\b(?:start(?:ed)?|begin)$)",
1025
            PCRE2_CASELESS);
41✔
1026

1027
        std::vector<opid_row*> start_tags;
41✔
1028
        for (auto& pair : this->ts_active_opids) {
1,199✔
1029
            if (pair.second.or_type != row_type::tag) {
1,158✔
1030
                continue;
1,158✔
1031
            }
1032
            if (START_RE.find_in(pair.second.or_name).ignore_error()) {
×
1033
                start_tags.emplace_back(&pair.second);
×
1034
            }
1035
        }
1036
        std::stable_sort(start_tags.begin(),
41✔
1037
                         start_tags.end(),
1038
                         [](const auto* lhs, const auto* rhs) {
×
1039
                             if (lhs->or_name == rhs->or_name) {
×
1040
                                 return lhs->or_value.otr_range.tr_begin
×
1041
                                     < rhs->or_value.otr_range.tr_begin;
×
1042
                             }
1043
                             return lhs->or_name < rhs->or_name;
×
1044
                         });
1045
        for (size_t i = 0; i < start_tags.size(); i++) {
41✔
1046
            if (i + 1 < start_tags.size()
×
1047
                && start_tags[i]->or_name == start_tags[i + 1]->or_name)
×
1048
            {
1049
                start_tags[i]->or_value.otr_range.tr_end
×
1050
                    = start_tags[i + 1]->or_value.otr_range.tr_begin - 1us;
×
1051
            } else {
1052
                start_tags[i]->or_value.otr_range.tr_end = last_log_time;
×
1053
            }
1054
        }
1055
    }
41✔
1056

1057
    for (auto part_iter = part_map.begin(); part_iter != part_map.end();
43✔
1058
         ++part_iter)
2✔
1059
    {
1060
        auto next_iter = std::next(part_iter);
2✔
1061
        auto part_name_sf = string_fragment::from_str(part_iter->second)
2✔
1062
                                .to_owned(this->ts_allocator);
2✔
1063
        auto part_otr = opid_time_range{};
2✔
1064
        part_otr.otr_range.tr_begin = part_iter->first;
2✔
1065
        if (next_iter != part_map.end()) {
2✔
1066
            part_otr.otr_range.tr_end = next_iter->first;
1✔
1067
        } else {
1068
            part_otr.otr_range.tr_end = last_log_time;
1✔
1069
        }
1070
        this->ts_active_opids.emplace(part_name_sf,
×
1071
                                      opid_row{
2✔
1072
                                          row_type::partition,
1073
                                          part_name_sf,
1074
                                          part_otr,
1075
                                          string_fragment::invalid(),
1076
                                      });
1077
    }
2✔
1078

1079
    log_info("active opids: %zu", this->ts_active_opids.size());
41✔
1080

1081
    size_t filtered_in_count = 0;
41✔
1082
    for (const auto& filt : this->tss_filters) {
43✔
1083
        if (!filt->is_enabled()) {
2✔
1084
            continue;
×
1085
        }
1086
        if (filt->get_type() == text_filter::INCLUDE) {
2✔
1087
            filtered_in_count += 1;
1✔
1088
        }
1089
    }
1090
    this->ts_filter_hits = {};
41✔
1091

1092
    this->ts_time_order.clear();
41✔
1093
    this->ts_time_order.reserve(this->ts_active_opids.size());
41✔
1094
    for (auto& pair : this->ts_active_opids) {
1,201✔
1095
        opid_row& row = pair.second;
1,160✔
1096
        opid_time_range& otr = pair.second.or_value;
1,160✔
1097
        std::string full_desc;
1,160✔
1098
        if (row.or_description.empty()) {
1,160✔
1099
            const auto& desc_defs = row.or_description_defs.odd_defs;
1,156✔
1100
            if (row.or_description_begin) {
1,156✔
1101
                auto desc_def_opt
1102
                    = desc_defs.value_for(row.or_description_def_key);
985✔
1103
                if (desc_def_opt) {
985✔
1104
                    full_desc = desc_def_opt.value()->to_string(
1,970✔
1105
                        row.or_description_value);
985✔
1106
                }
1107
            }
1108
            row.or_description_begin = std::nullopt;
1,156✔
1109
            auto full_desc_sf = string_fragment::from_str(full_desc);
1,156✔
1110
            auto desc_sf_iter = this->ts_descriptions.find(full_desc_sf);
1,156✔
1111
            if (desc_sf_iter == this->ts_descriptions.end()) {
1,156✔
1112
                full_desc_sf = string_fragment::from_str(full_desc).to_owned(
2,312✔
1113
                    this->ts_allocator);
1,156✔
1114
            }
1115
            pair.second.or_description = full_desc_sf;
1,156✔
1116
        } else {
1117
            full_desc += pair.second.or_description;
4✔
1118
        }
1119

1120
        if (!this->is_row_type_visible(row.or_type)) {
1,160✔
1121
            this->ts_filtered_count += 1;
4✔
1122
            continue;
4✔
1123
        }
1124

1125
        shared_buffer sb_opid;
1,156✔
1126
        shared_buffer_ref sbr_opid;
1,156✔
1127
        sbr_opid.share(
1,156✔
1128
            sb_opid, pair.second.or_name.data(), pair.second.or_name.length());
1,156✔
1129
        shared_buffer sb_desc;
1,156✔
1130
        shared_buffer_ref sbr_desc;
1,156✔
1131
        sbr_desc.share(sb_desc, full_desc.c_str(), full_desc.length());
1,156✔
1132
        if (this->tss_apply_filters) {
1,156✔
1133
            auto filtered_in = false;
1,156✔
1134
            auto filtered_out = false;
1,156✔
1135
            for (const auto& filt : this->tss_filters) {
1,324✔
1136
                if (!filt->is_enabled()) {
168✔
1137
                    continue;
×
1138
                }
1139
                for (const auto sbr : {&sbr_opid, &sbr_desc}) {
504✔
1140
                    if (filt->matches(std::nullopt, *sbr)) {
336✔
1141
                        this->ts_filter_hits[filt->get_index()] += 1;
2✔
1142
                        switch (filt->get_type()) {
2✔
1143
                            case text_filter::INCLUDE:
1✔
1144
                                filtered_in = true;
1✔
1145
                                break;
1✔
1146
                            case text_filter::EXCLUDE:
1✔
1147
                                filtered_out = true;
1✔
1148
                                break;
1✔
1149
                            default:
×
1150
                                break;
×
1151
                        }
1152
                    }
1153
                }
1154
            }
1155

1156
            if (min_log_time_opt
1,156✔
1157
                && otr.otr_range.tr_end < min_log_time_opt.value())
1,156✔
1158
            {
1159
                filtered_out = true;
16✔
1160
            }
1161
            if (max_log_time_opt
1,156✔
1162
                && max_log_time_opt.value() < otr.otr_range.tr_begin)
1,156✔
1163
            {
1164
                filtered_out = true;
16✔
1165
            }
1166

1167
            if ((filtered_in_count > 0 && !filtered_in) || filtered_out) {
1,156✔
1168
                this->ts_filtered_count += 1;
116✔
1169
                continue;
116✔
1170
            }
1171
        }
1172

1173
        if (pair.second.or_name.length() > this->ts_opid_width) {
1,040✔
1174
            this->ts_opid_width = pair.second.or_name.length();
106✔
1175
        }
1176
        if (full_desc.size() > max_desc_width) {
1,040✔
1177
            max_desc_width = full_desc.size();
31✔
1178
        }
1179

1180
        if (this->ts_lower_bound == 0us
1,040✔
1181
            || pair.second.or_value.otr_range.tr_begin < this->ts_lower_bound)
1,040✔
1182
        {
1183
            this->ts_lower_bound = pair.second.or_value.otr_range.tr_begin;
120✔
1184
        }
1185
        if (this->ts_upper_bound == 0us
1,040✔
1186
            || this->ts_upper_bound < pair.second.or_value.otr_range.tr_end)
1,040✔
1187
        {
1188
            this->ts_upper_bound = pair.second.or_value.otr_range.tr_end;
95✔
1189
        }
1190

1191
        this->ts_time_order.emplace_back(&pair.second);
1,040✔
1192
    }
1,624✔
1193
    std::stable_sort(
41✔
1194
        this->ts_time_order.begin(),
1195
        this->ts_time_order.end(),
1196
        [](const auto* lhs, const auto* rhs) { return *lhs < *rhs; });
5,405✔
1197
    for (size_t lpc = 0; lpc < this->ts_time_order.size(); lpc++) {
1,081✔
1198
        const auto& row = *this->ts_time_order[lpc];
1,040✔
1199
        if (row.or_type == row_type::logfile) {
1,040✔
1200
            bm_files.insert_once(vis_line_t(lpc));
40✔
1201
        } else if (row.or_type == row_type::tag) {
1,000✔
1202
            bm_meta.insert_once(vis_line_t(lpc));
×
1203
        } else if (row.or_type == row_type::partition) {
1,000✔
1204
            bm_parts.insert_once(vis_line_t(lpc));
2✔
1205
        }
1206
        if (row.or_value.otr_level_stats.lls_error_count > 0) {
1,040✔
1207
            bm_errs.insert_once(vis_line_t(lpc));
91✔
1208
        }
1209
        if (row.or_value.otr_level_stats.lls_warning_count > 0) {
1,040✔
1210
            bm_warns.insert_once(vis_line_t(lpc));
48✔
1211
        }
1212
    }
1213

1214
    this->ts_opid_width = std::min(this->ts_opid_width, MAX_OPID_WIDTH);
41✔
1215
    this->ts_total_width
1216
        = std::max<size_t>(22 + this->ts_opid_width + max_desc_width,
82✔
1217
                           1 + 16 + 5 + 8 + 5 + 16 + 1 /* header */);
41✔
1218

1219
    this->apply_pending_bookmarks();
41✔
1220

1221
    this->tss_view->set_needs_update();
41✔
1222
    this->ts_rebuild_in_progress = false;
41✔
1223

1224
    ensure(this->ts_time_order.empty() || this->ts_opid_width > 0);
41✔
1225

1226
    return true;
41✔
1227
}
41✔
1228

1229
std::optional<vis_line_t>
1230
timeline_source::row_for_time(timeval time_bucket)
18✔
1231
{
1232
    auto time_bucket_us = to_us(time_bucket);
18✔
1233
    auto iter = this->ts_time_order.begin();
18✔
1234
    while (true) {
1235
        if (iter == this->ts_time_order.end()) {
20✔
1236
            return std::nullopt;
1✔
1237
        }
1238

1239
        if ((*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
19✔
1240
            break;
17✔
1241
        }
1242
        ++iter;
2✔
1243
    }
1244

1245
    auto closest_iter = iter;
17✔
1246
    auto closest_diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
17✔
1247
    for (; iter != this->ts_time_order.end(); ++iter) {
54✔
1248
        if (time_bucket_us < (*iter)->or_value.otr_range.tr_begin) {
53✔
1249
            break;
16✔
1250
        }
1251
        if (!(*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
37✔
1252
            continue;
4✔
1253
        }
1254

1255
        auto diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
33✔
1256
        if (diff < closest_diff) {
33✔
1257
            closest_iter = iter;
2✔
1258
            closest_diff = diff;
2✔
1259
        }
1260

1261
        for (const auto& sub : (*iter)->or_value.otr_sub_ops) {
33✔
1262
            if (!sub.ostr_range.contains_inclusive(time_bucket_us)) {
×
1263
                continue;
×
1264
            }
1265

1266
            diff = time_bucket_us - sub.ostr_range.tr_begin;
×
1267
            if (diff < closest_diff) {
×
1268
                closest_iter = iter;
×
1269
                closest_diff = diff;
×
1270
            }
1271
        }
1272
    }
1273

1274
    return vis_line_t(std::distance(this->ts_time_order.begin(), closest_iter));
34✔
1275
}
1276

1277
std::optional<vis_line_t>
1278
timeline_source::row_for(const row_info& ri)
36✔
1279
{
1280
    auto vl_opt = this->ts_lss.row_for(ri);
36✔
1281
    if (!vl_opt) {
36✔
1282
        return this->row_for_time(ri.ri_time);
×
1283
    }
1284

1285
    auto vl = vl_opt.value();
36✔
1286
    auto win = this->ts_lss.window_at(vl);
36✔
1287
    for (const auto& msg_line : *win) {
72✔
1288
        const auto& lvv = msg_line.get_values();
36✔
1289

1290
        if (lvv.lvv_opid_value) {
36✔
1291
            auto opid_iter
1292
                = this->ts_active_opids.find(lvv.lvv_opid_value.value());
21✔
1293
            if (opid_iter != this->ts_active_opids.end()) {
21✔
1294
                for (const auto& [index, oprow] :
183✔
1295
                     lnav::itertools::enumerate(this->ts_time_order))
186✔
1296
                {
1297
                    if (oprow == &opid_iter->second) {
162✔
1298
                        return vis_line_t(index);
18✔
1299
                    }
1300
                }
1301
            }
1302
        }
1303
    }
54✔
1304

1305
    return this->row_for_time(ri.ri_time);
18✔
1306
}
36✔
1307

1308
std::optional<text_time_translator::row_info>
1309
timeline_source::time_for_row(vis_line_t row)
60✔
1310
{
1311
    if (row >= this->ts_time_order.size()) {
60✔
1312
        return std::nullopt;
×
1313
    }
1314

1315
    const auto& otr = this->ts_time_order[row]->or_value;
60✔
1316

1317
    if (this->tss_view->get_selection() == row) {
60✔
1318
        auto ov_sel = this->tss_view->get_overlay_selection();
60✔
1319

1320
        if (ov_sel && ov_sel.value() < otr.otr_sub_ops.size()) {
60✔
1321
            return row_info{
×
1322
                to_timeval(otr.otr_sub_ops[ov_sel.value()].ostr_range.tr_begin),
×
1323
                row,
1324
            };
1325
        }
1326
    }
1327

1328
    auto preview_selection = this->ts_preview_view.get_selection();
60✔
1329
    if (!preview_selection) {
60✔
1330
        return std::nullopt;
×
1331
    }
1332
    if (preview_selection < this->ts_preview_rows.size()) {
60✔
1333
        return this->ts_preview_rows[preview_selection.value()];
58✔
1334
    }
1335

1336
    return row_info{
4✔
1337
        to_timeval(otr.otr_range.tr_begin),
2✔
1338
        row,
1339
    };
4✔
1340
}
1341

1342
size_t
1343
timeline_source::text_line_width(textview_curses& curses)
7,425✔
1344
{
1345
    return this->ts_total_width;
7,425✔
1346
}
1347

1348
void
1349
timeline_source::text_selection_changed(textview_curses& tc)
83✔
1350
{
1351
    static const size_t MAX_PREVIEW_LINES = 200;
1352

1353
    auto sel = tc.get_selection();
83✔
1354

1355
    this->ts_preview_source.clear();
83✔
1356
    this->ts_preview_rows.clear();
83✔
1357
    if (!sel || sel.value() >= this->ts_time_order.size()) {
83✔
1358
        return;
41✔
1359
    }
1360

1361
    const auto& row = *this->ts_time_order[sel.value()];
42✔
1362
    auto low_us = row.or_value.otr_range.tr_begin;
42✔
1363
    auto high_us = row.or_value.otr_range.tr_end;
42✔
1364
    auto id_sf = row.or_name;
42✔
1365
    auto level_stats = row.or_value.otr_level_stats;
42✔
1366
    auto ov_sel = tc.get_overlay_selection();
42✔
1367
    if (ov_sel) {
42✔
1368
        const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
1369
        id_sf = sub.ostr_subid;
×
1370
        low_us = sub.ostr_range.tr_begin;
×
1371
        high_us = sub.ostr_range.tr_end;
×
1372
        level_stats = sub.ostr_level_stats;
×
1373
    }
1374
    high_us += 1s;
42✔
1375
    auto low_vl = this->ts_lss.row_for_time(to_timeval(low_us));
42✔
1376
    auto high_vl = this->ts_lss.row_for_time(to_timeval(high_us))
42✔
1377
                       .value_or(vis_line_t(this->ts_lss.text_line_count()));
42✔
1378

1379
    if (!low_vl) {
42✔
1380
        return;
×
1381
    }
1382

1383
    auto preview_content = attr_line_t();
42✔
1384
    auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
42✔
1385
    auto win = this->ts_lss.window_at(low_vl.value(), high_vl);
42✔
1386
    auto id_bloom_bits = row.or_name.bloom_bits();
42✔
1387
    auto msg_count = 0;
42✔
1388
    for (const auto& msg_line : *win) {
1,654✔
1389
        switch (row.or_type) {
806✔
1390
            case row_type::logfile:
397✔
1391
                if (msg_line.get_file_ptr() != row.or_logfile) {
397✔
1392
                    continue;
×
1393
                }
1394
                break;
397✔
1395
            case row_type::thread: {
136✔
1396
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
136✔
1397
                    continue;
115✔
1398
                }
1399
                const auto& lvv = msg_line.get_values();
21✔
1400
                if (!lvv.lvv_thread_id_value) {
21✔
1401
                    continue;
×
1402
                }
1403
                auto tid_sf = lvv.lvv_thread_id_value.value();
21✔
1404
                if (!(tid_sf == row.or_name)) {
21✔
1405
                    continue;
×
1406
                }
1407
                break;
21✔
1408
            }
1409
            case row_type::opid: {
273✔
1410
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
273✔
1411
                    continue;
233✔
1412
                }
1413

1414
                const auto& lvv = msg_line.get_values();
40✔
1415
                if (!lvv.lvv_opid_value) {
40✔
1416
                    continue;
×
1417
                }
1418
                auto opid_sf = lvv.lvv_opid_value.value();
40✔
1419

1420
                if (!(opid_sf == row.or_name)) {
40✔
1421
                    continue;
×
1422
                }
1423
                break;
40✔
1424
            }
40✔
1425
            case row_type::tag: {
×
1426
                const auto& bm
1427
                    = msg_line.get_file_ptr()->get_bookmark_metadata();
×
1428
                auto bm_iter = bm.find(msg_line.get_file_line_number());
×
1429
                if (bm_iter == bm.end()) {
×
1430
                    continue;
×
1431
                }
1432
                auto tag_name = row.or_name.to_string();
×
1433
                if (!(bm_iter->second.bm_tags
×
1434
                      | lnav::itertools::find(tag_name)))
×
1435
                {
1436
                    continue;
×
1437
                }
1438
                break;
×
1439
            }
1440
            case row_type::partition:
×
1441
                break;
×
1442
        }
1443

1444
        for (size_t lpc = 0; lpc < msg_line.get_line_count(); lpc++) {
919✔
1445
            auto vl = msg_line.get_vis_line() + vis_line_t(lpc);
461✔
1446
            auto cl = this->ts_lss.at(vl);
461✔
1447
            auto row_al = attr_line_t();
461✔
1448
            this->ts_log_view.textview_value_for_row(vl, row_al);
461✔
1449
            preview_content.append(row_al).append("\n");
461✔
1450
            this->ts_preview_rows.emplace_back(
461✔
1451
                msg_line.get_logline().get_timeval(), cl);
461✔
1452
            ++cl;
461✔
1453
        }
461✔
1454
        msg_count += 1;
458✔
1455
        msgs_remaining -= 1;
458✔
1456
        if (msgs_remaining == 0) {
458✔
1457
            break;
×
1458
        }
1459
    }
42✔
1460

1461
    this->ts_preview_source.replace_with(preview_content);
42✔
1462
    this->ts_preview_view.set_selection(0_vl);
42✔
1463
    this->ts_preview_status_source.get_description().set_value(
42✔
1464
        " ID %.*s", id_sf.length(), id_sf.data());
1465
    auto err_count = level_stats.lls_error_count;
42✔
1466
    if (err_count == 0) {
42✔
1467
        this->ts_preview_status_source
25✔
1468
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
25✔
1469
            .set_value("");
25✔
1470
    } else {
1471
        this->ts_preview_status_source
17✔
1472
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
17✔
1473
            .set_value("\u2022 %'d", err_count);
17✔
1474
    }
1475
    auto warn_count = level_stats.lls_warning_count;
42✔
1476
    if (warn_count == 0) {
42✔
1477
        this->ts_preview_status_source
39✔
1478
            .statusview_value_for_field(timeline_status_source::TSF_WARNINGS)
39✔
1479
            .set_value("");
39✔
1480
    } else {
1481
        this->ts_preview_status_source
3✔
1482
            .statusview_value_for_field(timeline_status_source::TSF_WARNINGS)
3✔
1483
            .set_value("\u2022 %'d", warn_count);
3✔
1484
    }
1485
    if (msg_count < level_stats.lls_total_count) {
42✔
1486
        this->ts_preview_status_source
40✔
1487
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
40✔
1488
            .set_value(
40✔
1489
                "%'d of %'d messages ", msg_count, level_stats.lls_total_count);
1490
    } else {
1491
        this->ts_preview_status_source
2✔
1492
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
2✔
1493
            .set_value("%'d messages ", level_stats.lls_total_count);
2✔
1494
    }
1495
    this->ts_preview_status_view.set_needs_update();
42✔
1496
}
42✔
1497

1498
void
1499
timeline_source::text_filters_changed()
16✔
1500
{
1501
    this->rebuild_indexes();
16✔
1502
    this->tss_view->reload_data();
16✔
1503
    this->tss_view->redo_search();
16✔
1504
}
16✔
1505

1506
void
1507
timeline_source::clear_preview()
×
1508
{
1509
    text_sub_source::clear_preview();
×
1510
    this->ts_preview_hidden_row_types.clear();
×
1511
}
1512

1513
void
1514
timeline_source::add_commands_for_session(
57✔
1515
    const std::function<void(const std::string&)>& receiver)
1516
{
1517
    text_sub_source::add_commands_for_session(receiver);
57✔
1518

1519
    for (const auto& rt : this->ts_hidden_row_types) {
57✔
1520
        receiver(fmt::format(FMT_STRING("hide-in-timeline {}"),
×
1521
                             row_type_to_string(rt)));
×
1522
    }
1523
}
57✔
1524

1525
void
1526
timeline_source::apply_pending_bookmarks()
66✔
1527
{
1528
    if (this->ts_pending_bookmarks.empty()) {
66✔
1529
        return;
64✔
1530
    }
1531

1532
    auto* tc = this->tss_view;
2✔
1533
    for (const auto& pb : this->ts_pending_bookmarks) {
4✔
1534
        auto row_name_sf = string_fragment::from_str(pb.pb_row_name);
2✔
1535
        for (size_t lpc = 0; lpc < this->ts_time_order.size(); lpc++) {
2✔
1536
            const auto& row = *this->ts_time_order[lpc];
2✔
1537
            if (row.or_name == row_name_sf && row.or_type == pb.pb_row_type) {
2✔
1538
                tc->set_user_mark(pb.pb_mark_type, vis_line_t(lpc), true);
2✔
1539
                break;
2✔
1540
            }
1541
        }
1542
    }
1543
    this->ts_pending_bookmarks.clear();
2✔
1544
}
1545

1546
int
1547
timeline_source::get_filtered_count() const
87✔
1548
{
1549
    return this->ts_filtered_count;
87✔
1550
}
1551

1552
int
1553
timeline_source::get_filtered_count_for(size_t filter_index) const
×
1554
{
1555
    return this->ts_filter_hits[filter_index];
×
1556
}
1557

1558
static const std::vector<breadcrumb::possibility>&
1559
timestamp_poss()
×
1560
{
1561
    const static std::vector<breadcrumb::possibility> retval = {
1562
        breadcrumb::possibility{"-1 day"},
1563
        breadcrumb::possibility{"-1h"},
1564
        breadcrumb::possibility{"-30m"},
1565
        breadcrumb::possibility{"-15m"},
1566
        breadcrumb::possibility{"-5m"},
1567
        breadcrumb::possibility{"-1m"},
1568
        breadcrumb::possibility{"+1m"},
1569
        breadcrumb::possibility{"+5m"},
1570
        breadcrumb::possibility{"+15m"},
1571
        breadcrumb::possibility{"+30m"},
1572
        breadcrumb::possibility{"+1h"},
1573
        breadcrumb::possibility{"+1 day"},
1574
    };
1575

1576
    return retval;
×
1577
}
1578

1579
void
1580
timeline_source::text_crumbs_for_line(int line,
×
1581
                                      std::vector<breadcrumb::crumb>& crumbs)
1582
{
1583
    text_sub_source::text_crumbs_for_line(line, crumbs);
×
1584

1585
    if (line >= this->ts_time_order.size()) {
×
1586
        return;
×
1587
    }
1588

1589
    const auto& row = *this->ts_time_order[line];
×
1590
    char ts[64];
1591

1592
    sql_strftime(ts, sizeof(ts), row.or_value.otr_range.tr_begin, 'T');
×
1593

1594
    crumbs.emplace_back(std::string(ts),
×
1595
                        timestamp_poss,
1596
                        [ec = this->ts_exec_context](const auto& ts) {
×
1597
                            auto cmd
×
1598
                                = fmt::format(FMT_STRING(":goto {}"),
×
1599
                                              ts.template get<std::string>());
1600
                            ec->execute(INTERNAL_SRC_LOC, cmd);
×
1601
                        });
×
1602
    crumbs.back().c_expected_input
×
1603
        = breadcrumb::crumb::expected_input_t::anything;
×
1604
    crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
×
1605
}
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