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

tstack / lnav / 20281835752-2752

16 Dec 2025 08:31PM UTC coverage: 68.903% (+0.03%) from 68.87%
20281835752-2752

push

github

tstack
[tests] update test data

51677 of 75000 relevant lines covered (68.9%)

434192.37 hits per line

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

64.25
/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 "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 = 22;
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(
635✔
177
    const std::shared_ptr<timeline_source>& src)
635✔
178
    : gho_src(src)
635✔
179
{
180
}
635✔
181

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

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

197
            if (this->gho_src->gs_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->gs_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->gs_lss.get_filters();
×
219
                for (const auto& filt : fs) {
×
220
                    auto hits = this->gho_src->gs_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->gs_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) {
28✔
264
        return false;
12✔
265
    }
266

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

277
    require(sel_begin_us >= 0us);
16✔
278
    require(sel_end_us >= 0us);
16✔
279

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

427
        auto block_attrs = text_attrs::with_reverse();
×
428
        attrs.emplace_back(lr, VC_STYLE.value(block_attrs));
×
429
    }
430
    if (!value_out.empty()) {
×
431
        value_out.back().get_attrs().emplace_back(
×
432
            line_range{0, -1}, VC_STYLE.value(text_attrs::with_underline()));
×
433
    }
434
}
435
std::optional<attr_line_t>
436
timeline_header_overlay::list_header_for_overlay(const listview_curses& lv,
×
437
                                                 media_t media,
438
                                                 vis_line_t line)
439
{
440
    if (lv.get_overlay_selection()) {
×
441
        return attr_line_t("\u258C Sub-operations: Press ")
×
442
            .append("Esc"_hotkey)
×
443
            .append(" to exit this panel");
×
444
    }
445
    return attr_line_t("\u258C Sub-operations: Press ")
×
446
        .append("CTRL-]"_hotkey)
×
447
        .append(" to focus on this panel");
×
448
}
449

450
timeline_source::timeline_source(textview_curses& log_view,
635✔
451
                                 logfile_sub_source& lss,
452
                                 textview_curses& preview_view,
453
                                 plain_text_source& preview_source,
454
                                 statusview_curses& preview_status_view,
455
                                 timeline_status_source& preview_status_source)
635✔
456
    : gs_log_view(log_view), gs_lss(lss), gs_preview_view(preview_view),
635✔
457
      gs_preview_source(preview_source),
635✔
458
      gs_preview_status_view(preview_status_view),
635✔
459
      gs_preview_status_source(preview_status_source)
635✔
460
{
461
    this->tss_supports_filtering = true;
635✔
462
    this->gs_preview_view.set_overlay_source(&this->gs_preview_overlay);
635✔
463
}
635✔
464

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

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

503
    return false;
×
504
}
505

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

519
    return false;
×
520
}
521

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

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

549
    ensure(lb <= ub);
415✔
550
    return {lb, ub};
415✔
551
}
552

553
size_t
554
timeline_source::text_line_count()
8,839✔
555
{
556
    return this->gs_time_order.size();
8,839✔
557
}
558

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

576
        this->gs_rendered_line.clear();
399✔
577

578
        auto total_msgs = row.or_value.otr_level_stats.lls_total_count;
399✔
579
        auto truncated_name
580
            = attr_line_t::from_table_cell_content(row.or_name, MAX_OPID_WIDTH);
399✔
581
        auto truncated_desc = attr_line_t::from_table_cell_content(
582
            row.or_description, MAX_DESC_WIDTH);
399✔
583
        std::optional<ui_icon_t> icon;
399✔
584
        auto padding = 1;
399✔
585
        switch (row.or_type) {
399✔
586
            case row_type::logfile:
11✔
587
                icon = ui_icon_t::file;
11✔
588
                break;
11✔
589
            case row_type::thread:
23✔
590
                icon = ui_icon_t::thread;
23✔
591
                break;
23✔
592
            case row_type::opid:
365✔
593
                padding = 3;
365✔
594
                break;
365✔
595
        }
596
        this->gs_rendered_line
597
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
798✔
598
            .append("  ")
399✔
599
            .append(lnav::roles::error(humanize::sparkline(
1,197✔
600
                row.or_value.otr_level_stats.lls_error_count, total_msgs)))
399✔
601
            .append(lnav::roles::warning(humanize::sparkline(
1,197✔
602
                row.or_value.otr_level_stats.lls_warning_count, total_msgs)))
399✔
603
            .append("  ")
399✔
604
            .append(icon)
399✔
605
            .append(padding, ' ')
