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

tstack / lnav / 19674547642-2710

25 Nov 2025 03:17PM UTC coverage: 68.841% (+0.001%) from 68.84%
19674547642-2710

push

github

tstack
[spectro] per-row thresholds

32 of 37 new or added lines in 3 files covered. (86.49%)

378 existing lines in 10 files now uncovered.

51184 of 74351 relevant lines covered (68.84%)

431944.62 hits per line

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

59.83
/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
UNCOV
69
abbrev_ftime(char* datebuf, size_t db_size, const tm& lb_tm, const tm& dt)
×
70
{
71
    char lb_fmt[32] = " ";
×
UNCOV
72
    bool same = true;
×
73

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

119
std::vector<attr_line_t>
UNCOV
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);
×
UNCOV
126
    std::vector<attr_line_t> retval;
×
127

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

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

134
    if (sti.sti_line != line) {
×
UNCOV
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();
×
UNCOV
140
    auto menu_line = vis_line_t{1};
×
141

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

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

UNCOV
148
    retval.emplace_back(attr_line_t().pad_to(left).append(title));
×
149
    {
150
        auto start = left;
×
UNCOV
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));
×
UNCOV
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()) {
×
UNCOV
162
                    log_error("unable to open clipboard: %s",
×
163
                              clip_res.unwrapErr().c_str());
UNCOV
164
                    return;
×
165
                }
166

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

173
    return retval;
×
UNCOV
174
}
×
175

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

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

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

UNCOV
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()))
×
UNCOV
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()))
×
UNCOV
215
                                     .append(" are not being shown"));
×
216
                }
217

218
                auto& fs = this->gho_src->gs_lss.get_filters();
×
219
                for (const auto& filt : fs) {
×
UNCOV
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) {
×
UNCOV
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)))
×
UNCOV
232
                            .append(" message(s) "));
×
233
                }
234
                this->gho_static_lines = um.to_attr_line().split_lines();
×
UNCOV
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")
×
UNCOV
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 ")
×
UNCOV
244
                                       .append("all_logs"_symbol));
×
245
                } else {
UNCOV
246
                    um.with_note(
×
247
                        "Operations are found in log files and none are loaded "
248
                        "right now");
249
                }
250

UNCOV
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];
×
UNCOV
257
            return true;
×
258
        }
259

UNCOV
260
        return false;
×
261
    }
262

263
    if (y > 0) {
25✔
264
        return false;
11✔
265
    }
266

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
501
    return false;
×
502
}
503

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

UNCOV
517
    return false;
×
518
}
519

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

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

547
    ensure(lb <= ub);
379✔
548
    return {lb, ub};
379✔
549
}
550

551
size_t
552
timeline_source::text_line_count()
8,715✔
553
{
554
    return this->gs_time_order.size();
8,715✔
555
}
556

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

574
        this->gs_rendered_line.clear();
365✔
575

576
        auto total_msgs = row.or_value.otr_level_stats.lls_total_count;
365✔
577
        auto truncated_name
578
            = attr_line_t::from_table_cell_content(row.or_name, MAX_OPID_WIDTH);
365✔
579
        auto truncated_desc = attr_line_t::from_table_cell_content(
580
            row.or_description, MAX_DESC_WIDTH);
365✔
581
        this->gs_rendered_line
582
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
730✔
583
            .append("  ")
365✔
584
            .append(lnav::roles::error(humanize::sparkline(
1,095✔
585
                row.or_value.otr_level_stats.lls_error_count, total_msgs)))
365✔
586
            .append(lnav::roles::warning(humanize::sparkline(
1,095✔
587
                row.or_value.otr_level_stats.lls_warning_count, total_msgs)))
365✔
588
            .append("  ")
365✔
589
            .append(lnav::roles::identifier(truncated_name))
730✔
590
            .append(
730✔
591
                this->gs_opid_width - truncated_name.utf8_length_or_length(),
365✔
592
                ' ')
593
            .append(truncated_desc);
365✔
594
        this->gs_rendered_line.with_attr_for_all(
365✔
595
            VC_ROLE.value(role_t::VCR_COMMENT));
730✔
596

597
        value_out = this->gs_rendered_line.get_string();
365✔
598
    }
365✔
599

