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

llnl / dftracer-utils / 30002033718

23 Jul 2026 11:08AM UTC coverage: 52.742% (+0.08%) from 52.66%
30002033718

Pull #99

github

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

41941 of 102829 branches covered (40.79%)

Branch coverage included in aggregate %.

2221 of 2839 new or added lines in 58 files covered. (78.23%)

137 existing lines in 13 files now uncovered.

37042 of 46924 relevant lines covered (78.94%)

59523.07 hits per line

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

60.76
/src/dftracer/utils/server/trace_index.cpp
1
#include <dftracer/utils/core/common/filesystem.h>
2
#include <dftracer/utils/core/common/logging.h>
3
#include <dftracer/utils/core/coro/channel.h>
4
#include <dftracer/utils/core/io/io_backend.h>
5
#include <dftracer/utils/core/pipeline/pipeline.h>
6
#include <dftracer/utils/core/pipeline/pipeline_config.h>
7
#include <dftracer/utils/core/tasks/coro_scope.h>
8
#include <dftracer/utils/core/tasks/task.h>
9
#include <dftracer/utils/server/router.h>
10
#include <dftracer/utils/server/trace_index.h>
11
#include <dftracer/utils/utilities/composites/dft/internal/utils.h>
12
#include <dftracer/utils/utilities/composites/dft/metadata_collector_utility.h>
13
#include <dftracer/utils/utilities/filesystem/pattern_directory_scanner_utility.h>
14
#include <dftracer/utils/utilities/indexer/index_builder_utility.h>
15
#include <dftracer/utils/utilities/indexer/index_database.h>
16
#include <dftracer/utils/utilities/indexer/index_database_writer_context.h>
17
#include <dftracer/utils/utilities/indexer/internal/helpers.h>
18
#include <dftracer/utils/utilities/reader/trace_reader.h>
19

20
#include <cinttypes>
21
#include <cstdio>
22
#include <cstring>
23
#include <fstream>
24
#include <limits>
25
#include <memory>
26
#include <unordered_map>
27
#include <vector>
28

