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

llnl / dftracer-utils / 28982597416

08 Jul 2026 11:22PM UTC coverage: 50.709% (-1.9%) from 52.577%
28982597416

Pull #87

github

web-flow
Merge fdf73a826 into 4908d9921
Pull Request #87: fix(comparator): fix wrong counting files

16259 of 43579 branches covered (37.31%)

Branch coverage included in aggregate %.

37 of 38 new or added lines in 4 files covered. (97.37%)

616 existing lines in 111 files now uncovered.

21634 of 31148 relevant lines covered (69.46%)

13117.41 hits per line

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

51.9
/src/dftracer/utils/server/viz_api.cpp
1
#include <dftracer/utils/core/common/logging.h>
2
#include <dftracer/utils/core/common/to_chars.h>
3
#include <dftracer/utils/core/coro/channel.h>
4
#include <dftracer/utils/core/pipeline/executor.h>
5
#include <dftracer/utils/core/tasks/coro_scope.h>
6
#include <dftracer/utils/server/http_request.h>
7
#include <dftracer/utils/server/http_response.h>
8
#include <dftracer/utils/server/router.h>
9
#include <dftracer/utils/server/trace_index.h>
10
#include <dftracer/utils/server/viz_api.h>
11
#include <dftracer/utils/utilities/common/json/json_doc_guard.h>
12
#include <dftracer/utils/utilities/common/json/json_value.h>
13
#include <dftracer/utils/utilities/common/query/query.h>
14
#include <dftracer/utils/utilities/composites/dft/views/view_builder_utility.h>
15
#include <dftracer/utils/utilities/composites/dft/views/view_definition.h>
16
#include <dftracer/utils/utilities/composites/dft/views/view_reader_utility.h>
17
#include <dftracer/utils/utilities/fileio/lines/sources/async_streaming_gz_line_generator.h>
18
#include <simdjson.h>
19

20
#include <atomic>
21
#include <cstddef>
22
#include <cstdint>
23
#include <limits>
24
#include <memory>
25
#include <mutex>
26
#include <string>
27
#include <unordered_set>
28
#include <vector>
29

30
namespace dftracer::utils::server {
31

32
using namespace dftracer::utils::utilities::composites::dft;
33
using namespace dftracer::utils::utilities::composites::dft::views;
34

35
static const std::unordered_set<std::string> HASH_METADATA_NAMES = {"FH", "HH",
36
                                                                    "SH"};
37

38
/// Normalize the "ts" field in a Chrome Trace Event JSON string by
39
/// subtracting an offset.  Returns the modified JSON.  Falls back to
40
/// the original string on parse failure.
41
static std::string normalize_event_ts(const std::string& event_json,
150✔
42
                                      std::uint64_t offset) {
43
    thread_local simdjson::dom::parser tl_parser;
150✔
44
    auto result = tl_parser.parse(event_json);
150✔
45
    if (result.error()) return event_json;
150!
46

47
    auto root = result.value_unsafe();
150✔
48
    if (!root.is_object()) return event_json;
150!
49

50
    auto ts_result = root["ts"];
150✔
51
    if (ts_result.error()) return event_json;
150!
52

53
    std::uint64_t old_ts = 0;
150✔
54
    if (ts_result.is_uint64()) {
150!
55
        old_ts = ts_result.get_uint64().value_unsafe();
150✔
UNCOV
56
    } else if (ts_result.is_int64()) {
×
57
        auto val = ts_result.get_int64().value_unsafe();
×
58
        old_ts = val >= 0 ? static_cast<std::uint64_t>(val) : 0;
×
59
    } else {
60
        return event_json;
×
61
    }
62

63
    std::uint64_t new_ts = old_ts >= offset ? old_ts - offset : 0;
150!
64

65
    // simdjson DOM is read-only, so we need to rebuild the JSON with the new ts
66
    // Find "ts": and replace the value
67
    std::string modified = event_json;
150!
68
    auto pos = modified.find("\"ts\":");
150✔
69
    if (pos == std::string::npos) return event_json;
150!
70

71
    pos += 5;  // Skip past "ts":
150✔
72
    while (pos < modified.size() && std::isspace(modified[pos])) ++pos;
150!
73

74
    auto end_pos = pos;
150✔
75
    while (end_pos < modified.size() &&
3,300!
76
           (std::isdigit(modified[end_pos]) || modified[end_pos] == '-')) {
1,650!
77
        ++end_pos;
1,500✔
78
    }
79

80
    modified.replace(pos, end_pos - pos, std::to_string(new_ts));
150!
81
    return modified;
150✔
82
}
150✔
83

84
/// Compute the minimum event duration threshold for a given summary level.
85
/// Level 1 = full detail, higher levels filter shorter events.
86
static double duration_threshold(double begin, double end, unsigned level,
6✔
87
                                 unsigned viewport_width = 1920) {
88
    if (level <= 1) return 0.0;
6!
89
    double range = end - begin;
×
90
    return range /
91
           (static_cast<double>(viewport_width) * static_cast<double>(level));
×
92
}
93

94
static std::string extract_json_value(simdjson::dom::element val) {
3✔
95
    if (val.is_string()) {
3✔
96
        return std::string(val.get_string().value_unsafe());
2!
97
    }
98
    if (val.is_int64()) {
2!
99
        return std::to_string(val.get_int64().value_unsafe());
4!
100
    }
101
    if (val.is_uint64()) {
×
102
        return std::to_string(val.get_uint64().value_unsafe());
×
103
    }
104
    return {};
×
105
}
106

107
static void append_lane_clause(std::string& dsl, const char* field,
1✔
108
                               const std::string& val) {
109
    if (!dsl.empty()) dsl += " and ";
1!
110
    bool numeric =
111
        !val.empty() && std::all_of(val.begin(), val.end(),
1!
112
                                    [](char c) { return std::isdigit(c); });
1✔
113
    if (numeric) {
1!
114
        dsl += std::string(field) + " == " + val;
1!
115
    } else {
116
        dsl += std::string(field) + " == \"" + val + "\"";
×
117
    }
118
}
1✔
119

120
static void apply_lanes(std::string& dsl, std::string_view lanes_str) {
6✔
121
    if (lanes_str.empty()) return;
6✔
122

123
    thread_local simdjson::dom::parser tl_parser;
1!
124
    auto result = tl_parser.parse(lanes_str.data(), lanes_str.size());
1✔
125
    if (result.error()) return;
1!
126

127
    auto root = result.value_unsafe();
1✔
128

129
    if (root.is_array()) {
1!
130
        auto arr = root.get_array().value_unsafe();
1✔
131
        for (auto item : arr) {
2✔
132
            if (!item.is_object()) continue;
1!
133
            auto obj = item.get_object().value_unsafe();
1✔
134

135
            auto field_result = obj["field"];
1✔
136
            if (field_result.error()) field_result = obj["fields"];
1!
137
            auto value_result = obj["value"];
1✔
138
            if (field_result.error() || value_result.error()) continue;
1!
139

140
            if (!field_result.value_unsafe().is_string()) continue;
1!
141
            const char* field =
142
                field_result.value_unsafe().get_c_str().value_unsafe();
1✔
143
            auto val = extract_json_value(value_result.value_unsafe());
1!
144
            if (!val.empty()) append_lane_clause(dsl, field, val);
1!
145
        }
1✔
UNCOV
146
    } else if (root.is_object()) {
×
147
        auto obj = root.get_object().value_unsafe();
×
148

149
        auto field_result = obj["field"];
×
150
        if (field_result.error()) field_result = obj["fields"];
×
151
        auto value_result = obj["value"];
×
152

153
        if (!field_result.error() && !value_result.error()) {
×
154
            if (field_result.value_unsafe().is_string()) {
×
155
                const char* field =
156
                    field_result.value_unsafe().get_c_str().value_unsafe();
×
157
                auto val = extract_json_value(value_result.value_unsafe());
×
158
                if (!val.empty()) append_lane_clause(dsl, field, val);
×
159
            }
×
160
        }
161
    }
162
}
163

164
static void apply_filters(std::string& dsl, std::string_view filters_str) {
6✔
165
    if (filters_str.empty()) return;
6✔
166

167
    thread_local simdjson::dom::parser tl_parser;
2✔
168
    auto result = tl_parser.parse(filters_str.data(), filters_str.size());
2✔
169
    if (result.error()) return;
2!
170

171
    auto root = result.value_unsafe();
2✔
172
    if (!root.is_array()) return;
2!
173

174
    auto arr = root.get_array().value_unsafe();
2✔
175
    for (auto item : arr) {
4✔
176
        if (!item.is_object()) continue;
2!
177
        auto obj = item.get_object().value_unsafe();
2✔
178

179
        auto field_result = obj["field"];
2✔
180
        auto op_result = obj["op"];
2✔
181
        auto value_result = obj["value"];
2✔
182
        if (field_result.error() || op_result.error() || value_result.error())
2!
183
            continue;
×
184

185
        if (!field_result.value_unsafe().is_string() ||
4!
186
            !op_result.value_unsafe().is_string())
2!
187
            continue;
×
188

189
        const char* field =
190
            field_result.value_unsafe().get_c_str().value_unsafe();
2✔
191
        const char* op = op_result.value_unsafe().get_c_str().value_unsafe();
2✔
192

193
        std::string val = extract_json_value(value_result.value_unsafe());
2!
194
        if (val.empty()) continue;
2!
195

196
        std::string op_str(op);
2!
197
        std::string field_str(field);
2!
198
        if (field_str == "begin") field_str = "ts";
2!
199
        if (field_str == "end") field_str = "ts";
2!
200
        if (field_str == "duration") field_str = "dur";
2!
201

202
        std::string query_op;
2✔
203
        if (op_str == "=")
2✔
204
            query_op = "==";
1!
205
        else if (op_str == ">=")
1!
206
            query_op = ">=";
1!
207
        else if (op_str == "<=")
×
208
            query_op = "<=";
×
209
        else if (op_str == ">")
×
210
            query_op = ">";
×
211
        else if (op_str == "<")
×
212
            query_op = "<";
×
213
        else
214
            continue;
×
215

216
        if (!dsl.empty()) dsl += " and ";
2!
217
        bool numeric = !val.empty() && (std::isdigit(val[0]) || val[0] == '-');
2!
218
        if (numeric || query_op != "==") {
2!
219
            dsl += field_str + " " + query_op + " " + val;
2!
220
        } else {
221
            dsl += field_str + " " + query_op + " \"" + val + "\"";
×
222
        }
223
    }
2!
224
}
225

226
// --- GET /api/v1/viz/events ---
227
// Build the query view (time range + lane/filter/pid/tid/cat predicates) for a
228
// viz request from the parsed parameters.
229
static ViewDefinition build_viz_view(const QueryParams& params, double begin,
6✔
230
                                     double end, double min_dur) {
231
    ViewDefinition view;
6✔
232
    view.name = "viz_query";
6!
233
    view.description = "Visualization query";
6!
234

235
    // Reserve once and append in place: numbers go through to_chars into a
236
    // stack buffer (no per-number heap allocation, unlike std::to_string), and
237
    // literals/string_views are appended directly (no temporary
238
    // concatenations).
239
    std::string dsl;
6✔
240
    dsl.reserve(128);
6!
241
    char numbuf[20];  // max digits of a uint64_t
242
    auto append_u64 = [&](std::uint64_t v) {
12✔
243
        dsl.append(numbuf, to_chars_u64(numbuf, numbuf + sizeof(numbuf), v));
12✔
244
    };
18✔
245

246
    dsl += "ts >= ";
6!
247
    append_u64(static_cast<std::uint64_t>(begin));
6!
248
    dsl += " and ts <= ";
6!
249
    append_u64(static_cast<std::uint64_t>(end));
6!
250
    if (min_dur > 0) {
6!
251
        dsl += " and dur >= ";
×
252
        append_u64(static_cast<std::uint64_t>(min_dur));
×
253
    }
254

255
    apply_lanes(dsl, params.get("lanes"));
6!
256
    apply_filters(dsl, params.get("filters"));
6!
257

258
    auto pid = params.get("pid");
6!
259
    if (!pid.empty()) {
6!
260
        dsl += " and pid == ";
×
261
        dsl += pid;
×
262
    }
263

264
    auto tid = params.get("tid");
6!
265
    if (!tid.empty()) {
6!
266
        dsl += " and tid == ";
×
267
        dsl += tid;
×
268
    }
269

270
    auto cat = params.get("cat");
6!
271
    if (!cat.empty()) {
6!
272
        dsl += " and cat == \"";
×
273
        dsl += cat;
×
274
        dsl += '"';
×
275
    }
276

277
    view.with_query(dsl);
6!
278
    return view;
12✔
279
}
6✔
280

281
// Select the files to scan: the explicit ?file= or all indexed files, then drop
282
// files whose cached time bounds don't overlap [begin, end]. Pure/synchronous.
283
static std::vector<const TraceIndex::FileInfo*> select_viz_target_files(
6✔
284
    TraceIndex& index, const QueryParams& params, double begin, double end) {
285
    auto target_files = collect_candidate_files(index, params);
6✔
286

287
    if (begin > 0 || end > 0) {
6!
288
        std::vector<const TraceIndex::FileInfo*> filtered;
6✔
289
        filtered.reserve(target_files.size());
6!
290
        for (auto* fi : target_files) {
12✔
291
            if (fi->min_timestamp_us == 0 && fi->max_timestamp_us == 0) {
6!
292
                filtered.push_back(fi);
×
293
                continue;
×
294
            }
295
            double fi_min = static_cast<double>(fi->min_timestamp_us);
6✔
296
            double fi_max = static_cast<double>(fi->max_timestamp_us);
6✔
297
            if (fi_max < begin || fi_min > end) continue;
6!
298
            filtered.push_back(fi);
5!
299
        }
300
        target_files = std::move(filtered);
6✔
301
    }
6✔
302
    return target_files;
6✔
UNCOV
303
}
×
304

305
// Normalize event timestamps (when global_min > 0) and serialize the collected
306
// events plus metadata into the Chrome Trace Event Format body. `global_min` is
307
// the de-normalization base (already 0 unless normalization is active);
308
// `display_global_min` is the value reported in the metadata. Pure/synchronous.
309
static std::string build_viz_events_body(std::vector<std::string>& events,
6✔
310
                                         std::uint64_t global_min,
311
                                         double meta_begin, double meta_end,
312
                                         int limit, bool truncated,
313
                                         std::uint64_t display_global_min) {
314
    if (global_min > 0) {
6✔
315
        for (auto& event : events) {
155✔
316
            event = normalize_event_ts(event, global_min);
150!
317
        }
318
    }
319

320
    // Pre-compute size to avoid repeated reallocations.
321
    std::size_t body_size = 256;
6✔
322
    for (const auto& ev : events) body_size += ev.size() + 1;
156✔
323
    std::string body;
6✔
324
    body.reserve(body_size);
6!
325
    body += "{\"events\":[";
6!
326
    for (std::size_t i = 0; i < events.size(); ++i) {
156✔
327
        if (i > 0) body += ',';
150!
328
        body += events[i];  // Already JSON
150!
329
    }
330
    body += "],\"metadata\":{\"begin\":";
6!
331
    body += std::to_string(meta_begin);
6!
332
    body += ",\"end\":";
6!
333
    body += std::to_string(meta_end);
6!
334
    body += ",\"count\":";
6!
335
    body += std::to_string(events.size());
6!
336
    body += ",\"limit\":";
6!
337
    body += std::to_string(limit);
6!
338
    body += ",\"truncated\":";
6!
339
    body += truncated ? "true" : "false";
6!
340
    body += ",\"ts_normalized\":";
6!
341
    body += (global_min > 0) ? "true" : "false";
6!
342
    body += ",\"global_min_timestamp_us\":";
6!
343
    body += std::to_string(display_global_min);
6!
344
    body += "}}";
6!
345
    return body;
6✔
UNCOV
346
}
×
347

348
static coro::CoroTask<HttpResponse> handle_viz_events(
7!
349
    const HttpRequest& /*req*/, const QueryParams& params, TraceIndex& index) {
350
    // Required: begin, end, summary
351
    if (!params.has("begin") || !params.has("end") || !params.has("summary")) {
352
        co_return HttpResponse::bad_request(
353
            "Missing required parameters: begin, end, summary");
354
    }
355

356
    double begin = params.get_double("begin", 0);
357
    double end = params.get_double("end", 0);
358
    int summary = params.get_int("summary", 1);
359
    if (summary < 1) summary = 1;
360

361
    // Timestamp normalization: default ON, opt-out with ?ts_normalize=0
362
    auto ts_norm_param = params.get("ts_normalize");
363
    bool normalize = ts_norm_param.empty() || ts_norm_param != "0";
364

365
    std::uint64_t global_min = 0;
366
    if (normalize) {
367
        global_min = index.global_min_timestamp_us();
368
        if (global_min == std::numeric_limits<std::uint64_t>::max()) {
369
            global_min = 0;  // No valid bounds, skip normalization
370
        }
371
    }
372

373
    // When normalization is active the user sends normalized
374
    // begin/end values (relative to global_min).  De-normalize them
375
    // so the predicate filters against absolute timestamps.
376
    double original_begin = begin;
377
    double original_end = end;
378
    if (normalize && global_min > 0) {
379
        begin += static_cast<double>(global_min);
380
        end += static_cast<double>(global_min);
381
    }
382

383
    double min_dur =
384
        duration_threshold(begin, end, static_cast<unsigned>(summary));
385

386
    ViewDefinition view = build_viz_view(params, begin, end, min_dur);
387

388
    // Optional limit: 0 (default) means no limit.
389
    int limit = params.get_int("limit", 0);
390
    if (limit < 0) limit = 0;
391

392
    std::vector<const TraceIndex::FileInfo*> target_files =
393
        select_viz_target_files(index, params, begin, end);
394

395
    std::vector<std::string> collected_events;
396

397
    bool truncated = false;
398

399
    if (target_files.size() <= 1) {
400
        for (auto* file_info : target_files) {
401
            if (limit > 0 &&
402
                static_cast<int>(collected_events.size()) >= limit) {
403
                truncated = true;
404
                break;
405
            }
406
            if (file_info->uncompressed_size == 0 &&
407
                file_info->num_checkpoints == 0)
408
                continue;
409

410
            ViewBuilderInput builder_input;
411
            builder_input.with_view(view)
412
                .with_file_path(file_info->path)
413
                .with_index_path(
414
                    file_info->has_bloom_data ? file_info->index_path : "")
415
                .with_uncompressed_size(file_info->uncompressed_size)
416
                .with_num_checkpoints(file_info->num_checkpoints)
417
                .with_bloom_cache(&index.bloom_cache())
418
                .with_time_range(begin, end);
419

420
            ViewBuilderUtility builder;
421
            auto build_output = co_await builder.process(builder_input);
422
            if (!build_output || !build_output->file_may_match) continue;
423

424
            for (const auto& candidate : build_output->candidates) {
425
                if (limit > 0 &&
426
                    static_cast<int>(collected_events.size()) >= limit) {
427
                    truncated = true;
428
                    break;
429
                }
430
                ViewReaderInput reader_input;
431
                reader_input.with_file_path(file_info->path)
432
                    .with_index_path(file_info->index_path)
433
                    .with_byte_range(candidate.start_byte, candidate.end_byte)
434
                    .with_checkpoint_idx(candidate.checkpoint_idx)
435
                    .with_view(view);
436

437
                ViewReaderUtility reader;
438
                auto gen = reader.process(reader_input);
439
                while (auto batch = co_await gen.next()) {
440
                    for (auto& event : batch->events) {
441
                        if (limit > 0 &&
442
                            static_cast<int>(collected_events.size()) >=
443
                                limit) {
444
                            truncated = true;
445
                            break;
446
                        }
447
                        collected_events.emplace_back(event);
448
                    }
449
                    if (truncated) break;
450
                }
451
            }
452
        }
453
    } else {
454
        std::size_t num_workers =
455
            std::min(index.max_concurrent(), target_files.size());
456
        auto* executor = Executor::current();
457

458
        auto file_chan = coro::make_channel<std::size_t>(num_workers * 2);
459
        auto collected_mutex = std::make_shared<std::mutex>();
460
        auto remaining = std::make_shared<std::atomic<int>>(
461
            limit > 0 ? limit : std::numeric_limits<int>::max());
462

463
        auto* target_files_ptr = &target_files;
464
        auto* collected_ptr = &collected_events;
465
        auto* view_ptr = &view;
466
        auto* bloom_cache_ptr = &index.bloom_cache();
467
        double t_begin = begin;
468
        double t_end = end;
469

470
        CoroScope scope(executor);
471

472
        scope.spawn([ch = file_chan->producer(), target_files_ptr](
×
473
                        CoroScope&) mutable -> coro::CoroTask<void> {
474
            auto guard = ch.guard();
475
            for (std::size_t i = 0; i < target_files_ptr->size(); ++i) {
476
                if (!co_await ch.send(i)) co_return;
477
            }
478
            co_return;
479
        });
×
480

481
        for (std::size_t w = 0; w < num_workers; ++w) {
482
            scope.spawn([file_chan, target_files_ptr, collected_mutex,
×
483
                         collected_ptr, view_ptr, bloom_cache_ptr, remaining,
484
                         t_begin, t_end](CoroScope&) -> coro::CoroTask<void> {
485
                while (auto fi_opt = co_await file_chan->receive()) {
486
                    if (remaining->load(std::memory_order_relaxed) <= 0)
487
                        co_return;
488
                    auto* file_info = (*target_files_ptr)[*fi_opt];
489

490
                    if (file_info->uncompressed_size == 0 &&
491
                        file_info->num_checkpoints == 0)
492
                        continue;
493

494
                    ViewBuilderInput builder_input;
495
                    builder_input.with_view(*view_ptr)
496
                        .with_file_path(file_info->path)
497
                        .with_index_path(file_info->has_bloom_data
498
                                             ? file_info->index_path
499
                                             : "")
500
                        .with_uncompressed_size(file_info->uncompressed_size)
501
                        .with_num_checkpoints(file_info->num_checkpoints)
502
                        .with_bloom_cache(bloom_cache_ptr)
503
                        .with_time_range(t_begin, t_end);
504

505
                    ViewBuilderUtility builder;
506
                    auto build_output = co_await builder.process(builder_input);
507
                    if (!build_output || !build_output->file_may_match)
508
                        continue;
509

510
                    for (const auto& candidate : build_output->candidates) {
511
                        if (remaining->load(std::memory_order_relaxed) <= 0)
512
                            break;
513

514
                        ViewReaderInput reader_input;
515
                        reader_input.with_file_path(file_info->path)
516
                            .with_index_path(file_info->index_path)
517
                            .with_byte_range(candidate.start_byte,
518
                                             candidate.end_byte)
519
                            .with_checkpoint_idx(candidate.checkpoint_idx)
520
                            .with_view(*view_ptr);
521

522
                        ViewReaderUtility reader;
523
                        auto gen = reader.process(reader_input);
524
                        while (auto batch = co_await gen.next()) {
525
                            if (!batch->events.empty()) {
526
                                std::lock_guard<std::mutex> lock(
527
                                    *collected_mutex);
528
                                for (auto& event : batch->events) {
529
                                    collected_ptr->emplace_back(event);
530
                                }
531
                                remaining->fetch_sub(
532
                                    static_cast<int>(batch->events.size()));
533
                            }
534
                        }
535
                    }
536
                }
537
                co_return;
538
            });
×
539
        }
540

541
        co_await scope.join();
542

543
        if (limit > 0 && static_cast<int>(collected_events.size()) > limit) {
544
            collected_events.resize(static_cast<std::size_t>(limit));
545
            truncated = true;
546
        }
547
    }
548

549
    std::string body = build_viz_events_body(
550
        collected_events, global_min, original_begin, original_end, limit,
551
        truncated, index.global_min_timestamp_us());
552
    co_return HttpResponse::ok(body);
553
}
14!
554

555
void register_viz_api(Router& router, TraceIndex& index) {
2✔
556
    auto* index_ptr = &index;
2✔
557

558
    router.get(
2!
559
        "/api/v1/viz/events",
560
        [index_ptr](const HttpRequest& req,
7!
561
                    const QueryParams& params) -> coro::CoroTask<HttpResponse> {
562
            co_return co_await handle_viz_events(req, params, *index_ptr);
563
        });
14!
564
}
2✔
565

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