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

llnl / dftracer-utils / 29185833209

12 Jul 2026 08:25AM UTC coverage: 51.235% (-1.5%) from 52.754%
29185833209

Pull #96

github

web-flow
Merge 0b9f07110 into 056c79287
Pull Request #96: fix: consolidate stale-index rebuilds across consumers and fix multi-run breaks

33724 of 84486 branches covered (39.92%)

Branch coverage included in aggregate %.

140 of 147 new or added lines in 15 files covered. (95.24%)

5188 existing lines in 197 files now uncovered.

34547 of 48765 relevant lines covered (70.84%)

10791.85 hits per line

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

51.84
/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/internal/utils.h>
8
#include <dftracer/utils/utilities/fileio/lines/sources/async_plain_file_bytes_generator.h>
9
#include <dftracer/utils/utilities/fileio/lines/sources/async_plain_file_line_generator.h>
10
#include <dftracer/utils/utilities/fileio/lines/sources/async_streaming_gz_line_generator.h>
11
#include <dftracer/utils/utilities/indexer/index_database.h>
12
#include <dftracer/utils/utilities/indexer/internal/helpers.h>
13
#include <dftracer/utils/utilities/indexer/internal/indexer_factory.h>
14
#include <dftracer/utils/utilities/reader/internal/reader.h>
15
#include <dftracer/utils/utilities/reader/internal/reader_factory.h>
16
#include <dftracer/utils/utilities/reader/internal/stream.h>
17
#include <dftracer/utils/utilities/reader/internal/stream_config.h>
18
#include <dftracer/utils/utilities/reader/internal/stream_type.h>
19
#include <dftracer/utils/utilities/reader/internal/trace_reader_prefilter.h>
20
#include <dftracer/utils/utilities/reader/internal/trace_reader_shared.h>
21
#include <dftracer/utils/utilities/reader/trace_reader.h>
22
#include <simdjson.h>
23

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

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

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

40
using internal::build_prefilter;
41
using internal::LinePrefilter;
42
using internal::ondemand_to_literal;
43
using internal::read_chunks_indexed;
44
using internal::strip_ndjson_bookends;
45

46
namespace {
47

48
thread_local simdjson::dom::parser tl_parser;
4✔
49

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

59
struct LineRange {
60
    std::size_t start_line;
61
    std::size_t end_line;
62
};
63

64
coro::AsyncGenerator<Line> yield_lines_from_stream(
5,625!
65
    std::unique_ptr<internal::ReaderStream> stream, std::size_t start_line_num,
66
    const Query* query, bool chunk_prune_only = false,
67
    const LinePrefilter* prefilter = nullptr) {
13!
68
    std::size_t line_num = start_line_num;
13✔
69
    while (!stream->done()) {
26!
70
        auto chunk = co_await stream->read_async();
117!
71
        if (chunk.empty()) break;
2,832✔
72
        const char* data = chunk.data();
2,819✔
73
        std::size_t len = chunk.size();
2,819✔
74

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

84
        std::size_t pos = 0;
2,819✔
85
        while (pos < len) {
4,916✔
86
            const void* nl_ptr = std::memchr(data + pos, '\n', len - pos);
4,903!
87
            std::size_t end_pos =
9,806✔
88
                nl_ptr ? static_cast<const char*>(nl_ptr) - data : len;
4,903!
89
            if (end_pos > pos) {
4,903!
90
                auto line_sv = std::string_view(data + pos, end_pos - pos);
4,903✔
91
                bool accept = chunk_prune_only || !query ||
6,215✔
92
                              line_matches_query(*query, line_sv);
1,312!
93
                if (accept && prefilter && !prefilter->empty() &&
4,903!
UNCOV
94
                    !prefilter->may_match(line_sv)) {
×
UNCOV
95
                    accept = false;
×
UNCOV
96
                }
×
97
                if (accept) {
4,903✔
98
                    co_yield Line(line_sv, line_num);
5,612!
99
                }
1,403✔
100
                ++line_num;
2,097✔
101
            } else {
2,097!
UNCOV
102
                ++line_num;
×
103
            }
104
            pos = end_pos + 1;
2,097✔
105
        }
2,097!
106
    }
2,832!
107
}
5,729✔
108

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

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

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

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

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

190
    if (range_type == internal::RangeType::LINE_RANGE) {
17✔
191
        auto total_lines = reader->get_num_lines();
2!
192
        if (start == 0) start = 1;
2!
193
        if (end == 0 || end > total_lines) end = total_lines;
2!
194
        if (start > total_lines) co_return;
19✔
195
    } else {
2✔
196
        auto max_bytes = reader->get_max_bytes();
15!
197
        if (end == 0 || end > max_bytes) end = max_bytes;
15!
198
        if (start >= max_bytes) co_return;
15✔
199
    }
15✔
200

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

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

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

UNCOV
226
                std::vector<LineRange> ranges;
×
UNCOV
227
                std::uint64_t prev_idx = UINT64_MAX;
×
228

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

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

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

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

262
    auto gen = yield_lines_from_stream(std::move(stream), start,
26!
263
                                       query ? &*query : nullptr,
13✔
264
                                       chunk_prune_only, &prefilter);
13✔
265
    while (auto line = co_await gen.next()) {
5,664!
266
        co_yield *line;
2,806!
267
    }
1,416✔
268
}
8,527✔
269