29
namespace dftracer::utils::server {
30

31
using namespace dftracer::utils::utilities::composites::dft;
32
using namespace dftracer::utils::utilities::composites::dft::indexing;
33
using namespace dftracer::utils::utilities::filesystem;
34
namespace indexer = dftracer::utils::utilities::indexer;
35

36
namespace {
37

38
// Bump VERSION when the on-disk layout below changes; the loader then ignores
39
// the stale cache.
40
constexpr std::uint32_t VIZ_SUMMARY_MAGIC = 0x315A5644;  // "DVZ1"
41
constexpr std::uint32_t VIZ_SUMMARY_FORMAT_VERSION = 4;
42

43
struct BufWriter {
44
    std::string b;
45
    void u32(std::uint32_t v) { b.append(reinterpret_cast<char*>(&v), 4); }
684✔
46
    void u64(std::uint64_t v) { b.append(reinterpret_cast<char*>(&v), 8); }
5,618✔
47
    void i64(std::int64_t v) { u64(static_cast<std::uint64_t>(v)); }
1,568✔
48
    void dbl(double v) { b.append(reinterpret_cast<char*>(&v), 8); }
233,964,338✔
49
    void str(const std::string& s) {
1,364✔
50
        u64(s.size());
1,364✔
51
        b.append(s);
1,364✔
52
    }
1,364✔
53
};
54

55
struct BufReader {
56
    const char* p;
57
    const char* end;
58
    bool ok = true;
59
    std::uint32_t u32() {
44✔
60
        std::uint32_t v = 0;
44✔
61
        if (!ok || p + 4 > end) {
44!
NEW
62
            ok = false;
×
NEW
63
            return 0;
×
64
        }
65
        std::memcpy(&v, p, 4);
44✔
66
        p += 4;
44✔
67
        return v;
44✔
68
    }
22✔
69
    std::uint64_t u64() {
304✔
70
        std::uint64_t v = 0;
304✔
71
        if (!ok || p + 8 > end) {
304!
NEW
72
            ok = false;
×
NEW
73
            return 0;
×
74
        }
75
        std::memcpy(&v, p, 8);
304✔
76
        p += 8;
304✔
77
        return v;
304✔
78
    }
152✔
79
    std::int64_t i64() { return static_cast<std::int64_t>(u64()); }
56✔
80
    double dbl() {
33,423,418✔
81
        double v = 0;
33,423,418✔
82
        if (!ok || p + 8 > end) {
33,423,418!
NEW
83
            ok = false;
×
NEW
84
            return 0;
×
85
        }
86
        std::memcpy(&v, p, 8);
33,423,418✔
87
        p += 8;
33,423,418✔
88
        return v;
33,423,418✔
89
    }
16,711,709✔
90
    std::string str() {
90✔
91
        std::uint64_t n = u64();
90✔
92
        if (!ok || n > static_cast<std::uint64_t>(end - p)) {
90!
NEW
93
            ok = false;
×
NEW
94
            return {};
×
95
        }
96
        std::string s(p, p + n);
90!
97
        p += n;
90✔
98
        return s;
90✔
99
    }
135!
100
    // Guard a count before resize: each element consumes >= elem_min bytes, so
101
    // a corrupt count can't request an absurd allocation.
102
    bool fits(std::uint64_t n, std::size_t elem_min) const {
60✔
103
        if (!ok) return false;
60!
104
        std::size_t rem = static_cast<std::size_t>(end - p);
60✔
105
        return elem_min == 0 || n <= rem / elem_min;
60!
106
    }
30✔
107
};
108

109
void write_group_rows(BufWriter& w,
56✔
110
                      const std::vector<VizSummary::GroupRow>& rows) {
111
    w.u64(rows.size());
56✔
112
    for (const auto& r : rows) {
258✔
113
        w.str(r.key);
202!
114
        w.u64(r.count);
202!
115
        w.dbl(r.total);
202!
116
        w.dbl(r.min);
202!
117
        w.dbl(r.max);
202!
118
    }
119
}
56✔
120

121
bool read_group_rows(BufReader& r, std::vector<VizSummary::GroupRow>& rows) {
8✔
122
    std::uint64_t n = r.u64();
8✔
123
    if (!r.fits(n, 8 + 8 + 8 * 3)) return false;
8✔
124
    rows.resize(n);
8✔
125
    for (auto& row : rows) {
22✔
126
        row.key = r.str();
14!
127
        row.count = r.u64();
14✔
128
        row.total = r.dbl();
14✔
129
        row.min = r.dbl();
14✔
130
        row.max = r.dbl();
14✔
131
    }
132
    return r.ok;
8✔
133
}
4✔
134

135
void write_spans(BufWriter& w, const std::vector<VizSummary::AppSpan>& spans) {
28✔
136
    w.u64(spans.size());
28✔
137
    for (const auto& s : spans) {
684✔
138
        w.u64(s.begin);
656!
139
        w.u64(s.end);
656!
140
        w.i64(s.pid);
656!
141
        w.i64(s.tid);
656!
142
        w.u32(s.name_id);
656!
143
        w.str(s.json);
656!
144
    }
145
}
28✔
146

147
bool read_spans(BufReader& r, std::vector<VizSummary::AppSpan>& spans) {
4✔
148
    std::uint64_t n = r.u64();
4✔
149
    if (!r.fits(n, 8 * 4 + 4 + 8)) return false;
4✔
150
    spans.resize(n);
4✔
151
    for (auto& s : spans) {
28✔
152
        s.begin = r.u64();
24✔
153
        s.end = r.u64();
24✔
154
        s.pid = r.i64();
24✔
155
        s.tid = r.i64();
24✔
156
        s.name_id = r.u32();
24✔
157
        s.json = r.str();
24!
158
    }
159
    return r.ok;
4✔
160
}
2✔
161

162
void serialize_summary(BufWriter& w, const VizSummary& s) {
14✔
163
    w.u64(s.t_begin);
14✔
164
    w.u64(s.t_end);
14✔
165
    w.u64(s.nbuckets);
14✔
166
    w.dbl(s.bucket_us);
14✔
167
    w.u64(s.max_dur);
14✔
168
    w.u64(s.total_files);
14✔
169
    w.u64(s.io_files);
14✔
170

171
    w.dbl(s.long_threshold_us);
14✔
172

173
    w.u64(s.names.size());
14✔
174
    for (const auto& n : s.names) w.str(n);
62✔
175

176
    w.u64(s.levels.size());
14✔
177
    for (const auto& lv : s.levels) {
70✔
178
        w.u64(lv.nbuckets);
56!
179
        w.dbl(lv.bucket_us);
56!
180
        auto wvec_lvl = [&](const std::vector<double>& v) {
196✔
181
            w.u64(v.size());
168✔
182
            for (double x : v) w.dbl(x);
233,963,688✔
183
        };
168✔
184
        wvec_lvl(lv.read_bytes);
56!
185
        wvec_lvl(lv.write_bytes);
56!
186
        wvec_lvl(lv.ops);
56!
187
        w.u64(lv.lanes.size());
56!
188
        for (const auto& l : lv.lanes) {
56!
NEW
189
            w.i64(l.pid);
×
NEW
190
            w.i64(l.tid);
×
NEW
191
            w.u64(l.cells.size());
×
NEW
192
            for (std::uint32_t bk : l.buckets) w.u32(bk);
×
NEW
193
            for (const auto& c : l.cells) {
×
NEW
194
                w.u32(c.count);
×
NEW
195
                w.u64(c.total);
×
NEW
196
                w.u32(c.max_dur);
×
NEW
197
                w.u32(c.name_id);
×
198
            }
199
        }
200
    }
201

202
    write_group_rows(w, s.by_name);
14✔
203
    write_group_rows(w, s.by_cat);
14✔
204
    write_group_rows(w, s.by_pid);
14✔
205
    write_group_rows(w, s.by_fhash);
14✔
206

207
    w.u64(s.name_cats.size());
14✔
208
    for (const auto& p : s.name_cats) {
62✔
209
        w.str(p.first);
48!
210
        w.str(p.second);
48!
211
    }
212

213
    w.u64(s.columns.size());
14✔
214
    for (const auto& c : s.columns) w.str(c);
106✔
215

216
    write_spans(w, s.app_spans);
14✔
217
    write_spans(w, s.long_events);
14✔
218

219
    w.u64(s.idle_gaps.size());
14✔
220
    for (const auto& g : s.idle_gaps) {
128✔
221
        w.u64(g.first);
114!
222
        w.u64(g.second);
114!
223
    }
224

225
    w.u64(s.procs.size());
14✔
226
    for (const auto& p : s.procs) {
142✔
227
        w.i64(p.pid);
128!
228
        w.i64(p.ppid);
128!
229
        w.u64(p.first_ts);
128!
230
        w.u64(p.bytes);
128!
231
        w.u64(p.io_ops);
128!
232
        w.dbl(p.io_busy);
128!
233
        w.str(p.hhash);
128!
234
        w.str(p.rank);
128!
235
    }
236

237
    w.u64(s.forks.size());
14✔
238
    for (const auto& f : s.forks) {
14!
NEW
239
        w.u64(f.ts);
×
NEW
240
        w.i64(f.pid);
×
NEW
241
        w.i64(f.child);
×
242
    }
243

244
    w.u64(s.hosts.size());
14✔
245
    for (const auto& h : s.hosts) {
14!
NEW
246
        w.str(h.first);
×
NEW
247
        w.str(h.second);
×
248
    }
249
}
14✔
250

251
bool deserialize_summary(BufReader& r, VizSummary& s) {
2✔
252
    s.t_begin = r.u64();
2✔
253
    s.t_end = r.u64();
2✔
254
    s.nbuckets = static_cast<std::size_t>(r.u64());
2✔
255
    s.bucket_us = r.dbl();
2✔
256
    s.max_dur = r.u64();
2✔
257
    s.total_files = static_cast<std::size_t>(r.u64());
2✔
258
    s.io_files = static_cast<std::size_t>(r.u64());
2✔
259

260
    s.long_threshold_us = r.dbl();
2✔
261

262
    std::uint64_t nnames = r.u64();
2✔
263
    if (!r.fits(nnames, 8)) return false;
2!
264
    s.names.resize(nnames);
2✔
265
    for (auto& n : s.names) n = r.str();
8✔
266

267
    std::uint64_t nlevels = r.u64();
2✔
268
    if (!r.fits(nlevels, 8 + 8 + 8)) return false;
2✔
269
    s.levels.resize(nlevels);
2✔
270
    for (auto& lv : s.levels) {
10✔
271
        lv.nbuckets = static_cast<std::size_t>(r.u64());
8✔
272
        lv.bucket_us = r.dbl();
8✔
273
        auto rvec_lvl = [&](std::vector<double>& v) -> bool {
28✔
274
            std::uint64_t n = r.u64();
24✔
275
            if (!r.fits(n, 8)) return false;
24✔
276
            v.resize(n);
24✔
277
            for (auto& x : v) x = r.dbl();
33,423,384✔
278
            return r.ok;
24✔
279
        };
16✔
280
        if (!rvec_lvl(lv.read_bytes) || !rvec_lvl(lv.write_bytes) ||
12!
281
            !rvec_lvl(lv.ops))
8!
NEW
282
            return false;
×
283
        std::uint64_t nlanes = r.u64();
8✔
284
        if (!r.fits(nlanes, 8 * 2 + 8)) return false;
8✔
285
        lv.lanes.resize(nlanes);
8!
286
        for (auto& l : lv.lanes) {
8!
NEW
287
            l.pid = r.i64();
×
NEW
288
            l.tid = r.i64();
×
NEW
289
            std::uint64_t ncells = r.u64();
×
NEW
290
            if (!r.fits(ncells, 4 + 4 + 8 + 4 + 4)) return false;
×
NEW
291
            l.buckets.resize(ncells);
×
NEW
292
            for (auto& bk : l.buckets) bk = r.u32();
×
NEW
293
            l.cells.resize(ncells);
×
NEW
294
            for (auto& c : l.cells) {
×
NEW
295
                c.count = r.u32();
×
NEW
296
                c.total = r.u64();
×
NEW
297
                c.max_dur = r.u32();
×
NEW
298
                c.name_id = r.u32();
×
299
            }
300
        }
301
    }
302

303
    if (!read_group_rows(r, s.by_name) || !read_group_rows(r, s.by_cat) ||
3!
304
        !read_group_rows(r, s.by_pid) || !read_group_rows(r, s.by_fhash))
3!
NEW
305
        return false;
×
306

307
    std::uint64_t ncats = r.u64();
2✔
308
    if (!r.fits(ncats, 8 * 2)) return false;
2✔
309
    s.name_cats.resize(ncats);
2✔
310
    for (auto& p : s.name_cats) {
8✔
311
        p.first = r.str();
6!
312
        p.second = r.str();
6!
313
    }
314

315
    std::uint64_t ncols = r.u64();
2✔
316
    if (!r.fits(ncols, 8)) return false;
2✔
317
    s.columns.resize(ncols);
2✔
318
    for (auto& c : s.columns) c = r.str();
18✔
319

320
    if (!read_spans(r, s.app_spans) || !read_spans(r, s.long_events))
2!
NEW
321
        return false;
×
322

323
    std::uint64_t ngaps = r.u64();
2✔
324
    if (!r.fits(ngaps, 8 * 2)) return false;
2✔
325
    s.idle_gaps.resize(ngaps);
2✔
326
    for (auto& g : s.idle_gaps) {
4✔
327
        g.first = r.u64();
2✔
328
        g.second = r.u64();
2✔
329
    }
330

331
    std::uint64_t nprocs = r.u64();
2✔
332
    if (!r.fits(nprocs, 8 * 5 + 8 * 2)) return false;
2✔
333
    s.procs.resize(nprocs);
2✔
334
    for (auto& p : s.procs) {
6✔
335
        p.pid = r.i64();
4✔
336
        p.ppid = r.i64();
4✔
337
        p.first_ts = r.u64();
4✔
338
        p.bytes = r.u64();
4✔
339
        p.io_ops = r.u64();
4✔
340
        p.io_busy = r.dbl();
4✔
341
        p.hhash = r.str();
4!
342
        p.rank = r.str();
4!
343
    }
344

345
    std::uint64_t nforks = r.u64();
2✔
346
    if (!r.fits(nforks, 8 * 3)) return false;
2✔
347
    s.forks.resize(nforks);
2✔
348
    for (auto& f : s.forks) {
2!
NEW
349
        f.ts = r.u64();
×
NEW
350
        f.pid = r.i64();
×
NEW
351
        f.child = r.i64();
×
352
    }
353

354
    std::uint64_t nhosts = r.u64();
2✔
355
    if (!r.fits(nhosts, 8 * 2)) return false;
2✔
356
    s.hosts.resize(nhosts);
2✔
357
    for (auto& h : s.hosts) {
2!
NEW
358
        h.first = r.str();
×
NEW
359
        h.second = r.str();
×
360
    }
361

362
    return r.ok;
2✔
363
}
1✔
364

365
}  // namespace
366

367
TraceIndex::TraceIndex(const std::string& directory,
68!
368
                       const std::string& index_dir, std::size_t max_concurrent,
369
                       std::size_t checkpoint_size)
17✔
370
    : directory_(directory),
34✔
371
      index_dir_(index_dir),
34!
372
      max_concurrent_(max_concurrent == 0 ? 8 : max_concurrent),
34!
373
      checkpoint_size_(checkpoint_size == 0
51!
374
                           ? constants::indexer::DEFAULT_CHECKPOINT_SIZE
17!
375
                           : checkpoint_size) {}
85!
376

377
coro::CoroTask<void> TraceIndex::initialize() {
128!
378
    PatternDirectoryScannerUtility scanner;
48!
379
    PatternDirectoryScannerUtilityInput scan_input{
96!
380
        directory_, {".pfw", ".pfw.gz"}, false};
48!
381
    auto entries = co_await scanner.process(scan_input);
64!
382

383
    files_.clear();
16✔
384
    path_to_index_.clear();
16✔
385
    files_.reserve(entries.size());
16!
386

387
    global_min_ts_ = std::numeric_limits<std::uint64_t>::max();
16✔
388
    global_max_ts_ = 0;
16✔
389

390
    std::vector<std::size_t> needs_build;
16✔
391
    std::vector<std::size_t> large_files;
16✔
392

393
    // The reuse decision below is existence-only, so a changed .pfw would be
394
    // served from a stale index. Drop any (shared) index root whose sources
395
    // changed since indexing; the loop then rebuilds it as if absent.
396
    {
397
        std::unordered_map<std::string, std::vector<std::string>> by_root;
16✔
398
        for (const auto& entry : entries)
33✔
399
            by_root[internal::determine_index_path(entry.path.string(),
34!
400
                                                   index_dir_)]
17✔
401
                .push_back(entry.path.string());
17!
402
        for (auto& [root, paths] : by_root) {
31!
403
            if (!fs::exists(root)) continue;
15!
404
            bool stale = false;
3✔
405
            try {
406
                indexer::IndexDatabase db(root);
3!
407
                stale = db.find_stale_files(paths).stale();
3!
408
            } catch (const std::exception& e) {
3!
409
                DFTRACER_UTILS_LOG_WARN(
×
410
                    "TraceIndex: stale check failed for %s: %s; rebuilding",
411
                    root.c_str(), e.what());
412
                stale = true;
413
            }
×
414
            if (stale) {
3✔
415
                DFTRACER_UTILS_LOG_WARN(
2!
416
                    "TraceIndex: source changed since indexing; rebuilding %s",
417
                    root.c_str());
418
                std::error_code ec;
2✔
419
                fs::remove_all(root, ec);
2!
420
            }
2✔
421
        }
15✔
422
    }
16✔
423

424
    for (const auto& entry : entries) {
33✔
425
        FileInfo info;
17✔
426
        info.path = entry.path.string();
17!
427
        info.index_path = internal::determine_index_path(info.path, index_dir_);
17!
428

429
        std::error_code ec;
17✔
430
        auto fsize = fs::file_size(info.path, ec);
17!
431
        info.compressed_size = (!ec && fsize > 0) ? fsize : 0;
17!
432

433
        std::size_t idx = files_.size();
17✔
434
        path_to_index_[info.path] = idx;
17!
435

436
        info.has_bloom_data = fs::exists(info.index_path);
17!
437
        info.has_checkpoint_index = fs::exists(info.index_path);
17!
438
        if (!info.has_bloom_data) {
17✔
439
            needs_build.push_back(idx);
16!
440
        } else {
16✔
441
            large_files.push_back(idx);
1!
442
        }
443

444
        files_.push_back(std::move(info));
17!
445
    }
17✔
446

447
    if (!needs_build.empty() || !large_files.empty()) {
16✔
448
        auto pipeline_config =
43✔
449
            PipelineConfig()
58!
450
                .with_name("TraceIndex Init")
15!
451
                .with_compute_threads(max_concurrent_)
15!
452
                .with_watchdog(false)
15!
453
                .with_global_timeout(std::chrono::seconds(0))
15!
454
                .with_task_timeout(std::chrono::seconds(0))
15!
455
                .with_io_backend(io::IoBackendType::THREADPOOL)
15!
456
                .with_io_batch_size(1);
15!
457

458
        Pipeline pipeline(pipeline_config);
15!
459

460
        auto* files_ptr = &files_;
15✔
461
        auto* needs_build_ptr = &needs_build;
15✔
462
        auto* large_files_ptr = &large_files;
15✔
463
        auto* global_min_ts_ptr = &global_min_ts_;
15✔
464
        auto* global_max_ts_ptr = &global_max_ts_;
15✔
465
        std::string index_dir = index_dir_;
15!
466
        std::size_t max_concurrent = max_concurrent_;
15✔
467
        std::size_t checkpoint_size = checkpoint_size_;
15✔
468

469
        auto init_task = make_task(
15!
470
            [files_ptr, needs_build_ptr, large_files_ptr, global_min_ts_ptr,
178!
471
             global_max_ts_ptr, index_dir, max_concurrent,
30!
472
             checkpoint_size](CoroScope& ctx) -> coro::CoroTask<void> {
30!
473
                if (!needs_build_ptr->empty()) {
73✔
474
                    DFTRACER_UTILS_LOG_INFO(
42!
475
                        "TraceIndex: building index for %zu file(s) ...",
476
                        needs_build_ptr->size());
477

478
                    // Assign file identities up front, single-threaded, so the
479
                    // concurrent builders below don't race the shared next-id
480
                    // counter and collide two files onto one id. Hold each
481
                    // read-write handle open across the build so builders reuse
482
                    // it instead of hitting a read-only -> read-write upgrade.
483
                    std::vector<std::unique_ptr<indexer::IndexDatabase>>
42✔
484
                        held_dbs;
42✔
485
                    {
486
                        std::unordered_map<std::string,
42✔
487
                                           std::vector<std::size_t>>
488
                            by_index;
42✔
489
                        for (auto idx : *needs_build_ptr)
58✔
490
                            by_index[(*files_ptr)[idx].index_path].push_back(
16!
491
                                idx);
16✔
492
                        for (auto& [ipath, idxs] : by_index) {
56!
493
                            try {
494
                                auto db =
14✔
495
                                    std::make_unique<indexer::IndexDatabase>(
14!
496
                                        ipath);
14✔
497
                                auto w = db->begin_write();
14!
498
                                for (auto idx : idxs) {
30✔
499
                                    const auto& p = (*files_ptr)[idx].path;
16✔
500
                                    w->get_or_create_file_info(
32!
501
                                        indexer::internal::get_logical_path(p),
16!
502
                                        indexer::internal::calculate_file_hash(
16!
503
                                            p));
16✔
504
                                }
16✔
505
                                w->commit();
14!
506
                                held_dbs.push_back(std::move(db));
14!
507
                            } catch (const std::exception& e) {
14!
508
                                DFTRACER_UTILS_LOG_WARN(
×
509
                                    "TraceIndex: file-id pre-assign failed for "
510
                                    "%s: %s",
511
                                    ipath.c_str(), e.what());
512
                            }
×
513
                        }
14✔
514
                    }
42✔
515

516
                    auto file_chan =
42✔
517
                        coro::make_channel<std::size_t>(max_concurrent * 2);
42!
518

519
                    const auto* index_dir_ptr = &index_dir;
42✔
520
                    co_await ctx.scope([&file_chan, files_ptr, needs_build_ptr,
281!
521
                                        index_dir_ptr, max_concurrent,
84✔
522
                                        checkpoint_size](CoroScope& scope)
42✔
523
                                           -> coro::CoroTask<void> {
14!
524
                        scope.spawn(
14!
525
                            [ch = file_chan->producer(), needs_build_ptr](
162!
526
                                CoroScope&) mutable -> coro::CoroTask<void> {
14!
527
                                auto guard = ch.guard();
14!
528
                                for (auto idx : *needs_build_ptr) {
94✔
529
                                    if (!co_await ch.send(idx)) co_return;
78!
530
                                }
16✔
531
                                co_return;
14✔
532
                            });
138!
533

534
                        for (std::size_t w = 0; w < max_concurrent; ++w) {
66✔
535
                            scope.spawn(
52!
536
                                [ch = file_chan->consumer(), files_ptr,
372!
537
                                 index_dir_ptr, checkpoint_size](
104✔
538
                                    CoroScope&) -> coro::CoroTask<void> {
52!
539
                                    while (auto fi_opt =
204✔
540
                                               co_await ch.receive()) {
174!
541
                                        std::size_t fi = *fi_opt;
48✔
542
                                        auto* info = &(*files_ptr)[fi];
48✔
543

544
                                        indexer::IndexBuilderUtility builder;
48!
545
                                        auto config =
48✔
546
                                            indexer::IndexBuildConfig::for_file(
144!
547
                                                info->path)
48✔
548
                                                .with_index_dir(*index_dir_ptr)
48✔
549
                                                .with_checkpoint_size(
16!
550
                                                    checkpoint_size);
16✔
551
                                        auto result =
48✔
552
                                            co_await builder.process(config);
64!
553

554
                                        if (result.success) {
16!
555
                                            info->index_path =
16✔
556
                                                internal::determine_index_path(
32!
557
                                                    info->path, *index_dir_ptr);
16✔
558
                                            info->has_bloom_data = true;
16✔
559
                                            info->has_checkpoint_index =
16✔
560
                                                fs::exists(info->index_path);
16!
561
                                        } else {
16✔
562
                                            DFTRACER_UTILS_LOG_WARN(
×
563
                                                "TraceIndex: failed to "
564
                                                "index %s: %s",
565
                                                info->path.c_str(),
566
                                                result.error_message.c_str());
567
                                        }
568
                                    }
100!
569
                                    co_return;
52✔
570
                                });
272!
571
                        }
52✔
572
                        co_return;
28✔
573
                    });
28!
574

575
                    for (auto idx : *needs_build_ptr) {
30✔
576
                        if ((*files_ptr)[idx].has_bloom_data) {
16!
577
                            large_files_ptr->push_back(idx);
16!
578
                        }
16✔
579
                    }
16✔
580
                }
14!
581

582
                if (!large_files_ptr->empty()) {
45!
583
                    auto meta_chan =
45✔
584
                        coro::make_channel<std::size_t>(max_concurrent * 2);
45!
585

586
                    co_await ctx.scope([&meta_chan, files_ptr, large_files_ptr,
195!
587
                                        max_concurrent](CoroScope& scope)
45✔
588
                                           -> coro::CoroTask<void> {
15!
589
                        scope.spawn(
15!
590
                            [ch = meta_chan->producer(), large_files_ptr](
173!
591
                                CoroScope&) mutable -> coro::CoroTask<void> {
15!
592
                                auto guard = ch.guard();
15!
593
                                for (auto idx : *large_files_ptr) {
100✔
594
                                    if (!co_await ch.send(idx)) co_return;
83!
595
                                }
17✔
596
                                co_return;
15✔
597
                            });
147!
598

599
                        for (std::size_t w = 0; w < max_concurrent; ++w) {
69✔
600
                            scope.spawn([ch = meta_chan->consumer(),
397!
601
                                         files_ptr](CoroScope&)
54✔
602
                                            -> coro::CoroTask<void> {
54!
603
                                while (auto fi_opt = co_await ch.receive()) {
193!
604
                                    std::size_t fi = *fi_opt;
51✔
605
                                    auto* info = &(*files_ptr)[fi];
51✔
606

607
                                    if (info->has_bloom_data) {
51✔
608
                                        try {
609
                                            indexer::IndexDatabase idx_db(
34!
610
                                                info->index_path);
17✔
611
                                            auto logical = indexer::internal::
34!
612
                                                get_logical_path(info->path);
17✔
613
                                            int fid = idx_db.get_file_info_id(
34!
614
                                                logical);
17✔
615
                                            if (fid >= 0) {
17!
616
                                                auto bounds =
17✔
617
                                                    idx_db.query_time_bounds(
17!
618
                                                        fid);
17✔
619
                                                if (bounds.valid) {
17!
620
                                                    info->min_timestamp_us =
17✔
621
                                                        bounds.min_timestamp_us;
17✔
622
                                                    info->max_timestamp_us =
17✔
623
                                                        bounds.max_timestamp_us;
17✔
624
                                                }
17✔
625
                                            }
17✔
626
                                        } catch (const std::exception& e) {
17!
627
                                            DFTRACER_UTILS_LOG_WARN(
×
628
                                                "TraceIndex: failed to "
629
                                                "read time bounds from "
630
                                                "%s: %s",
631
                                                info->index_path.c_str(),
632
                                                e.what());
633
                                        }
×
634
                                    }
17✔
635

636
                                    auto meta_input =
51✔
637
                                        MetadataCollectorUtilityInput::
102!
638
                                            from_file(info->path)
51!
639
                                                .with_index(info->index_path);
51!
640
                                    auto metadata =
51✔
641
                                        co_await MetadataCollectorUtility{}
119!
642
                                            .process(meta_input);
51!
643
                                    if (metadata.success) {
17!
644
                                        info->uncompressed_size =
17✔
645
                                            metadata.uncompressed_size;
17✔
646
                                        info->num_checkpoints =
17✔
647
                                            metadata.num_checkpoints;
17✔
648
                                        info->checkpoint_size =
17✔
649
                                            metadata.checkpoint_size;
17✔
650
                                        info->compressed_size =
17✔
651
                                            metadata.compressed_size;
17✔
652
                                        info->num_lines = metadata.num_lines;
17✔
653
                                        info->size_mb = metadata.size_mb;
17✔
654
                                    }
17✔
655
                                }
106!
656
                                co_return;
54✔
657
                            });
302!
658
                        }
54✔
659
                        co_return;
30✔
660
                    });
30!
661

662
                    for (auto fi : *large_files_ptr) {
32✔
663
                        const auto& info = (*files_ptr)[fi];
17✔
664
                        if (info.min_timestamp_us > 0 &&
17!
665
                            info.min_timestamp_us < *global_min_ts_ptr)
17✔
666
                            *global_min_ts_ptr = info.min_timestamp_us;
15✔
667
                        if (info.max_timestamp_us > *global_max_ts_ptr)
17✔
668
                            *global_max_ts_ptr = info.max_timestamp_us;
16✔
669
                    }
17✔
670
                }
15!
671

672
                co_return;
15✔
673
            },
88!
674
            "TraceIndexInit");
15!
675

676
        pipeline.set_source(init_task);
15!
677
        pipeline.set_destination(init_task);
15!
678
        pipeline.execute();
15!
679
    }
15✔
680

681
    // One time unit per trace: resolve it once from the first file's CM.
682
    if (!files_.empty()) {
46✔
683
        try {
684
            utilities::reader::TraceReaderConfig cfg;
45✔
685
            cfg.file_path = files_.front().path;
45✔
686
            cfg.index_dir = index_dir_;
15✔
687
            utilities::reader::TraceReader reader(cfg);
45!
688
            time_metric_ = co_await reader.read_time_metric();
60!
689
        } catch (const std::exception& e) {
15!
690
            DFTRACER_UTILS_LOG_WARN(
×
691
                "TraceIndex: failed to resolve time_metric: %s", e.what());
692
        }
×
693
        if (time_metric_ != TimeMetric::US) {
15✔
694
            DFTRACER_UTILS_LOG_INFO(
1!
695
                "TraceIndex: trace time unit is %s (scaling viz to us)",
696
                std::string(time_metric_to_string(time_metric_)).c_str());
697
        }
1✔
698
    }
15✔
699

700
    DFTRACER_UTILS_LOG_INFO("TraceIndex: found %zu trace files in %s",
16!
701
                            files_.size(), directory_.c_str());
702
    if (global_max_ts_ > 0) {
16✔
703
        DFTRACER_UTILS_LOG_INFO("TraceIndex: global time range [%" PRIu64
15!
704
                                ", %" PRIu64 "] us",
705
                                global_min_ts_, global_max_ts_);
706
    }
15✔
707

708
    // Adopt a valid cached summary now rather than on the first request: the
709
    // viewer opens with several summary-backed requests at once, and all of
710
    // them would otherwise queue behind the load.
711
    load_persisted_viz_summary();
16!
712
}
234!
713

714
// Looks up single hashes and keeps each root's database open, rather than
715
// caching whole hash tables per request.
NEW
716
std::string TraceIndex::resolve_hash(HashType type, const std::string& hash) {
×
NEW
717
    if (hash.empty()) return {};
×
718
    // Memoized across requests, misses included: the viewer re-asks for the
719
    // same hashes on every zoom.
NEW
720
    std::string memo_key;
×
NEW
721
    memo_key.reserve(hash.size() + 1);
×
NEW
722
    memo_key.push_back(static_cast<char>(static_cast<int>(type)));
×
NEW
723
    memo_key.append(hash);
×
724
    {
NEW
725
        std::lock_guard<std::mutex> lk(hash_db_mutex_);
×
NEW
726
        auto it = hash_names_.find(memo_key);
×
NEW
727
        if (it != hash_names_.end()) return it->second;
×
NEW
728
    }
×
NEW
729
    std::string resolved;
×
NEW
730
    for (const auto& f : files_) {
×
NEW
731
        if (f.index_path.empty()) continue;
×
NEW
732
        std::shared_ptr<indexer::IndexDatabase> db;
×
733
        {
NEW
734
            std::lock_guard<std::mutex> lk(hash_db_mutex_);
×
NEW
735
            auto it = hash_dbs_.find(f.index_path);
×
NEW
736
            if (it != hash_dbs_.end()) {
×
NEW
737
                db = it->second;
×
738
            } else {
739
                try {
NEW
740
                    db = std::make_shared<indexer::IndexDatabase>(
×
NEW
741
                        f.index_path,
×
NEW
742
                        rocksdb::RocksDatabase::OpenMode::ReadOnly);
×
NEW
743
                } catch (const std::exception&) {
×
NEW
744
                    db = nullptr;
×
NEW
745
                }
×
NEW
746
                hash_dbs_.emplace(f.index_path, db);
×
747
            }
NEW
748
        }
×
NEW
749
        if (!db) continue;
×
750
        try {
NEW
751
            auto name = db->lookup_hash(type, hash);
×
NEW
752
            if (name && !name->empty()) {
×
NEW
753
                resolved = std::move(*name);
×
NEW
754
                break;
×
755
            }
NEW
756
        } catch (const std::exception&) {
×
757
            continue;
NEW
758
        }
×
NEW
759
    }
×
760
    {
NEW
761
        std::lock_guard<std::mutex> lk(hash_db_mutex_);
×
NEW
762
        hash_names_.emplace(std::move(memo_key), resolved);
×
NEW
763
    }
×
NEW
764
    return resolved;
×
NEW
765
}
×
766

767
std::string TraceIndex::viz_summary_cache_path() const {
60✔
768
    return (fs::path(index_dir_) / ".dftviz_summary").string();
90!
769
}
770

771
// A changed source is re-indexed in initialize(), shifting these per-file
772
// values, so the cache invalidates itself on mismatch.
773
std::string TraceIndex::viz_summary_fingerprint() const {
24✔
774
    std::uint64_t h = 1469598103934665603ULL;
24✔
775
    auto mix = [&](const void* data, std::size_t n) {
228✔
776
        const auto* b = static_cast<const unsigned char*>(data);
216✔
777
        for (std::size_t i = 0; i < n; ++i) {
3,646✔
778
            h ^= b[i];
3,430✔
779
            h *= 1099511628211ULL;
3,430✔
780
        }
1,985✔
781
    };
228✔
782
    std::uint32_t ver = VIZ_SUMMARY_FORMAT_VERSION;
24✔
783
    mix(&ver, sizeof ver);
24✔
784
    int tm = static_cast<int>(time_metric_);
24✔
785
    mix(&tm, sizeof tm);
24✔
786
    mix(&global_min_ts_, sizeof global_min_ts_);
24✔
787
    mix(&global_max_ts_, sizeof global_max_ts_);
24✔
788
    for (const auto& f : files_) {
48✔
789
        mix(f.path.data(), f.path.size());
24✔
790
        mix(&f.uncompressed_size, sizeof f.uncompressed_size);
24✔
791
        mix(&f.num_checkpoints, sizeof f.num_checkpoints);
24✔
792
        mix(&f.min_timestamp_us, sizeof f.min_timestamp_us);
24✔
793
        mix(&f.max_timestamp_us, sizeof f.max_timestamp_us);
24✔
794
    }
795
    char out[17];
796
    std::snprintf(out, sizeof out, "%016llx",
36✔
797
                  static_cast<unsigned long long>(h));
12✔
798
    return std::string(out);
24!
799
}
800

801
bool TraceIndex::load_persisted_viz_summary() {
46✔
802
    try {
803
        auto path = viz_summary_cache_path();
46!
804
        std::ifstream is(path, std::ios::binary | std::ios::ate);
46!
805
        if (!is) return false;
46!
806
        auto size = is.tellg();
10!
807
        if (size <= 0) return false;
10!
808
        std::string buf(static_cast<std::size_t>(size), '\0');
10!
809
        is.seekg(0);
10!
810
        if (!is.read(buf.data(), size)) return false;
10!
811

812
        BufReader r{buf.data(), buf.data() + buf.size()};
10✔
813
        if (r.u32() != VIZ_SUMMARY_MAGIC ||
20!
814
            r.u32() != VIZ_SUMMARY_FORMAT_VERSION)
10!
NEW
815
            return false;
×
816
        std::string fp = r.str();
10!
817
        if (!r.ok || fp != viz_summary_fingerprint()) return false;
10!
818

819
        auto summary = std::make_unique<VizSummary>();
2!
820
        if (!deserialize_summary(r, *summary)) return false;
2!
821

822
        set_viz_summary(std::move(summary));
2!
823
        DFTRACER_UTILS_LOG_INFO("TraceIndex: loaded cached viz summary from %s",
2!
824
                                path.c_str());
825
        return true;
2✔
826
    } catch (const std::exception& e) {
46!
NEW
827
        DFTRACER_UTILS_LOG_WARN("TraceIndex: viz summary cache load failed: %s",
×
828
                                e.what());
NEW
829
        return false;
×
NEW
830
    }
×
831
}
23✔
832

833
void TraceIndex::persist_viz_summary() const {
14✔
834
    if (viz_summary_state_.load(std::memory_order_acquire) != 2 ||
28!
835
        !viz_summary_)
14!
NEW
836
        return;
×
837
    try {
838
        BufWriter w;
14✔
839
        w.u32(VIZ_SUMMARY_MAGIC);
14!
840
        w.u32(VIZ_SUMMARY_FORMAT_VERSION);
14!
841
        w.str(viz_summary_fingerprint());
14!
842
        serialize_summary(w, *viz_summary_);
14!
843

844
        auto path = viz_summary_cache_path();
14!
845
        auto tmp = path + ".tmp";
14!
846
        {
847
            std::ofstream os(tmp, std::ios::binary | std::ios::trunc);
14!
848
            if (!os.write(w.b.data(),
21!
849
                          static_cast<std::streamsize>(w.b.size()))) {
14✔
NEW
850
                std::error_code rec;
×
NEW
851
                fs::remove(tmp, rec);
×
NEW
852
                return;
×
853
            }
854
        }
14!
855
        std::error_code ec;
14✔
856
        fs::rename(tmp, path, ec);
14!
857
        if (ec) fs::remove(tmp, ec);
14!
858
    } catch (const std::exception& e) {
14!
NEW
859
        DFTRACER_UTILS_LOG_WARN(
×
860
            "TraceIndex: viz summary cache write failed: %s", e.what());
NEW
861
    }
×
862
}
7✔
863

864
const TraceIndex::FileInfo* TraceIndex::find_file(
6✔
865
    const std::string& path) const {
866
    auto it = path_to_index_.find(path);
6!
867
    if (it == path_to_index_.end()) return nullptr;
6✔
868
    return &files_[it->second];
4✔
869
}
3✔
870

871
const TraceIndex::FileInfo* TraceIndex::file_at(std::size_t index) const {
4✔
872
    if (index >= files_.size()) return nullptr;
4✔
873
    return &files_[index];
2✔
874
}
2✔
875

876
std::vector<const TraceIndex::FileInfo*> collect_candidate_files(
26✔
877
    TraceIndex& index, const QueryParams& params) {
878
    std::vector<const TraceIndex::FileInfo*> files;
26✔
879
    auto file_param = params.get("file");
26!
880
    if (!file_param.empty()) {
26!
881
        auto* f = index.find_file(std::string(file_param));
×
882
        if (f) files.push_back(f);
×
883
    } else {
884
        for (const auto& f : index.files()) {
60✔
885
            files.push_back(&f);
34!
886
        }
887
    }
888
    return files;
39✔
889
}
13!
890

891
}  // namespace dftracer::utils::server
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