600
    return {};
365✔
601
}
602

603
void
604
timeline_source::text_attrs_for_line(textview_curses& tc,
365✔
605
                                     int line,
606
                                     string_attrs_t& value_out)
607
{
608
    if (!this->ts_rebuild_in_progress
730✔
609
        && line < (ssize_t) this->gs_time_order.size())
365✔
610
    {
611
        const auto& row = *this->gs_time_order[line];
365✔
612

613
        value_out = this->gs_rendered_line.get_attrs();
365✔
614

615
        auto lr = line_range{-1, -1, line_range::unit::codepoint};
365✔
616
        auto [sel_lb, sel_ub]
365✔
617
            = this->get_time_bounds_for(tc.get_selection().value_or(0_vl));
365✔
618

619
        if (row.or_value.otr_range.tr_begin <= sel_ub
365✔
620
            && sel_lb <= row.or_value.otr_range.tr_end)
365✔
621
        {
622
            auto width = tc.get_dimensions().second;
365✔
623

624
            if (width > CHART_INDENT) {
365✔
625
                width -= CHART_INDENT;
365✔
626
                double span = (sel_ub - sel_lb).count();
365✔
627
                auto us_per_ch = std::chrono::microseconds{
628
                    static_cast<int64_t>(ceil(span / (double) width))};
365✔
629

630
                if (row.or_value.otr_range.tr_begin <= sel_lb) {
365✔
UNCOV
631
                    lr.lr_start = CHART_INDENT;
×
632
                } else {
633
                    auto start_diff
634
                        = (row.or_value.otr_range.tr_begin - sel_lb);
365✔
635

636
                    lr.lr_start = CHART_INDENT + floor(start_diff / us_per_ch);
365✔
637
                }
638

639
                if (sel_ub < row.or_value.otr_range.tr_end) {
365✔
640
                    lr.lr_end = -1;
7✔
641
                } else {
642
                    auto end_diff = (row.or_value.otr_range.tr_end - sel_lb);
358✔
643

644
                    lr.lr_end = CHART_INDENT + ceil(end_diff / us_per_ch);
358✔
645
                    if (lr.lr_start == lr.lr_end) {
358✔
646
                        lr.lr_end += 1;
275✔
647
                    }
648
                }
649

650
                auto block_attrs = text_attrs::with_reverse();
365✔
651
                require(lr.lr_start >= 0);
365✔
652
                value_out.emplace_back(lr, VC_STYLE.value(block_attrs));
365✔
653
            }
654
        }
655
        auto alt_row_index = line % 4;
365✔
656
        if (alt_row_index == 2 || alt_row_index == 3) {
365✔
657
            value_out.emplace_back(line_range{0, -1},
176✔
658
                                   VC_ROLE.value(role_t::VCR_ALT_ROW));
352✔
659
        }
660
    }
661
}
365✔
662

663
size_t
UNCOV
664
timeline_source::text_size_for_line(textview_curses& tc,
×
665
                                    int line,
666
                                    text_sub_source::line_flags_t raw)
667
{
UNCOV
668
    return this->gs_total_width;
×
669
}
670