270
coro::AsyncGenerator<Line> read_lines_gz(std::string file_path,
27,360!
271
                                         ReadConfig config,
272
                                         std::optional<Query> query,
273
                                         bool chunk_prune_only = false) {
173!
274
    std::size_t start = config.has_line_range() ? config.start_line : 0;
172✔
275
    std::size_t end = config.has_line_range() ? config.end_line : 0;
172✔
276
    auto gen =
172✔
277
        fileio::lines::sources::async_streaming_gz_lines(file_path, start, end);
172!
278
    while (auto opt = co_await gen.next()) {
26,671!
279
        if (chunk_prune_only || !query ||
7,699✔
280
            line_matches_query(*query, opt->content)) {
1,246!
281
            co_yield *opt;
11,193!
282
        }
5,597✔
283
    }
6,627✔
284
}
39,914✔
285

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

299
coro::AsyncGenerator<Line> read_lines_plain(std::string file_path,
10,045!
300
                                            ReadConfig config,
301
                                            std::optional<Query> query,
302
                                            bool chunk_prune_only = false) {
37!
303
    std::size_t start = config.has_line_range() ? config.start_line : 0;
37!
304
    std::size_t end = config.has_line_range() ? config.end_line : 0;
37!
305
    auto gen =
37✔
306
        fileio::lines::sources::async_plain_file_lines(file_path, start, end);
37!
307
    while (auto opt = co_await gen.next()) {
9,899!
308
        if (chunk_prune_only || !query ||
2,579✔
309
            line_matches_query(*query, opt->content)) {
150!
310
            co_yield *opt;
4,758!
311
        }
2,379✔
312
    }
2,466✔
313
}
14,827✔
314

315
}  // namespace
316

317
namespace internal {
318

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

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

342
    if (range_type == internal::RangeType::LINE_RANGE) {
55✔
343
        auto total_lines = reader->get_num_lines();
3!
344
        if (start == 0) start = 1;
3!
345
        if (end == 0 || end > total_lines) end = total_lines;
3!
346
        if (start > total_lines) co_return;
58!
347
    } else {
3!
348
        auto max_bytes = reader->get_max_bytes();
52!
349
        if (end == 0 || end > max_bytes) end = max_bytes;
52!
350
        if (start >= max_bytes) co_return;
52✔
351
    }
52✔
352

353
    if (query && !index_path.empty() && !config.skip_pruning) {
54!
354
        ChunkPrunerInput pruner_input{index_path, file_path, *query, nullptr};
120!
355
        ChunkPrunerUtility pruner;
120!
356
        auto pruner_out = co_await pruner.process(pruner_input);
160!
357
        if (pruner_out.success && !pruner_out.file_may_match) {
40!
358
            co_return;
1✔
359
        }
360

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

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

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

UNCOV
403
                if (ranges.empty()) {
×
UNCOV
404
                    co_return;
×
405
                }
406

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

417
    auto stream_type = (range_type == internal::RangeType::BYTE_RANGE)
133✔
418
                           ? internal::StreamType::MULTI_LINES_BYTES
419
                           : internal::StreamType::MULTI_LINES;
420
    auto stream =
133✔
421
        reader->stream(internal::StreamConfig()
186✔
422
                           .stream_type(stream_type)
133✔
423
                           .range_type(range_type)
53!
424
                           .from(start)
53!
425
                           .to(end)
53✔
426
                           .buffer_size(config.buffer_size)
52!
427
                           .extend_to_line_boundary(
52!
428
                               extend_to_line_boundary &&
52✔
429
                               range_type == internal::RangeType::BYTE_RANGE));
1✔
430

431
    auto gen = yield_chunks_from_stream(std::move(stream), &prefilter);
29!
432
    while (auto chunk = co_await gen.next()) {
424!
433
        co_yield *chunk;
106!
434
    }
106✔
435
}
957✔
436

437
}  // namespace internal
438

