• 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

58.61
/src/dftracer/utils/python/utilities/aggregator.cpp
1
#define PY_SSIZE_T_CLEAN
2
#include <dftracer/utils/core/common/config.h>
3
#include <dftracer/utils/core/common/constants.h>
4
#include <dftracer/utils/core/common/memory_budget.h>
5
#include <dftracer/utils/core/coro/task.h>
6
#include <dftracer/utils/core/runtime.h>
7
#include <dftracer/utils/python/arrow_helpers.h>
8
#include <dftracer/utils/python/py_dict_helpers.h>
9
#include <dftracer/utils/python/py_list_helpers.h>
10
#include <dftracer/utils/python/py_runtime_mixin.h>
11
#include <dftracer/utils/python/py_type_helpers.h>
12
#include <dftracer/utils/python/runtime.h>
13
#include <dftracer/utils/python/trace_reader_iterator.h>
14
#include <dftracer/utils/python/utilities/aggregator.h>
15
#include <dftracer/utils/utilities/composites/dft/aggregators/aggregator_utility.h>
16

17
#ifdef DFTRACER_UTILS_ENABLE_ARROW
18
#include <dftracer/utils/python/batch_byte_size.h>
19
#include <dftracer/utils/python/streaming_iterator.h>
20
#endif
21
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
22
#include <dftracer/utils/utilities/common/arrow/partition_router.h>
23
#include <dftracer/utils/utilities/common/arrow/partition_writer.h>
24
#include <dftracer/utils/utilities/common/query/query.h>
25
#endif
26

27
#include <cctype>
28
#include <memory>
29
#include <optional>
30
#include <string>
31
#include <vector>
32

33
using dftracer::utils::CoroScope;
34
using dftracer::utils::Runtime;
35
using dftracer::utils::coro::CoroTask;
36
using namespace dftracer::utils::utilities::composites::dft::aggregators;
37

38
using dftracer::utils::python::wrap_arrow_result;
39
using dftracer::utils::python::wrap_arrow_table;
40

41
#ifdef DFTRACER_UTILS_ENABLE_ARROW
42
using dftracer::utils::python::ArrowStreamingIteratorObject;
43
using dftracer::utils::python::ArrowStreamingIteratorType;
44
using dftracer::utils::python::StreamingState;
45
using dftracer::utils::utilities::common::arrow::ArrowExportResult;
46
#endif
47
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
48
using dftracer::utils::utilities::common::arrow::IpcCompression;
49
using dftracer::utils::utilities::common::arrow::PartitionWriter;
50
using dftracer::utils::utilities::common::arrow::PartitionWriteStats;
51
using dftracer::utils::utilities::common::query::Query;
52
#endif
53

54
DFTRACER_UTILS_RUNTIME_BACKED_SLOTS(Aggregator, AggregatorObject)
72✔
55

56
// ---------------------------------------------------------------------------
57
// Helpers
58
// ---------------------------------------------------------------------------
59

60
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
61
// Parse a view query string into an optional Query
62
static int parse_view_query(PyObject *query_obj, std::optional<Query> &out) {
20✔
63
    if (!query_obj || query_obj == Py_None) {
20!
64
        out = std::nullopt;
18✔
65
        return 0;
18✔
66
    }
67
    const char *query_str = PyUnicode_AsUTF8(query_obj);
2✔
68
    if (!query_str) return -1;
2!
69
    auto parsed = Query::from_string(query_str);
2✔
70
    if (!parsed) {
2!
71
        PyErr_Format(PyExc_ValueError, "Invalid query: %s",
×
72
                     parsed.error().format().c_str());
×
73
        return -1;
×
74
    }
75
    out = std::move(*parsed);
2!
76
    return 0;
2✔
77
}
20✔
78
#endif
79

