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

llnl / dftracer-utils / 29787659139

20 Jul 2026 11:34PM UTC coverage: 51.578% (-1.1%) from 52.66%
29787659139

Pull #99

github

web-flow
Merge ee805adeb into 06bc84ec9
Pull Request #99: Support CM time_metric (NS/MS/SEC/US) across reader and viz

34832 of 86278 branches covered (40.37%)

Branch coverage included in aggregate %.

1034 of 1319 new or added lines in 30 files covered. (78.39%)

5193 existing lines in 197 files now uncovered.

35349 of 49791 relevant lines covered (70.99%)

9765.54 hits per line

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

53.1
/src/dftracer/utils/utilities/reader/trace_reader.cpp
1
#include <dftracer/utils/core/common/archive_format.h>
2
#include <dftracer/utils/core/common/filesystem.h>
3
#include <dftracer/utils/core/utils/string.h>
4
#include <dftracer/utils/utilities/common/json/json_value.h>
5
#include <dftracer/utils/utilities/common/query/query.h>
6
#include <dftracer/utils/utilities/composites/dft/indexing/chunk_pruner_utility.h>
7
#include <dftracer/utils/utilities/composites/dft/indexing/resolved_field_rewriter.h>
8
#include <dftracer/utils/utilities/composites/dft/internal/utils.h>
9
#include <dftracer/utils/utilities/fileio/lines/sources/async_plain_file_bytes_generator.h>
10
#include <dftracer/utils/utilities/fileio/lines/sources/async_plain_file_line_generator.h>
11
#include <dftracer/utils/utilities/fileio/lines/sources/async_streaming_gz_line_generator.h>
12
#include <dftracer/utils/utilities/indexer/index_database.h>
13
#include <dftracer/utils/utilities/indexer/internal/helpers.h>
14
#include <dftracer/utils/utilities/indexer/internal/indexer_factory.h>
15
#include <dftracer/utils/utilities/reader/internal/reader.h>
16
#include <dftracer/utils/utilities/reader/internal/reader_factory.h>
17
#include <dftracer/utils/utilities/reader/internal/stream.h>
18
#include <dftracer/utils/utilities/reader/internal/stream_config.h>
19
#include <dftracer/utils/utilities/reader/internal/stream_type.h>
20
#include <dftracer/utils/utilities/reader/internal/trace_reader_prefilter.h>
21
#include <dftracer/utils/utilities/reader/internal/trace_reader_shared.h>
22
#include <dftracer/utils/utilities/reader/trace_reader.h>
23
#include <simdjson.h>
24

25
#include <algorithm>
26
#include <cstring>
27
#include <optional>
28
#include <span>
29
#include <type_traits>
30
#include <unordered_map>
31