399✔
606
            .append(lnav::roles::identifier(truncated_name))
798✔
607
            .append(
798✔
608
                this->gs_opid_width - truncated_name.utf8_length_or_length(),
399✔
609
                ' ')
610
            .append(truncated_desc);
399✔
611
        this->gs_rendered_line.with_attr_for_all(
399✔
612
            VC_ROLE.value(role_t::VCR_COMMENT));
798✔
613

614
        value_out = this->gs_rendered_line.get_string();
399✔
615
    }
399✔
616

617
    return {};
399✔
618
}
619

620
void
621
timeline_source::text_attrs_for_line(textview_curses& tc,
399✔
622
                                     int line,
623
                                     string_attrs_t& value_out)
624
{
625
    if (!this->ts_rebuild_in_progress
798✔
626
        && line < (ssize_t) this->gs_time_order.size())
399✔
627
    {
628
        const auto& row = *this->gs_time_order[line];
399✔
629

630
        value_out = this->gs_rendered_line.get_attrs();
399✔
631

632
        auto lr = line_range{-1, -1, line_range::unit::codepoint};
399✔
633
        auto [sel_lb, sel_ub]
399✔
634
            = this->get_time_bounds_for(tc.get_selection().value_or(0_vl));
399✔
635

636
        if (row.or_value.otr_range.tr_begin <= sel_ub
399✔
637
            && sel_lb <= row.or_value.otr_range.tr_end)
399✔
638
        {
639
            auto width = tc.get_dimensions().second;
399✔
640

641
            if (width > CHART_INDENT) {
399✔
642
                width -= CHART_INDENT;
399✔
643
                double span = (sel_ub - sel_lb).count();
399✔
644
                auto us_per_ch = std::chrono::microseconds{
645
                    static_cast<int64_t>(ceil(span / (double) width))};
399✔
646

647
                if (row.or_value.otr_range.tr_begin <= sel_lb) {
399✔
648
                    lr.lr_start = CHART_INDENT;
×
649
                } else {
650
                    auto start_diff
651
                        = (row.or_value.otr_range.tr_begin - sel_lb);
399✔
652

653
                    lr.lr_start = CHART_INDENT + floor(start_diff / us_per_ch);
399✔
654
                }
655

656
                if (sel_ub < row.or_value.otr_range.tr_end) {
399✔
657
                    lr.lr_end = -1;
13✔
658
                } else {
659
                    auto end_diff = (row.or_value.otr_range.tr_end - sel_lb);
386✔
660

661
                    lr.lr_end = CHART_INDENT + ceil(end_diff / us_per_ch);
386✔
662
                    if (lr.lr_start == lr.lr_end) {
386✔
663
                        lr.lr_end += 1;
280✔
664
                    }
665
                }
666

667
                auto block_attrs = text_attrs::with_reverse();
399✔
668
                require(lr.lr_start >= 0);
399✔
669
                value_out.emplace_back(lr, VC_STYLE.value(block_attrs));
399✔
670
            }
671
        }
672
        auto alt_row_index = line % 4;
399✔
673
        if (alt_row_index == 2 || alt_row_index == 3) {
399✔
674
            value_out.emplace_back(line_range{0, -1},
194✔
675
                                   VC_ROLE.value(role_t::VCR_ALT_ROW));
388✔
676
        }
677
    }
678
}
399✔
679

680
size_t
681
timeline_source::text_size_for_line(textview_curses& tc,
×
682
                                    int line,
683
                                    text_sub_source::line_flags_t raw)
684
{
685
    return this->gs_total_width;
×
686
}
687