80
static int parse_aggregator_args(PyObject *args, PyObject *kwds,
20✔
81
                                 AggregatorInput &input,
82
                                 std::size_t *buffer_size_out = nullptr,
83
                                 std::optional<Query> *query_out = nullptr) {
84
    static const char *kwlist[] = {"directory",
85
                                   "time_interval_ms",
86
                                   "group_keys",
87
                                   "categories",
88
                                   "names",
89
                                   "index_dir",
90
                                   "checkpoint_size",
91
                                   "force_rebuild",
92
                                   "parallelism",
93
                                   "event_batch_size",
94
                                   "custom_metric_fields",
95
                                   "compute_percentiles",
96
                                   "buffer_size",
97
                                   "query",
98
                                   NULL};
99

100
    const char *directory = NULL;
20✔
101
    double time_interval_ms = 5000.0;
20✔
102
    PyObject *group_keys_obj = Py_None;
20✔
103
    PyObject *categories_obj = Py_None;
20✔
104
    PyObject *names_obj = Py_None;
20✔
105
    const char *index_dir = "";
20✔
106
    Py_ssize_t checkpoint_size = static_cast<Py_ssize_t>(
20✔
107
        dftracer::utils::constants::indexer::DEFAULT_CHECKPOINT_SIZE);
108
    int force_rebuild = 0;
20✔
109
    Py_ssize_t parallelism = 0;
20✔
110
    Py_ssize_t event_batch_size = 10000;
20✔
111
    PyObject *custom_metrics_obj = Py_None;
20✔
112
    int compute_percentiles = 0;
20✔
113
    Py_ssize_t buffer_size = 8;
20✔
114
    PyObject *query_obj = Py_None;
20✔
115

116
    if (!PyArg_ParseTupleAndKeywords(
20!
117
            args, kwds, "s|dOOOsnpnnOpnO", (char **)kwlist, &directory,
20✔
118
            &time_interval_ms, &group_keys_obj, &categories_obj, &names_obj,
119
            &index_dir, &checkpoint_size, &force_rebuild, &parallelism,
120
            &event_batch_size, &custom_metrics_obj, &compute_percentiles,
121
            &buffer_size, &query_obj))
122
        return -1;
×
123

124
    if (buffer_size_out) {
20✔
125
        *buffer_size_out = static_cast<std::size_t>(buffer_size);
3✔
126
    }
3✔
127

128
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
129
    if (query_out) {
20!
130
        if (parse_view_query(query_obj, *query_out) < 0) return -1;
20!
131
    }
20✔
132
#else
133
    (void)query_obj;
134
#endif
135

136
    input.directory = directory;
20✔
137
    input.config.time_interval_us =
20✔
138
        static_cast<std::uint64_t>(time_interval_ms * 1000.0);
20✔
139
    input.index_dir = index_dir;
20✔
140
    input.checkpoint_size = static_cast<std::size_t>(checkpoint_size);
20✔
141
    input.force_rebuild = force_rebuild != 0;
20✔
142
    input.parallelism = static_cast<std::size_t>(parallelism);
20✔
143
    input.event_batch_size = static_cast<std::size_t>(event_batch_size);
20✔
144
    input.config.compute_percentiles = compute_percentiles != 0;
20✔
145

146
    if (!parse_str_list(group_keys_obj, "group_keys",
40!
147
                        input.config.extra_group_keys))
20✔
148
        return -1;
×
149
    if (!parse_str_list(custom_metrics_obj, "custom_metric_fields",
40!
150
                        input.config.custom_metric_fields))
20✔
151
        return -1;
×
152

153
    return 0;
20✔
154
}
20✔
155

156
#ifdef DFTRACER_UTILS_ENABLE_ARROW
157
static bool run_aggregator_pipeline(
17✔
158
    AggregatorObject *self, const AggregatorInput &input,
159
    std::vector<ArrowExportResult> &results,
160
    const std::optional<Query> *query = nullptr) {
161
    auto *rp = &results;
17✔
162
    AggregatorInput input_copy = input;
17✔
163
    std::optional<Query> query_copy;
17✔
164
    if (query) query_copy = *query;
17!
165

166
    return run_blocking([&] {
34!
167
        Runtime *rt = resolve_runtime(self);
17✔
168
        rt->submit(run_coro_scope(
68!
169
                       rt->executor(),
17✔
170
                       [](CoroScope &scope, std::vector<ArrowExportResult> *out,
225!
171
                          AggregatorInput input,
172
                          std::optional<Query> query) -> CoroTask<void> {
17!
173
                           AggregatorUtility util;
17!
174
                           util.bind_context(scope);
17!
175
                           try {
176
                               auto gen = util.process(input);
17!
177
                               while (auto batch = co_await gen.next()) {
157!
178
                                   if (batch->entries.empty()) continue;
18!
179
                                   AggregationBatch filtered;
18✔
180
                                   if (query) {
18✔
181
                                       filtered = batch->filter(*query);
1!
182
                                       if (filtered.entries.empty()) continue;
1!
183
                                   } else {
1✔
184
                                       filtered = std::move(*batch);
17✔
185
                                   }
186
                                   auto arrow_result = filtered.to_arrow();
18!
187
                                   if (!arrow_result.valid()) continue;
18!
188
                                   out->push_back(std::move(arrow_result));
18!
189
                               }
35!
190
                               util.unbind_context();
17!
191
                           } catch (...) {
87✔
UNCOV
192
                               util.unbind_context();
×
UNCOV
193
                               throw;
×
UNCOV
194
                           }
×
195
                       },
227✔
196
                       rp, std::move(input_copy), std::move(query_copy)),
17✔
197
                   "aggregator")
17!
198
            .get();
17!
199
    });
17✔
200
}
17✔
201
#endif  // DFTRACER_UTILS_ENABLE_ARROW
202

203
#ifdef DFTRACER_UTILS_ENABLE_ARROW
204