671
bool
672
timeline_source::rebuild_indexes()
26✔
673
{
674
    static auto op = lnav_operation{"timeline_rebuild"};
26✔
675

676
    auto op_guard = lnav_opid_guard::internal(op);
26✔
677
    auto& bm = this->tss_view->get_bookmarks();
26✔
678
    auto& bm_errs = bm[&textview_curses::BM_ERRORS];
26✔
679
    auto& bm_warns = bm[&textview_curses::BM_WARNINGS];
26✔
680

681
    this->ts_rebuild_in_progress = true;
26✔
682
    bm_errs.clear();
26✔
683
    bm_warns.clear();
26✔
684

685
    this->gs_lower_bound = {};
26✔
686
    this->gs_upper_bound = {};
26✔
687
    this->gs_opid_width = 0;
26✔
688
    this->gs_total_width = 0;
26✔
689
    this->gs_filtered_count = 0;
26✔
690
    this->gs_active_opids.clear();
26✔
691
    this->gs_descriptions.clear();
26✔
692
    this->gs_subid_map.clear();
26✔
693
    this->gs_allocator.reset();
26✔
694
    this->gs_preview_source.clear();
26✔
695
    this->gs_preview_rows.clear();
26✔
696
    this->gs_preview_status_source.get_description().clear();
26✔
697

698
    auto min_log_time_tv_opt = this->get_min_row_time();
26✔
699
    auto max_log_time_tv_opt = this->get_max_row_time();
26✔
700
    std::optional<std::chrono::microseconds> min_log_time_opt;
26✔
701
    std::optional<std::chrono::microseconds> max_log_time_opt;
26✔
702
    auto max_desc_width = size_t{0};
26✔
703

704
    if (min_log_time_tv_opt) {
26✔
705
        min_log_time_opt = to_us(min_log_time_tv_opt.value());
1✔
706
    }
707
    if (max_log_time_tv_opt) {
26✔
708
        max_log_time_opt = to_us(max_log_time_tv_opt.value());
1✔
709
    }
710

711
    log_info("building opid table");
26✔
712
    tlx::btree_map<std::chrono::microseconds, std::string> part_map;
26✔
713
    for (const auto& [index, ld] : lnav::itertools::enumerate(this->gs_lss)) {
53✔
714
        if (ld->get_file_ptr() == nullptr) {
27✔
715
            continue;
1✔
716
        }
717
        if (!ld->is_visible()) {
27✔
718
            continue;
1✔
719
        }
720

721
        auto* lf = ld->get_file_ptr();
26✔
722
        lf->enable_cache();
26✔
723

724
        const auto& mark_meta = lf->get_bookmark_metadata();
26✔
725
        {
726
            for (const auto& [line_num, line_meta] : mark_meta) {
33✔
727
                if (line_meta.bm_name.empty()) {
7✔
728
                    continue;
7✔
729
                }
UNCOV
730
                const auto ll = std::next(lf->begin(), line_num);
×
UNCOV
731
                part_map.insert2(ll->get_time<std::chrono::microseconds>(),
×
UNCOV
732
                                 line_meta.bm_name);
×
733
            }
734
        }
735

736
        auto format = lf->get_format();
26✔
737
        safe::ReadAccess<logfile::safe_opid_state> r_opid_map(
738
            ld->get_file_ptr()->get_opids());
26✔
739
        for (const auto& pair : r_opid_map->los_opid_ranges) {
1,048✔
740
            const opid_time_range& otr = pair.second;
1,022✔
741
            auto active_iter = this->gs_active_opids.find(pair.first);
1,022✔
742
            if (active_iter == this->gs_active_opids.end()) {
1,022✔
743
                auto opid = pair.first.to_owned(this->gs_allocator);
1,003✔
744
                auto active_emp_res = this->gs_active_opids.emplace(
2,006✔
745
                    opid,
746
                    opid_row{
1,003✔
747
                        opid,
748
                        otr,
749
                        string_fragment::invalid(),
750
                    });
751
                active_iter = active_emp_res.first;
1,003✔
752
            } else {
753
                active_iter->second.or_value |= otr;
19✔
754
            }
755

756
            opid_row& row = active_iter->second;
1,022✔
757
            for (auto& sub : row.or_value.otr_sub_ops) {
1,022✔
UNCOV
758
                auto subid_iter = this->gs_subid_map.find(sub.ostr_subid);
×
759

UNCOV
760
                if (subid_iter == this->gs_subid_map.end()) {
×
UNCOV
761
                    subid_iter = this->gs_subid_map
×
UNCOV
762
                                     .emplace(sub.ostr_subid.to_owned(
×
UNCOV
763
                                                  this->gs_allocator),
×
UNCOV
764
                                              true)
×
765
                                     .first;
766
                }
UNCOV
767
                sub.ostr_subid = subid_iter->first;
×
UNCOV
768
                if (sub.ostr_subid.length() > row.or_max_subid_width) {
×
UNCOV
769
                    row.or_max_subid_width = sub.ostr_subid.length();
×
770
                }
771
            }
772

773
            if (otr.otr_description.lod_index) {
1,022✔
774
                auto desc_id = otr.otr_description.lod_index.value();
979✔
775
                auto desc_def_iter
776
                    = format->lf_opid_description_def_vec->at(desc_id);
979✔
777

778
                auto desc_key
779
                    = opid_description_def_key{format->get_name(), desc_id};
979✔
780
                auto desc_defs_opt
781
                    = row.or_description_defs.odd_defs.value_for(desc_key);
979✔
782
                if (!desc_defs_opt) {
979✔
783
                    row.or_description_defs.odd_defs.insert(desc_key,
979✔
784
                                                            *desc_def_iter);
785
                }
786

787
                auto& all_descs = row.or_descriptions;
979✔
788
                const auto& new_desc_v = otr.otr_description.lod_elements;
979✔
789
                all_descs.insert(desc_key, new_desc_v);
979✔
790
            } else if (!otr.otr_description.lod_elements.empty()) {
43✔
791
                auto desc_sf = string_fragment::from_str(
4✔
792
                    otr.otr_description.lod_elements.values().front());
4✔
793
                row.or_description = desc_sf.to_owned(this->gs_allocator);
4✔
794
            }
795
            row.or_value.otr_description.lod_elements.clear();
1,022✔
796
        }
797

798
        if (this->gs_index_progress) {
26✔
UNCOV
799
            switch (this->gs_index_progress(
×
UNCOV
800
                progress_t{index, this->gs_lss.file_count()}))
×
801
            {
UNCOV
802
                case lnav::progress_result_t::ok:
×
803
                    break;
×
UNCOV
804
                case lnav::progress_result_t::interrupt:
×
UNCOV
805
                    log_debug("timeline rebuild interrupted");
×
UNCOV
806
                    this->ts_rebuild_in_progress = false;
×
UNCOV
807
                    return false;
×
808
            }
809
        }
810
    }
26✔
811
    if (this->gs_index_progress) {
26✔
UNCOV
812
        this->gs_index_progress(std::nullopt);
×
813
    }
814
    log_info("active opids: %zu", this->gs_active_opids.size());
26✔
815

816
    size_t filtered_in_count = 0;
26✔
817
    for (const auto& filt : this->tss_filters) {
28✔
818
        if (!filt->is_enabled()) {
2✔
UNCOV
819
            continue;
×
820
        }
821
        if (filt->get_type() == text_filter::INCLUDE) {
2✔
822
            filtered_in_count += 1;
1✔
823
        }
824
    }
825
    this->gs_filter_hits = {};
26✔
826
    this->gs_time_order.clear();
26✔
827
    this->gs_time_order.reserve(this->gs_active_opids.size());
26✔
828
    for (auto& pair : this->gs_active_opids) {
1,029✔
829
        opid_row& row = pair.second;
1,003✔
830
        opid_time_range& otr = pair.second.or_value;
1,003✔
831
        std::string full_desc;
1,003✔
832
        if (row.or_description.empty()) {
1,003✔
833
            const auto& desc_defs = row.or_description_defs.odd_defs;
999✔
834
            if (!row.or_descriptions.empty()) {
999✔
835
                auto desc_def_opt
836
                    = desc_defs.value_for(row.or_descriptions.keys().front());
979✔
837
                if (desc_def_opt) {
979✔
838
                    full_desc = desc_def_opt.value()->to_string(
2,937✔
839
                        row.or_descriptions.values().front());
1,958✔
840
                }
841
            }
842
            row.or_descriptions.clear();
999✔
843
            auto full_desc_sf = string_fragment::from_str(full_desc);
999✔
844
            auto desc_sf_iter = this->gs_descriptions.find(full_desc_sf);
999✔
845
            if (desc_sf_iter == this->gs_descriptions.end()) {
999✔
846
                full_desc_sf = string_fragment::from_str(full_desc).to_owned(
1,998✔
847
                    this->gs_allocator);
999✔
848
            }
849
            pair.second.or_description = full_desc_sf;
999✔
850
        } else {
851
            full_desc += pair.second.or_description;
4✔
852
        }
853

854
        shared_buffer sb_opid;
1,003✔
855
        shared_buffer_ref sbr_opid;
1,003✔
856
        sbr_opid.share(
1,003✔
857
            sb_opid, pair.second.or_name.data(), pair.second.or_name.length());
1,003✔
858
        shared_buffer sb_desc;
1,003✔
859
        shared_buffer_ref sbr_desc;
1,003✔
860
        sbr_desc.share(sb_desc, full_desc.c_str(), full_desc.length());
1,003✔
861
        if (this->tss_apply_filters) {
1,003✔
862
            auto filtered_in = false;
1,003✔
863
            auto filtered_out = false;
1,003✔
864
            for (const auto& filt : this->tss_filters) {
1,169✔
865
                if (!filt->is_enabled()) {
166✔
UNCOV
866
                    continue;
×
867
                }
868
                for (const auto sbr : {&sbr_opid, &sbr_desc}) {
498✔
869
                    if (filt->matches(std::nullopt, *sbr)) {
332✔
870
                        this->gs_filter_hits[filt->get_index()] += 1;
2✔
871
                        switch (filt->get_type()) {
2✔
872
                            case text_filter::INCLUDE:
1✔
873
                                filtered_in = true;
1✔
874
                                break;
1✔
875
                            case text_filter::EXCLUDE:
1✔
876
                                filtered_out = true;
1✔
877
                                break;
1✔
UNCOV
878
                            default:
×
UNCOV
879
                                break;
×
880
                        }
881
                    }
882
                }
883
            }
884

885
            if (min_log_time_opt
1,003✔
886
                && otr.otr_range.tr_end < min_log_time_opt.value())
1,003✔
887
            {
888
                filtered_out = true;
16✔
889
            }
890
            if (max_log_time_opt
1,003✔
891
                && max_log_time_opt.value() < otr.otr_range.tr_begin)
1,003✔
892
            {
893
                filtered_out = true;
16✔
894
            }
895

896
            if ((filtered_in_count > 0 && !filtered_in) || filtered_out) {
1,003✔
897
                this->gs_filtered_count += 1;
115✔
898
                continue;
115✔
899
            }
900
        }
901

902
        if (pair.second.or_name.length() > this->gs_opid_width) {
888✔
903
            this->gs_opid_width = pair.second.or_name.length();
23✔
904
        }
905
        if (full_desc.size() > max_desc_width) {
888✔
906
            max_desc_width = full_desc.size();
26✔
907
        }
908

909
        if (this->gs_lower_bound == 0us
888✔
910
            || pair.second.or_value.otr_range.tr_begin < this->gs_lower_bound)
888✔
911
        {
912
            this->gs_lower_bound = pair.second.or_value.otr_range.tr_begin;
88✔
913
        }
914
        if (this->gs_upper_bound == 0us
888✔
915
            || this->gs_upper_bound < pair.second.or_value.otr_range.tr_end)
888✔
916
        {
917
            this->gs_upper_bound = pair.second.or_value.otr_range.tr_end;
80✔
918
        }
919

920
        this->gs_time_order.emplace_back(&pair.second);
888✔
921
    }
1,463✔
922
    std::stable_sort(
26✔
923
        this->gs_time_order.begin(),
924
        this->gs_time_order.end(),
925
        [](const auto* lhs, const auto* rhs) { return *lhs < *rhs; });
4,926✔
926
    for (size_t lpc = 0; lpc < this->gs_time_order.size(); lpc++) {
914✔
927
        const auto& row = *this->gs_time_order[lpc];
888✔
928
        if (row.or_value.otr_level_stats.lls_error_count > 0) {
888✔
929
            bm_errs.insert_once(vis_line_t(lpc));
21✔
930
        } else if (row.or_value.otr_level_stats.lls_warning_count > 0) {
867✔
UNCOV
931
            bm_warns.insert_once(vis_line_t(lpc));
×
932
        }
933
    }
934

935
    this->gs_opid_width = std::min(this->gs_opid_width, MAX_OPID_WIDTH);
26✔
936
    this->gs_total_width
937
        = std::max<size_t>(22 + this->gs_opid_width + max_desc_width,
52✔
938
                           1 + 16 + 5 + 8 + 5 + 16 + 1 /* header */);
26✔
939

940
    this->tss_view->set_needs_update();
26✔
941
    this->ts_rebuild_in_progress = false;
26✔
942

943
    ensure(this->gs_time_order.empty() || this->gs_opid_width > 0);
26✔
944

945
    return true;
26✔
946
}
26✔
947