439
TraceReader::TraceReader(TraceReaderConfig config)
1,416✔
440
    : config_(std::move(config)) {
944✔
441
    probe_index();
472!
442
}
944✔
443

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

474
bool TraceReader::has_index() const { return has_index_; }
173✔
475

476
void TraceReader::ensure_metadata_cached() {
19✔
477
    if (metadata_cached_) return;
19!
478

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

496
std::size_t TraceReader::get_max_bytes() {
12✔
497
    ensure_metadata_cached();
12✔
498
    return cached_max_bytes_;
12✔
499
}
500

501
std::size_t TraceReader::get_num_lines() {
7✔
502
    ensure_metadata_cached();
7✔
503
    return cached_num_lines_;
7✔
504
}
505

506
std::shared_ptr<internal::Reader> TraceReader::create_indexed_reader() {
91✔
507
    auto indexer = IndexerFactory::create(config_.file_path, index_path_,
182✔
508
                                          config_.checkpoint_size, false);
91✔
509
    return internal::ReaderFactory::create(indexer);
91!
510
}
91✔
511

512
internal::StreamType TraceReader::resolve_raw_stream_type(
6✔
513
    const ReadConfig& config) const {
514
    if (!config.line_aligned) return internal::StreamType::BYTES;
6!
515
    if (config.multi_line) return internal::StreamType::MULTI_LINES_BYTES;
6✔
516
    return internal::StreamType::LINE_BYTES;
1✔
517
}
6✔
518

519
internal::RangeType TraceReader::resolve_range_type(
6✔
520
    const ReadConfig& config) const {
521
    if (config.has_line_range()) return internal::RangeType::LINE_RANGE;
6!
522
    return internal::RangeType::BYTE_RANGE;
6✔
523
}
6✔
524

525
coro::AsyncGenerator<Line> TraceReader::read_lines(ReadConfig config) {
232✔
526
    std::optional<Query> query;
232✔
527
    if (!config.query.empty()) {
232✔
528
        auto parsed = Query::from_string(config.query);
41!
529
        if (!parsed) throw common::query::QueryParseError(parsed.error());
41!
530
        query = std::move(*parsed);
41!
531
    }
41✔
532

533
    bool cpo = config.chunk_prune_only;
232✔
534

535
    if (has_index_) {
232✔
536
        return read_lines_indexed(create_indexed_reader(), index_path_,
34!
537
                                  config_.file_path, std::move(config),
17!
538
                                  std::move(query), cpo);
17✔
539
    }
540
    if (format_ == ArchiveFormat::GZIP || format_ == ArchiveFormat::TAR_GZ) {
215!
541
        return read_lines_gz(config_.file_path, std::move(config),
173!
542
                             std::move(query), cpo);
173✔
543
    }
544
    if (config.has_byte_range()) {
42!
545
        return read_lines_plain_bytes(config_.file_path, std::move(config),
5!
546
                                      std::move(query), cpo);
5✔
547
    }
548
    return read_lines_plain(config_.file_path, std::move(config),
37!
549
                            std::move(query), cpo);
37✔
550
}
232✔
551

552
coro::AsyncGenerator<JsonLine> TraceReader::read_json(ReadConfig config) {
12,641!
553
    std::optional<Query> query;
11,978✔
554
    if (!config.query.empty()) {
11,978✔
555
        auto parsed = Query::from_string(config.query);
52!
556
        if (!parsed) throw common::query::QueryParseError(parsed.error());
52!
557
        query = std::move(*parsed);
52!
558
    }
52✔
559

560
    // chunk_prune_only path: dim_stats already proved every event with the
561
    // predicate field matches; we still need to skip events lacking the
562
    // field (e.g., metadata "ph":"M" events). Field-presence probe is
563
    // cheaper than full ValueMap eval.
564
    std::vector<std::string> presence_check_paths;
11,978✔
565
    if (query && config.chunk_prune_only) {
11,978!
UNCOV
566
        const auto& fset = query->fields();
×
UNCOV
567
        presence_check_paths.assign(fset.begin(), fset.end());
×
UNCOV
568
    }
×
569

570
    // Fast path: indexed gz files go through a chunk generator with
571
    // simdjson iterate_many. Query is evaluated on the ondemand document
572
    // directly, so non-matching docs never hit the yield_parser.
573
    if (has_index_) {
11,978✔
574
        auto reader = create_indexed_reader();
198!
575
        auto chunk_gen = read_chunks_indexed(reader, index_path_,
396!
576
                                             config_.file_path, config, query);
198!
577

578
        simdjson::ondemand::parser bulk_parser;
198✔
579
        common::json::JsonParser yield_parser;
198!
580

581
        while (auto chunk_opt = co_await chunk_gen.next()) {
482!
582
            auto chunk = *chunk_opt;
39✔
583
            if (chunk.empty()) continue;
39!
584
            auto trimmed = strip_ndjson_bookends(
78!
585
                std::string_view(chunk.data(), chunk.size()));
39✔
586
            if (trimmed.empty()) continue;
39!
587
            simdjson::padded_string padded(trimmed);
39✔
588

589
            auto docs_r = bulk_parser.iterate_many(
39✔
590
                padded, 1 << 20, /*allow_comma_separated=*/false);
591
            if (docs_r.error()) continue;
39!
592
            auto& docs = docs_r.value();
39!
593

594
            for (auto it = docs.begin(); it != docs.end(); ++it) {
2,007!
595
                auto doc_result = *it;
1,968✔
596
                if (doc_result.error()) continue;
1,968!
597
                auto& doc = doc_result.value();
1,968!
598

599
                std::string_view src(it.source().data(), it.source().size());
1,968✔
600

601
                if (query && config.chunk_prune_only) {
1,968!
UNCOV
602
                    bool all_present = true;
×
UNCOV
603
                    for (const auto& path : presence_check_paths) {
×
UNCOV
604
                        auto fld = doc.find_field_unordered(path);
×
UNCOV
605
                        if (fld.error()) {
×
UNCOV
606
                            all_present = false;
×
UNCOV
607
                            break;
×
608
                        }
UNCOV
609
                    }
×
UNCOV
610
                    if (!all_present) continue;
×
UNCOV
611
                    doc.rewind();
×
612
                } else if (query) {
1,968!
613
                    common::query::ValueMap fields;
1,968!
614
                    auto obj = doc.get_object();
1,968✔
615
                    if (obj.error()) continue;
1,968!
616
                    for (auto field : obj.value()) {
17,937!
617
                        if (field.error()) continue;
15,969!
618
                        auto key_r = field.unescaped_key();
15,969✔
619
                        if (key_r.error()) continue;
15,969!
620
                        auto val_r = field.value();
15,969✔
621
                        if (val_r.error()) continue;
15,969!
622
                        auto key = key_r.value();
15,969!
623
                        auto val = val_r.value();
15,969!
624
                        auto type_r = val.type();
15,969✔
625
                        if (type_r.error()) continue;
15,969!
626
                        auto type = type_r.value();
15,969✔
627
                        if (type == simdjson::ondemand::json_type::object) {
15,958✔
628
                            auto nested = val.get_object();
1,968✔
629
                            if (nested.error()) continue;
1,968!
630
                            for (auto nf : nested.value()) {
5,352!
631
                                if (nf.error()) continue;
3,384!
632
                                auto nk_r = nf.unescaped_key();
3,384✔
633
                                if (nk_r.error()) continue;
3,384!
634
                                auto nv_r = nf.value();
3,384✔
635
                                if (nv_r.error()) continue;
3,384!
636
                                auto nk = nk_r.value();
3,384✔
637
                                if (!query->references(nk)) continue;
3,382!
UNCOV
638
                                fields[std::string(nk)] =
×
UNCOV
639
                                    ondemand_to_literal(nv_r.value());
×
640
                            }
3,384!
641
                        } else if (query->references(key)) {
15,958!
642
                            fields[std::string(key)] = ondemand_to_literal(val);
1,933!
643
                        }
1,933✔
644
                    }
15,969✔
645
                    if (!query->evaluate(fields)) continue;
1,968!
646
                }
1,968✔
647

648
                // Matched (or no query): lend the iterate_many doc to
649
                // yield_parser without re-parsing. Consumers like
650
                // build_arrow_row call parser.for_each_field which now
651
                // iterates the borrowed doc_reference.
652
                doc.rewind();
1,514✔
653
                yield_parser.set_borrowed_document(
3,028✔
654
                    simdjson::ondemand::document_reference(doc));
1,514✔
655
                co_yield JsonLine{src, 0, &yield_parser};
3,028!
656
            }
1,968!
657
        }
79!
658
        co_return;
40✔
659
    }
40✔
660

661
    // Fallback: non-indexed paths use the per-line pipeline unchanged.
662
    config.chunk_prune_only = true;
11,780✔
663
    auto line_gen = read_lines(config);
11,780!
664

665
    common::json::JsonParser parser;
11,780!
666

667
    while (auto opt = co_await line_gen.next()) {
23,311!
668
        const char* trimmed;
5,703✔
669
        std::size_t trimmed_len;
5,703✔
670
        if (!dftracer::utils::json_trim_and_validate_with_comma(
5,703✔
671
                opt->content.data(), opt->content.size(), trimmed, trimmed_len))
5,703✔
672
            continue;
142✔
673
        if (!parser.parse(std::string_view(trimmed, trimmed_len))) continue;
5,558!
674

675
        if (query) {
5,561✔
676
            common::query::ValueMap fields;
692!
677
            std::vector<std::string> nested_keys;
692✔
678
            parser.for_each_field(
692!
679
                [&](std::string_view key, simdjson::ondemand::value val) {
6,528✔
680
                    auto type = val.type().value_unsafe();
5,836✔
681
                    if (type == simdjson::ondemand::json_type::object) {
5,836✔
682
                        nested_keys.emplace_back(key);
692✔
683
                    } else if (query->references(key)) {
5,836✔
684
                        fields[std::string(key)] = ondemand_to_literal(val);
792!
685
                    }
792✔
686
                });
5,836✔
687
            for (auto& nk : nested_keys) {
1,384✔
688
                parser.rewind();
692!
689
                parser.for_each_field(nk, [&](std::string_view key,
1,709!
690
                                              simdjson::ondemand::value val) {
691
                    if (query->references(key)) {
1,017!
692
                        fields[std::string(key)] = ondemand_to_literal(val);
×
UNCOV
693
                    }
×
694
                });
1,017✔
695
            }
692✔
696
            if (!query->evaluate(fields)) continue;
692!
697
            parser.rewind();
220!
698
        }
692✔
699

700
        co_yield JsonLine{opt->content, opt->line_number, &parser};
10,175!
701
    }
5,830!
702
}
23,826!
703

704
coro::AsyncGenerator<std::span<const char>> TraceReader::read_raw(
650!
705
    ReadConfig config) {
27!
706
    if (has_index_) {
27✔
707
        // Keep RocksDB alive for the generator's lifetime so per-method
708
        // opens in GzipIndexer reuse DBManager's cached handle.
709
        std::optional<indexer::IndexDatabase> db_keep_alive;
6✔
710
        if (!index_path_.empty()) {
6!
711
            try {
712
                db_keep_alive.emplace(
6!
713
                    index_path_, rocksdb::RocksDatabase::OpenMode::ReadOnly);
6✔
714
            } catch (...) {
6✔
UNCOV
715
            }
×
716
        }
6✔
717
        auto reader = create_indexed_reader();
6!
718
        auto stream_type = resolve_raw_stream_type(config);
6✔
719
        auto range_type = resolve_range_type(config);
6!
720
        std::size_t start =
12✔
721
            config.has_line_range() ? config.start_line : config.start_byte;
6!
722
        std::size_t end =
12✔
723
            config.has_line_range() ? config.end_line : config.end_byte;
6!
724

725
        if (range_type == internal::RangeType::LINE_RANGE) {
6!
UNCOV
726
            auto total_lines = reader->get_num_lines();
×
UNCOV
727
            if (start == 0) start = 1;
×
UNCOV
728
            if (end == 0 || end > total_lines) end = total_lines;
×
729
            if (start > total_lines) co_return;
27!
UNCOV
730
        } else {
×
731
            auto max_bytes = reader->get_max_bytes();
6!
732
            if (end == 0 || end > max_bytes) end = max_bytes;
6!
733
            if (start >= max_bytes) co_return;
6!
734
        }
6!
735

736
        if (!config.query.empty() && !index_path_.empty() &&
6!
737
            range_type == internal::RangeType::BYTE_RANGE) {
2✔
738
            auto parsed = Query::from_string(config.query);
6!
739
            if (!parsed) throw common::query::QueryParseError(parsed.error());
6!
740
            ChunkPrunerInput pruner_input{index_path_, config_.file_path,
6!
741
                                          std::move(*parsed), nullptr};
6!
742
            ChunkPrunerUtility pruner;
6!
743
            auto pruner_out = co_await pruner.process(pruner_input);
8!
744
            if (pruner_out.success && !pruner_out.file_may_match) {
2!
745
                co_return;
1✔
746
            }
747
        }
2✔
748

749
        auto stream = reader->stream(internal::StreamConfig()
14✔
750
                                         .stream_type(stream_type)
9✔
751
                                         .range_type(range_type)
5!
752
                                         .from(start)
5!
753
                                         .to(end)
5!
754
                                         .buffer_size(config.buffer_size));
5!
755

756
        while (!stream->done()) {
107!
757
            auto chunk = co_await stream->read_async();
444!
758
            if (chunk.empty()) break;
111✔
759
            co_yield chunk;
212!
760
        }
111✔
761
    } else if (format_ == ArchiveFormat::GZIP ||
685!
UNCOV
762
               format_ == ArchiveFormat::TAR_GZ) {
×
763
        auto gen =
21✔
764
            fileio::lines::sources::async_streaming_gz_lines(config_.file_path);
21!
765
        std::size_t byte_pos = 0;
21✔
766
        while (auto opt = co_await gen.next()) {
3,972!
767
            const auto& line = *opt;
2,916✔
768
            std::size_t line_end = byte_pos + line.content.size() + 1;
2,916✔
769
            if (config.end_byte > 0 && byte_pos >= config.end_byte) break;
2,916✔
770
            if (line_end > config.start_byte) {
2,915✔
771
                co_yield std::span<const char>(line.content.data(),
6,800!
772
                                               line.content.size());
2,914✔
773
            }
971✔
774
            byte_pos = line_end;
972✔
775
        }
4,879✔
776
    } else {
63✔
UNCOV
777
        auto gen =
×
UNCOV
778
            fileio::lines::sources::async_plain_file_lines(config_.file_path);
×
UNCOV
779
        std::size_t byte_pos = 0;
×
UNCOV
780
        while (auto opt = co_await gen.next()) {
×
UNCOV
781
            const auto& line = *opt;
×
UNCOV
782
            std::size_t line_end = byte_pos + line.content.size() + 1;
×
UNCOV
783
            if (config.end_byte > 0 && byte_pos >= config.end_byte) break;
×
UNCOV
784
            if (line_end > config.start_byte) {
×
UNCOV
785
                co_yield std::span<const char>(line.content.data(),
×
UNCOV
786
                                               line.content.size());
×
UNCOV
787
            }
×
UNCOV
788
            byte_pos = line_end;
×
UNCOV
789
        }
×
UNCOV
790
    }
×
791
}
6,150✔
792

793
}  // 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