205
static CoroTask<void> run_aggregator_stream(
47!
206
    CoroScope &scope, std::shared_ptr<StreamingState<ArrowExportResult>> state,
207
    AggregatorInput input, std::optional<Query> query) {
3!
208
    if (state->cancelled()) {
3!
UNCOV
209
        state->complete();
×
210
        co_return;
3✔
211
    }
212

213
    try {
214
        AggregatorUtility util;
3!
215
        util.bind_context(scope);
3!
216
        auto gen = util.process(input);
3!
217

218
        while (auto batch = co_await gen.next()) {
32!
219
            if (state->cancelled()) break;
5!
220
            if (batch->entries.empty()) continue;
5!
221

222
            AggregationBatch filtered;
5✔
223
            if (query) {
5✔
224
                filtered = batch->filter(*query);
1!
225
                if (filtered.entries.empty()) continue;
1!
226
            } else {
1✔
227
                filtered = std::move(*batch);
4✔
228
            }
229

230
            auto arrow_result = filtered.to_arrow();
5!
231
            if (!arrow_result.valid()) continue;
5!
232

233
            auto result_bytes =
5✔
234
                dftracer::utils::python::byte_size(arrow_result);
5!
235
            if (!state->push(std::move(arrow_result), result_bytes)) {
5!
UNCOV
236
                break;
×
237
            }
238
        }
8!
239

240
        util.unbind_context();
3!
241
        state->complete();
3!
242
    } catch (const std::exception &e) {
19!
UNCOV
243
        state->fail(std::current_exception());
×
UNCOV
244
    } catch (...) {
×
UNCOV
245
        state->fail(std::current_exception());
×
UNCOV
246
    }
×
247
}
32✔
248

249
#endif  // DFTRACER_UTILS_ENABLE_ARROW
250

251
// ---------------------------------------------------------------------------
252
// process() - returns ArrowTable (materialized)
253
// ---------------------------------------------------------------------------
254