948
std::optional<vis_line_t>
949
timeline_source::row_for_time(timeval time_bucket)
7✔
950
{
951
    auto time_bucket_us = to_us(time_bucket);
7✔
952
    auto iter = this->gs_time_order.begin();
7✔
953
    while (true) {
954
        if (iter == this->gs_time_order.end()) {
81✔
955
            return std::nullopt;
7✔
956
        }
957

958
        if ((*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
74✔
959
            break;
×
960
        }
961
        ++iter;
74✔
962
    }
963

964
    auto closest_iter = iter;
×
965
    auto closest_diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
×
966
    for (; iter != this->gs_time_order.end(); ++iter) {
×
UNCOV
967
        if (time_bucket_us < (*iter)->or_value.otr_range.tr_begin) {
×
UNCOV
968
            break;
×
969
        }
970
        if (!(*iter)->or_value.otr_range.contains_inclusive(time_bucket_us)) {
×
971
            continue;
×
972
        }
973

UNCOV
974
        auto diff = time_bucket_us - (*iter)->or_value.otr_range.tr_begin;
×
UNCOV
975
        if (diff < closest_diff) {
×
UNCOV
976
            closest_iter = iter;
×
977
            closest_diff = diff;
×
978
        }
979

UNCOV
980
        for (const auto& sub : (*iter)->or_value.otr_sub_ops) {
×
UNCOV
981
            if (!sub.ostr_range.contains_inclusive(time_bucket_us)) {
×
UNCOV
982
                continue;
×
983
            }
984

985
            diff = time_bucket_us - sub.ostr_range.tr_begin;
×
UNCOV
986
            if (diff < closest_diff) {
×
UNCOV
987
                closest_iter = iter;
×
UNCOV
988
                closest_diff = diff;
×
989
            }
990
        }
991
    }
992

UNCOV
993
    return vis_line_t(std::distance(this->gs_time_order.begin(), closest_iter));
×
994
}
995

996
std::optional<vis_line_t>
997
timeline_source::row_for(const row_info& ri)
20✔
998
{
999
    auto vl_opt = this->gs_lss.row_for(ri);
20✔
1000
    if (!vl_opt) {
20✔
UNCOV
1001
        return this->row_for_time(ri.ri_time);
×
1002
    }
1003

1004
    auto vl = vl_opt.value();
20✔
1005
    auto win = this->gs_lss.window_at(vl);
20✔
1006
    for (const auto& msg_line : *win) {
34✔
1007
        const auto& lvv = msg_line.get_values();
20✔
1008

1009
        if (lvv.lvv_opid_value) {
20✔
1010
            auto opid_iter
1011
                = this->gs_active_opids.find(lvv.lvv_opid_value.value());
15✔
1012
            if (opid_iter != this->gs_active_opids.end()) {
15✔
1013
                for (const auto& [index, oprow] :
96✔
1014
                     lnav::itertools::enumerate(this->gs_time_order))
98✔
1015
                {
1016
                    if (oprow == &opid_iter->second) {
81✔
1017
                        return vis_line_t(index);
13✔
1018
                    }
1019
                }
1020
            }
1021
        }
1022
    }
33✔
1023

1024
    return this->row_for_time(ri.ri_time);
7✔
1025
}
20✔
1026

1027
std::optional<text_time_translator::row_info>
1028
timeline_source::time_for_row(vis_line_t row)
19✔
1029
{
1030
    if (row >= this->gs_time_order.size()) {
19✔
UNCOV
1031
        return std::nullopt;
×
1032
    }
1033

1034
    const auto& otr = this->gs_time_order[row]->or_value;
19✔
1035

1036
    if (this->tss_view->get_selection() == row) {
19✔
1037
        auto ov_sel = this->tss_view->get_overlay_selection();
19✔
1038

1039
        if (ov_sel && ov_sel.value() < otr.otr_sub_ops.size()) {
19✔
1040
            return row_info{
×
UNCOV
1041
                to_timeval(otr.otr_sub_ops[ov_sel.value()].ostr_range.tr_begin),
×
1042
                row,
1043
            };
1044
        }
1045
    }
1046

1047
    auto preview_selection = this->gs_preview_view.get_selection();
19✔
1048
    if (!preview_selection) {
19✔
UNCOV
1049
        return std::nullopt;
×
1050
    }
1051
    if (preview_selection < this->gs_preview_rows.size()) {
19✔
1052
        return this->gs_preview_rows[preview_selection.value()];
19✔
1053
    }
1054

UNCOV
1055
    return row_info{
×
UNCOV
1056
        to_timeval(otr.otr_range.tr_begin),
×
1057
        row,
1058
    };
1059
}
1060

1061
size_t
1062
timeline_source::text_line_width(textview_curses& curses)
6,563✔
1063
{
1064
    return this->gs_total_width;
6,563✔
1065
}
1066

1067
void
1068
timeline_source::text_selection_changed(textview_curses& tc)
49✔
1069
{
1070
    static const size_t MAX_PREVIEW_LINES = 200;
1071

1072
    auto sel = tc.get_selection();
49✔
1073

1074
    this->gs_preview_source.clear();
49✔
1075
    this->gs_preview_rows.clear();
49✔
1076
    if (!sel || sel.value() >= this->gs_time_order.size()) {
49✔
1077
        return;
30✔
1078
    }
1079

1080
    const auto& row = *this->gs_time_order[sel.value()];
19✔
1081
    auto low_us = row.or_value.otr_range.tr_begin;
19✔
1082
    auto high_us = row.or_value.otr_range.tr_end;
19✔
1083
    auto id_sf = row.or_name;
19✔
1084
    auto level_stats = row.or_value.otr_level_stats;
19✔
1085
    auto ov_sel = tc.get_overlay_selection();
19✔
1086
    if (ov_sel) {
19✔
UNCOV
1087
        const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
UNCOV
1088
        id_sf = sub.ostr_subid;
×
UNCOV
1089
        low_us = sub.ostr_range.tr_begin;
×
UNCOV
1090
        high_us = sub.ostr_range.tr_end;
×
UNCOV
1091
        level_stats = sub.ostr_level_stats;
×
1092
    }
1093
    high_us += 1s;
19✔
1094
    auto low_vl = this->gs_lss.row_for_time(to_timeval(low_us));
19✔
1095
    auto high_vl = this->gs_lss.row_for_time(to_timeval(high_us))
19✔
1096
                       .value_or(vis_line_t(this->gs_lss.text_line_count()));
19✔
1097

1098
    if (!low_vl) {
19✔
UNCOV
1099
        return;
×
1100
    }
1101

1102
    auto preview_content = attr_line_t();
19✔
1103
    auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
19✔
1104
    auto win = this->gs_lss.window_at(low_vl.value(), high_vl);
19✔
1105
    auto id_hash = row.or_name.hash();
19✔
1106
    auto msg_count = 0;
19✔
1107
    for (const auto& msg_line : *win) {
1,059✔
1108
        if (!msg_line.get_logline().match_opid_hash(id_hash)) {
520✔
1109
            continue;
470✔
1110
        }
1111

1112
        const auto& lvv = msg_line.get_values();
50✔
1113
        if (!lvv.lvv_opid_value) {
50✔
UNCOV
1114
            continue;
×
1115
        }
1116
        auto opid_sf = lvv.lvv_opid_value.value();
50✔
1117

1118
        if (opid_sf == row.or_name) {
50✔
1119
            for (size_t lpc = 0; lpc < msg_line.get_line_count(); lpc++) {
115✔
1120
                auto vl = msg_line.get_vis_line() + vis_line_t(lpc);
65✔
1121
                auto cl = this->gs_lss.at(vl);
65✔
1122
                auto row_al = attr_line_t();
65✔
1123
                this->gs_log_view.textview_value_for_row(vl, row_al);
65✔
1124
                preview_content.append(row_al).append("\n");
65✔
1125
                this->gs_preview_rows.emplace_back(
65✔
1126
                    msg_line.get_logline().get_timeval(), cl);
65✔
1127
                ++cl;
65✔
1128
            }
65✔
1129
            msg_count += 1;
50✔
1130
            msgs_remaining -= 1;
50✔
1131
            if (msgs_remaining == 0) {
50✔
1132
                break;
×
1133
            }
1134
        }
1135
    }
69✔
1136

1137
    this->gs_preview_source.replace_with(preview_content);
19✔
1138
    this->gs_preview_view.set_selection(0_vl);
19✔
1139
    this->gs_preview_status_source.get_description().set_value(
19✔
1140
        " ID %.*s", id_sf.length(), id_sf.data());
1141
    auto err_count = level_stats.lls_error_count;
19✔
1142
    if (err_count == 0) {
19✔
1143
        this->gs_preview_status_source
17✔
1144
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
17✔
1145
            .set_value("");
17✔
1146
    } else if (err_count > 1) {
2✔
UNCOV
1147
        this->gs_preview_status_source
×
UNCOV
1148
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
×
UNCOV
1149
            .set_value("%'d errors", err_count);
×
1150
    } else {
1151
        this->gs_preview_status_source
2✔
1152
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
2✔
1153
            .set_value("%'d error", err_count);
2✔
1154
    }
1155
    if (msg_count < level_stats.lls_total_count) {
19✔
1156
        this->gs_preview_status_source
12✔
1157
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
12✔
1158
            .set_value(
12✔
1159
                "%'d of %'d messages ", msg_count, level_stats.lls_total_count);
1160
    } else {
1161
        this->gs_preview_status_source
7✔
1162
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
7✔
1163
            .set_value("%'d messages ", level_stats.lls_total_count);
7✔
1164
    }
1165
    this->gs_preview_status_view.set_needs_update();
19✔
1166
}
19✔
1167

1168
void
1169
timeline_source::text_filters_changed()
12✔
1170
{
1171
    this->rebuild_indexes();
12✔
1172
    this->tss_view->reload_data();
12✔
1173
    this->tss_view->redo_search();
12✔
1174
}
12✔
1175

1176
int
1177
timeline_source::get_filtered_count() const
43✔
1178
{
1179
    return this->gs_filtered_count;
43✔
1180
}
1181

1182
int
UNCOV
1183
timeline_source::get_filtered_count_for(size_t filter_index) const
×
1184
{
UNCOV
1185
    return this->gs_filter_hits[filter_index];
×
1186
}
1187

1188
static const std::vector<breadcrumb::possibility>&
UNCOV
1189
timestamp_poss()
×
1190
{
1191
    const static std::vector<breadcrumb::possibility> retval = {
1192
        breadcrumb::possibility{"-1 day"},
1193
        breadcrumb::possibility{"-1h"},
1194
        breadcrumb::possibility{"-30m"},
1195
        breadcrumb::possibility{"-15m"},
1196
        breadcrumb::possibility{"-5m"},
1197
        breadcrumb::possibility{"-1m"},
1198
        breadcrumb::possibility{"+1m"},
1199
        breadcrumb::possibility{"+5m"},
1200
        breadcrumb::possibility{"+15m"},
1201
        breadcrumb::possibility{"+30m"},
1202
        breadcrumb::possibility{"+1h"},
1203
        breadcrumb::possibility{"+1 day"},
1204
    };
1205

1206
    return retval;
×
1207
}
1208

1209
void
1210
timeline_source::text_crumbs_for_line(int line,
×
1211
                                      std::vector<breadcrumb::crumb>& crumbs)
1212
{
UNCOV
1213
    text_sub_source::text_crumbs_for_line(line, crumbs);
×
1214

1215
    if (line >= this->gs_time_order.size()) {
×
1216
        return;
×
1217
    }
1218

UNCOV
1219
    const auto& row = *this->gs_time_order[line];
×
1220
    char ts[64];
1221

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

UNCOV
1224
    crumbs.emplace_back(std::string(ts),
×
1225
                        timestamp_poss,
UNCOV
1226
                        [ec = this->gs_exec_context](const auto& ts) {
×
UNCOV
1227
                            auto cmd
×
UNCOV
1228
                                = fmt::format(FMT_STRING(":goto {}"),
×
1229
                                              ts.template get<std::string>());
UNCOV
1230
                            ec->execute(INTERNAL_SRC_LOC, cmd);
×
UNCOV
1231
                        });
×
UNCOV
1232
    crumbs.back().c_expected_input
×
UNCOV
1233
        = breadcrumb::crumb::expected_input_t::anything;
×
UNCOV
1234
    crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
×
1235
}
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