Skip to content

Commit ace66cd

Browse files
committed
[ui] add cpu/mem consumed for indexing a file
Related to #1689
1 parent 6987ed3 commit ace66cd

23 files changed

Lines changed: 371 additions & 190 deletions

NEWS.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ Features:
4343
separator will be automatically detected and the header
4444
compared against the columns defined in the tabular
4545
formats. If a good match is found, it will be used as
46-
the format for the file.
46+
the format for the file. Quoted cells that span
47+
multiple physical lines (an embedded LF inside `"..."`)
48+
are stitched back into a single log message, so an
49+
Excel-exported CSV with a multi-line free-text column
50+
shows one logline per row and SQL/search operates on
51+
the merged cell value.
4752
* Added a log format for the `fsck_apfs` and `fsck_hfs` tools on
4853
macOS, covering both the `started`/`completed` lifecycle lines
4954
and legacy `run` entries. This replaces the previous
@@ -67,6 +72,19 @@ Features:
6772
from a HyperLogLog sketch (~4 KB per text column,
6873
~1.6% standard error). Stats render in the column's
6974
declared unit when one is set.
75+
* The FILES panel now reports per-file indexing cost: an
76+
`Index Time` row shows cumulative wall-clock vs.
77+
thread-CPU spent indexing the file. In addition,
78+
`Index Memory` and `Line Buffer` rows show allocations
79+
for indexing and I/O-cache. The same data is queryable
80+
as a JSON object through the new `stats` column on the
81+
`lnav_file` SQL vtab, with keys `polls`, `reads`,
82+
`index/wall-us`, `index/cpu-us`, `index/memory-bytes`,
83+
and `line-buffer-memory-bytes`.
84+
* The `humanize_duration()` SQL function now preserves
85+
nanosecond inputs: a value like `0.0000001` (100 ns)
86+
renders as `"100ns"` instead of being floored to
87+
`"0s"`.
7088

7189
Interface Changes:
7290
* Moving horizontally now defaults to moving to the

src/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,6 @@ add_library(
730730
logfile_sub_source.cfg.hh
731731
logfile.hh
732732
logfile_fwd.hh
733-
logfile_stats.hh
734733
logline_window.hh
735734
md2attr_line.hh
736735
md4cpp.hh

src/base/humanize.cc

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -500,11 +500,9 @@ format(double value, string_fragment suffix, alignment align)
500500
const auto sr = si_scale(value, -4);
501501
return fmt::format(FMT_STRING("{:.3g}{}s"), sr.value, sr.prefix);
502502
}
503-
timeval tv;
504-
const auto secs = std::trunc(value);
505-
tv.tv_sec = static_cast<time_t>(secs);
506-
tv.tv_usec = static_cast<suseconds_t>((value - secs) * 1e6);
507-
return humanize::time::duration::from_tv(tv).to_string();
503+
return humanize::time::duration::from(
504+
std::chrono::duration<double>{value})
505+
.to_string();
508506
}
509507
// Any other suffix gets up-only SI scaling so large counts /
510508
// rates / frequencies render compactly (e.g. `1.2kHz`,

src/base/humanize.time.cc

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,6 @@ point::as_precise_time_ago() const
139139
return this->as_time_ago();
140140
}
141141

142-
duration
143-
duration::from_tv(const timeval& tv)
144-
{
145-
return duration{tv};
146-
}
147-
148142
std::string
149143
duration::to_string() const
150144
{
@@ -163,33 +157,66 @@ duration::to_string() const
163157
};
164158

165159
const auto* curr_interval = intervals;
166-
auto usecs = to_us(this->d_timeval);
160+
auto nsecs = this->d_nsecs;
167161
std::string retval;
168162
bool neg = false;
169163