688
bool
689
timeline_source::rebuild_indexes()
26✔
690
{
691
    static auto op = lnav_operation{"timeline_rebuild"};
26✔
692

693
    auto op_guard = lnav_opid_guard::internal(op);
26✔
694
    auto& bm = this->tss_view->get_bookmarks();
26✔
695
    auto& bm_errs = bm[&textview_curses::BM_ERRORS];
26✔
696
    auto& bm_warns = bm[&textview_curses::BM_WARNINGS];
26✔
697

698
    this->ts_rebuild_in_progress = true;
26✔
699
    bm_errs.clear();
26✔
700
    bm_warns.clear();
26✔
701

702
    this->gs_lower_bound = {};
26✔
703
    this->gs_upper_bound = {};
26✔
704
    this->gs_opid_width = 0;
26✔
705
    this->gs_total_width = 0;
26✔
706
    this->gs_filtered_count = 0;
26✔
707
    this->gs_active_opids.clear();
26✔
708
    this->gs_descriptions.clear();
26✔
709
    this->gs_subid_map.clear();
26✔
710
    this->gs_allocator.reset();
26✔
711
    this->gs_preview_source.clear();
26✔
712
    this->gs_preview_rows.clear();
26✔
713
    this->gs_preview_status_source.get_description().clear();
26✔
714

715
    auto min_log_time_tv_opt = this->get_min_row_time();
26✔
716
    auto max_log_time_tv_opt = this->get_max_row_time();
26✔
717
    std::optional<std::chrono::microseconds> min_log_time_opt;
26✔
718
    std::optional<std::chrono::microseconds> max_log_time_opt;
26✔
719
    auto max_desc_width = size_t{0};
26✔
720

721
    if (min_log_time_tv_opt) {
26✔
722
        min_log_time_opt = to_us(min_log_time_tv_opt.value());
1✔
723
    }
724
    if (max_log_time_tv_opt) {
26✔
725
        max_log_time_opt = to_us(max_log_time_tv_opt.value());
1✔
726
    }
727

728
    log_info("building opid table");
26✔
729
    tlx::btree_map<std::chrono::microseconds, std::string> part_map;
26✔
730
    for (const auto& [index, ld] : lnav::itertools::enumerate(this->gs_lss)) {
53✔
731
        if (ld->get_file_ptr() == nullptr) {
27✔
732
            continue;
1✔
733
        }
734
        if (!ld->is_visible()) {
27✔
735
            continue;
1✔
736
        }
737

738
        auto* lf = ld->get_file_ptr();
26✔
739
        lf->enable_cache();
26✔
740

741
        const auto& mark_meta = lf->get_bookmark_metadata();
26✔
742
        {
743
            for (const auto& [line_num, line_meta] : mark_meta) {
33✔
744
                if (line_meta.bm_name.empty()) {
7✔
745
                    continue;
7✔
746
                }
747
                const auto ll = std::next(lf->begin(), line_num);
×
748
                part_map.insert2(ll->get_time<std::chrono::microseconds>(),
×
749
                                 line_meta.bm_name);
×
750
            }
751
        }
752

753
        auto path = string_fragment::from_str(lf->get_unique_path())
26✔
754
                        .to_owned(this->gs_allocator);
26✔
755
        auto lf_otr = opid_time_range{};
26✔
756
        lf_otr.otr_range = lf->get_content_time_range();
26✔
757
        lf_otr.otr_level_stats = lf->get_level_stats();
26✔
758
        auto lf_row = opid_row{
26✔
759
            row_type::logfile,
760
            path,
761
            lf_otr,
762
            string_fragment::invalid(),
763
        };
26✔
764
        lf_row.or_logfile = lf;
26✔
765
        this->gs_active_opids.emplace(path, lf_row);
26✔
766

767
        {
768
            auto r_tid_map = lf->get_thread_ids().readAccess();
26✔
769

770
            for (const auto& [tid_sf, tid_meta] : r_tid_map->ltis_tid_ranges) {
70✔
771
                auto active_iter = this->gs_active_opids.find(tid_sf);
44✔
772
                if (active_iter == this->gs_active_opids.end()) {
44✔
773
                    auto tid = tid_sf.to_owned(this->gs_allocator);
36✔
774
                    auto tid_otr = opid_time_range{};
36✔
775
                    tid_otr.otr_range = tid_meta.titr_range;
36✔
776
                    tid_otr.otr_level_stats = tid_meta.titr_level_stats;
36✔
777
                    this->gs_active_opids.emplace(
×
778
                        tid,
779
                        opid_row{
36✔
780
                            row_type::thread,
781
                            tid,
782
                            tid_otr,
783
                            string_fragment::invalid(),
784
                        });
785
                } else {
36✔
786
                    active_iter->second.or_value.otr_range
16✔
787
                        |= tid_meta.titr_range;
8✔
788
                }
789
            }
790
        }
26✔
791

792
        auto format = lf->get_format();
26✔
793
        safe::ReadAccess<logfile::safe_opid_state> r_opid_map(
794
            ld->get_file_ptr()->get_opids());
26✔
795
        for (const auto& pair : r_opid_map->los_opid_ranges) {
1,010✔
796
            const opid_time_range& otr = pair.second;
984✔
797
            auto active_iter = this->gs_active_opids.find(pair.first);
984✔
798
            if (active_iter == this->gs_active_opids.end()) {
984✔
799
                auto opid = pair.first.to_owned(this->gs_allocator);
984✔
800
                auto active_emp_res = this->gs_active_opids.emplace(
1,968✔
801
                    opid,
802
                    opid_row{
984✔
803
                        row_type::opid,
804
                        opid,
805
                        otr,
806
                        string_fragment::invalid(),
807
                    });
808
                active_iter = active_emp_res.first;
984✔
809
            } else {
810
                active_iter->second.or_value |= otr;
×
811
            }
812

813
            opid_row& row = active_iter->second;
984✔
814
            for (auto& sub : row.or_value.otr_sub_ops) {
984✔
815
                auto subid_iter = this->gs_subid_map.find(sub.ostr_subid);
×
816

817
                if (subid_iter == this->gs_subid_map.end()) {
×
818
                    subid_iter = this->gs_subid_map
×
819
                                     .emplace(sub.ostr_subid.to_owned(
×
820
                                                  this->gs_allocator),
×
821
                                              true)
×
822
                                     .first;
823
                }
824
                sub.ostr_subid = subid_iter->first;
×
825
                if (sub.ostr_subid.length() > row.or_max_subid_width) {
×
826
                    row.or_max_subid_width = sub.ostr_subid.length();
×
827
                }
828
            }
829

830
            if (otr.otr_description.lod_index) {
984✔
831
                auto desc_id = otr.otr_description.lod_index.value();
979✔
832
                auto desc_def_iter
833
                    = format->lf_opid_description_def_vec->at(desc_id);
979✔
834

835
                auto desc_key
836
                    = opid_description_def_key{format->get_name(), desc_id};
979✔
837
                auto desc_defs_opt
838
                    = row.or_description_defs.odd_defs.value_for(desc_key);
979✔
839
                if (!desc_defs_opt) {
979✔
840
                    row.or_description_defs.odd_defs.insert(desc_key,
979✔
841
                                                            *desc_def_iter);
842
                }
843

844
                auto& all_descs = row.or_descriptions;
979✔
845
                const auto& new_desc_v = otr.otr_description.lod_elements;
979✔
846
                all_descs.insert(desc_key, new_desc_v);
979✔
847
            } else if (!otr.otr_description.lod_elements.empty()) {
5✔
848
                auto desc_sf = string_fragment::from_str(
4✔
849
                    otr.otr_description.lod_elements.values().front());
4✔
850
                row.or_description = desc_sf.to_owned(this->gs_allocator);
4✔
851
            }
852
            row.or_value.otr_description.lod_elements.clear();
984✔
853
        }
854

855
        if (this->gs_index_progress) {
26✔
856
            switch (this->gs_index_progress(
×
857
                progress_t{index, this->gs_lss.file_count()}))
×
858
            {
859
                case lnav::progress_result_t::ok:
×
860
                    break;
×
861
                case lnav::progress_result_t::interrupt:
×
862
                    log_debug("timeline rebuild interrupted");
×
863
                    this->ts_rebuild_in_progress = false;
×
864
                    return false;
×
865
            }
866
        }
867
    }
26✔
868
    if (this->gs_index_progress) {
26✔
869
        this->gs_index_progress(std::nullopt);
×
870
    }
871
    log_info("active opids: %zu", this->gs_active_opids.size());
26✔
872

873
    size_t filtered_in_count = 0;
26✔
874
    for (const auto& filt : this->tss_filters) {
28✔
875
        if (!filt->is_enabled()) {
2✔
876
            continue;
×
877
        }
878
        if (filt->get_type() == text_filter::INCLUDE) {
2✔
879
            filtered_in_count += 1;
1✔
880
        }
881
    }
882
    this->gs_filter_hits = {};
26✔
883
    this->gs_time_order.clear();
26✔
884
    this->gs_time_order.reserve(this->gs_active_opids.size());
26✔
885
    for (auto& pair : this->gs_active_opids) {
1,072✔
886
        opid_row& row = pair.second;
1,046✔
887
        opid_time_range& otr = pair.second.or_value;
1,046✔
888
        std::string full_desc;
1,046✔
889
        if (row.or_description.empty()) {
1,046✔
890
            const auto& desc_defs = row.or_description_defs.odd_defs;
1,042✔
891
            if (!row.or_descriptions.empty()) {
1,042✔
892
                auto desc_def_opt
893
                    = desc_defs.value_for(row.or_descriptions.keys().front());
979✔
894
                if (desc_def_opt) {
979✔
895
                    full_desc = desc_def_opt.value()->to_string(
2,937✔
896
                        row.or_descriptions.values().front());
1,958✔
897
                }
898
            }
899
            row.or_descriptions.clear();
1,042✔
900
            auto full_desc_sf = string_fragment::from_str(full_desc);
1,042✔
901
            auto desc_sf_iter = this->gs_descriptions.find(full_desc_sf);
1,042✔
902
            if (desc_sf_iter == this->gs_descriptions.end()) {
1,042✔
903
                full_desc_sf = string_fragment::from_str(full_desc).to_owned(
2,084✔
904
                    this->gs_allocator);
1,042✔
905
            }
906
            pair.second.or_description = full_desc_sf;
1,042✔
907
        } else {
908
            full_desc += pair.second.or_description;
4✔
909
        }
910

911
        shared_buffer sb_opid;
1,046✔
912
        shared_buffer_ref sbr_opid;
1,046✔
913
        sbr_opid.share(
1,046✔
914
            sb_opid, pair.second.or_name.data(), pair.second.or_name.length());
1,046✔
915
        shared_buffer sb_desc;
1,046✔
916
        shared_buffer_ref sbr_desc;
1,046✔
917
        sbr_desc.share(sb_desc, full_desc.c_str(), full_desc.length());
1,046✔
918
        if (this->tss_apply_filters) {
1,046✔
919
            auto filtered_in = false;
1,046✔
920
            auto filtered_out = false;
1,046✔
921
            for (const auto& filt : this->tss_filters) {
1,214✔
922
                if (!filt->is_enabled()) {
168✔
923
                    continue;
×
924
                }
925
                for (const auto sbr : {&sbr_opid, &sbr_desc}) {
504✔
926
                    if (filt->matches(std::nullopt, *sbr)) {
336✔
927
                        this->gs_filter_hits[filt->get_index()] += 1;
2✔
928
                        switch (filt->get_type()) {
2✔
929
                            case text_filter::INCLUDE:
1✔
930
                                filtered_in = true;
1✔
931
                                break;
1✔
932
                            case text_filter::EXCLUDE:
1✔
933
                                filtered_out = true;
1✔
934
                                break;
1✔
935
                            default:
×
936
                                break;
×
937
                        }
938
                    }
939
                }
940
            }
941

942
            if (min_log_time_opt
1,046✔
943
                && otr.otr_range.tr_end < min_log_time_opt.value())
1,046✔
944
            {
945
                filtered_out = true;
16✔
946
            }
947
            if (max_log_time_opt
1,046✔
948
                && max_log_time_opt.value() < otr.otr_range.tr_begin)
1,046✔
949
            {
950
                filtered_out = true;
16✔
951
            }
952

953
            if ((filtered_in_count > 0 && !filtered_in) || filtered_out) {
1,046✔
954
                this->gs_filtered_count += 1;
116✔
955
                continue;
116✔
956
            }
957
        }
958

959
        if (pair.second.or_name.length() > this->gs_opid_width) {
930✔
960
            this->gs_opid_width = pair.second.or_name.length();
48✔
961
        }
962
        if (full_desc.size() > max_desc_width) {
930✔
963
            max_desc_width = full_desc.size();
26✔
964
        }
965

966
        if (this->gs_lower_bound == 0us
930✔
967
            || pair.second.or_value.otr_range.tr_begin < this->gs_lower_bound)
930✔
968
        {
969
            this->gs_lower_bound = pair.second.or_value.otr_range.tr_begin;
67✔
970
        }
971
        if (this->gs_upper_bound == 0us
930✔
972
            || this->gs_upper_bound < pair.second.or_value.otr_range.tr_end)
930✔
973
        {
974
            this->gs_upper_bound = pair.second.or_value.otr_range.tr_end;
70✔
975
        }
976

977
        this->gs_time_order.emplace_back(&pair.second);
930✔
978
    }
1,510✔
979
    std::stable_sort(
26✔
980
        this->gs_time_order.begin(),
981
        this->gs_time_order.end(),
982
        [](const auto* lhs, const auto* rhs) { return *lhs < *rhs; });
5,168✔
983
    for (size_t lpc = 0; lpc < this->gs_time_order.size(); lpc++) {
956✔
984
        const auto& row = *this->gs_time_order[lpc];
930✔
985
        if (row.or_value.otr_level_stats.lls_error_count > 0) {
930✔
986
            bm_errs.insert_once(vis_line_t(lpc));
53✔
987
        } else if (row.or_value.otr_level_stats.lls_warning_count > 0) {
877✔
988
            bm_warns.insert_once(vis_line_t(lpc));
6✔
989
        }
990
    }
991

992
    this->gs_opid_width = std::min(this->gs_opid_width, MAX_OPID_WIDTH);
26✔
993
    this->gs_total_width
994
        = std::max<size_t>(22 + this->gs_opid_width + max_desc_width,
52✔
995
                           1 + 16 + 5 + 8 + 5 + 16 + 1 /* header */);
26✔
996

997
    this->tss_view->set_needs_update();
26✔
998
    this->ts_rebuild_in_progress = false;
26✔
999

1000
    ensure(this->gs_time_order.empty() || this->gs_opid_width > 0);
26✔
1001

1002
    return true;
26✔
1003
}
26✔
1004