255
static PyObject *Aggregator_process(AggregatorObject *self, PyObject *args,
17✔
256
                                    PyObject *kwds) {
257
    AggregatorInput input;
17✔
258
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
259
    std::optional<Query> query;
17✔
260
    if (parse_aggregator_args(args, kwds, input, nullptr, &query) < 0)
17!
261
        return NULL;
×
262
#else
263
    if (parse_aggregator_args(args, kwds, input) < 0) return NULL;
264
#endif
265

266
#ifdef DFTRACER_UTILS_ENABLE_ARROW
267
    std::vector<ArrowExportResult> results;
17✔
268
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
269
    if (!run_aggregator_pipeline(self, input, results, &query)) {
17!
270
#else
271
    if (!run_aggregator_pipeline(self, input, results)) {
272
#endif
273
        return NULL;
×
274
    }
275

276
    PyObject *batch_list = PyList_New(0);
17!
277
    if (!batch_list) return NULL;
17!
278

279
    for (auto &result : results) {
35✔
280
        PyObject *cap = wrap_arrow_result(std::move(result));
18!
281
        if (!cap) {
18!
UNCOV
282
            Py_DECREF(batch_list);
×
283
            return NULL;
×
284
        }
285
        int rc = PyList_Append(batch_list, cap);
18!
286
        Py_DECREF(cap);
18!
287
        if (rc < 0) {
18!
UNCOV
288
            Py_DECREF(batch_list);
×
289
            return NULL;
×
290
        }
291
    }
292

293
    return wrap_arrow_table(batch_list);
17!
294
#else
295
    PyErr_SetString(PyExc_RuntimeError,
296
                    "dftracer-utils was built without Arrow support");
297
    return NULL;
298
#endif
299
}
17✔
300

301
// ---------------------------------------------------------------------------
302
// iter_arrow() - returns true streaming iterator
303
// ---------------------------------------------------------------------------
304

305
static PyObject *Aggregator_iter_arrow(AggregatorObject *self, PyObject *args,
3✔
306
                                       PyObject *kwds) {
307
    AggregatorInput input;
3✔
308
    std::size_t buffer_size = 8;
3✔
309
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
310
    std::optional<Query> query;
3✔
311
    if (parse_aggregator_args(args, kwds, input, &buffer_size, &query) < 0)
3!
312
        return NULL;
×
313
#else
314
    if (parse_aggregator_args(args, kwds, input, &buffer_size) < 0) return NULL;
315
#endif
316

317
#ifdef DFTRACER_UTILS_ENABLE_ARROW
318
    auto state = std::make_shared<StreamingState<ArrowExportResult>>(
3!
319
        dftracer::utils::compute_memory_budget(0));
3!
320

321
    ArrowStreamingIteratorObject *iter_obj =
3✔
322
        (ArrowStreamingIteratorObject *)ArrowStreamingIteratorType.tp_new(
3!
323
            &ArrowStreamingIteratorType, NULL, NULL);
324
    if (!iter_obj) {
3!
325
        return NULL;
×
326
    }
327

328
    iter_obj->cpp_state->state = state;
3✔
329
    iter_obj->cpp_state->pull_next =
3!
330
        [state]() -> std::optional<ArrowExportResult> { return state->pull(); };
11✔
331
    iter_obj->cpp_state->get_error = [state]() -> std::exception_ptr {
6!
332
        return state->error();
3✔
333
    };
334
    iter_obj->cpp_state->cancel = [state]() { state->cancel(); };
6!
335

336
    Runtime *rt = resolve_runtime(self);
3!
337
    AggregatorInput input_copy = input;
3!
338
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
339
    std::optional<Query> query_copy = std::move(query);
3✔
340
    Py_BEGIN_ALLOW_THREADS rt->submit(
6!
341
        run_coro_scope(rt->executor(), run_aggregator_stream, state,
6!
342
                       std::move(input_copy), std::move(query_copy)),
3✔
343
        "aggregator_stream");
3!
344
#else
345
    Py_BEGIN_ALLOW_THREADS rt->submit(
346
        run_coro_scope(rt->executor(), run_aggregator_stream, state,
347
                       std::move(input_copy), std::nullopt),
348
        "aggregator_stream");
349
#endif
350
    Py_END_ALLOW_THREADS
3!
351

352
        return (PyObject *)iter_obj;
3✔
353
#else
354
    PyErr_SetString(PyExc_RuntimeError,
355
                    "dftracer-utils was built without Arrow support");
356
    return NULL;
357
#endif
358
}
3✔
359

360
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
361

362
struct AggregatorViewDef {
363
    std::string name;
364
    std::optional<Query> query;
365
};
366

367
struct AggregatorWriteArrowResult {
4✔
368
    std::unordered_map<std::string, PartitionWriteStats> view_stats;
369
    int64_t total_rows = 0;
4✔
370
    int64_t total_bytes = 0;
4✔
371
    std::string error;
372
};
373

374
static CoroTask<void> run_aggregator_write_arrow(
40!
375
    CoroScope &scope, AggregatorWriteArrowResult *out, AggregatorInput input,
376
    std::string output_path, std::vector<AggregatorViewDef> views,
377
    int64_t chunk_size_bytes, IpcCompression compression) {
4!
378
    try {
379
        // If no views specified, create a default "all" view
380
        if (views.empty()) {
4✔
381
            views.push_back({"all", std::nullopt});
3!
382
        }
3✔
383

384
        // Open a writer for each view
385
        std::vector<PartitionWriter> writers(views.size());
4!
386
        for (std::size_t i = 0; i < views.size(); ++i) {
21✔
387
            std::string view_path = output_path;
11!
388
            if (views.size() > 1 || views[i].name != "all") {
11✔
389
                view_path = output_path + "/" + views[i].name;
18!
390
            }
2✔
391
            int rc = co_await writers[i].open(view_path, chunk_size_bytes,
39!
392
                                              compression);
15✔
393
            if (rc != 0) {
5!
UNCOV
394
                out->error = "Failed to open writer for view: " + views[i].name;
×
UNCOV
395
                co_return;
×
396
            }
397
        }
5✔
398

399
        AggregatorUtility util;
20✔
400
        util.bind_context(scope);
4✔
401
        auto gen = util.process(input);
20!
402

403
        while (auto batch = co_await gen.next()) {
32!
404
            if (batch->entries.empty()) continue;
4!
405

406
            // Write to each view (with optional filtering)
407
            for (std::size_t i = 0; i < views.size(); ++i) {
29✔
408
                AggregationBatch filtered_batch;
15✔
409
                if (views[i].query) {
15✔
410
                    filtered_batch = batch->filter(*views[i].query);
2!
411
                    if (filtered_batch.entries.empty()) continue;
2!
412
                } else {
2✔
413
                    filtered_batch = *batch;
13!
414
                }
415

416
                auto arrow_result = filtered_batch.to_arrow();
15!
417
                if (!arrow_result.valid()) continue;
15!
418

419
                int rc = co_await writers[i].write_batch(arrow_result);
20!
420
                if (rc != 0) {
5!
UNCOV
421
                    util.unbind_context();
×
UNCOV
422
                    out->error =
×
UNCOV
423
                        "Failed to write batch for view: " + views[i].name;
×
UNCOV
424
                    co_return;
×
425
                }
426
            }
5✔
427
        }
18!
428

429
        util.unbind_context();
4!
430

431
        // Close writers and collect stats
432
        for (std::size_t i = 0; i < views.size(); ++i) {
29✔
433
            auto stats = co_await writers[i].close();
20!
434
            out->view_stats[views[i].name] = std::move(stats);
5!
435
            out->total_rows += out->view_stats[views[i].name].total_rows;
5!
436
            out->total_bytes +=
5✔
437
                out->view_stats[views[i].name].total_uncompressed_bytes;
5!
438
        }
5✔
439
    } catch (const std::exception &e) {
34!
UNCOV
440
        out->error = e.what();
×
UNCOV
441
    }
×
442
}
94✔
443

444
static PyObject *Aggregator_write_arrow(AggregatorObject *self, PyObject *args,
4✔
445
                                        PyObject *kwds) {
446
    static const char *kwlist[] = {"directory",
447
                                   "path",
448
                                   "time_interval_ms",
449
                                   "group_keys",
450
                                   "categories",
451
                                   "names",
452
                                   "index_dir",
453
                                   "checkpoint_size",
454
                                   "force_rebuild",
455
                                   "parallelism",
456
                                   "event_batch_size",
457
                                   "custom_metric_fields",
458
                                   "compute_percentiles",
459
                                   "views",
460
                                   "chunk_size_mb",
461
                                   "compression",
462
                                   NULL};
463

464
    const char *directory = NULL;
4✔
465
    const char *output_path = NULL;
4✔
466
    double time_interval_ms = 5000.0;
4✔
467
    PyObject *group_keys_obj = Py_None;
4✔
468
    PyObject *categories_obj = Py_None;
4✔
469
    PyObject *names_obj = Py_None;
4✔
470
    const char *index_dir = "";
4✔
471
    Py_ssize_t checkpoint_size = static_cast<Py_ssize_t>(
4✔
472
        dftracer::utils::constants::indexer::DEFAULT_CHECKPOINT_SIZE);
473
    int force_rebuild = 0;
4✔
474
    Py_ssize_t parallelism = 0;
4✔
475
    Py_ssize_t event_batch_size = 10000;
4✔
476
    PyObject *custom_metrics_obj = Py_None;
4✔
477
    int compute_percentiles = 0;
4✔
478
    PyObject *views_obj = Py_None;
4✔
479
    int chunk_size_mb = 32;
4✔
480
    const char *compression_str = "zstd";
4✔
481

482
    if (!PyArg_ParseTupleAndKeywords(
4!
483
            args, kwds, "ss|dOOOsnpnnOpOis", (char **)kwlist, &directory,
4✔
484
            &output_path, &time_interval_ms, &group_keys_obj, &categories_obj,
485
            &names_obj, &index_dir, &checkpoint_size, &force_rebuild,
486
            &parallelism, &event_batch_size, &custom_metrics_obj,
487
            &compute_percentiles, &views_obj, &chunk_size_mb, &compression_str))
488
        return NULL;
×
489

490
    // Parse views
491
    std::vector<AggregatorViewDef> views;
4✔
492
    if (views_obj && views_obj != Py_None) {
4!
493
        if (!PyList_Check(views_obj)) {
1!
494
            PyErr_SetString(PyExc_TypeError,
×
495
                            "views must be a list of dicts with 'name' and "
496
                            "optional 'query' keys");
497
            return NULL;
×
498
        }
499
        Py_ssize_t n = PyList_Size(views_obj);
1!
500
        for (Py_ssize_t i = 0; i < n; i++) {
3✔
501
            PyObject *item = PyList_GetItem(views_obj, i);
2!
502
            if (!PyDict_Check(item)) {
2!
503
                PyErr_SetString(PyExc_TypeError,
×
504
                                "each view must be a dict with 'name' key");
505
                return NULL;
×
506
            }
507
            AggregatorViewDef view;
2✔
508
            PyObject *name_obj = PyDict_GetItemString(item, "name");
2!
509
            if (!name_obj) {
2!
510
                PyErr_SetString(PyExc_ValueError,
×
511
                                "each view must have a 'name' key");
512
                return NULL;
×
513
            }
514
            const char *name_str = PyUnicode_AsUTF8(name_obj);
2!
515
            if (!name_str) return NULL;
2!
516
            view.name = name_str;
2!
517

518
            PyObject *query_obj = PyDict_GetItemString(item, "query");
2!
519
            if (query_obj && query_obj != Py_None) {
2!
520
                const char *query_str = PyUnicode_AsUTF8(query_obj);
2!
521
                if (!query_str) return NULL;
2!
522
                auto parsed = Query::from_string(query_str);
2!
523
                if (!parsed) {
2!
524
                    PyErr_Format(PyExc_ValueError,
×
UNCOV
525
                                 "Invalid query for view '%s': %s", name_str,
×
526
                                 parsed.error().format().c_str());
×
527
                    return NULL;
×
528
                }
529
                view.query = std::move(*parsed);
2!
530
            }
2!
531
            views.push_back(std::move(view));
2!
532
        }
2!
533
    }
1✔
534

535
    // Parse compression
536
    IpcCompression compression = IpcCompression::ZSTD;
4✔
537
    if (compression_str) {
4!
538
        std::string comp_lower(compression_str);
4!
539
        for (auto &c : comp_lower) c = std::tolower(c);
20!
540
        if (comp_lower == "none") {
4✔
541
            compression = IpcCompression::NONE;
1✔
542
        } else if (comp_lower == "zstd") {
4!
543
#ifdef DFTRACER_UTILS_ENABLE_ZSTD
544
            compression = IpcCompression::ZSTD;
3✔
545
#else
546
            PyErr_SetString(PyExc_ValueError, "ZSTD compression not available");
547
            return NULL;
548
#endif
549
        } else {
3✔
550
            PyErr_Format(PyExc_ValueError,
×
551
                         "Unknown compression: %s (use 'none' or 'zstd')",
UNCOV
552
                         compression_str);
×
553
            return NULL;
×
554
        }
555
    }
4!
556

557
    int64_t chunk_size_bytes =
4✔
558
        static_cast<int64_t>(chunk_size_mb) * 1024 * 1024;
4✔
559

560
    // Parse group_keys
561
    std::vector<std::string> group_keys;
4✔
562
    if (group_keys_obj && group_keys_obj != Py_None) {
4!
563
        if (!PyList_Check(group_keys_obj)) {
×
564
            PyErr_SetString(PyExc_TypeError,
×
565
                            "group_keys must be a list of str");
566
            return NULL;
×
567
        }
568
        Py_ssize_t n = PyList_Size(group_keys_obj);
×
569
        for (Py_ssize_t i = 0; i < n; i++) {
×
570
            const char *s = PyUnicode_AsUTF8(PyList_GetItem(group_keys_obj, i));
×
571
            if (!s) return NULL;
×
572
            group_keys.emplace_back(s);
×
UNCOV
573
        }
×
UNCOV
574
    }
×
575

576
    // Parse custom_metric_fields
577
    std::vector<std::string> custom_metrics;
4✔
578
    if (custom_metrics_obj && custom_metrics_obj != Py_None) {
4!
579
        if (!PyList_Check(custom_metrics_obj)) {
×
580
            PyErr_SetString(PyExc_TypeError,
×
581
                            "custom_metric_fields must be a list of str");
582
            return NULL;
×
583
        }
584
        Py_ssize_t n = PyList_Size(custom_metrics_obj);
×
585
        for (Py_ssize_t i = 0; i < n; i++) {
×
UNCOV
586
            const char *s =
×
587
                PyUnicode_AsUTF8(PyList_GetItem(custom_metrics_obj, i));
×
588
            if (!s) return NULL;
×
589
            custom_metrics.emplace_back(s);
×
UNCOV
590
        }
×
UNCOV
591
    }
×
592

593
    AggregatorInput input;
4!
594
    input.directory = directory;
4!
595
    input.config.time_interval_us =
4✔
596
        static_cast<std::uint64_t>(time_interval_ms * 1000.0);
4✔
597
    input.config.extra_group_keys = std::move(group_keys);
4✔
598
    input.config.custom_metric_fields = std::move(custom_metrics);
4✔
599
    input.config.compute_percentiles = compute_percentiles != 0;
4✔
600
    input.index_dir = index_dir;
4!
601
    input.checkpoint_size = static_cast<std::size_t>(checkpoint_size);
4✔
602
    input.force_rebuild = force_rebuild != 0;
4✔
603
    input.parallelism = static_cast<std::size_t>(parallelism);
4✔
604
    input.event_batch_size = static_cast<std::size_t>(event_batch_size);
4✔
605

606
    std::string output_path_str(output_path);
4!
607
    AggregatorWriteArrowResult result;
4✔
608
    auto *rp = &result;
4✔
609

610
    if (!run_blocking([&] {
8!
611
            Runtime *rt = resolve_runtime(self);
4✔
612
            rt->submit(run_coro_scope(
12!
613
                           rt->executor(), run_aggregator_write_arrow, rp,
4✔
614
                           std::move(input), output_path_str, std::move(views),
4!
615
                           chunk_size_bytes, compression),
4✔
616
                       "aggregator_write_arrow")
4!
617
                .get();
4!
618
        })) {
4✔
619
        return NULL;
×
620
    }
621

622
    if (!result.error.empty()) {
4!
623
        PyErr_SetString(PyExc_RuntimeError, result.error.c_str());
×
624
        return NULL;
×
625
    }
626

627
    // Build result dict
628
    PyObject *dict = PyDict_New();
4!
629
    if (!dict) return NULL;
4!
630

631
    PyObject *views_dict = PyDict_New();
4!
632
    if (!views_dict) {
4!
UNCOV
633
        Py_DECREF(dict);
×
634
        return NULL;
×
635
    }
636

637
    for (const auto &[view_name, view_stats] : result.view_stats) {
9!
638
        PyObject *view_dict = PyDict_New();
5!
639
        if (!view_dict) {
5!
UNCOV
640
            Py_DECREF(views_dict);
×
UNCOV
641
            Py_DECREF(dict);
×
642
            return NULL;
×
643
        }
644

645
        PyObject *files_list = PyList_New(0);
5!
646
        if (!files_list) {
5!
UNCOV
647
            Py_DECREF(view_dict);
×
UNCOV
648
            Py_DECREF(views_dict);
×
UNCOV
649
            Py_DECREF(dict);
×
650
            return NULL;
×
651
        }
652

653
        for (const auto &f : view_stats.files) {
10✔
654
            PyObject *file_str = PyUnicode_FromString(f.c_str());
5!
655
            if (!file_str || PyList_Append(files_list, file_str) < 0) {
5!
656
                Py_XDECREF(file_str);
×
UNCOV
657
                Py_DECREF(files_list);
×
UNCOV
658
                Py_DECREF(view_dict);
×
UNCOV
659
                Py_DECREF(views_dict);
×
UNCOV
660
                Py_DECREF(dict);
×
661
                return NULL;
×
662
            }
663
            Py_DECREF(file_str);
5!
664
        }
665

666
        PyDict_SetItemString(view_dict, "files", files_list);
5!
667
        dict_set_steal(view_dict, "rows",
5!
668
                       PyLong_FromLongLong(view_stats.total_rows));
5!
669
        dict_set_steal(
5!
670
            view_dict, "bytes",
5✔
671
            PyLong_FromLongLong(view_stats.total_uncompressed_bytes));
5!
672
        Py_DECREF(files_list);
5!
673

674
        PyObject *key = PyUnicode_FromString(view_name.c_str());
5!
675
        PyDict_SetItem(views_dict, key, view_dict);
5!
676
        Py_DECREF(key);
5!
677
        Py_DECREF(view_dict);
5!
678
    }
679

680
    PyDict_SetItemString(dict, "views", views_dict);
4!
681
    dict_set_steal(dict, "total_rows", PyLong_FromLongLong(result.total_rows));
4!
682
    dict_set_steal(dict, "total_bytes",
4!
683
                   PyLong_FromLongLong(result.total_bytes));
4!
684
    Py_DECREF(views_dict);
4!
685

686
    return dict;
4✔
687
}
4✔
688

689
#endif  // DFTRACER_UTILS_ENABLE_ARROW_IPC
690

691
static PyObject *Aggregator_call(PyObject *self, PyObject *args,
1✔
692
                                 PyObject *kwds) {
693
    return Aggregator_process((AggregatorObject *)self, args, kwds);
1✔
694
}
695

696
static PyMethodDef Aggregator_methods[] = {
697
    {"process", (PyCFunction)Aggregator_process, METH_VARARGS | METH_KEYWORDS,
698
     "process(directory, time_interval_ms=5000.0, group_keys=None,\n"
699
     "        categories=None, names=None, index_dir='',\n"
700
     "        checkpoint_size=33554432, force_rebuild=False,\n"
701
     "        parallelism=0, event_batch_size=10000,\n"
702
     "        custom_metric_fields=None, compute_percentiles=False)\n"
703
     "--\n"
704
     "\n"
705
     "Run aggregation pipeline, return materialized ArrowTable.\n"
706
     "\n"
707
     "Uses parallel, RocksDB-backed, fused indexing and aggregation.\n"
708
     "\n"
709
     "Args:\n"
710
     "    directory (str): Directory containing .pfw/.pfw.gz files.\n"
711
     "    time_interval_ms (float): Time bucket in milliseconds (default "
712
     "5000).\n"
713
     "    group_keys (list[str] or None): Extra grouping dims (default None).\n"
714
     "    categories (list[str] or None): Category filter (default None).\n"
715
     "    names (list[str] or None): Name filter (default None).\n"
716
     "    index_dir (str): Directory for .dftindex stores (default '').\n"
717
     "    checkpoint_size (int): Checkpoint size (default 33554432).\n"
718
     "    force_rebuild (bool): Force index rebuild (default False).\n"
719
     "    parallelism (int): Number of parallel workers (0 = all cores).\n"
720
     "    event_batch_size (int): Entries per batch (default 10000).\n"
721
     "    custom_metric_fields (list[str] or None): Extra numeric args\n"
722
     "        fields to aggregate into ``*_total``/``*_min``/``*_max``/\n"
723
     "        ``*_mean``/``*_std`` columns (default None).\n"
724
     "    compute_percentiles (bool): Enable percentile sketch collection\n"
725
     "        during aggregation (default False).\n"
726
     "\n"
727
     "Returns:\n"
728
     "    ArrowTable: Aggregated results.\n"},
729
    {"iter_arrow", (PyCFunction)Aggregator_iter_arrow,
730
     METH_VARARGS | METH_KEYWORDS,
731
     "iter_arrow(directory, time_interval_ms=5000.0, group_keys=None,\n"
732
     "           categories=None, names=None, index_dir='',\n"
733
     "           checkpoint_size=33554432, force_rebuild=False,\n"
734
     "           parallelism=0, event_batch_size=10000,\n"
735
     "           custom_metric_fields=None, compute_percentiles=False,\n"
736
     "           buffer_size=8)\n"
737
     "--\n"
738
     "\n"
739
     "Run aggregation pipeline, stream Arrow batches.\n"
740
     "\n"
741
     "Returns immediately with a streaming iterator. Batches are produced\n"
742
     "in the background with a bounded buffer. GIL is released while waiting\n"
743
     "for the next batch, allowing other Python threads to run.\n"
744
     "\n"
745
     "Uses parallel, RocksDB-backed, fused indexing and aggregation.\n"
746
     "\n"
747
     "Args:\n"
748
     "    directory (str): Directory containing .pfw/.pfw.gz files.\n"
749
     "    time_interval_ms (float): Time bucket in milliseconds (default "
750
     "5000).\n"
751
     "    group_keys (list[str] or None): Extra grouping dims (default None).\n"
752
     "    categories (list[str] or None): Category filter (default None).\n"
753
     "    names (list[str] or None): Name filter (default None).\n"
754
     "    index_dir (str): Directory for .dftindex stores (default '').\n"
755
     "    checkpoint_size (int): Checkpoint size (default 33554432).\n"
756
     "    force_rebuild (bool): Force index rebuild (default False).\n"
757
     "    parallelism (int): Number of parallel workers (0 = all cores).\n"
758
     "    event_batch_size (int): Entries per batch (default 10000).\n"
759
     "    custom_metric_fields (list[str] or None): Extra numeric args\n"
760
     "        fields to aggregate into ``*_total``/``*_min``/``*_max``/\n"
761
     "        ``*_mean``/``*_std`` columns (default None).\n"
762
     "    compute_percentiles (bool): Enable percentile sketch collection\n"
763
     "        during aggregation (default False).\n"
764
     "    buffer_size (int): Max batches to buffer (default 8).\n"
765
     "\n"
766
     "Returns:\n"
767
     "    _ArrowStreamingIterator: Streaming iterator yielding Arrow record\n"
768
     "        batches. Supports cancel() to stop early.\n"},
769
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
770
    {"write_arrow", (PyCFunction)Aggregator_write_arrow,
771
     METH_VARARGS | METH_KEYWORDS,
772
     "write_arrow(directory, path, time_interval_ms=5000.0, ..., views=None)\n"
773
     "--\n"
774
     "\n"
775
     "Run aggregation and write results to Arrow IPC files with optional "
776
     "views.\n"
777
     "\n"
778
     "Views allow filtering aggregated entries before writing. Each view\n"
779
     "writes to a separate subdirectory. Query syntax supports: cat, name,\n"
780
     "pid, tid, hhash, fhash, time_bucket, extra group keys, and aggregation\n"
781
     "metrics (count, dur_total, dur_min, dur_max, size_total, etc.).\n"
782
     "\n"
783
     "Args:\n"
784
     "    directory (str): Directory containing .pfw/.pfw.gz files.\n"
785
     "    path (str): Output directory for Arrow files.\n"
786
     "    time_interval_ms (float): Time bucket in milliseconds.\n"
787
     "    group_keys (list[str] or None): Extra grouping dims.\n"
788
     "    categories (list[str] or None): Category filter.\n"
789
     "    names (list[str] or None): Name filter.\n"
790
     "    index_dir (str): Directory for .dftindex stores.\n"
791
     "    checkpoint_size (int): Checkpoint size.\n"
792
     "    force_rebuild (bool): Force index rebuild.\n"
793
     "    parallelism (int): Number of parallel workers.\n"
794
     "    event_batch_size (int): Entries per batch.\n"
795
     "    custom_metric_fields (list[str] or None): Extra numeric fields.\n"
796
     "    compute_percentiles (bool): Enable percentile collection.\n"
797
     "    views (list[dict] or None): View definitions, each with 'name' and\n"
798
     "        optional 'query' keys. If None, writes all entries to path.\n"
799
     "        Example: [{'name': 'io', 'query': 'cat == \"POSIX\"'}]\n"
800
     "    chunk_size_mb (int): Max uncompressed MB per file (default 32).\n"
801
     "    compression (str): 'zstd' or 'none' (default 'zstd').\n"
802
     "\n"
803
     "Returns:\n"
804
     "    dict: Statistics with 'views' (per-view stats), 'total_rows',\n"
805
     "        'total_bytes'. Each view has 'files', 'rows', 'bytes'.\n"},
806
#endif
807
    {NULL}};
808

809
PyTypeObject AggregatorType = {
810
    PyVarObject_HEAD_INIT(
811
        NULL, 0) "dftracer_utils_ext.AggregatorUtility", /* tp_name */
812
    sizeof(AggregatorObject),                            /* tp_basicsize */
813
    0,                                                   /* tp_itemsize */
814
    (destructor)Aggregator_dealloc,                      /* tp_dealloc */
815
    0,                                        /* tp_vectorcall_offset */
816
    0,                                        /* tp_getattr */
817
    0,                                        /* tp_setattr */
818
    0,                                        /* tp_as_async */
819
    0,                                        /* tp_repr */
820
    0,                                        /* tp_as_number */
821
    0,                                        /* tp_as_sequence */
822
    0,                                        /* tp_as_mapping */
823
    0,                                        /* tp_hash */
824
    Aggregator_call,                          /* tp_call */
825
    0,                                        /* tp_str */
826
    0,                                        /* tp_getattro */
827
    0,                                        /* tp_setattro */
828
    0,                                        /* tp_as_buffer */
829
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
830
    "AggregatorUtility(runtime: Runtime | None = None)\n"
831
    "--\n\n"
832
    "High-level aggregation pipeline for DFTracer trace files.\n\n"
833
    "Args:\n"
834
    "    runtime (Runtime or None): Runtime for thread pool control.\n"
835
    "        If None, uses the default global Runtime.\n\n"
836
    "process(directory, time_interval_ms=5000.0, ...) -> ArrowTable\n"
837
    "    Run aggregation and return a materialized Arrow table.\n\n"
838
    "iter_arrow(directory, time_interval_ms=5000.0, ...) -> "
839
    "Iterator[ArrowBatch]\n"
840
    "    Run aggregation and stream Arrow batches.\n", /* tp_doc */
841
    0,                                                 /* tp_traverse */
842
    0,                                                 /* tp_clear */
843
    0,                                                 /* tp_richcompare */
844
    0,                                                 /* tp_weaklistoffset */
845
    0,                                                 /* tp_iter */
846
    0,                                                 /* tp_iternext */
847
    Aggregator_methods,                                /* tp_methods */
848
    0,                                                 /* tp_members */
849
    0,                                                 /* tp_getset */
850
    0,                                                 /* tp_base */
851
    0,                                                 /* tp_dict */
852
    0,                                                 /* tp_descr_get */
853
    0,                                                 /* tp_descr_set */
854
    0,                                                 /* tp_dictoffset */
855
    (initproc)Aggregator_init,                         /* tp_init */
856
    0,                                                 /* tp_alloc */
857
    Aggregator_new,                                    /* tp_new */
858
};
859

860
int init_aggregator(PyObject *m) {
1✔
861
    if (register_type(m, &AggregatorType, "AggregatorUtility") < 0) return -1;
1!
862

863
    return 0;
1✔
864
}
1✔
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