170-
if (usecs == 0us) {
164+
if (nsecs == 0ns) {
171165
if (!this->d_compact) {
172166
retval = "0s";
173167
}
174168
return retval;
175169
}
176170

177-
if (usecs < 0s) {
171+
if (nsecs < 0ns) {
178172
neg = true;
179-
usecs = -usecs;
173+
nsecs = -nsecs;
180174
}
181175

182-
if (usecs < 1ms) {
183-
return fmt::format(FMT_STRING("{}us"), usecs.count());
176+
// Sub-microsecond: render as `Nns`. Always emitted at full
177+
// precision — there's no segmented form below `us`, and rounding
178+
// the only value we have produces zeros.
179+
if (nsecs < 1us) {
180+
auto out = fmt::format(FMT_STRING("{}ns"), nsecs.count());
181+
if (neg) {
182+
out.insert(0, "-");
183+
}
184+
return out;
184185
}
185186

186-
uint64_t remaining = usecs.count() / 1000;
187-
uint64_t scale = 1;
188-
if (this->d_msecs_resolution > 0) {
189-
remaining = roundup(remaining, this->d_msecs_resolution);
187+
// `[1us, 1ms)`: render as `Nus`, truncating any sub-microsecond
188+
// remainder. Matches the pre-nanosecond behavior of the old
189+
// timeval-based path which couldn't represent sub-us anyway.
190+
if (nsecs < 1ms) {
191+
auto usecs
192+
= std::chrono::duration_cast<std::chrono::microseconds>(nsecs);
193+
auto out = fmt::format(FMT_STRING("{}us"), usecs.count());
194+
if (neg) {
195+
out.insert(0, "-");
196+
}
197+
return out;
198+
}
199+
200+
// `>= 1ms`: fall into the segmented hh/mm/ss/ms peeling loop.
201+
// Resolution is interpreted in milliseconds here; anything finer
202+
// (e.g. `with_resolution(1ns)`) collapses to a 1ms floor — full
203+
// sub-ms precision in the segmented form would require extending
204+
// the intervals table below the ms tier.
205+
auto resolution_ms
206+
= std::chrono::duration_cast<std::chrono::milliseconds>(
207+
this->d_resolution)
208+
.count();
209+
if (resolution_ms < 1) {
210+
resolution_ms = 1;
190211
}
212+
213+
uint64_t remaining
214+
= std::chrono::duration_cast<std::chrono::milliseconds>(nsecs)
215+
.count();
216+
uint64_t scale = 1;
217+
remaining = roundup(remaining, static_cast<uint64_t>(resolution_ms));
191218
auto skipped = 0;
192-
if (usecs >= 10min) {
219+
if (nsecs >= 10min) {
193220
remaining /= curr_interval->length;
194221
scale *= curr_interval->length;
195222
++curr_interval;
@@ -199,7 +226,7 @@ duration::to_string() const
199226
for (; curr_interval != std::end(intervals); curr_interval++) {
200227
uint64_t amount;
201228
char segment[32];
202-
auto skip = scale < this->d_msecs_resolution;
229+
auto skip = scale < static_cast<uint64_t>(resolution_ms);
203230

204231
if (curr_interval->length) {
205232
amount = remaining % curr_interval->length;

src/base/humanize.time.hh

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,30 +77,42 @@ private:
7777

7878
class duration {
7979
public:
80-
static duration from_tv(const timeval& tv);
80+
// Accept any chrono::duration source and canonicalize to ns.
81+
// Lets callers pass e.g. `std::chrono::nanoseconds`, `microseconds`,
82+
// or a `steady_clock::duration` directly.
83+
template<class Rep, class Period>
84+
static duration from(const std::chrono::duration<Rep, Period>& d)
85+
{
86+
return duration{
87+
std::chrono::duration_cast<std::chrono::nanoseconds>(d)};
88+
}
8189

8290
duration& with_compact(bool compact)
8391
{
8492
this->d_compact = compact;
8593
return *this;
8694
}
8795

96+
// The smallest unit `to_string()` will render. Sub-microsecond
97+
// values (below `1us`) always print as `Nns` regardless of this;
98+
// values in `[1us, 1ms)` print as `Nus`; for `>= 1ms`, the
99+
// resolution is interpreted in milliseconds (anything finer than
100+
// 1ms collapses to 1ms in the segmented hh/mm/ss/ms output).
88101
template<class Rep, class Period>
89102
duration& with_resolution(const std::chrono::duration<Rep, Period>& res)
90103
{
91-
this->d_msecs_resolution
92-
= std::chrono::duration_cast<std::chrono::milliseconds>(res)
93-
.count();
104+
this->d_resolution
105+
= std::chrono::duration_cast<std::chrono::nanoseconds>(res);
94106
return *this;
95107
}
96108

97109
[[nodiscard]] std::string to_string() const;
98110

99111
private:
100-
explicit duration(const timeval& tv) : d_timeval(tv) {}
112+
explicit duration(std::chrono::nanoseconds d) : d_nsecs(d) {}
101113

102-
timeval d_timeval;
103-
uint64_t d_msecs_resolution{1};
114+
std::chrono::nanoseconds d_nsecs{0};
115+
std::chrono::nanoseconds d_resolution{std::chrono::milliseconds{1}};
104116
bool d_compact{true};
105117
};
106118

src/base/humanize.time.tests.cc

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,31 +103,74 @@ TEST_CASE("time ago")
103103

104104
TEST_CASE("duration to_string")
105105
{
106+
using namespace std::chrono_literals;
106107
std::string val;
107108

108-
val = humanize::time::duration::from_tv({15 * 60, 0})
109+
val = humanize::time::duration::from(15min)
109110
.with_compact(false)
110111
.to_string();
111112
CHECK(val == "15m00s");
112-
val = humanize::time::duration::from_tv({0, 100000})
113-
.with_compact(false)
114-
.to_string();
113+
val = humanize::time::duration::from(100ms).with_compact(false).to_string();
115114
CHECK(val == "100ms");
116-
val = humanize::time::duration::from_tv({25 * 60 * 60, 123000}).to_string();
115+
val = humanize::time::duration::from(25h + 123ms).to_string();
117116
CHECK(val == "1d01h00m00s");
118-
val = humanize::time::duration::from_tv({25 * 60 * 60 + 25 * 60, 123000})
119-
.to_string();
117+
val = humanize::time::duration::from(25h + 25min + 123ms).to_string();
120118
CHECK(val == "1d01h25m00s");
121-
val = humanize::time::duration::from_tv({10, 123000}).to_string();
119+
val = humanize::time::duration::from(10s + 123ms).to_string();
122120
CHECK(val == "10s123");
123-
val = humanize::time::duration::from_tv({10, 0}).to_string();
121+
val = humanize::time::duration::from(10s).to_string();
124122
CHECK(val == "10s");
125-
val = humanize::time::duration::from_tv({0, 100000}).to_string();
123+
val = humanize::time::duration::from(100ms).to_string();
126124
CHECK(val == "100");
127-
val = humanize::time::duration::from_tv({0, 0}).to_string();
125+
val = humanize::time::duration::from(0s).to_string();
128126
CHECK(val == "");
129-
val = humanize::time::duration::from_tv({0, -10000}).to_string();
127+
val = humanize::time::duration::from(-10ms).to_string();
130128
CHECK(val == "-010");
131-
val = humanize::time::duration::from_tv({-10, 0}).to_string();
129+
val = humanize::time::duration::from(-10s).to_string();
132130
CHECK(val == "-10s");
133131
}
132+
133+
TEST_CASE("duration nanosecond input")
134+
{
135+
using namespace std::chrono_literals;
136+
std::string val;
137+
138+
// Sub-microsecond → `Nns`, full precision regardless of resolution.
139+
val = humanize::time::duration::from(500ns).to_string();
140+
CHECK(val == "500ns");
141+
142+
// Negative sub-microsecond carries the leading sign.
143+
val = humanize::time::duration::from(-1ns).to_string();
144+
CHECK(val == "-1ns");
145+
146+
// `[1us, 1ms)` → `Nus`, sub-microsecond remainder dropped (matches
147+
// pre-nanosecond timeval truncation behavior).
148+
val = humanize::time::duration::from(1234ns).to_string();
149+
CHECK(val == "1us");
150+
151+
val = humanize::time::duration::from(999us + 999ns).to_string();
152+
CHECK(val == "999us");
153+
154+
// `>= 1ms` falls into the segmented path; sub-millisecond
155+
// precision collapses to 1ms (the existing format doesn't carry
156+
// sub-ms segments).
157+
val = humanize::time::duration::from(1ms + 500us).to_string();
158+
CHECK(val == "001");
159+
160+
val = humanize::time::duration::from(10s + 123ms + 456us).to_string();
161+
CHECK(val == "10s123");
162+
163+
// Zero stays empty in compact mode regardless of source duration.
164+
val = humanize::time::duration::from(0ns).to_string();
165+
CHECK(val == "");
166+
val = humanize::time::duration::from(0ns).with_compact(false).to_string();
167+
CHECK(val == "0s");
168+
169+
// with_resolution(ns) doesn't grant sub-ms precision in the
170+
// segmented path — explicitly characterize the floor so a future
171+
// extension shows up as a diff.
172+
val = humanize::time::duration::from(10s + 123ms + 456us)
173+
.with_resolution(1ns)
174+
.to_string();
175+
CHECK(val == "10s123");
176+
}

src/base/time_util.hh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ to_us(const timeval& tv)
5252
+ std::chrono::microseconds{tv.tv_usec};
5353
}
5454

55+
inline std::chrono::microseconds
56+
to_us(const timespec& ts)
57+
{
58+
return std::chrono::duration_cast<std::chrono::microseconds>(
59+
std::chrono::seconds{ts.tv_sec})
60+
+ std::chrono::duration_cast<std::chrono::microseconds>(
61+
std::chrono::nanoseconds{ts.tv_nsec});
62+
}
63+
5564
namespace lnav {
5665

5766
using time64_t = uint64_t;

src/command_executor.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ bind_sql_parameters(exec_context& ec, sqlite3_stmt* stmt)
272272
} else {
273273
auto zoom_level
274274
= ec.ec_label_source_stack.back()->get_zoom_level();
275-
auto zoom_level_str = humanize::time::duration::from_tv(
276-
to_timeval(zoom_level))
275+
auto zoom_level_str = humanize::time::duration::from(
276+
zoom_level)
277277
.with_compact(false)
278278
.to_string();
279279

src/db_status_source.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ db_status_source::update_from_db_source()
8989
if (dls.dls_query_end.has_value()) {
9090
auto dur_us = std::chrono::duration_cast<std::chrono::microseconds>(
9191
dls.dls_query_end.value() - dls.dls_query_start.value());
92-
auto dur = humanize::time::duration::from_tv(to_timeval(dur_us))
92+
auto dur = humanize::time::duration::from(dur_us)
9393
.with_compact(false)
9494
.to_string();
9595
timing_al.append("ran ").append(lnav::roles::time_ago(ago));

src/field_overlay_source.cc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
152152
}
153153

154154
char old_timestamp[64], curr_timestamp[64], orig_timestamp[64];
155-
timeval curr_tv, offset_tv, orig_tv, diff_tv = {0, 0};
155+
timeval curr_tv, offset_tv, orig_tv;
156+
std::chrono::microseconds diff{0};
156157
attr_line_t time_line;
157158
auto& time_str = time_line.get_string();
158159
line_range time_lr;
@@ -256,11 +257,11 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
256257
time_line.with_attr(
257258
string_attr(time_lr, VC_ROLE.value(role_t::VCR_SKEWED_TIME)));
258259

259-
timersub(&curr_tv, &actual_tv, &diff_tv);
260+
diff = to_us(curr_tv) - to_us(actual_tv);
260261
time_str.append("; Diff: ");
261262
time_lr.lr_start = time_str.length();
262263
time_str.append(
263-
humanize::time::duration::from_tv(diff_tv).to_string());
264+
humanize::time::duration::from(diff).to_string());
264265
time_lr.lr_end = time_str.length();
265266
time_line.with_attr(
266267
string_attr(time_lr, VC_STYLE.value(text_attrs::with_bold())));
@@ -305,7 +306,7 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
305306
}
306307

307308
if ((!this->fos_contexts.empty() && this->fos_contexts.top().c_show)
308-
|| diff_tv.tv_sec > 0 || ll->is_time_skewed())
309+
|| diff >= std::chrono::seconds{1} || ll->is_time_skewed())
309310
{
310311
this->fos_lines.emplace_back(time_line);
311312
}

0 commit comments

Comments
 (0)