32
namespace dftracer::utils::utilities::reader {
33

34
namespace dft_internal = composites::dft::internal;
35
namespace indexing = composites::dft::indexing;
36
using common::json::JsonValue;
37
using common::query::Query;
38
using composites::dft::indexing::ChunkPrunerInput;
39
using composites::dft::indexing::ChunkPrunerUtility;
40
using indexer::internal::IndexerFactory;
41

42
using internal::build_prefilter;
43
using internal::LinePrefilter;
44
using internal::ondemand_to_literal;
45
using internal::read_chunks_indexed;
46
using internal::strip_ndjson_bookends;
47

48
namespace {
49

50
thread_local simdjson::dom::parser tl_parser;
4✔
51

52
bool line_matches_query(const Query& q, std::string_view content) {
2,710✔
53
    auto result = tl_parser.parse(content.data(), content.size());
2,710✔
54
    if (result.error()) return false;
2,710✔
55
    auto root = result.value_unsafe();
2,691✔
56
    if (!root.is_object()) return false;
2,691!
57
    JsonValue json(root);
2,691✔
58
    return q.evaluate(json);
2,691✔
59
}
2,710✔
60

61
struct LineRange {
62
    std::size_t start_line;
63
    std::size_t end_line;
64
};
65

66
coro::AsyncGenerator<Line> yield_lines_from_stream(
5,673!
67
    std::unique_ptr<internal::ReaderStream> stream, std::size_t start_line_num,
68
    const Query* query, bool chunk_prune_only = false,
69
    const LinePrefilter* prefilter = nullptr) {
25!
70
    std::size_t line_num = start_line_num;
25✔
71
    while (!stream->done()) {
38!
72
        auto chunk = co_await stream->read_async();
177!
73
        if (chunk.empty()) break;
2,868✔
74
        const char* data = chunk.data();
2,855✔
75
        std::size_t len = chunk.size();
2,855✔
76

77
        // Chunk-level pre-filter: if any required literal is absent from this
78
        // entire buffer, no line within it can match. Skip without splitting.
79
        // Line numbers must stay correct for subsequent chunks.
80
        if (prefilter && !prefilter->empty() &&
2,855!
UNCOV
81
            !prefilter->may_match(std::string_view(data, len))) {
×
UNCOV
82
            line_num += std::count(data, data + len, '\n');
×
UNCOV
83
            continue;
×
84
        }
85

86
        std::size_t pos = 0;
2,855✔
87
        while (pos < len) {
4,964✔
88
            const void* nl_ptr = std::memchr(data + pos, '\n', len - pos);
4,951!
89
            std::size_t end_pos =
9,902✔
90
                nl_ptr ? static_cast<const char*>(nl_ptr) - data : len;
4,951!
91
            if (end_pos > pos) {
4,951!
92
                auto line_sv = std::string_view(data + pos, end_pos - pos);
4,951✔
93
                bool accept = chunk_prune_only || !query ||
6,263✔
94
                              line_matches_query(*query, line_sv);
1,312!
95
                if (accept && prefilter && !prefilter->empty() &&
4,951!
UNCOV
96
                    !prefilter->may_match(line_sv)) {
×
UNCOV
97
                    accept = false;
×
UNCOV
98
                }
×
99
                if (accept) {
4,951✔
100
                    co_yield Line(line_sv, line_num);
5,684!
101
                }
1,415✔
102
                ++line_num;
2,109✔
103
            } else {
2,121✔
UNCOV
104
                ++line_num;
×
105
            }
106
            pos = end_pos + 1;
2,109✔
107
        }
2,121✔
108
    }
2,868!
109
}
5,861✔
110

111
coro::AsyncGenerator<Line> yield_lines_from_ranges(
×
112
    std::shared_ptr<internal::Reader> reader, std::vector<LineRange> ranges,
113
    std::size_t buffer_size, Query query, bool chunk_prune_only = false,
UNCOV
114
    LinePrefilter prefilter = {}) {
×
UNCOV
115
    for (const auto& range : ranges) {
×
UNCOV
116
        auto stream =
×
UNCOV
117
            reader->stream(internal::StreamConfig()
×
UNCOV
118
                               .stream_type(internal::StreamType::MULTI_LINES)
×
UNCOV
119
                               .range_type(internal::RangeType::LINE_RANGE)
×
UNCOV
120
                               .from(range.start_line)
×
UNCOV
121
                               .to(range.end_line)
×
UNCOV
122
                               .buffer_size(buffer_size));
×
UNCOV
123
        auto gen =
×
UNCOV
124
            yield_lines_from_stream(std::move(stream), range.start_line, &query,
×
UNCOV
125
                                    chunk_prune_only, &prefilter);
×
UNCOV
126
        while (auto line = co_await gen.next()) {
×
UNCOV
127
            co_yield *line;
×
UNCOV
128
        }
×
UNCOV
129
    }
×
130
}
×
131

132
// Raw-chunk variants of the yield/read helpers. Same pruning logic as the
133
// line-yielding flavors but emit std::span<const char> buffers untouched
134
// (multi-line boundary respected by stream type). Used by read_json to run
135
// simdjson iterate_many over each chunk instead of parsing line by line.
136
coro::AsyncGenerator<std::span<const char>> yield_chunks_from_stream(
830!
137
    std::unique_ptr<internal::ReaderStream> stream,
138
    const LinePrefilter* prefilter = nullptr) {
55!
139
    while (!stream->done()) {
109!
140
        auto chunk = co_await stream->read_async();
495!
141
        if (chunk.empty()) break;
110✔
142
        if (prefilter && !prefilter->empty() &&
55!
UNCOV
143
            !prefilter->may_match(
×
UNCOV
144
                std::string_view(chunk.data(), chunk.size()))) {
×
UNCOV
145
            continue;
×
146
        }
147
        co_yield chunk;
110!
148
    }
110!
149
}
220✔
150

151
coro::AsyncGenerator<std::span<const char>> yield_chunks_from_ranges(
×
152
    std::shared_ptr<internal::Reader> reader, std::vector<LineRange> ranges,
UNCOV
153
    std::size_t buffer_size, LinePrefilter prefilter = {}) {
×
UNCOV
154
    for (const auto& range : ranges) {
×
UNCOV
155
        auto stream =
×
UNCOV
156
            reader->stream(internal::StreamConfig()
×
UNCOV
157
                               .stream_type(internal::StreamType::MULTI_LINES)
×
UNCOV
158
                               .range_type(internal::RangeType::LINE_RANGE)
×
UNCOV
159
                               .from(range.start_line)
×
UNCOV
160
                               .to(range.end_line)
×
UNCOV
161
                               .buffer_size(buffer_size));
×
UNCOV
162
        auto gen = yield_chunks_from_stream(std::move(stream), &prefilter);
×
UNCOV
163
        while (auto chunk = co_await gen.next()) {
×
UNCOV
164
            co_yield *chunk;
×
UNCOV
165
        }
×
UNCOV
166
    }
×
167
}
×
168

169
coro::AsyncGenerator<Line> read_lines_indexed(
5,821!
170
    std::shared_ptr<internal::Reader> reader, std::string index_path,
171
    std::string file_path, ReadConfig config, std::optional<Query> query,
172
    bool chunk_prune_only = false) {
29!
173
    // Keep RocksDB alive for the generator's lifetime so per-method opens
174
    // in GzipIndexer reuse DBManager's cached handle.
175
    std::optional<indexer::IndexDatabase> db_keep_alive;
29✔
176
    if (!index_path.empty()) {
29!
177
        try {
178
            db_keep_alive.emplace(index_path,
29!
179
                                  rocksdb::RocksDatabase::OpenMode::ReadOnly);
29✔
180
        } catch (...) {
29✔
UNCOV
181
        }
×
182
    }
29✔
183

184
    LinePrefilter prefilter = query ? build_prefilter(*query) : LinePrefilter{};
29!
185
    auto range_type = config.has_line_range() ? internal::RangeType::LINE_RANGE
29✔
186
                                              : internal::RangeType::BYTE_RANGE;
187
    std::size_t start =
58✔
188
        config.has_line_range() ? config.start_line : config.start_byte;
29✔
189
    std::size_t end =
58✔
190
        config.has_line_range() ? config.end_line : config.end_byte;
29✔
191

192
    if (range_type == internal::RangeType::LINE_RANGE) {
29✔
193
        auto total_lines = reader->get_num_lines();
14!
194
        if (start == 0) start = 1;
14✔
195
        if (end == 0 || end > total_lines) end = total_lines;
14!
196
        if (start > total_lines) co_return;
43✔
197
    } else {
14✔
198
        auto max_bytes = reader->get_max_bytes();
15!
199
        if (end == 0 || end > max_bytes) end = max_bytes;
15!
200
        if (start >= max_bytes) co_return;
15✔
201
    }
15✔
202

203
    if (query && !index_path.empty() &&
15!
204
        range_type == internal::RangeType::BYTE_RANGE) {
7✔
205
        ChunkPrunerInput pruner_input{index_path, file_path, *query, nullptr};
21!
206
        ChunkPrunerUtility pruner;
21!
207
        auto pruner_out = co_await pruner.process(pruner_input);
28!
208
        if (pruner_out.success && !pruner_out.file_may_match) {
7!
209
            co_return;
2✔
210
        }
211

212
        if (pruner_out.success && !pruner_out.candidate_checkpoints.empty() &&
5!
213
            pruner_out.candidate_checkpoints.size() <
8✔
214
                pruner_out.total_checkpoints) {
4✔
UNCOV
215
            indexer::IndexDatabase idx_db(
×
216
                index_path, rocksdb::RocksDatabase::OpenMode::ReadOnly);
UNCOV
217
            auto logical = indexer::internal::get_logical_path(file_path);
×
UNCOV
218
            int fid = idx_db.get_file_info_id(logical);
×
219

UNCOV
220
            if (fid >= 0) {
×
UNCOV
221
                auto all_ckpts = idx_db.query_checkpoints(fid);
×
UNCOV
222
                std::unordered_map<std::uint64_t, indexer::IndexerCheckpoint>
×
UNCOV
223
                    ckpt_map;
×
UNCOV
224
                for (auto& ckpt : all_ckpts) {
×
UNCOV
225
                    ckpt_map.emplace(ckpt.checkpoint_idx, std::move(ckpt));
×
UNCOV
226
                }
×
227

UNCOV
228
                std::vector<LineRange> ranges;
×
UNCOV
229
                std::uint64_t prev_idx = UINT64_MAX;
×
230

UNCOV
231
                for (auto ckpt_idx : pruner_out.candidate_checkpoints) {
×
UNCOV
232
                    auto it = ckpt_map.find(ckpt_idx);
×
UNCOV
233
                    if (it == ckpt_map.end()) continue;
×
UNCOV
234
                    const auto& ckpt = it->second;
×
235

UNCOV
236
                    if (ranges.empty() || ckpt_idx != prev_idx + 1) {
×
UNCOV
237
                        ranges.push_back(
×
UNCOV
238
                            {ckpt.first_line_num, ckpt.last_line_num});
×
UNCOV
239
                    } else {
×
UNCOV
240
                        ranges.back().end_line = ckpt.last_line_num;
×
241
                    }
UNCOV
242
                    prev_idx = ckpt_idx;
×
UNCOV
243
                }
×
244

UNCOV
245
                auto gen = yield_lines_from_ranges(reader, std::move(ranges),
×
UNCOV
246
                                                   config.buffer_size, *query,
×
UNCOV
247
                                                   chunk_prune_only, prefilter);
×
UNCOV
248
                while (auto line = co_await gen.next()) {
×
UNCOV
249
                    co_yield *line;
×
UNCOV
250
                }
×
UNCOV
251
                co_return;
×
UNCOV
252
            }
×
UNCOV
253
        }
×
254
    }
7✔
255

256
    auto stream =
25✔
257
        reader->stream(internal::StreamConfig()
50!
258
                           .stream_type(internal::StreamType::MULTI_LINES)
25✔
259
                           .range_type(range_type)
25✔
260
                           .from(start)
25✔
261
                           .to(end)
25✔
262
                           .buffer_size(config.buffer_size));
25✔
263

264
    auto gen = yield_lines_from_stream(std::move(stream), start,
50!
265
                                       query ? &*query : nullptr,
25✔
266
                                       chunk_prune_only, &prefilter);
25✔
267
    while (auto line = co_await gen.next()) {
5,760!
268
        co_yield *line;
2,830!
269
    }
1,464✔
270
}
8,647✔
271

272
coro::AsyncGenerator<Line> read_lines_gz(std::string file_path,
27,390!
273
                                         ReadConfig config,
274
                                         std::optional<Query> query,
275
                                         bool chunk_prune_only = false) {
175!
276
    std::size_t start = config.has_line_range() ? config.start_line : 0;
175✔
277
    std::size_t end = config.has_line_range() ? config.end_line : 0;
175✔
278
    auto gen =
175✔
279
        fileio::lines::sources::async_streaming_gz_lines(file_path, start, end);
175!
280
    while (auto opt = co_await gen.next()) {
26,702!
281
        if (chunk_prune_only || !query ||
7,701✔
282
            line_matches_query(*query, opt->content)) {
1,246!
283
            co_yield *opt;
11,204!
284
        }
5,601✔
285
    }
6,635✔
286
}
39,959✔
287

288
coro::AsyncGenerator<Line> read_lines_plain_bytes(
89!
289
    std::string file_path, ReadConfig config, std::optional<Query> query,
290
    bool chunk_prune_only = false) {
5!
291
    auto gen = fileio::lines::sources::async_plain_file_bytes(
5!
292
        file_path, config.start_byte, config.end_byte, config.buffer_size);
5!
293
    while (auto opt = co_await gen.next()) {
69!
294
        if (chunk_prune_only || !query ||
13✔
295
            line_matches_query(*query, opt->content)) {
2!
296
            co_yield *opt;
20!
297
        }
10✔
298
    }
16✔
299
}
101✔
300

301
coro::AsyncGenerator<Line> read_lines_plain(std::string file_path,
10,323!
302
                                            ReadConfig config,
303
                                            std::optional<Query> query,
304
                                            bool chunk_prune_only = false) {
55!
305
    std::size_t start = config.has_line_range() ? config.start_line : 0;
55✔
306
    std::size_t end = config.has_line_range() ? config.end_line : 0;
55✔
307
    auto gen =
55✔
308
        fileio::lines::sources::async_plain_file_lines(file_path, start, end);
55!
309
    while (auto opt = co_await gen.next()) {
10,177!
310
        if (chunk_prune_only || !query ||
2,618✔
311
            line_matches_query(*query, opt->content)) {
150!
312
            co_yield *opt;
4,852!
313
        }
2,426✔
314
    }
2,547✔
315
}
15,203✔
316

317
}  // namespace
318

319
namespace internal {
320

321
coro::AsyncGenerator<std::span<const char>> read_chunks_indexed(
773!
322
    std::shared_ptr<internal::Reader> reader, std::string index_path,
323
    std::string file_path, ReadConfig config, std::optional<Query> query,
324
    bool extend_to_line_boundary) {
57!
325
    // Keep RocksDB alive for the generator's lifetime so per-method opens
326
    // in GzipIndexer reuse DBManager's cached handle.
327
    std::optional<indexer::IndexDatabase> db_keep_alive;
57✔
328
    if (!index_path.empty()) {
57!
329
        try {
330
            db_keep_alive.emplace(index_path,
57!
331
                                  rocksdb::RocksDatabase::OpenMode::ReadOnly);
57✔
332
        } catch (...) {
57✔
UNCOV
333
        }
×
334
    }
57✔
335

336
    LinePrefilter prefilter = query ? build_prefilter(*query) : LinePrefilter{};
57!
337
    auto range_type = config.has_line_range() ? internal::RangeType::LINE_RANGE
57!
338
                                              : internal::RangeType::BYTE_RANGE;
339
    std::size_t start =
114✔
340
        config.has_line_range() ? config.start_line : config.start_byte;
57!
341
    std::size_t end =
114✔
342
        config.has_line_range() ? config.end_line : config.end_byte;
57!
343

344
    if (range_type == internal::RangeType::LINE_RANGE) {
57✔
345
        auto total_lines = reader->get_num_lines();
3!
346
        if (start == 0) start = 1;
3!
347
        if (end == 0 || end > total_lines) end = total_lines;
3!
348
        if (start > total_lines) co_return;
60!
349
    } else {
3!
350
        auto max_bytes = reader->get_max_bytes();
54!
351
        if (end == 0 || end > max_bytes) end = max_bytes;
54!
352
        if (start >= max_bytes) co_return;
54✔
353
    }
54✔
354

355
    if (query && !index_path.empty() && !config.skip_pruning) {
56!
356
        ChunkPrunerInput pruner_input{index_path, file_path, *query, nullptr};
126!
357
        ChunkPrunerUtility pruner;
126!
358
        auto pruner_out = co_await pruner.process(pruner_input);
168!
359
        if (pruner_out.success && !pruner_out.file_may_match) {
42!
360
            co_return;
1✔
361
        }
362

363
        if (pruner_out.success && !pruner_out.candidate_checkpoints.empty() &&
41!
364
            pruner_out.candidate_checkpoints.size() <
78✔
365
                pruner_out.total_checkpoints) {
39✔
UNCOV
366
            indexer::IndexDatabase idx_db(
×
367
                index_path, rocksdb::RocksDatabase::OpenMode::ReadOnly);
UNCOV
368
            auto logical = indexer::internal::get_logical_path(file_path);
×
UNCOV
369
            int fid = idx_db.get_file_info_id(logical);
×
370

UNCOV
371
            if (fid >= 0) {
×
UNCOV
372
                auto all_ckpts = idx_db.query_checkpoints(fid);
×
UNCOV
373
                std::unordered_map<std::uint64_t, indexer::IndexerCheckpoint>
×
UNCOV
374
                    ckpt_map;
×
UNCOV
375
                for (auto& ckpt : all_ckpts) {
×
UNCOV
376
                    ckpt_map.emplace(ckpt.checkpoint_idx, std::move(ckpt));
×
UNCOV
377
                }
×
378

UNCOV
379
                std::vector<LineRange> ranges;
×
UNCOV
380
                std::uint64_t prev_idx = UINT64_MAX;
×
UNCOV
381
                for (auto ckpt_idx : pruner_out.candidate_checkpoints) {
×
UNCOV
382
                    auto it = ckpt_map.find(ckpt_idx);
×
UNCOV
383
                    if (it == ckpt_map.end()) continue;
×
UNCOV
384
                    const auto& ckpt = it->second;
×
385
                    // Intersect with the caller's window (byte or line) so
386
                    // checkpoint-level parallel work items stay disjoint.
UNCOV
387
                    if (range_type == internal::RangeType::BYTE_RANGE) {
×
UNCOV
388
                        std::size_t ckpt_start = ckpt.uc_offset;
×
UNCOV
389
                        std::size_t ckpt_end = ckpt.uc_offset + ckpt.uc_size;
×
UNCOV
390
                        if (ckpt_end <= start) continue;
×
UNCOV
391
                        if (ckpt_start >= end) continue;
×
UNCOV
392
                    } else {
×
UNCOV
393
                        if (ckpt.last_line_num < start) continue;
×
UNCOV
394
                        if (ckpt.first_line_num > end) continue;
×
395
                    }
UNCOV
396
                    if (ranges.empty() || ckpt_idx != prev_idx + 1) {
×
UNCOV
397
                        ranges.push_back(
×
UNCOV
398
                            {ckpt.first_line_num, ckpt.last_line_num});
×
UNCOV
399
                    } else {
×
UNCOV
400
                        ranges.back().end_line = ckpt.last_line_num;
×
401
                    }
UNCOV
402
                    prev_idx = ckpt_idx;
×
UNCOV
403
                }
×
404

UNCOV
405
                if (ranges.empty()) {
×
UNCOV
406
                    co_return;
×
407
                }
408

UNCOV
409
                auto gen = yield_chunks_from_ranges(
×
UNCOV
410
                    reader, std::move(ranges), config.buffer_size, prefilter);
×
UNCOV
411
                while (auto chunk = co_await gen.next()) {
×
UNCOV
412
                    co_yield *chunk;
×
UNCOV
413
                }
×
UNCOV
414
                co_return;
×
UNCOV
415
            }
×
UNCOV
416
        }
×
417
    }
42✔
418

419
    auto stream_type = (range_type == internal::RangeType::BYTE_RANGE)
139✔
420
                           ? internal::StreamType::MULTI_LINES_BYTES
421
                           : internal::StreamType::MULTI_LINES;
422
    auto stream =
139✔
423
        reader->stream(internal::StreamConfig()
194✔
424
                           .stream_type(stream_type)
139✔
425
                           .range_type(range_type)
55!
426
                           .from(start)
55!
427
                           .to(end)
55!
428
                           .buffer_size(config.buffer_size)
55✔
429
                           .extend_to_line_boundary(
54!
430
                               extend_to_line_boundary &&
54✔
431
                               range_type == internal::RangeType::BYTE_RANGE));
1✔
432

433
    auto gen = yield_chunks_from_stream(std::move(stream), &prefilter);
31!
434
    while (auto chunk = co_await gen.next()) {
440!
435
        co_yield *chunk;
110!
436
    }
110✔
437
}
995✔
438

439
}  // namespace internal
440

441
TraceReader::TraceReader(TraceReaderConfig config)
1,524✔
442
    : config_(std::move(config)) {
1,016✔
443
    probe_index();
508!
444
}
1,016✔
445

446
void TraceReader::probe_index() {
507✔
447
    format_ = IndexerFactory::detect_format(config_.file_path);
507✔
448
    index_path_ = dft_internal::determine_index_path(config_.file_path,
1,014✔
449
                                                     config_.index_dir);
507✔
450
    has_index_ =
507!
451
        (format_ == ArchiveFormat::GZIP || format_ == ArchiveFormat::TAR_GZ) &&
507✔
452
        fs::exists(index_path_);
420!
453
    // Do not trust an index whose source changed since it was built; fall back
454
    // to a raw read rather than serving stale data. Records predating stat
455
    // tracking have no stored stat and keep the prior trust-on-existence path.
456
    if (has_index_) {
507✔
457
        try {
458
            indexer::IndexDatabase db(
131!
459
                index_path_, rocksdb::RocksDatabase::OpenMode::ReadOnly);
131✔
460
            auto stored = db.get_file_stat(
129!
461
                indexer::internal::get_logical_path(config_.file_path));
131✔
462
            if (stored) {
129!
463
                auto mtime = static_cast<std::uint64_t>(
128✔
464
                    indexer::internal::get_file_modification_time(
129✔
465
                        config_.file_path));
129✔
466
                auto size =
129✔
467
                    indexer::internal::file_size_bytes(config_.file_path);
128✔
468
                if (stored->mtime != mtime || stored->size != size)
129!
469
                    has_index_ = false;
2✔
470
            }
129✔
471
        } catch (...) {
129✔
472
        }
×
473
    }
129✔
474
}
509✔
475

476
bool TraceReader::has_index() const { return has_index_; }
186✔
477

478
void TraceReader::ensure_metadata_cached() {
19✔
479
    if (metadata_cached_) return;
19!
480

481
    if (has_index_) {
19✔
482
        auto reader = create_indexed_reader();
13✔
483
        cached_max_bytes_ = reader->get_max_bytes();
13!
484
        cached_num_lines_ = reader->get_num_lines();
13!
485
    } else if (format_ == ArchiveFormat::GZIP ||
19!
486
               format_ == ArchiveFormat::TAR_GZ) {
×
487
        cached_max_bytes_ = 0;
6✔
488
        cached_num_lines_ = 0;
6✔
489
    } else {
6✔
490
        std::error_code ec;
×
491
        auto size = fs::file_size(config_.file_path, ec);
×
492
        cached_max_bytes_ = ec ? 0 : static_cast<std::size_t>(size);
×
493
        cached_num_lines_ = 0;
×
494
    }
495
    metadata_cached_ = true;
19✔
496
}
19✔
497

498
coro::CoroTask<composites::dft::TimeMetric> TraceReader::read_time_metric(
273!
499
    std::size_t max_lines) {
21!
500
    using composites::dft::DFTracerEvent;
501
    using composites::dft::TimeMetric;
502

503
    ReadConfig probe;
21✔
504
    probe.end_line = max_lines;
21✔
505
    auto gen = read_lines(probe);
21!
506
    simdjson::dom::parser parser;
21✔
507
    TimeMetric metric = TimeMetric::US;
21✔
508
    while (auto line_opt = co_await gen.next()) {
189!
509
        const char* start = nullptr;
42✔
510
        std::size_t len = 0;
42✔
511
        if (!json_trim_and_validate(line_opt->content.data(),
84!
512
                                    line_opt->content.size(), start, len))
42✔
513
            continue;
21✔
514
        auto doc = parser.parse(start, len);
21✔
515
        if (doc.error()) continue;
21!
516
        DFTracerEvent event;
21!
517
        if (!DFTracerEvent::parse(common::json::JsonValue(doc.value()), event))
21!
NEW
518
            continue;
×
519
        if (composites::dft::extract_time_metric(event, metric)) break;
21!
520
        // CM precedes timeline events; stop once past the metadata header.
521
        if (event.is_event()) break;
12!
522
    }
42!
523
    co_return metric;
21!
524
}
273✔
525

526
std::size_t TraceReader::get_max_bytes() {
12✔
527
    ensure_metadata_cached();
12✔
528
    return cached_max_bytes_;
12✔
529
}
530

531
std::size_t TraceReader::get_num_lines() {
7✔
532
    ensure_metadata_cached();
7✔
533
    return cached_num_lines_;
7✔
534
}
535

536
std::shared_ptr<internal::Reader> TraceReader::create_indexed_reader() {
105✔
537
    auto indexer = IndexerFactory::create(config_.file_path, index_path_,
210✔
538
                                          config_.checkpoint_size, false);
105✔
539
    return internal::ReaderFactory::create(indexer);
105!
540
}
105✔
541

542
internal::StreamType TraceReader::resolve_raw_stream_type(
6✔
543
    const ReadConfig& config) const {
544
    if (!config.line_aligned) return internal::StreamType::BYTES;
6!
545
    if (config.multi_line) return internal::StreamType::MULTI_LINES_BYTES;
6✔
546
    return internal::StreamType::LINE_BYTES;
1✔
547
}
6✔
548

549
internal::RangeType TraceReader::resolve_range_type(
6✔
550
    const ReadConfig& config) const {
551
    if (config.has_line_range()) return internal::RangeType::LINE_RANGE;
6!
552
    return internal::RangeType::BYTE_RANGE;
6✔
553
}
6✔
554

555
coro::AsyncGenerator<Line> TraceReader::read_lines(ReadConfig config) {
264✔
556
    std::optional<Query> query;
264✔
557
    if (!config.query.empty()) {
264✔
558
        auto parsed = Query::from_string(config.query);
43!
559
        if (!parsed) throw common::query::QueryParseError(parsed.error());
43!
560
        query = std::move(*parsed);
43!
561
    }
43✔
562

563
    bool cpo = config.chunk_prune_only;
264✔
564

565
    if (has_index_) {
264✔
566
        return read_lines_indexed(create_indexed_reader(), index_path_,
58!
567
                                  config_.file_path, std::move(config),
29!
568
                                  std::move(query), cpo);
29✔
569
    }
570
    if (format_ == ArchiveFormat::GZIP || format_ == ArchiveFormat::TAR_GZ) {
235!
571
        return read_lines_gz(config_.file_path, std::move(config),
175!
572
                             std::move(query), cpo);
175✔
573
    }
574
    if (config.has_byte_range()) {
60!
575
        return read_lines_plain_bytes(config_.file_path, std::move(config),
5!
576
                                      std::move(query), cpo);
5✔
577
    }
578
    return read_lines_plain(config_.file_path, std::move(config),
55!
579
                            std::move(query), cpo);
55✔
580
}
264✔
581

582
coro::AsyncGenerator<JsonLine> TraceReader::read_json(ReadConfig config) {
12,823!
583
    std::optional<Query> query;
12,107✔
584
    if (!config.query.empty()) {
12,107✔
585
        auto parsed = Query::from_string(config.query);
56!
586
        if (!parsed) throw common::query::QueryParseError(parsed.error());
56!
587
        query = std::move(*parsed);
56!
588
    }
56✔
589

590
    // Resolve virtual fields (resolved.*/r.*) to concrete hash in-clauses via
591
    // the index before either the pruner or the per-event evaluator sees the
592
    // query, so both operate on plain fhash/hhash/shash predicates.
593
    if (query && has_index_ && !index_path_.empty() &&
12,149!
594
        indexing::has_resolved_fields(*query)) {
42!
595
        try {
NEW
596
            indexer::IndexDatabase db(
×
NEW
597
                index_path_, rocksdb::RocksDatabase::OpenMode::ReadOnly);
×
NEW
598
            if (auto rewritten =
×
NEW
599
                    indexing::rewrite_resolved_fields(*query, db)) {
×
NEW
600
                query = std::move(*rewritten);
×
NEW
601
            }
×
NEW
602
        } catch (...) {
×
NEW
603
        }
×
NEW
604
    }
×
605

606
    // chunk_prune_only path: dim_stats already proved every event with the
607
    // predicate field matches; we still need to skip events lacking the
608
    // field (e.g., metadata "ph":"M" events). Field-presence probe is
609
    // cheaper than full ValueMap eval.
610
    std::vector<std::string> presence_check_paths;
12,107✔
611
    if (query && config.chunk_prune_only) {
12,107!
UNCOV
612
        const auto& fset = query->fields();
×
UNCOV
613
        presence_check_paths.assign(fset.begin(), fset.end());
×
UNCOV
614
    }
×
615

616
    // Whether any nested field is referenced by dotted path (e.g. "args.ret"),
617
    // in which case the ValueMap builders store the dotted key too.
618
    const bool query_has_dotted =
12,163✔
619
        query && internal::query_references_dotted(*query);
12,107!
620

621
    // Fast path: indexed gz files go through a chunk generator with
622
    // simdjson iterate_many. Query is evaluated on the ondemand document
623
    // directly, so non-matching docs never hit the yield_parser.
624
    if (has_index_) {
12,107✔
625
        auto reader = create_indexed_reader();
208!
626
        auto chunk_gen = read_chunks_indexed(reader, index_path_,
416!
627
                                             config_.file_path, config, query);
208!
628

629
        simdjson::ondemand::parser bulk_parser;
208✔
630
        common::json::JsonParser yield_parser;
208!
631

632
        while (auto chunk_opt = co_await chunk_gen.next()) {
511!
633
            auto chunk = *chunk_opt;
41✔
634
            if (chunk.empty()) continue;
41!
635
            auto trimmed = strip_ndjson_bookends(
82!
636
                std::string_view(chunk.data(), chunk.size()));
41✔
637
            if (trimmed.empty()) continue;
41!
638
            simdjson::padded_string padded(trimmed);
41✔
639

640
            auto docs_r = bulk_parser.iterate_many(
41✔
641
                padded, 1 << 20, /*allow_comma_separated=*/false);
642
            if (docs_r.error()) continue;
41!
643
            auto& docs = docs_r.value();
41!
644

645
            for (auto it = docs.begin(); it != docs.end(); ++it) {
2,208!
646
                auto doc_result = *it;
2,167✔
647
                if (doc_result.error()) continue;
2,167!
648
                auto& doc = doc_result.value();
2,167!
649

650
                std::string_view src(it.source().data(), it.source().size());
2,167✔
651

652
                if (query && config.chunk_prune_only) {
2,167!
UNCOV
653
                    bool all_present = true;
×
UNCOV
654
                    for (const auto& path : presence_check_paths) {
×
UNCOV
655
                        auto fld = doc.find_field_unordered(path);
×
UNCOV
656
                        if (fld.error()) {
×
UNCOV
657
                            all_present = false;
×
UNCOV
658
                            break;
×
659
                        }
UNCOV
660
                    }
×
UNCOV
661
                    if (!all_present) continue;
×
UNCOV
662
                    doc.rewind();
×
663
                } else if (query) {
2,167!
664
                    common::query::ValueMap fields;
2,167!
665
                    auto obj = doc.get_object();
2,167✔
666
                    if (obj.error()) continue;
2,167!
667
                    for (auto field : obj.value()) {
19,937!
668
                        if (field.error()) continue;
17,770!
669
                        auto key_r = field.unescaped_key();
17,770✔
670
                        if (key_r.error()) continue;
17,770!
671
                        auto val_r = field.value();
17,770✔
672
                        if (val_r.error()) continue;
17,770!
673
                        auto key = key_r.value();
17,770!
674
                        auto val = val_r.value();
17,770!
675
                        auto type_r = val.type();
17,770✔
676
                        if (type_r.error()) continue;
17,770!
677
                        auto type = type_r.value();
17,770✔
678
                        if (type == simdjson::ondemand::json_type::object) {
17,766✔
679
                            auto nested = val.get_object();
2,168✔
680
                            if (nested.error()) continue;
2,168!
681
                            for (auto nf : nested.value()) {
5,956!
682
                                if (nf.error()) continue;
3,784!
683
                                auto nk_r = nf.unescaped_key();
3,784✔
684
                                if (nk_r.error()) continue;
3,784!
685
                                auto nv_r = nf.value();
3,784✔
686
                                if (nv_r.error()) continue;
3,784!
687
                                internal::store_referenced_nested(
7,568✔
688
                                    fields, *query, query_has_dotted, key,
7,568✔
689
                                    nk_r.value(), nv_r.value());
3,784✔
690
                            }
3,788!
691
                        } else if (query->references(key)) {
17,766!
692
                            fields[std::string(key)] = ondemand_to_literal(val);
1,933!
693
                        }
1,933✔
694
                    }
17,770!
695
                    if (!query->evaluate(fields)) continue;
2,167!
696
                }
2,167✔
697

698
                // Matched (or no query): lend the iterate_many doc to
699
                // yield_parser without re-parsing. Consumers like
700
                // build_arrow_row call parser.for_each_field which now
701
                // iterates the borrowed doc_reference.
702
                doc.rewind();
1,520✔
703
                yield_parser.set_borrowed_document(
3,040✔
704
                    simdjson::ondemand::document_reference(doc));
1,520✔
705
                co_yield JsonLine{src, 0, &yield_parser};
3,040!
706
            }
2,167!
707
        }
83!
708
        co_return;
42✔
709
    }
42✔
710

711
    // Fallback: non-indexed paths use the per-line pipeline unchanged.
712
    config.chunk_prune_only = true;
11,899✔
713
    auto line_gen = read_lines(config);
11,899!
714

715
    common::json::JsonParser parser;
11,899!
716

717
    while (auto opt = co_await line_gen.next()) {
23,525!
718
        const char* trimmed;
5,745✔
719
        std::size_t trimmed_len;
5,745✔
720
        if (!dftracer::utils::json_trim_and_validate_with_comma(
5,745!
721
                opt->content.data(), opt->content.size(), trimmed, trimmed_len))
5,745✔
722
            continue;
153✔
723
        if (!parser.parse(std::string_view(trimmed, trimmed_len))) continue;
5,592!
724

725
        if (query) {
5,592✔
726
            common::query::ValueMap fields;
698!
727
            std::vector<std::string> nested_keys;
698✔
728
            parser.for_each_field(
698!
729
                [&](std::string_view key, simdjson::ondemand::value val) {
6,580✔
730
                    auto type = val.type().value_unsafe();
5,882✔
731
                    if (type == simdjson::ondemand::json_type::object) {
5,882✔
732
                        nested_keys.emplace_back(key);
694✔
733
                    } else if (query->references(key)) {
5,882✔
734
                        fields[std::string(key)] = ondemand_to_literal(val);
798!
735
                    }
798✔
736
                });
5,882✔
737
            for (auto& nk : nested_keys) {
1,392✔
738
                parser.rewind();
694!
739
                parser.for_each_field(nk, [&](std::string_view key,
1,715!
740
                                              simdjson::ondemand::value val) {
741
                    internal::store_referenced_nested(
1,021✔
742
                        fields, *query, query_has_dotted, nk, key, val);
1,021✔
743
                });
1,021✔
744
            }
694✔
745
            if (!query->evaluate(fields)) continue;
698!
746
            parser.rewind();
222!
747
        }
698✔
748

749
        co_yield JsonLine{opt->content, opt->line_number, &parser};
10,234!
750
    }
5,883!
751
}
24,051!
752

753
coro::AsyncGenerator<std::span<const char>> TraceReader::read_raw(
650!
754
    ReadConfig config) {
27!
755
    if (has_index_) {
27✔
756
        // Keep RocksDB alive for the generator's lifetime so per-method
757
        // opens in GzipIndexer reuse DBManager's cached handle.
758
        std::optional<indexer::IndexDatabase> db_keep_alive;
6✔
759
        if (!index_path_.empty()) {
6!
760
            try {
761
                db_keep_alive.emplace(
6!
762
                    index_path_, rocksdb::RocksDatabase::OpenMode::ReadOnly);
6✔
763
            } catch (...) {
6✔
UNCOV
764
            }
×
765
        }
6✔
766
        auto reader = create_indexed_reader();
6!
767
        auto stream_type = resolve_raw_stream_type(config);
6✔
768
        auto range_type = resolve_range_type(config);
6!
769
        std::size_t start =
12✔
770
            config.has_line_range() ? config.start_line : config.start_byte;
6!
771
        std::size_t end =
12✔
772
            config.has_line_range() ? config.end_line : config.end_byte;
6!
773

774
        if (range_type == internal::RangeType::LINE_RANGE) {
6!
UNCOV
775
            auto total_lines = reader->get_num_lines();
×
UNCOV
776
            if (start == 0) start = 1;
×
UNCOV
777
            if (end == 0 || end > total_lines) end = total_lines;
×
778
            if (start > total_lines) co_return;
27!
UNCOV
779
        } else {
×
780
            auto max_bytes = reader->get_max_bytes();
6!
781
            if (end == 0 || end > max_bytes) end = max_bytes;
6!
782
            if (start >= max_bytes) co_return;
6!
783
        }
6!
784

785
        if (!config.query.empty() && !index_path_.empty() &&
6!
786
            range_type == internal::RangeType::BYTE_RANGE) {
2✔
787
            auto parsed = Query::from_string(config.query);
6!
788
            if (!parsed) throw common::query::QueryParseError(parsed.error());
6!
789
            ChunkPrunerInput pruner_input{index_path_, config_.file_path,
6!
790
                                          std::move(*parsed), nullptr};
6!
791
            ChunkPrunerUtility pruner;
6!
792
            auto pruner_out = co_await pruner.process(pruner_input);
8!
793
            if (pruner_out.success && !pruner_out.file_may_match) {
2!
794
                co_return;
1✔
795
            }
796
        }
2✔
797

798
        auto stream = reader->stream(internal::StreamConfig()
14✔
799
                                         .stream_type(stream_type)
9✔
800
                                         .range_type(range_type)
5!
801
                                         .from(start)
5!
802
                                         .to(end)
5!
803
                                         .buffer_size(config.buffer_size));
5!
804

805
        while (!stream->done()) {
107!
806
            auto chunk = co_await stream->read_async();
444!
807
            if (chunk.empty()) break;
111✔
808
            co_yield chunk;
212!
809
        }
111✔
810
    } else if (format_ == ArchiveFormat::GZIP ||
685!
UNCOV
811
               format_ == ArchiveFormat::TAR_GZ) {
×
812
        auto gen =
21✔
813
            fileio::lines::sources::async_streaming_gz_lines(config_.file_path);
21!
814
        std::size_t byte_pos = 0;
21✔
815
        while (auto opt = co_await gen.next()) {
4,024!
816
            const auto& line = *opt;
2,955✔
817
            std::size_t line_end = byte_pos + line.content.size() + 1;
2,955✔
818
            if (config.end_byte > 0 && byte_pos >= config.end_byte) break;
2,955✔
819
            if (line_end > config.start_byte) {
2,954✔
820
                co_yield std::span<const char>(line.content.data(),
6,891!
821
                                               line.content.size());
2,953✔
822
            }
984✔
823
            byte_pos = line_end;
985✔
824
        }
4,944✔
825
    } else {
63✔
UNCOV
826
        auto gen =
×
UNCOV
827
            fileio::lines::sources::async_plain_file_lines(config_.file_path);
×
UNCOV
828
        std::size_t byte_pos = 0;
×
UNCOV
829
        while (auto opt = co_await gen.next()) {
×
UNCOV
830
            const auto& line = *opt;
×
UNCOV
831
            std::size_t line_end = byte_pos + line.content.size() + 1;
×
UNCOV
832
            if (config.end_byte > 0 && byte_pos >= config.end_byte) break;
×
UNCOV
833
            if (line_end > config.start_byte) {
×
UNCOV
834
                co_yield std::span<const char>(line.content.data(),
×
UNCOV
835
                                               line.content.size());
×
UNCOV
836
            }
×
UNCOV
837
            byte_pos = line_end;
×
UNCOV
838
        }
×
UNCOV
839
    }
×
840
}
6,228✔
841

842
}  // namespace dftracer::utils::utilities::reader
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc