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

llnl / dftracer-utils / 27171677342

08 Jun 2026 10:43PM UTC coverage: 51.99% (+0.05%) from 51.937%
27171677342

Pull #77

github

web-flow
Merge 3a1432eec into 8045f0be3
Pull Request #77: chore: bump version to 0.0.10

36972 of 92663 branches covered (39.9%)

Branch coverage included in aggregate %.

33405 of 42703 relevant lines covered (78.23%)

20411.31 hits per line

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

57.44
/src/dftracer/utils/utilities/call_tree/call_tree_save_binary.cpp
1
// Compact custom-binary save/load for in-memory CallTree.
2
//
3
// On-disk layout (little-endian, all multi-byte fields native u32/u64/...):
4
//
5
//   magic[8]      = "DFTCGRP2"
6
//   version u32   = 2
7
//   flags   u32   (reserved, currently 0)
8
//   string_table:
9
//     count u32
10
//     for each: u32 length + raw bytes (utf-8; embedded NULs OK)
11
//   process_count u32
12
//   for each ProcessCallTree:
13
//     pid u32, tid u32, node_id u32
14
//     call_count u32
15
//     for each CallTreeNode:
16
//       id u64
17
//       name_str_id u32, cat_str_id u32
18
//       start_time u64, duration u64
19
//       level i32, parent_id u64
20
//       child_count u32, then u64 ids
21
//       arg_count u32, then per arg:
22
//         key_str_id u32
23
//         type u8   { 0:string-id-u32, 1:u64, 2:i64, 3:double, 4:bool-u8 }
24
//         payload (type-dependent)
25
//     root_count u32, then u64 ids
26
//     seq_count  u32, then u64 ids
27

28
#include <ankerl/unordered_dense.h>
29
#include <dftracer/utils/call_tree/internal/call_tree.h>
30
#include <dftracer/utils/call_tree/internal/process_call_tree.h>
31
#include <dftracer/utils/call_tree/internal/process_key.h>
32
#include <dftracer/utils/call_tree/mpi/serializable.h>
33
#include <dftracer/utils/core/common/byte_view.h>
34
#include <dftracer/utils/core/common/logging.h>
35
#include <dftracer/utils/core/common/string_intern.h>
36
#include <dftracer/utils/utilities/composites/dft/args_map.h>
37
#include <dftracer/utils/utilities/fileio/parallel/parallel_writer.h>
38
#include <fcntl.h>
39
#include <sys/stat.h>
40
#include <unistd.h>
41

42
#include <cstdint>
43
#include <cstring>
44
#include <deque>
45
#include <string>
46
#include <string_view>
47
#include <vector>
48

49
namespace dftracer::utils::call_tree {
50

51
namespace {
52

53
using utilities::composites::dft::ArgsMap;
54
using utilities::composites::dft::ArgsValueProxy;
55

56
enum ArgTypeTag : std::uint8_t {
57
    ARG_STRING = 0,
58
    ARG_U64 = 1,
59
    ARG_I64 = 2,
60
    ARG_DOUBLE = 3,
61
    ARG_BOOL = 4,
62
};
63

64
dftracer::utils::StringIntern& binary_load_intern() {
16✔
65
    static dftracer::utils::StringIntern instance;
16!
66
    return instance;
16✔
67
}
68

69
void append_bytes(std::vector<char>& out, const void* p, std::size_t n) {
212✔
70
    out.insert(out.end(), static_cast<const char*>(p),
318!
71
               static_cast<const char*>(p) + n);
106✔
72
}
212✔
73

74
template <typename T>
75
void put_pod(std::vector<char>& out, T v) {
194✔
76
    append_bytes(out, &v, sizeof(v));
194✔
77
}
194✔
78

79
// Builder maintains the global string table and assigns dense ids; identical
80
// strings get the same id. `strings_` is a deque (not vector) so push_back
81
// keeps existing element addresses stable -- the index_ map stores
82
// string_views into those elements, and a vector realloc would dangle them
83
// (especially nasty for SSO strings whose storage lives inside the string
84
// object).
85
class StringTable {
1✔
86
   public:
87
    std::uint32_t intern(std::string_view s) {
40✔
88
        auto it = index_.find(s);
40!
89
        if (it != index_.end()) return it->second;
40✔
90
        const std::uint32_t id = static_cast<std::uint32_t>(strings_.size());
14✔
91
        strings_.emplace_back(s);
14!
92
        index_.emplace(std::string_view(strings_.back()), id);
14!
93
        return id;
14✔
94
    }
20✔
95

96
    void write(std::vector<char>& out) const {
2✔
97
        put_pod<std::uint32_t>(out,
3✔
98
                               static_cast<std::uint32_t>(strings_.size()));
2✔
99
        for (const auto& s : strings_) {
16✔
100
            put_pod<std::uint32_t>(out, static_cast<std::uint32_t>(s.size()));
14!
101
            append_bytes(out, s.data(), s.size());
14!
102
        }
103
    }
2✔
104

105
   private:
106
    std::deque<std::string> strings_;
107
    ankerl::unordered_dense::map<std::string_view, std::uint32_t> index_;
108
};
109

110
// Cursor over an in-memory buffer; tracks bounds and an `ok` flag so callers
111
// can early-exit on truncation without per-call error plumbing.
112
struct Cursor {
113
    const char* p;
114
    const char* end;
115
    bool ok = true;
116

117
    template <typename T>
118
    bool get_pod(T& out) {
194✔
119
        if (end - p < static_cast<std::ptrdiff_t>(sizeof(T))) {
194!
120
            ok = false;
×
121
            return false;
×
122
        }
123
        std::memcpy(&out, p, sizeof(T));
194✔
124
        p += sizeof(T);
194✔
125
        return true;
194✔
126
    }
97✔
127
    bool get_string_view(std::string_view& out, std::uint32_t len) {
14✔
128
        if (end - p < static_cast<std::ptrdiff_t>(len)) {
14!
129
            ok = false;
×
130
            return false;
×
131
        }
132
        out = std::string_view(p, len);
14✔
133
        p += len;
14✔
134
        return true;
14✔
135
    }
7✔
136
};
137

138
void serialize_node(std::vector<char>& out, StringTable& strings,
8✔
139
                    const internal::CallTreeNode& n) {
140
    put_pod<std::uint64_t>(out, n.get_id());
8!
141
    put_pod<std::uint32_t>(out, strings.intern(n.get_name()));
8!
142
    put_pod<std::uint32_t>(out, strings.intern(n.get_category()));
8!
143
    put_pod<std::uint64_t>(out, n.get_start_time());
8!
144
    put_pod<std::uint64_t>(out, n.get_duration());
8!
145
    put_pod<std::int32_t>(out, static_cast<std::int32_t>(n.get_level()));
8!
146
    put_pod<std::uint64_t>(out, n.get_parent_id());
8!
147

148
    const auto& children = n.get_children();
8✔
149
    put_pod<std::uint32_t>(out, static_cast<std::uint32_t>(children.size()));
8!
150
    for (auto id : children) put_pod<std::uint64_t>(out, id);
12✔
151

152
    // Pull args out as (key, ArgsValueProxy) so we can preserve typing.
153
    std::vector<std::pair<std::string_view, ArgsValueProxy>> args;
8✔
154
    n.get_args().for_each_member(
8!
155
        [&](std::string_view k, ArgsValueProxy v) { args.emplace_back(k, v); });
24✔
156

157
    put_pod<std::uint32_t>(out, static_cast<std::uint32_t>(args.size()));
8!
158
    for (auto& [k, v] : args) {
56✔
159
        put_pod<std::uint32_t>(out, strings.intern(k));
30!
160
        if (v.is_string()) {
20✔
161
            put_pod<std::uint8_t>(out, ARG_STRING);
4!
162
            const auto s = v.get<std::string>();
4✔
163
            put_pod<std::uint32_t>(out, strings.intern(s));
4!
164
        } else if (v.is_uint()) {
20!
165
            put_pod<std::uint8_t>(out, ARG_U64);
16!
166
            put_pod<std::uint64_t>(out, v.get<std::uint64_t>());
24!
167
        } else if (v.is_int()) {
8!
168
            put_pod<std::uint8_t>(out, ARG_I64);
×
169
            put_pod<std::int64_t>(out, v.get<std::int64_t>());
×
170
        } else if (v.is_number()) {
×
171
            put_pod<std::uint8_t>(out, ARG_DOUBLE);
×
172
            put_pod<double>(out, v.get<double>());
×
173
        } else if (v.is_bool()) {
×
174
            put_pod<std::uint8_t>(out, ARG_BOOL);
×
175
            put_pod<std::uint8_t>(out, v.get<bool>() ? 1 : 0);
×
176
        } else {
177
            put_pod<std::uint8_t>(out, ARG_STRING);
×
178
            put_pod<std::uint32_t>(out, strings.intern(""));
×
179
        }
180
    }
181
}
8✔
182

183
}  // namespace
184

185
coro::CoroTask<bool> save_binary(CoroScope* scope,
16!
186
                                 const internal::CallTree& tree,
187
                                 std::string output_path) {
1!
188
    // First pass: emit body to a scratch buffer while populating the
189
    // string table. Then write header + table + body.
190
    std::vector<char> body;
3✔
191
    body.reserve(1 << 20);
3!
192
    StringTable strings;
3!
193

194
    auto keys = const_cast<internal::CallTree&>(tree).keys();
3!
195
    put_pod<std::uint32_t>(body, static_cast<std::uint32_t>(keys.size()));
3!
196

197
    for (const auto& key : keys) {
5✔
198
        auto* graph = const_cast<internal::CallTree&>(tree).get(key);
2!
199
        if (!graph) continue;
2!
200

201
        put_pod<std::uint32_t>(body, key.pid);
2!
202
        put_pod<std::uint32_t>(body, key.tid);
2!
203
        put_pod<std::uint32_t>(body, key.node_id);
2!
204
        put_pod<std::uint32_t>(body,
2!
205
                               static_cast<std::uint32_t>(graph->calls.size()));
2✔
206
        for (const auto& [id, node] : graph->calls) {
6!
207
            if (node) serialize_node(body, strings, *node);
4!
208
        }
4✔
209
        put_pod<std::uint32_t>(
2!
210
            body, static_cast<std::uint32_t>(graph->root_calls.size()));
2✔
211
        for (auto id : graph->root_calls) put_pod<std::uint64_t>(body, id);
4!
212
        put_pod<std::uint32_t>(
2!
213
            body, static_cast<std::uint32_t>(graph->call_sequence.size()));
2✔
214
        for (auto id : graph->call_sequence) put_pod<std::uint64_t>(body, id);
6!
215
    }
2!
216

217
    std::vector<char> out;
3✔
218
    out.reserve(8 + 4 + 4 + body.size() + 16);
3✔
219
    append_bytes(out, CALLTREE_BINARY_MAGIC, sizeof(CALLTREE_BINARY_MAGIC));
1!
220
    put_pod<std::uint32_t>(out, CALLTREE_BINARY_VERSION);
1!
221
    put_pod<std::uint32_t>(out, 0u);  // flags
1!
222
    strings.write(out);
1!
223
    append_bytes(out, body.data(), body.size());
1✔
224

225
    utilities::fileio::parallel::WriterConfig wc;
3✔
226
    wc.layout = utilities::fileio::parallel::FileLayout::STRIPED;
3✔
227
    wc.gzip = false;
3✔
228
    auto writer = utilities::fileio::parallel::make_writer(wc);
3!
229
    if (co_await writer->open(output_path, 1, false, scope) != 0) {
5!
230
        DFTRACER_UTILS_LOG_ERROR("save_binary: open failed: %s",
×
231
                                 output_path.c_str());
232
        co_return false;
×
233
    }
234
    if (co_await writer->write_chunk(0, ByteView(out.data(), out.size())) !=
4!
235
        0) {
236
        co_return false;
×
237
    }
238
    if (co_await writer->close() != 0) co_return false;
4!
239
    co_return true;
1!
240
}
21!
241

242
coro::CoroTask<std::unique_ptr<internal::CallTree>> load_binary(
8!
243
    CoroScope* /*scope*/, std::string input_path) {
1!
244
    int fd = ::open(input_path.c_str(), O_RDONLY);
1!
245
    if (fd < 0) {
1!
246
        DFTRACER_UTILS_LOG_ERROR("load_binary: cannot open %s",
×
247
                                 input_path.c_str());
248
        co_return nullptr;
1!
249
    }
250
    struct stat st;
1✔
251
    if (::fstat(fd, &st) != 0 || st.st_size <= 0) {
1!
252
        ::close(fd);
×
253
        co_return nullptr;
×
254
    }
255
    std::vector<char> buf(static_cast<std::size_t>(st.st_size));
1!
256
    std::size_t got = 0;
1✔
257
    while (got < buf.size()) {
2✔
258
        ssize_t n = ::read(fd, buf.data() + got, buf.size() - got);
1!
259
        if (n <= 0) break;
1!
260
        got += static_cast<std::size_t>(n);
1✔
261
    }
1!
262
    ::close(fd);
1!
263
    if (got != buf.size()) co_return nullptr;
1!
264

265
    Cursor c{buf.data(), buf.data() + buf.size()};
1✔
266
    if (c.end - c.p < 8 || std::memcmp(c.p, CALLTREE_BINARY_MAGIC,
1!
267
                                       sizeof(CALLTREE_BINARY_MAGIC)) != 0) {
1✔
268
        DFTRACER_UTILS_LOG_ERROR("load_binary: bad magic in %s",
×
269
                                 input_path.c_str());
270
        co_return nullptr;
×
271
    }
272
    c.p += 8;
1✔
273
    std::uint32_t version = 0, flags = 0;
1✔
274
    if (!c.get_pod(version) || !c.get_pod(flags)) co_return nullptr;
1!
275
    if (version != CALLTREE_BINARY_VERSION) {
1!
276
        DFTRACER_UTILS_LOG_ERROR("load_binary: unsupported version %u",
×
277
                                 version);
278
        co_return nullptr;
×
279
    }
280

281
    std::uint32_t nstr = 0;
1✔
282
    if (!c.get_pod(nstr)) co_return nullptr;
1!
283
    std::vector<std::string_view> table;
1✔
284
    table.reserve(nstr);
1!
285
    for (std::uint32_t i = 0; i < nstr && c.ok; ++i) {
8!
286
        std::uint32_t len = 0;
7✔
287
        std::string_view s;
7✔
288
        if (!c.get_pod(len) || !c.get_string_view(s, len)) co_return nullptr;
7!
289
        table.push_back(s);
7!
290
    }
7!
291
    auto lookup_str = [&](std::uint32_t id) -> std::string_view {
41✔
292
        return id < table.size() ? table[id] : std::string_view{};
40!
293
    };
294

295
    auto tree = std::make_unique<internal::CallTree>();
1!
296
    tree->initialize();
1!
297

298
    std::uint32_t nprocs = 0;
1✔
299
    if (!c.get_pod(nprocs)) co_return nullptr;
1!
300

301
    for (std::uint32_t pi = 0; pi < nprocs && c.ok; ++pi) {
3✔
302
        std::uint32_t pid = 0, tid = 0, node_id = 0, ncalls = 0;
2✔
303
        if (!c.get_pod(pid) || !c.get_pod(tid) || !c.get_pod(node_id) ||
4!
304
            !c.get_pod(ncalls))
2!
305
            break;
306
        internal::ProcessKey key(pid, tid, node_id);
2!
307

308
        for (std::uint32_t ci = 0; ci < ncalls && c.ok; ++ci) {
6✔
309
            std::uint64_t id = 0, start = 0, dur = 0, parent = 0;
4✔
310
            std::uint32_t name_id = 0, cat_id = 0;
4✔
311
            std::int32_t level = 0;
4✔
312
            if (!c.get_pod(id) || !c.get_pod(name_id) || !c.get_pod(cat_id) ||
8!
313
                !c.get_pod(start) || !c.get_pod(dur) || !c.get_pod(level) ||
4!
314
                !c.get_pod(parent))
4!
315
                break;
316

317
            std::uint32_t nchildren = 0;
4✔
318
            if (!c.get_pod(nchildren)) break;
4!
319
            std::vector<std::uint64_t> children;
4✔
320
            children.reserve(nchildren);
4!
321
            for (std::uint32_t k = 0; k < nchildren && c.ok; ++k) {
6✔
322
                std::uint64_t cid = 0;
2✔
323
                if (!c.get_pod(cid)) break;
2!
324
                children.push_back(cid);
2!
325
            }
2!
326

327
            std::uint32_t nargs = 0;
4✔
328
            if (!c.get_pod(nargs)) break;
4!
329
            ArgsMap args;
4!
330
            if (nargs > 0) args.set_valid(true);
4!
331
            for (std::uint32_t k = 0; k < nargs && c.ok; ++k) {
14✔
332
                std::uint32_t key_id = 0;
10✔
333
                std::uint8_t type = 0;
10✔
334
                if (!c.get_pod(key_id) || !c.get_pod(type)) break;
10!
335
                auto key_sv = lookup_str(key_id);
10!
336
                switch (type) {
10!
337
                    case ARG_STRING: {
338
                        std::uint32_t val_id = 0;
2✔
339
                        if (!c.get_pod(val_id)) {
2!
340
                            c.ok = false;
341
                            break;
342
                        }
343
                        args.insert(key_sv, std::string(lookup_str(val_id)));
2!
344
                        break;
2✔
345
                    }
2✔
346
                    case ARG_U64: {
347
                        std::uint64_t v = 0;
8✔
348
                        if (!c.get_pod(v)) {
8!
349
                            c.ok = false;
350
                            break;
351
                        }
352
                        args.insert(key_sv, v);
8!
353
                        break;
8✔
354
                    }
8✔
355
                    case ARG_I64: {
356
                        std::int64_t v = 0;
357
                        if (!c.get_pod(v)) {
×
358
                            c.ok = false;
359
                            break;
360
                        }
361
                        args.insert(key_sv, v);
×
362
                        break;
363
                    }
364
                    case ARG_DOUBLE: {
365
                        double v = 0;
366
                        if (!c.get_pod(v)) {
×
367
                            c.ok = false;
368
                            break;
369
                        }
370
                        args.insert(key_sv, v);
×
371
                        break;
372
                    }
373
                    case ARG_BOOL: {
374
                        std::uint8_t v = 0;
375
                        if (!c.get_pod(v)) {
×
376
                            c.ok = false;
377
                            break;
378
                        }
379
                        args.insert(key_sv, v != 0);
×
380
                        break;
381
                    }
382
                    default:
383
                        c.ok = false;
384
                        break;
385
                }
386
            }
10!
387

388
            auto name = binary_load_intern().intern(lookup_str(name_id));
4!
389
            auto cat = binary_load_intern().intern(lookup_str(cat_id));
4!
390
            auto node = tree->get_factory().create_node(
4!
391
                id, name, cat, start, dur, static_cast<int>(level),
12✔
392
                std::move(args));
4✔
393
            node->set_parent_id(parent);
4!
394
            for (auto cid : children) node->add_child(cid);
6!
395
            tree->add_call(key, node);
4!
396
        }
4!
397

398
        auto* pgraph = tree->get(key);
2!
399
        if (!pgraph) continue;
2!
400
        // add_call already appended each new node id into call_sequence in
401
        // insertion order; the saved roots/sequence are authoritative, so
402
        // clear before replacing.
403
        pgraph->root_calls.clear();
2✔
404
        pgraph->call_sequence.clear();
2✔
405
        std::uint32_t nroots = 0;
2✔
406
        if (!c.get_pod(nroots)) break;
2!
407
        for (std::uint32_t k = 0; k < nroots && c.ok; ++k) {
4✔
408
            std::uint64_t id = 0;
2✔
409
            if (!c.get_pod(id)) break;
2!
410
            pgraph->root_calls.push_back(id);
2!
411
        }
2!
412
        std::uint32_t nseq = 0;
2✔
413
        if (!c.get_pod(nseq)) break;
2!
414
        for (std::uint32_t k = 0; k < nseq && c.ok; ++k) {
6✔
415
            std::uint64_t id = 0;
4✔
416
            if (!c.get_pod(id)) break;
4!
417
            pgraph->call_sequence.push_back(id);
4!
418
        }
4!
419
    }
2!
420

421
    if (!c.ok) {
1!
422
        DFTRACER_UTILS_LOG_ERROR("load_binary: truncated/malformed file %s",
×
423
                                 input_path.c_str());
424
        co_return nullptr;
×
425
    }
426
    co_return tree;
1!
427
}
3!
428

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

© 2026 Coveralls, Inc