1005
std::optional<vis_line_t>
1006
timeline_source::row_for_time(timeval time_bucket)
8✔
1007
{
1008
    auto time_bucket_us = to_us(time_bucket);
8✔
1009
    auto iter = this->gs_time_order.begin();
8✔
1010
    while (true) {
1011
        if (iter == this->gs_time_order.end()) {
9✔
1012
            return std::nullopt;
1✔
1013
        }
1014

1015
        if ((*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
8✔
1016
            break;
7✔
1017
        }
1018
        ++iter;
1✔
1019
    }
1020

1021
    auto closest_iter = iter;
7✔
1022
    auto closest_diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
7✔
1023
    for (; iter != this->gs_time_order.end(); ++iter) {
19✔
1024
        if (time_bucket_us < (*iter)->or_value.otr_range.tr_begin) {
18✔
1025
            break;
6✔
1026
        }
1027
        if (!(*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
12✔
1028
            continue;
×
1029
        }
1030

1031
        auto diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
12✔
1032
        if (diff < closest_diff) {
12✔
1033
            closest_iter = iter;
×
1034
            closest_diff = diff;
×
1035
        }
1036

1037
        for (const auto& sub : (*iter)->or_value.otr_sub_ops) {
12✔
1038
            if (!sub.ostr_range.contains_inclusive(time_bucket_us)) {
×
1039
                continue;
×
1040
            }
1041

1042
            diff = time_bucket_us - sub.ostr_range.tr_begin;
×
1043
            if (diff < closest_diff) {
×
1044
                closest_iter = iter;
×
1045
                closest_diff = diff;
×
1046
            }
1047
        }
1048
    }
1049

1050
    return vis_line_t(std::distance(this->gs_time_order.begin(), closest_iter));
14✔
1051
}
1052

1053
std::optional<vis_line_t>
1054
timeline_source::row_for(const row_info& ri)
20✔
1055
{
1056
    auto vl_opt = this->gs_lss.row_for(ri);
20✔
1057
    if (!vl_opt) {
20✔
1058
        return this->row_for_time(ri.ri_time);
×
1059
    }
1060

1061
    auto vl = vl_opt.value();
20✔
1062
    auto win = this->gs_lss.window_at(vl);
20✔
1063
    for (const auto& msg_line : *win) {
36✔
1064
        const auto& lvv = msg_line.get_values();
20✔
1065

1066
        if (lvv.lvv_opid_value) {
20✔
1067
            auto opid_iter
1068
                = this->gs_active_opids.find(lvv.lvv_opid_value.value());
15✔
1069
            if (opid_iter != this->gs_active_opids.end()) {
15✔
1070
                for (const auto& [index, oprow] :
165✔
1071
                     lnav::itertools::enumerate(this->gs_time_order))
168✔
1072
                {
1073
                    if (oprow == &opid_iter->second) {
150✔
1074
                        return vis_line_t(index);
12✔
1075
                    }
1076
                }
1077
            }
1078
        }
1079
    }
32✔
1080

1081
    return this->row_for_time(ri.ri_time);
8✔
1082
}
20✔
1083

1084
std::optional<text_time_translator::row_info>
1085
timeline_source::time_for_row(vis_line_t row)
21✔
1086
{
1087
    if (row >= this->gs_time_order.size()) {
21✔
1088
        return std::nullopt;
×
1089
    }
1090

1091
    const auto& otr = this->gs_time_order[row]->or_value;
21✔
1092

1093
    if (this->tss_view->get_selection() == row) {
21✔
1094
        auto ov_sel = this->tss_view->get_overlay_selection();
21✔
1095

1096
        if (ov_sel && ov_sel.value() < otr.otr_sub_ops.size()) {
21✔
1097
            return row_info{
×
1098
                to_timeval(otr.otr_sub_ops[ov_sel.value()].ostr_range.tr_begin),
×
1099
                row,
1100
            };
1101
        }
1102
    }
1103

1104
    auto preview_selection = this->gs_preview_view.get_selection();
21✔
1105
    if (!preview_selection) {
21✔
1106
        return std::nullopt;
×
1107
    }
1108
    if (preview_selection < this->gs_preview_rows.size()) {
21✔
1109
        return this->gs_preview_rows[preview_selection.value()];
19✔
1110
    }
1111

1112
    return row_info{
4✔
1113
        to_timeval(otr.otr_range.tr_begin),
2✔
1114
        row,
1115
    };
4✔
1116
}
1117

1118
size_t
1119
timeline_source::text_line_width(textview_curses& curses)
6,665✔
1120
{
1121
    return this->gs_total_width;
6,665✔
1122
}
1123

1124
void
1125
timeline_source::text_selection_changed(textview_curses& tc)
50✔
1126
{
1127
    static const size_t MAX_PREVIEW_LINES = 200;
1128

1129
    auto sel = tc.get_selection();
50✔
1130

1131
    this->gs_preview_source.clear();
50✔
1132
    this->gs_preview_rows.clear();
50✔
1133
    if (!sel || sel.value() >= this->gs_time_order.size()) {
50✔
1134
        return;
30✔
1135
    }
1136

1137
    const auto& row = *this->gs_time_order[sel.value()];
20✔
1138
    auto low_us = row.or_value.otr_range.tr_begin;
20✔
1139
    auto high_us = row.or_value.otr_range.tr_end;
20✔
1140
    auto id_sf = row.or_name;
20✔
1141
    auto level_stats = row.or_value.otr_level_stats;
20✔
1142
    auto ov_sel = tc.get_overlay_selection();
20✔
1143
    if (ov_sel) {
20✔
1144
        const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
1145
        id_sf = sub.ostr_subid;
×
1146
        low_us = sub.ostr_range.tr_begin;
×
1147
        high_us = sub.ostr_range.tr_end;
×
1148
        level_stats = sub.ostr_level_stats;
×
1149
    }
1150
    high_us += 1s;
20✔
1151
    auto low_vl = this->gs_lss.row_for_time(to_timeval(low_us));
20✔
1152
    auto high_vl = this->gs_lss.row_for_time(to_timeval(high_us))
20✔
1153
                       .value_or(vis_line_t(this->gs_lss.text_line_count()));
20✔
1154

1155
    if (!low_vl) {
20✔
1156
        return;
×
1157
    }
1158

1159
    auto preview_content = attr_line_t();
20✔
1160
    auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
20✔
1161
    auto win = this->gs_lss.window_at(low_vl.value(), high_vl);
20✔
1162
    auto id_bloom_bits = row.or_name.bloom_bits();
20✔
1163
    auto msg_count = 0;
20✔
1164
    for (const auto& msg_line : *win) {
1,440✔
1165
        switch (row.or_type) {
710✔
1166
            case row_type::logfile:
394✔
1167
                if (msg_line.get_file_ptr() != row.or_logfile) {
394✔
1168
                    continue;
×
1169
                }
1170
                break;
394✔
1171
            case row_type::thread: {
60✔
1172
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
60✔
1173
                    continue;
54✔
1174
                }
1175
                const auto& lvv = msg_line.get_values();
6✔
1176
                if (!lvv.lvv_thread_id_value) {
6✔
1177
                    continue;
×
1178
                }
1179
                auto tid_sf = lvv.lvv_thread_id_value.value();
6✔
1180
                if (!(tid_sf == row.or_name)) {
6✔
1181
                    continue;
×
1182
                }
1183
                break;
6✔
1184
            }
6✔
1185
            case row_type::opid: {
256✔
1186
                if (!msg_line.get_logline().match_bloom_bits(id_bloom_bits)) {
256✔
1187
                    continue;
233✔
1188
                }
1189

1190
                const auto& lvv = msg_line.get_values();
23✔
1191
                if (!lvv.lvv_opid_value) {
23✔
1192
                    continue;
×
1193
                }
1194
                auto opid_sf = lvv.lvv_opid_value.value();
23✔
1195

1196
                if (!(opid_sf == row.or_name)) {
23✔
1197
                    continue;
×
1198
                }
1199
                break;
23✔
1200
            }
23✔
1201
        }
1202

1203
        for (size_t lpc = 0; lpc < msg_line.get_line_count(); lpc++) {
849✔
1204
            auto vl = msg_line.get_vis_line() + vis_line_t(lpc);
426✔
1205
            auto cl = this->gs_lss.at(vl);
426✔
1206
            auto row_al = attr_line_t();
426✔
1207
            this->gs_log_view.textview_value_for_row(vl, row_al);
426✔
1208
            preview_content.append(row_al).append("\n");
426✔
1209
            this->gs_preview_rows.emplace_back(
426✔
1210
                msg_line.get_logline().get_timeval(), cl);
426✔
1211
            ++cl;
426✔
1212
        }
426✔
1213
        msg_count += 1;
423✔
1214
        msgs_remaining -= 1;
423✔
1215
        if (msgs_remaining == 0) {
423✔
1216
            break;
×
1217
        }
1218
    }
20✔
1219

1220
    this->gs_preview_source.replace_with(preview_content);
20✔
1221
    this->gs_preview_view.set_selection(0_vl);
20✔
1222
    this->gs_preview_status_source.get_description().set_value(
20✔
1223
        " ID %.*s", id_sf.length(), id_sf.data());
1224
    auto err_count = level_stats.lls_error_count;
20✔
1225
    if (err_count == 0) {
20✔
1226
        this->gs_preview_status_source
15✔
1227
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
15✔
1228
            .set_value("");
15✔
1229
    } else if (err_count > 1) {
5✔
1230
        this->gs_preview_status_source
2✔
1231
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
2✔
1232
            .set_value("%'d errors", err_count);
2✔
1233
    } else {
1234
        this->gs_preview_status_source
3✔
1235
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
3✔
1236
            .set_value("%'d error", err_count);
3✔
1237
    }
1238
    if (msg_count < level_stats.lls_total_count) {
20✔
1239
        this->gs_preview_status_source
19✔
1240
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
19✔
1241
            .set_value(
19✔
1242
                "%'d of %'d messages ", msg_count, level_stats.lls_total_count);
1243
    } else {
1244
        this->gs_preview_status_source
1✔
1245
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
1✔
1246
            .set_value("%'d messages ", level_stats.lls_total_count);
1✔
1247
    }
1248
    this->gs_preview_status_view.set_needs_update();
20✔
1249
}
20✔
1250

1251
void
1252
timeline_source::text_filters_changed()
12✔
1253
{
1254
    this->rebuild_indexes();
12✔
1255
    this->tss_view->reload_data();
12✔
1256
    this->tss_view->redo_search();
12✔
1257
}
12✔
1258

1259
int
1260
timeline_source::get_filtered_count() const
43✔
1261
{
1262
    return this->gs_filtered_count;
43✔
1263
}
1264

1265
int
1266
timeline_source::get_filtered_count_for(size_t filter_index) const
×
1267
{
1268
    return this->gs_filter_hits[filter_index];
×
1269
}
1270

1271
static const std::vector<breadcrumb::possibility>&
1272
timestamp_poss()
×
1273
{
1274
    const static std::vector<breadcrumb::possibility> retval = {
1275
        breadcrumb::possibility{"-1 day"},
1276
        breadcrumb::possibility{"-1h"},
1277
        breadcrumb::possibility{"-30m"},
1278
        breadcrumb::possibility{"-15m"},
1279
        breadcrumb::possibility{"-5m"},
1280
        breadcrumb::possibility{"-1m"},
1281
        breadcrumb::possibility{"+1m"},
1282
        breadcrumb::possibility{"+5m"},
1283
        breadcrumb::possibility{"+15m"},
1284
        breadcrumb::possibility{"+30m"},
1285
        breadcrumb::possibility{"+1h"},
1286
        breadcrumb::possibility{"+1 day"},
1287
    };
1288

1289
    return retval;
×
1290
}
1291

1292
void
1293
timeline_source::text_crumbs_for_line(int line,
×
1294
                                      std::vector<breadcrumb::crumb>& crumbs)
1295
{
1296
    text_sub_source::text_crumbs_for_line(line, crumbs);
×
1297

1298
    if (line >= this->gs_time_order.size()) {
×
1299
        return;
×
1300
    }
1301

1302
    const auto& row = *this->gs_time_order[line];
×
1303
    char ts[64];
1304

1305
    sql_strftime(ts, sizeof(ts), row.or_value.otr_range.tr_begin, 'T');
×
1306

1307
    crumbs.emplace_back(std::string(ts),
×
1308
                        timestamp_poss,
1309
                        [ec = this->gs_exec_context](const auto& ts) {
×
1310
                            auto cmd
×
1311
                                = fmt::format(FMT_STRING(":goto {}"),
×
1312
                                              ts.template get<std::string>());
1313
                            ec->execute(INTERNAL_SRC_LOC, cmd);
×
1314
                        });
×
1315
    crumbs.back().c_expected_input
×
1316
        = breadcrumb::crumb::expected_input_t::anything;
×
1317
    crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
×
1318
}
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