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

tstack / lnav / 25560458570-3031

08 May 2026 02:05PM UTC coverage: 70.162% (+0.08%) from 70.083%
25560458570-3031

push

github

tstack
[stats] compute cardinality for identifiers using hyperloglog

90 of 120 new or added lines in 6 files covered. (75.0%)

7 existing lines in 4 files now uncovered.

57376 of 81777 relevant lines covered (70.16%)

632883.24 hits per line

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

70.05
/src/spectro_impls.cc
1
/**
2
 * Copyright (c) 2022, 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 <memory>
31
#include <optional>
32

33
#include "spectro_impls.hh"
34

35
#include "base/itertools.hh"
36
#include "lnav.hh"
37
#include "logfile_sub_source.hh"
38
#include "logline_window.hh"
39
#include "textview_curses.hh"
40

41
using namespace lnav::roles::literals;
42

43
namespace {
44
// Scale a raw column value into the display unit declared by the
45
// column's `lvm_unit_divisor`.  Spectro consumers (axis labels,
46
// bucket boundaries, mark-range comparison) all see display units,
47
// so the divisor is applied once at the source instead of being a
48
// parameter on the spectrogram_value_source interface.
49
double
50
scale_for_display(const std::optional<logline_value_meta>& meta, double v)
557✔
51
{
52
    if (meta && meta->lvm_unit_divisor > 0.0
1,114✔
53
        && meta->lvm_unit_divisor != 1.0)
1,114✔
54
    {
NEW
55
        return v / meta->lvm_unit_divisor;
×
56
    }
57
    return v;
557✔
58
}
59
}  // namespace
60

61
class filtered_sub_source
62
    : public text_sub_source
63
    , public text_time_translator
64
    , public list_overlay_source {
65
public:
66
    bool empty() const override { return this->fss_delegate->empty(); }
×
67

68
    size_t text_line_count() override { return this->fss_lines.size(); }
×
69

70
    line_info text_value_for_line(textview_curses& tc,
×
71
                                  int line,
72
                                  std::string& value_out,
73
                                  line_flags_t flags) override
74
    {
75
        this->fss_lines | lnav::itertools::nth(line)
×
76
            | lnav::itertools::for_each([&](const auto row) {
×
77
                  this->fss_delegate->text_value_for_line(
×
78
                      tc, *row, value_out, flags);
×
79
              });
×
80

81
        return {};
×
82
    }
83

84
    size_t text_size_for_line(textview_curses& tc,
×
85
                              int line,
86
                              line_flags_t raw) override
87
    {
88
        return this->fss_lines | lnav::itertools::nth(line)
×
89
            | lnav::itertools::map([&](const auto row) {
×
90
                   return this->fss_delegate->text_size_for_line(tc, *row, raw);
×
91
               })
92
            | lnav::itertools::unwrap_or(size_t{0});
×
93
    }
94

95
    void text_attrs_for_line(textview_curses& tc,
×
96
                             int line,
97
                             string_attrs_t& value_out) override
98
    {
99
        this->fss_lines | lnav::itertools::nth(line)
×
100
            | lnav::itertools::for_each([&](const auto row) {
×
101
                  this->fss_delegate->text_attrs_for_line(tc, *row, value_out);
×
102
              });
×
103
    }
104

105
    std::optional<vis_line_t> row_for_time(struct timeval time_bucket) override
×
106
    {
107
        return this->fss_time_delegate->row_for_time(time_bucket);
×
108
    }
109

110
    std::optional<row_info> time_for_row(vis_line_t row) override
×
111
    {
112
        return this->fss_lines | lnav::itertools::nth(row)
×
113
            | lnav::itertools::flat_map([this](const auto row) {
×
114
                   return this->fss_time_delegate->time_for_row(*row);
×
115
               });
×
116
    }
117

118
    void list_value_for_overlay(const listview_curses& lv,
×
119
                                vis_line_t line,
120
                                std::vector<attr_line_t>& value_out) override
121
    {
122
        if (this->fss_overlay_delegate != nullptr) {
×
123
            this->fss_overlay_delegate->list_value_for_overlay(
×
124
                lv, line, value_out);
125
        }
126
    }
127

128
    text_sub_source* fss_delegate;
129
    text_time_translator* fss_time_delegate;
130
    list_overlay_source* fss_overlay_delegate{nullptr};
131
    std::vector<vis_line_t> fss_lines;
132
};
133

134
// Invoke the callback for each metric row in the fan-out behind `vl`
135
// (the lead row plus any suppressed metric siblings at the same
136
// timestamp) that carries `colname` in its parsed values.  Siblings
137
// get suppressed from the filtered index by rebuild_index, so they're
138
// invisible to logline_window but still contribute samples to a
139
// spectrogram for a column that lives in a sibling file.
140
template<typename F>
141
static void
142
for_each_metric_row_value(logfile_sub_source& lss,
12✔
143
                          vis_line_t vl,
144
                          const intern_string_t& colname,
145
                          F&& on_value)
146
{
147
    logline_window::logmsg_info lead_msg{lss, vl};
12✔
148
    for (const auto& sib : lead_msg.metric_siblings()) {
48✔
149
        if (sib.get_file_ptr()->stats_for_value(colname) == nullptr) {
18✔
150
            continue;
6✔
151
        }
152
        const auto& sib_values = sib.get_values();
12✔
153
        auto lv_iter = std::find_if(sib_values.lvv_values.begin(),
12✔
154
                                    sib_values.lvv_values.end(),
155
                                    logline_value_name_cmp(&colname));
156
        if (lv_iter != sib_values.lvv_values.end()) {
12✔
157
            on_value(*lv_iter);
12✔
158
        }
159
    }
160
}
12✔
161

162
log_spectro_value_source::log_spectro_value_source(intern_string_t colname)
6✔
163
    : lsvs_colname(colname)
6✔
164
{
165
    for (auto& ls : lnav_data.ld_log_source) {
8✔
166
        auto* lf = ls->get_file_ptr();
7✔
167
        if (lf == nullptr) {
7✔
168
            continue;
×
169
        }
170
        for (auto& lvm : lf->get_format_ptr()->get_value_metadata()) {
44✔
171
            if (lvm.lvm_name == this->lsvs_colname) {
42✔
172
                this->lsvs_meta = lvm;
5✔
173
                break;
5✔
174
            }
175
        }
7✔
176
        if (this->lsvs_meta) {
7✔
177
            break;
5✔
178
        }
179
    }
180
    this->update_stats();
6✔
181
}
6✔
182

183
void
184
log_spectro_value_source::update_stats()
174✔
185
{
186
    auto& lss = lnav_data.ld_log_source;
174✔
187

188
    this->lsvs_begin_time = std::chrono::microseconds::zero();
174✔
189
    this->lsvs_end_time = std::chrono::microseconds::zero();
174✔
190
    this->lsvs_stats = {};
174✔
191
    for (auto& ls : lss) {
380✔
192
        auto* lf = ls->get_file_ptr();
206✔
193

194
        if (lf == nullptr) {
206✔
195
            continue;
33✔
196
        }
197

198
        const auto* stats = lf->stats_for_value(this->lsvs_colname);
206✔
199

200
        if (stats == nullptr) {
206✔
201
            continue;
33✔
202
        }
203

204
        // Skip ignored lines (e.g., the metric-format header row
205
        // carries a zeroed timestamp) when probing the time range.
206
        auto ll = lf->begin();
173✔
207
        while (ll != lf->end() && ll->is_ignored()) {
525✔
208
            ++ll;
352✔
209
        }
210
        if (ll == lf->end()) {
173✔
211
            continue;
×
212
        }
213
        if (this->lsvs_begin_time == std::chrono::microseconds::zero()
173✔
214
            || ll->get_time<>() < this->lsvs_begin_time)
173✔
215
        {
216
            this->lsvs_begin_time = ll->get_time<>();
173✔
217
        }
218
        auto last = lf->end();
173✔
219
        --last;
173✔
220
        while (last != ll && last->is_ignored()) {
209✔
221
            --last;
36✔
222
        }
223
        if (last->get_time<>() > this->lsvs_end_time) {
173✔
224
            this->lsvs_end_time = last->get_time<>();
173✔
225
        }
226

227
        this->lsvs_found = true;
173✔
228
        auto stats_cp = *stats;
173✔
229
        this->lsvs_stats.merge(stats_cp);
173✔
230
    }
173✔
231

232
    if (this->lsvs_begin_time > std::chrono::microseconds::zero()) {
174✔
233
        auto filtered_begin_time = lss.find_line(lss.at(0_vl))->get_time<>();
173✔
234
        auto filtered_end_time
235
            = lss.find_line(lss.at(vis_line_t(lss.text_line_count() - 1)))
173✔
236
                  ->get_time<>();
173✔
237

238
        if (filtered_begin_time > this->lsvs_begin_time) {
173✔
239
            this->lsvs_begin_time = filtered_begin_time;
×
240
        }
241
        if (filtered_end_time < this->lsvs_end_time) {
173✔
242
            this->lsvs_end_time = filtered_end_time;
×
243
        }
244
    }
245
}
348✔
246

247
void
248
log_spectro_value_source::spectro_bounds(spectrogram_bounds& sb_out)
168✔
249
{
250
    auto& lss = lnav_data.ld_log_source;
168✔
251

252
    if (lss.text_line_count() == 0) {
168✔
253
        return;
×
254
    }
255

256
    this->update_stats();
168✔
257

258
    sb_out.sb_begin_time = this->lsvs_begin_time;
168✔
259
    sb_out.sb_end_time = this->lsvs_end_time;
168✔
260
    sb_out.sb_min_value_out
261
        = scale_for_display(this->lsvs_meta, this->lsvs_stats.lvs_min_value);
168✔
262
    sb_out.sb_max_value_out
263
        = scale_for_display(this->lsvs_meta, this->lsvs_stats.lvs_max_value);
168✔
264
    sb_out.sb_count = this->lsvs_stats.lvs_count;
168✔
265
    sb_out.sb_mark_generation = lnav_data.ld_views[LNV_LOG]
168✔
266
                                    .get_bookmarks()[&textview_curses::BM_USER]
168✔
267
                                    .bv_generation;
168✔
268
    sb_out.sb_tdigest = this->lsvs_stats.lvs_tdigest;
168✔
269
}
270

271
void
272
log_spectro_value_source::spectro_row(spectrogram_request& sr,
7✔
273
                                      spectrogram_row& row_out)
274
{
275
    auto& lss = lnav_data.ld_log_source;
7✔
276
    auto begin_line
277
        = lss.find_from_time(timeval{to_time_t(sr.sr_begin_time), 0})
7✔
278
              .value_or(0_vl);
7✔
279
    auto end_line = lss.find_from_time(timeval{to_time_t(sr.sr_end_time), 0})
7✔
280
                        .value_or(vis_line_t(lss.text_line_count()));
7✔
281

282
    auto add_value_from = [&](const logline_value& lv, bool marked) {
218✔
283
        switch (lv.lv_meta.lvm_kind) {
218✔
NEW
284
            case value_kind_t::VALUE_FLOAT: {
×
NEW
285
                auto d = scale_for_display(this->lsvs_meta, lv.lv_value.d);
×
NEW
286
                row_out.add_value(
×
287
                    sr, spectrogram_row::value_type::real, d, marked);
NEW
288
                row_out.sr_tdigest.insert(d);
×
UNCOV
289
                break;
×
290
            }
291
            case value_kind_t::VALUE_INTEGER: {
218✔
292
                auto d = scale_for_display(
218✔
293
                    this->lsvs_meta, static_cast<double>(lv.lv_value.i));
218✔
294
                row_out.add_value(
218✔
295
                    sr, spectrogram_row::value_type::integer, d, marked);
296
                row_out.sr_tdigest.insert(d);
218✔
297
                break;
218✔
298
            }
299
            default:
×
300
                break;
×
301
        }
302
    };
225✔
303

304
    auto win = lss.window_at(begin_line, end_line);
7✔
305
    for (const auto& msg_info : *win) {
443✔
306
        const auto& ll = msg_info.get_logline();
218✔
307
        if (ll.get_time<>() >= sr.sr_end_time) {
218✔
308
            break;
×
309
        }
310

311
        if (msg_info.is_metric_line()) {
218✔
312
            // Metric rows fan out across sibling files at the same
313
            // timestamp; helper iterates the lead + suppressed
314
            // siblings together.
315
            for_each_metric_row_value(
12✔
316
                lss,
317
                msg_info.get_vis_line(),
318
                this->lsvs_colname,
12✔
319
                [&](const logline_value& lv) {
24✔
320
                    add_value_from(lv, ll.is_marked());
12✔
321
                });
12✔
322
        } else {
323
            const auto& values = msg_info.get_values();
206✔
324
            auto lv_iter
325
                = find_if(values.lvv_values.begin(),
206✔
326
                          values.lvv_values.end(),
327
                          logline_value_name_cmp(&this->lsvs_colname));
206✔
328
            if (lv_iter != values.lvv_values.end()) {
206✔
329
                add_value_from(*lv_iter, ll.is_marked());
206✔
330
            }
331
        }
332
    }
7✔
333

334
    row_out.sr_details_source_provider = [this](const spectrogram_request& sr,
14✔
335
                                                double range_min,
336
                                                double range_max) {
337
        auto& lss = lnav_data.ld_log_source;
×
338
        auto retval = std::make_unique<filtered_sub_source>();
×
339
        auto begin_line
340
            = lss.find_from_time(timeval{to_time_t(sr.sr_begin_time), 0})
×
341
                  .value_or(0_vl);
×
342
        auto end_line
343
            = lss.find_from_time(timeval{to_time_t(sr.sr_end_time), 0})
×
344
                  .value_or(vis_line_t(lss.text_line_count()));
×
345

346
        retval->fss_delegate = &lss;
×
347
        retval->fss_time_delegate = &lss;
×
348
        retval->fss_overlay_delegate = nullptr;
×
349

350
        auto in_range = [&](const logline_value& lv) {
×
351
            switch (lv.lv_meta.lvm_kind) {
×
352
                case value_kind_t::VALUE_FLOAT:
×
353
                    return range_min <= lv.lv_value.d
×
354
                        && lv.lv_value.d < range_max;
×
355
                case value_kind_t::VALUE_INTEGER:
×
356
                    return range_min <= lv.lv_value.i
×
357
                        && lv.lv_value.i < range_max;
×
358
                default:
×
359
                    return false;
×
360
            }
361
        };
362

363
        auto win = lss.window_at(begin_line, end_line);
×
364
        for (const auto& msg_info : *win) {
×
365
            const auto& ll = msg_info.get_logline();
×
366
            if (ll.get_time<>() >= sr.sr_end_time) {
×
367
                break;
×
368
            }
369

370
            bool matched = false;
×
371
            if (msg_info.is_metric_line()) {
×
372
                for_each_metric_row_value(
×
373
                    lss,
374
                    msg_info.get_vis_line(),
375
                    this->lsvs_colname,
×
376
                    [&](const logline_value& lv) {
×
377
                        if (in_range(lv)) {
×
378
                            matched = true;
×
379
                        }
380
                    });
×
381
            } else {
382
                const auto& values = msg_info.get_values();
×
383
                auto lv_iter = find_if(
×
384
                    values.lvv_values.begin(),
385
                    values.lvv_values.end(),
386
                    logline_value_name_cmp(&this->lsvs_colname));
×
387
                if (lv_iter != values.lvv_values.end() && in_range(*lv_iter))
×
388
                {
389
                    matched = true;
×
390
                }
391
            }
392
            if (matched) {
×
393
                retval->fss_lines.emplace_back(msg_info.get_vis_line());
×
394
            }
395
        }
396

397
        return retval;
×
398
    };
7✔
399
}
7✔
400

401
bool
402
log_spectro_value_source::spectro_is_marked(spectrogram_request& sr)
7✔
403
{
404
    auto& lss = lnav_data.ld_log_source;
7✔
405
    auto begin_line
406
        = lss.find_from_time(timeval{to_time_t(sr.sr_begin_time), 0})
7✔
407
              .value_or(0_vl);
7✔
408
    auto end_line = lss.find_from_time(timeval{to_time_t(sr.sr_end_time), 0})
7✔
409
                        .value_or(vis_line_t(lss.text_line_count()));
7✔
410
    auto win = lss.window_at(begin_line, end_line);
7✔
411
    for (const auto& msg_info : *win) {
431✔
412
        const auto& ll = msg_info.get_logline();
214✔
413
        if (ll.get_time<>() >= sr.sr_end_time) {
214✔
414
            break;
×
415
        }
416

417
        if (ll.is_marked()) {
214✔
418
            return true;
2✔
419
        }
420
    }
9✔
421
    return false;
5✔
422
}
7✔
423

424
void
425
log_spectro_value_source::spectro_mark(textview_curses& tc,
1✔
426
                                       std::chrono::microseconds begin_time,
427
                                       std::chrono::microseconds end_time,
428
                                       double range_min,
429
                                       double range_max,
430
                                       mark_op_t op)
431
{
432
    // XXX need to refactor this and the above method
433
    auto& log_tc = lnav_data.ld_views[LNV_LOG];
1✔
434
    auto& lss = lnav_data.ld_log_source;
1✔
435
    auto begin_line
436
        = lss.find_from_time(timeval{to_time_t(begin_time), 0}).value_or(0_vl);
1✔
437
    auto end_line = lss.find_from_time(timeval{to_time_t(end_time), 0})
1✔
438
                        .value_or(vis_line_t(lss.text_line_count()));
1✔
439
    logline_value_vector values;
1✔
440
    string_attrs_t sa;
1✔
441

442
    auto in_range = [&](const logline_value& lv) {
3✔
443
        switch (lv.lv_meta.lvm_kind) {
3✔
NEW
444
            case value_kind_t::VALUE_FLOAT: {
×
NEW
445
                auto d = scale_for_display(this->lsvs_meta, lv.lv_value.d);
×
NEW
446
                return range_min <= d && d <= range_max;
×
447
            }
448
            case value_kind_t::VALUE_INTEGER: {
3✔
449
                auto d = scale_for_display(
3✔
450
                    this->lsvs_meta, static_cast<double>(lv.lv_value.i));
3✔
451
                return range_min <= d && d <= range_max;
3✔
452
            }
453
            default:
×
454
                return false;
×
455
        }
456
    };
1✔
457

458
    for (auto curr_line = begin_line; curr_line < end_line; ++curr_line) {
4✔
459
        auto cl = lss.at(curr_line);
3✔
460
        const auto lf = lss.find(cl);
3✔
461
        auto ll = lf->begin() + cl;
3✔
462
        const auto* format = lf->get_format_ptr();
3✔
463

464
        if (!ll->is_message()) {
3✔
465
            continue;
×
466
        }
467

468
        bool matched = false;
3✔
469
        if (format->lf_is_metric) {
3✔
470
            // Metric rows fan out across sibling files at the same
471
            // timestamp; helper iterates lead + suppressed siblings.
472
            for_each_metric_row_value(
×
473
                lss,
474
                curr_line,
475
                this->lsvs_colname,
×
476
                [&](const logline_value& lv) {
×
477
                    if (in_range(lv)) {
×
478
                        matched = true;
×
479
                    }
480
                });
×
481
        } else {
482
            values.clear();
3✔
483
            lf->read_full_message(ll, values.lvv_sbr);
3✔
484
            values.lvv_sbr.erase_ansi();
3✔
485
            sa.clear();
3✔
486
            format->annotate(lf.get(), cl, sa, values);
3✔
487

488
            auto lv_iter
489
                = find_if(values.lvv_values.begin(),
3✔
490
                          values.lvv_values.end(),
491
                          logline_value_name_cmp(&this->lsvs_colname));
3✔
492
            if (lv_iter != values.lvv_values.end() && in_range(*lv_iter)) {
3✔
493
                matched = true;
1✔
494
            }
495
        }
496
        if (matched) {
3✔
497
            // Mark the lead; text_mark fan-out covers the siblings.
498
            log_tc.set_user_mark(&textview_curses::BM_USER,
1✔
499
                                 curr_line,
500
                                 op == mark_op_t::add);
501
        }
502
    }
3✔
503
}
1✔
504

505
std::string
506
log_spectro_value_source::spectro_value_suffix() const
8✔
507
{
508
    if (this->lsvs_meta && !this->lsvs_meta->lvm_unit_suffix.empty()) {
8✔
509
        return this->lsvs_meta->lvm_unit_suffix.to_string();
2✔
510
    }
511
    return {};
6✔
512
}
513

514
db_spectro_value_source::db_spectro_value_source(std::string colname)
5✔
515
    : dsvs_colname(std::move(colname))
5✔
516
{
517
    this->update_stats();
5✔
518
}
5✔
519

520
void
521
db_spectro_value_source::update_stats()
36✔
522
{
523
    this->dsvs_begin_time = std::chrono::microseconds::zero();
36✔
524
    this->dsvs_end_time = std::chrono::microseconds::zero();
36✔
525
    this->dsvs_stats = {};
36✔
526

527
    auto& dls = lnav_data.ld_db_row_source;
36✔
528

529
    this->dsvs_column_index = dls.column_name_to_index(this->dsvs_colname);
36✔
530

531
    if (!dls.has_log_time_column()) {
36✔
532
        if (dls.dls_time_column_invalidated_at) {
2✔
533
            static const auto order_by_help = attr_line_t()
1✔
534
                                                  .append("ORDER BY"_keyword)
1✔
535
                                                  .append(" ")
1✔
536
                                                  .append("log_time"_variable)
1✔
537
                                                  .append(" ")
1✔
538
                                                  .append("ASC"_keyword)
1✔
539
                                                  .move();
2✔
540

541
            this->dsvs_error_msg
542
                = lnav::console::user_message::error(
2✔
543
                      "Cannot generate spectrogram for database results")
544
                      .with_reason(
2✔
545
                          attr_line_t()
2✔
546
                              .append("The ")
1✔
547
                              .append_quoted("log_time"_variable)
1✔
548
                              .appendf(
2✔
549
                                  FMT_STRING(" column is not in ascending "
2✔
550
                                             "order between rows {} and {}"),
551
                                  dls.dls_time_column_invalidated_at.value()
1✔
552
                                      - 1,
1✔
553
                                  dls.dls_time_column_invalidated_at.value()))
554
                      .with_note(
2✔
555
                          attr_line_t("An ascending ")
2✔
556
                              .append_quoted("log_time"_variable)
1✔
557
                              .append(
1✔
558
                                  " column is needed to render a spectrogram"))
559
                      .with_help(attr_line_t("Add an ")
2✔
560
                                     .append_quoted(order_by_help)
2✔
561
                                     .append(" clause to your ")
1✔
562
                                     .append("SELECT"_keyword)
1✔
563
                                     .append(" statement"))
1✔
564
                      .move();
2✔
565
        } else {
566
            this->dsvs_error_msg
567
                = lnav::console::user_message::error(
2✔
568
                      "Cannot generate spectrogram for database results")
569
                      .with_reason(
2✔
570
                          attr_line_t()
2✔
571
                              .append("No ")
1✔
572
                              .append_quoted("log_time"_variable)
1✔
573
                              .append(" column found in the result set"))
1✔
574
                      .with_note(
2✔
575
                          attr_line_t("An ascending ")
2✔
576
                              .append_quoted("log_time"_variable)
1✔
577
                              .append(
1✔
578
                                  " column is needed to render a spectrogram"))
579
                      .with_help(
2✔
580
                          attr_line_t("Include a ")
2✔
581
                              .append_quoted("log_time"_variable)
1✔
582
                              .append(" column in your ")
1✔
583
                              .append(" statement. Use an ")
1✔
584
                              .append("AS"_keyword)
1✔
585
                              .append(
1✔
586
                                  " directive to alias a computed timestamp"))
587
                      .move();
2✔
588
        }
589
        return;
4✔
590
    }
591

592
    if (!this->dsvs_column_index) {
34✔
593
        this->dsvs_error_msg
594
            = lnav::console::user_message::error(
2✔
595
                  "Cannot generate spectrogram for database results")
596
                  .with_reason(attr_line_t("unknown column -- ")
2✔
597
                                   .append_quoted(lnav::roles::variable(
2✔
598
                                       this->dsvs_colname)))
1✔
599
                  .with_help("Expecting a numeric column to visualize")
2✔
600
                  .move();
1✔
601
        return;
1✔
602
    }
603

604
    if (!dls.dls_headers[this->dsvs_column_index.value()].is_graphable()) {
33✔
605
        this->dsvs_error_msg
606
            = lnav::console::user_message::error(
2✔
607
                  "Cannot generate spectrogram for database results")
608
                  .with_reason(attr_line_t()
2✔
609
                                   .append_quoted(lnav::roles::variable(
2✔
610
                                       this->dsvs_colname))
1✔
611
                                   .append(" is not a numeric column"))
1✔
612
                  .with_help("Only numeric columns can be visualized")
2✔
613
                  .move();
1✔
614
        return;
1✔
615
    }
616

617
    if (dls.dls_row_cursors.empty()) {
32✔
618
        this->dsvs_error_msg
619
            = lnav::console::user_message::error(
×
620
                  "Cannot generate spectrogram for database results")
621
                  .with_reason("Result set is empty")
×
622
                  .move();
×
623
        return;
×
624
    }
625

626
    this->dsvs_begin_time = dls.dls_time_column.front();
32✔
627
    this->dsvs_end_time = dls.dls_time_column.back();
32✔
628

629
    auto find_res
630
        = dls.dls_headers | lnav::itertools::find_if([this](const auto& elem) {
32✔
631
              return elem.hm_name == this->dsvs_colname;
64✔
632
          });
32✔
633
    if (find_res) {
32✔
634
        auto hm = find_res.value();
32✔
635
        auto& bs = hm->hm_chart.get_stats_for(this->dsvs_colname);
32✔
636
        this->dsvs_stats.lvs_min_value = bs.bs_min_value;
32✔
637
        this->dsvs_stats.lvs_max_value = bs.bs_max_value;
32✔
638
        this->dsvs_stats.lvs_tdigest = hm->hm_tdigest;
32✔
639
    }
640

641
    this->dsvs_stats.lvs_count = dls.dls_row_cursors.size();
32✔
642
}
36✔
643

644
std::string
645
db_spectro_value_source::spectro_value_suffix() const
2✔
646
{
647
    if (!this->dsvs_column_index) {
2✔
NEW
648
        return {};
×
649
    }
650
    auto& dls = lnav_data.ld_db_row_source;
2✔
651
    return dls.dls_headers[this->dsvs_column_index.value()].hm_unit_suffix;
2✔
652
}
653

654
void
655
db_spectro_value_source::spectro_bounds(spectrogram_bounds& sb_out)
31✔
656
{
657
    auto& dls = lnav_data.ld_db_row_source;
31✔
658

659
    if (dls.text_line_count() == 0) {
31✔
660
        return;
×
661
    }
662

663
    this->update_stats();
31✔
664

665
    sb_out.sb_begin_time = this->dsvs_begin_time;
31✔
666
    sb_out.sb_end_time = this->dsvs_end_time;
31✔
667
    sb_out.sb_min_value_out = this->dsvs_stats.lvs_min_value;
31✔
668
    sb_out.sb_max_value_out = this->dsvs_stats.lvs_max_value;
31✔
669
    sb_out.sb_count = this->dsvs_stats.lvs_count;
31✔
670
    sb_out.sb_tdigest = this->dsvs_stats.lvs_tdigest;
31✔
671
}
672

673
void
674
db_spectro_value_source::spectro_row(spectrogram_request& sr,
1✔
675
                                     spectrogram_row& row_out)
676
{
677
    auto& dls = lnav_data.ld_db_row_source;
1✔
678
    auto begin_row
679
        = dls.row_for_time({to_time_t(sr.sr_begin_time), 0}).value_or(0_vl);
1✔
680
    auto end_row = dls.row_for_time({to_time_t(sr.sr_end_time), 0})
1✔
681
                       .value_or(vis_line_t(dls.dls_row_cursors.size()));
1✔
682

683
    for (auto lpc = begin_row; lpc < end_row; ++lpc) {
10✔
684
        auto get_res
685
            = dls.get_cell_as_numeric(lpc, this->dsvs_column_index.value());
9✔
686

687
        if (get_res.valid()) {
9✔
688
            if (get_res.is<int64_t>()) {
9✔
689
                row_out.add_value(sr,
×
690
                                  spectrogram_row::value_type::integer,
691
                                  get_res.get<int64_t>(),
×
692
                                  false);
693
            } else {
694
                row_out.add_value(sr,
9✔
695
                                  spectrogram_row::value_type::real,
696
                                  get_res.get<double>(),
9✔
697
                                  false);
698
            }
699
        }
700
    }
9✔
701

702
    row_out.sr_details_source_provider = [this](const spectrogram_request& sr,
2✔
703
                                                double range_min,
704
                                                double range_max) {
705
        auto& dls = lnav_data.ld_db_row_source;
×
706
        auto retval = std::make_unique<filtered_sub_source>();
×
707

708
        retval->fss_delegate = &dls;
×
709
        retval->fss_time_delegate = &dls;
×
710
        retval->fss_overlay_delegate = &lnav_data.ld_db_overlay;
×
711
        auto begin_row
712
            = dls.row_for_time({to_time_t(sr.sr_begin_time), 0}).value_or(0_vl);
×
713
        auto end_row = dls.row_for_time({to_time_t(sr.sr_end_time), 0})
×
714
                           .value_or(vis_line_t(dls.dls_row_cursors.size()));
×
715

716
        for (auto lpc = begin_row; lpc < end_row; ++lpc) {
×
717
            auto get_res
718
                = dls.get_cell_as_double(lpc, this->dsvs_column_index.value());
×
719
            if (!get_res) {
×
720
                continue;
×
721
            }
722
            auto value = get_res.value();
×
723
            if ((range_min == value)
×
724
                || (range_min < value && value < range_max))
×
725
            {
726
                retval->fss_lines.emplace_back(lpc);
×
727
            }
728
        }
729

730
        return retval;
×
731
    };
1✔
732
}
1✔
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