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

llnl / dftracer-utils / 30058987329

24 Jul 2026 01:26AM UTC coverage: 52.76% (+0.1%) from 52.66%
30058987329

Pull #99

github

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

41934 of 102825 branches covered (40.78%)

Branch coverage included in aggregate %.

2278 of 2903 new or added lines in 61 files covered. (78.47%)

144 existing lines in 14 files now uncovered.

37072 of 46920 relevant lines covered (79.01%)

59158.32 hits per line

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

39.82
/src/dftracer/utils/python/batch_indexer.cpp
1
#include <dftracer/utils/core/common/constants.h>
2
#include <dftracer/utils/core/common/filesystem.h>
3
#include <dftracer/utils/core/common/hash_combine.h>
4
#include <dftracer/utils/core/common/string_intern.h>
5
#include <dftracer/utils/core/coro/task.h>
6
#include <dftracer/utils/core/coro/when_all.h>
7
#include <dftracer/utils/core/rocksdb/db_manager.h>
8
#include <dftracer/utils/core/runtime.h>
9
#include <dftracer/utils/core/tasks/coro_scope.h>
10
#include <dftracer/utils/python/batch_indexer.h>
11
#include <dftracer/utils/python/indexer.h>
12
#include <dftracer/utils/python/py_dict_helpers.h>
13
#include <dftracer/utils/python/py_list_helpers.h>
14
#include <dftracer/utils/python/py_runtime_mixin.h>
15
#include <dftracer/utils/python/py_type_helpers.h>
16
#include <dftracer/utils/python/runtime.h>
17
#include <dftracer/utils/utilities/common/query/query.h>
18
#include <dftracer/utils/utilities/composites/dft/aggregators/aggregation_config.h>
19
#include <dftracer/utils/utilities/composites/dft/aggregators/aggregation_serialization.h>
20
#include <dftracer/utils/utilities/composites/dft/aggregators/aggregator_types.h>
21
#include <dftracer/utils/utilities/composites/dft/aggregators/event_aggregator.h>
22
#include <dftracer/utils/utilities/composites/dft/aggregators/system_metrics.h>
23
#include <dftracer/utils/utilities/composites/dft/aggregators/system_metrics_serialization.h>
24
#include <dftracer/utils/utilities/composites/dft/indexing/index_resolver_utility.h>
25
#include <dftracer/utils/utilities/composites/dft/indexing/resolve_and_build.h>
26
#include <dftracer/utils/utilities/composites/dft/internal/utils.h>
27
#include <dftracer/utils/utilities/indexer/index_database.h>
28

29
#ifdef DFTRACER_UTILS_ENABLE_ARROW
30
#include <dftracer/utils/utilities/common/arrow/column_builder.h>
31
#endif
32

33
#include <algorithm>
34
#include <chrono>
35
#include <cstdio>
36
#include <optional>
37
#include <string>
38
#include <unordered_map>
39
#include <unordered_set>
40
#include <vector>
41

42
using dftracer::utils::CoroScope;
43
using dftracer::utils::Runtime;
44
using dftracer::utils::coro::CoroTask;
45
using namespace dftracer::utils::utilities::composites::dft::indexing;
46
using namespace dftracer::utils::utilities::composites::dft::aggregators;
47

48
// ---------------------------------------------------------------------------
49
// BatchIndexer - directory-level indexer with resolve/build pattern
50
// ---------------------------------------------------------------------------
51

52
static void Indexer_dealloc(IndexerObject* self) {
112✔
53
    Py_XDECREF(self->runtime_obj);
112✔
54
    Py_XDECREF(self->directory);
112✔
55
    Py_XDECREF(self->files);
112✔
56
    Py_XDECREF(self->index_dir);
112✔
57
    Py_XDECREF(self->group_keys);
112✔
58
    Py_XDECREF(self->custom_metric_fields);
112✔
59
    Py_TYPE(self)->tp_free((PyObject*)self);
112✔
60
}
112✔
61

62
static PyObject* Indexer_new(PyTypeObject* type, PyObject* args,
112✔
63
                             PyObject* kwds) {
64
    IndexerObject* self = (IndexerObject*)type->tp_alloc(type, 0);
112✔
65
    if (self) {
112✔
66
        self->runtime_obj = nullptr;
112✔
67
        self->directory = nullptr;
112✔
68
        self->files = nullptr;
112✔
69
        self->index_dir = nullptr;
112✔
70
        self->require_checkpoint = 1;
112✔
71
        self->require_bloom = 1;
112✔
72
        self->build_bloom = 1;
112✔
73
        self->require_manifest = 1;
112✔
74
        self->require_aggregation = 0;
112✔
75
        self->time_interval_ms = 5000.0;
112✔
76
        self->group_keys = nullptr;
112✔
77
        self->custom_metric_fields = nullptr;
112✔
78
        self->compute_percentiles = 0;
112✔
79
        self->group_by_file = 1;
112✔
80
        self->checkpoint_size =
112✔
81
            dftracer::utils::constants::indexer::DEFAULT_CHECKPOINT_SIZE;
82
        self->parallelism = 0;
112✔
83
        self->force_rebuild = 0;
112✔
84
    }
56✔
85
    return (PyObject*)self;
112✔
86
}
87

88
static int Indexer_init(IndexerObject* self, PyObject* args, PyObject* kwds) {
112✔
89
    static const char* kwlist[] = {"directory",
90
                                   "files",
91
                                   "index_dir",
92
                                   "require_checkpoint",
93
                                   "require_bloom",
94
                                   "build_bloom",
95
                                   "require_manifest",
96
                                   "require_aggregation",
97
                                   "time_interval_ms",
98
                                   "group_keys",
99
                                   "custom_metric_fields",
100
                                   "compute_percentiles",
101
                                   "group_by_file",
102
                                   "checkpoint_size",
103
                                   "parallelism",
104
                                   "force_rebuild",
105
                                   "runtime",
106
                                   nullptr};
107

108
    const char* directory = "";
112✔
109
    PyObject* files_obj = Py_None;
112✔
110
    const char* index_dir = "";
112✔
111
    int require_checkpoint = 1;
112✔
112
    int require_bloom = 1;
112✔
113
    int build_bloom = 1;
112✔
114
    int require_manifest = 1;
112✔
115
    int require_aggregation = 0;
112✔
116
    double time_interval_ms = 5000.0;
112✔
117
    PyObject* group_keys_obj = Py_None;
112✔
118
    PyObject* custom_metrics_obj = Py_None;
112✔
119
    int compute_percentiles = 0;
112✔
120
    int group_by_file = 1;
112✔
121
    Py_ssize_t checkpoint_size = static_cast<Py_ssize_t>(
112✔
122
        dftracer::utils::constants::indexer::DEFAULT_CHECKPOINT_SIZE);
123
    Py_ssize_t parallelism = 0;
112✔
124
    int force_rebuild = 0;
112✔
125
    PyObject* runtime_arg = nullptr;
112✔
126

127
    if (!PyArg_ParseTupleAndKeywords(
112!
128
            args, kwds, "|sOspppppdOOppnnpO", (char**)kwlist, &directory,
56✔
129
            &files_obj, &index_dir, &require_checkpoint, &require_bloom,
130
            &build_bloom, &require_manifest, &require_aggregation,
131
            &time_interval_ms, &group_keys_obj, &custom_metrics_obj,
132
            &compute_percentiles, &group_by_file, &checkpoint_size,
133
            &parallelism, &force_rebuild, &runtime_arg)) {
UNCOV
134
        return -1;
×
135
    }
136

137
    // Validate: at least one of directory or files must be provided
138
    bool has_directory = directory && directory[0] != '\0';
112✔
139
    bool has_files = files_obj && files_obj != Py_None &&
170✔
140
                     PyList_Check(files_obj) && PyList_Size(files_obj) > 0;
170!
141

142
    if (!has_directory && !has_files) {
112✔
143
        PyErr_SetString(PyExc_ValueError,
2!
144
                        "At least one of 'directory' or 'files' must be "
145
                        "provided");
146
        return -1;
2✔
147
    }
148

149
    // Store runtime
150
    if (runtime_arg && runtime_arg != Py_None) {
110!
151
        if (PyObject_TypeCheck(runtime_arg, &RuntimeType)) {
×
152
            Py_INCREF(runtime_arg);
×
153
            self->runtime_obj = runtime_arg;
×
154
        } else {
155
            PyObject* native = PyObject_GetAttrString(runtime_arg, "_native");
×
156
            if (native && PyObject_TypeCheck(native, &RuntimeType)) {
×
157
                self->runtime_obj = native;
×
158
            } else {
159
                Py_XDECREF(native);
×
160
                PyErr_SetString(PyExc_TypeError,
×
161
                                "runtime must be a Runtime instance or None");
162
                return -1;
×
163
            }
164
        }
165
    }
166

167
    self->directory = PyUnicode_FromString(directory);
110!
168
    self->index_dir = PyUnicode_FromString(index_dir);
110!
169
    self->require_checkpoint = require_checkpoint;
110✔
170
    self->require_bloom = require_bloom;
110✔
171
    self->build_bloom = build_bloom;
110✔
172
    self->require_manifest = require_manifest;
110✔
173
    self->require_aggregation = require_aggregation;
110✔
174
    self->time_interval_ms = time_interval_ms;
110✔
175
    self->compute_percentiles = compute_percentiles;
110✔
176
    self->group_by_file = group_by_file;
110✔
177
    self->checkpoint_size = static_cast<std::size_t>(checkpoint_size);
110✔
178
    self->parallelism = static_cast<std::size_t>(parallelism);
110✔
179
    self->force_rebuild = force_rebuild;
110✔
180

181
    // Store files list
182
    if (has_files) {
110✔
183
        Py_INCREF(files_obj);
58!
184
        self->files = files_obj;
58✔
185
    } else {
29✔
186
        self->files = nullptr;
52✔
187
    }
188

189
    // Store group_keys
190
    if (group_keys_obj && group_keys_obj != Py_None) {
110!
191
        Py_INCREF(group_keys_obj);
×
192
        self->group_keys = group_keys_obj;
×
193
    } else {
194
        self->group_keys = nullptr;
110✔
195
    }
196

197
    // Store custom_metric_fields
198
    if (custom_metrics_obj && custom_metrics_obj != Py_None) {
110!
199
        Py_INCREF(custom_metrics_obj);
×
200
        self->custom_metric_fields = custom_metrics_obj;
×
201
    } else {
202
        self->custom_metric_fields = nullptr;
110✔
203
    }
204

205
    return 0;
110✔
206
}
56✔
207

208
static Runtime* get_batch_indexer_runtime(IndexerObject* self) {
326✔
209
    if (self->runtime_obj) {
326!
210
        return ((RuntimeObject*)self->runtime_obj)->runtime.get();
×
211
    }
212
    return get_default_runtime();
326✔
213
}
163✔
214

215
static std::optional<AggregationConfig> build_aggregation_config(
302✔
216
    IndexerObject* self) {
217
    if (!self->require_aggregation) {
302✔
218
        return std::nullopt;
182✔
219
    }
220

221
    AggregationConfig config;
120!
222
    config.time_interval_us =
120✔
223
        static_cast<std::uint64_t>(self->time_interval_ms * 1000.0);
120✔
224

225
    if (self->group_keys && PyList_Check(self->group_keys)) {
120!
226
        Py_ssize_t n = PyList_Size(self->group_keys);
×
227
        for (Py_ssize_t i = 0; i < n; i++) {
×
228
            const char* s =
229
                PyUnicode_AsUTF8(PyList_GetItem(self->group_keys, i));
×
230
            if (s) config.extra_group_keys.emplace_back(s);
×
231
        }
232
    }
233
    if (self->custom_metric_fields &&
120!
234
        PyList_Check(self->custom_metric_fields)) {
×
235
        Py_ssize_t n = PyList_Size(self->custom_metric_fields);
×
236
        for (Py_ssize_t i = 0; i < n; i++) {
×
237
            const char* s =
238
                PyUnicode_AsUTF8(PyList_GetItem(self->custom_metric_fields, i));
×
239
            if (s) config.custom_metric_fields.emplace_back(s);
×
240
        }
241
    }
242

243
    config.compute_percentiles = self->compute_percentiles != 0;
120✔
244
    config.group_by_file = self->group_by_file != 0;
120✔
245
    return config;
120!
246
}
211✔
247

248
// ---------------------------------------------------------------------------
249
// resolve() - check what exists vs needs building
250
// ---------------------------------------------------------------------------
251

252
static PyObject* Indexer_resolve(IndexerObject* self,
226✔
253
                                 PyObject* Py_UNUSED(ignored)) {
254
    const char* directory = PyUnicode_AsUTF8(self->directory);
226!
255
    const char* index_dir = PyUnicode_AsUTF8(self->index_dir);
226!
256

257
    ResolverInput input;
226✔
258
    input.directory = directory ? directory : "";
226!
259
    input.index_dir = index_dir ? index_dir : "";
226!
260
    input.require_checkpoints = self->require_checkpoint;
226✔
261
    input.require_bloom = self->require_bloom;
226✔
262
    input.require_manifest = self->require_manifest;
226✔
263
    input.require_aggregation = self->require_aggregation;
226✔
264
    input.aggregation_config = build_aggregation_config(self);
226!
265

266
    // Add files if provided
267
    if (self->files && PyList_Check(self->files)) {
226!
268
        Py_ssize_t n = PyList_Size(self->files);
132!
269
        for (Py_ssize_t i = 0; i < n; i++) {
286✔
270
            const char* s = PyUnicode_AsUTF8(PyList_GetItem(self->files, i));
154!
271
            if (s) input.files.emplace_back(s);
154!
272
        }
77✔
273
    }
66✔
274

275
    ResolverResult result;
226✔
276

277
    if (!run_blocking([&] {
339!
278
            Runtime* rt = get_batch_indexer_runtime(self);
226✔
279
            rt->submit(run_coro_scope(
791!
280
                           rt->executor(),
113✔
281
                           [](CoroScope& scope, ResolverInput in,
904!
282
                              ResolverResult* out) -> CoroTask<void> {
113!
283
                               IndexResolverUtility resolver;
339!
284
                               // scope.spawn(utility, input) auto-binds context
285
                               // for utilities with the NeedsContext tag
286
                               *out = co_await scope.spawn(resolver,
904!
287
                                                           std::move(in));
339✔
288
                           },
565!
289
                           std::move(input), &result),
226✔
290
                       "batch-indexer-resolve")
113!
291
                .get();
226!
292
        })) {
226✔
293
        return nullptr;
×
294
    }
295

296
    // Build result dict
297
    PyObject* dict = PyDict_New();
226!
298
    if (!dict) return nullptr;
226✔
299

300
    dict_set_steal(dict, "total_files",
226!
301
                   PyLong_FromSize_t(result.all_files.size()));
113!
302
    dict_set_steal(dict, "index_path",
226!
303
                   PyUnicode_FromString(result.index_path.c_str()));
113!
304
    dict_set_steal(dict, "aggregation_interval_us",
226!
305
                   PyLong_FromUnsignedLongLong(result.stored_time_interval_us));
226!
306
    dict_set_steal(dict, "needs_rebuild",
226!
307
                   PyBool_FromLong(result.needs_augmentation));
226!
308

309
    // Ready files
310
    PyObject* ready_list = PyList_New(result.cached.size());
226!
311
    for (std::size_t i = 0; i < result.cached.size(); ++i) {
416✔
312
        PyList_SetItem(
190!
313
            ready_list, i,
95✔
314
            PyUnicode_FromString(result.cached[i].file_path.c_str()));
190!
315
    }
95✔
316
    PyDict_SetItemString(dict, "ready", ready_list);
226!
317

318
    // Needs work files (union of all needs_* lists)
319
    std::vector<std::string> needs_work;
226✔
320
    for (const auto& item : result.needs_checkpoint) {
322✔
321
        needs_work.push_back(item.file_path);
96!
322
    }
323
    for (const auto& item : result.needs_bloom) {
226!
324
        bool found = false;
×
325
        for (const auto& existing : needs_work) {
×
326
            if (existing == item.file_path) {
×
327
                found = true;
×
328
                break;
×
329
            }
330
        }
331
        if (!found) needs_work.push_back(item.file_path);
×
332
    }
333
    for (const auto& item : result.needs_manifest) {
226!
334
        bool found = false;
×
335
        for (const auto& existing : needs_work) {
×
336
            if (existing == item.file_path) {
×
337
                found = true;
×
338
                break;
×
339
            }
340
        }
341
        if (!found) needs_work.push_back(item.file_path);
×
342
    }
343
    for (const auto& item : result.needs_aggregation) {
226✔
344
        bool found = false;
×
345
        for (const auto& existing : needs_work) {
×
346
            if (existing == item.file_path) {
×
347
                found = true;
×
348
                break;
×
349
            }
350
        }
351
        if (!found) needs_work.push_back(item.file_path);
×
352
    }
353

354
    PyObject* needs_list = PyList_New(needs_work.size());
226!
355
    for (std::size_t i = 0; i < needs_work.size(); ++i) {
322✔
356
        PyList_SetItem(needs_list, i,
96!
357
                       PyUnicode_FromString(needs_work[i].c_str()));
96!
358
    }
48✔
359
    PyDict_SetItemString(dict, "needs_work", needs_list);
226!
360

361
    return dict;
226✔
362
}
226✔
363

364
// ---------------------------------------------------------------------------
365
// build() - build missing index tiers
366
// ---------------------------------------------------------------------------
367

368
static PyObject* Indexer_build(IndexerObject* self,
76✔
369
                               PyObject* Py_UNUSED(ignored)) {
370
    const char* directory = PyUnicode_AsUTF8(self->directory);
76!
371
    const char* index_dir = PyUnicode_AsUTF8(self->index_dir);
76!
372

373
    ResolveAndBuildInput input;
76✔
374
    input.directory = directory ? directory : "";
76!
375
    input.index_dir = index_dir ? index_dir : "";
76!
376
    input.require_checkpoints = self->require_checkpoint;
76✔
377
    input.require_bloom = self->require_bloom;
76✔
378
    input.build_bloom = self->build_bloom;
76✔
379
    input.require_manifest = self->require_manifest;
76✔
380
    input.require_aggregation = self->require_aggregation;
76✔
381
    input.aggregation_config = build_aggregation_config(self);
76!
382
    input.checkpoint_size = self->checkpoint_size;
76✔
383
    input.parallelism = self->parallelism;
76✔
384
    input.force_rebuild = self->force_rebuild;
76✔
385

386
    // Add files if provided
387
    if (self->files && PyList_Check(self->files)) {
76!
388
        Py_ssize_t n = PyList_Size(self->files);
54!
389
        for (Py_ssize_t i = 0; i < n; i++) {
118✔
390
            const char* s = PyUnicode_AsUTF8(PyList_GetItem(self->files, i));
64!
391
            if (s) input.files.emplace_back(s);
64!
392
        }
32✔
393
    }
27✔
394

395
    if (!run_blocking([&] {
114!
396
            Runtime* rt = get_batch_indexer_runtime(self);
76✔
397
            rt->submit(run_coro_scope(
266!
398
                           rt->executor(),
38✔
399
                           [](CoroScope& scope,
304!
400
                              ResolveAndBuildInput in) -> CoroTask<void> {
38!
401
                               co_await resolve_and_build_index(&scope,
304!
402
                                                                std::move(in));
114✔
403
                           },
152!
404
                           std::move(input)),
76✔
405
                       "batch-indexer-build")
38!
406
                .get();
76!
407
        })) {
76✔
408
        return nullptr;
×
409
    }
410

411
    Py_RETURN_NONE;
76✔
412
}
76✔
413

414
// ---------------------------------------------------------------------------
415
// ensure_indexed() - resolve + build if needed
416
// ---------------------------------------------------------------------------
417

418
static PyObject* Indexer_ensure_indexed(IndexerObject* self,
94✔
419
                                        PyObject* Py_UNUSED(ignored)) {
420
    // First resolve
421
    PyObject* status = Indexer_resolve(self, nullptr);
94✔
422
    if (!status) return nullptr;
94✔
423

424
    // Build if files need work, or the aggregation tier must be rebuilt
425
    // (stored time interval differs from the requested one).
426
    PyObject* needs_work = PyDict_GetItemString(status, "needs_work");
94✔
427
    PyObject* needs_rebuild = PyDict_GetItemString(status, "needs_rebuild");
94✔
428
    bool work_pending = needs_work && PyList_Size(needs_work) > 0;
94✔
429
    bool rebuild_pending = needs_rebuild && PyObject_IsTrue(needs_rebuild);
94✔
430
    if (work_pending || rebuild_pending) {
94✔
431
        Py_DECREF(status);
37✔
432

433
        // Build
434
        PyObject* result = Indexer_build(self, nullptr);
74✔
435
        if (!result) return nullptr;
74✔
436
        Py_DECREF(result);
37✔
437

438
        // Re-resolve
439
        status = Indexer_resolve(self, nullptr);
74✔
440
    }
37✔
441

442
    return status;
94✔
443
}
47✔
444

445
// ---------------------------------------------------------------------------
446
// get_checkpoint_indexer() - get a single-file checkpoint indexer
447
// ---------------------------------------------------------------------------
448

449
static PyObject* Indexer_get_checkpoint_indexer(IndexerObject* self,
12✔
450
                                                PyObject* args) {
451
    const char* file_path = nullptr;
12✔
452
    if (!PyArg_ParseTuple(args, "s", &file_path)) {
12!
453
        return nullptr;
×
454
    }
455

456
    // Determine index path using BatchIndexer's index_dir setting
457
    const char* index_dir = PyUnicode_AsUTF8(self->index_dir);
12!
458
    std::string index_path = dftracer::utils::utilities::composites::dft::
6!
459
        internal::determine_index_path(file_path, index_dir ? index_dir : "");
18!
460

461
    // Create IndexerObject
462
    CheckpointIndexerObject* indexer =
6✔
463
        (CheckpointIndexerObject*)CheckpointIndexerType.tp_alloc(
12!
464
            &CheckpointIndexerType, 0);
465
    if (!indexer) {
12✔
466
        return nullptr;
×
467
    }
468

469
    indexer->handle = nullptr;
12✔
470
    indexer->gz_path = PyUnicode_FromString(file_path);
12!
471
    indexer->index_path = PyUnicode_FromString(index_path.c_str());
12!
472
    indexer->checkpoint_size = self->checkpoint_size;
12✔
473
    indexer->build_bloom = 0;
12✔
474
    indexer->build_manifest = 0;
12✔
475

476
    // Share runtime reference
477
    if (self->runtime_obj) {
12!
478
        Py_INCREF(self->runtime_obj);
×
479
        indexer->runtime_obj = self->runtime_obj;
×
480
    } else {
481
        indexer->runtime_obj = nullptr;
12✔
482
    }
483

484
    // Create the native handle
485
    indexer->handle = dft_indexer_create(file_path, index_path.c_str(),
18!
486
                                         self->checkpoint_size, 0);
6✔
487
    if (!indexer->handle) {
12!
488
        Py_DECREF((PyObject*)indexer);
×
489
        PyErr_SetString(PyExc_RuntimeError,
×
490
                        "Failed to create checkpoint indexer");
491
        return nullptr;
×
492
    }
493

494
    return (PyObject*)indexer;
12✔
495
}
12✔
496

497
static std::optional<std::string> resolve_index_path(IndexerObject* self) {
44✔
498
    PyObject* status = Indexer_resolve(self, nullptr);
44!
499
    if (!status) return std::nullopt;
44✔
500
    PyObject* obj = PyDict_GetItemString(status, "index_path");
44!
501
    const char* path = obj ? PyUnicode_AsUTF8(obj) : nullptr;
44!
502
    if (!path || path[0] == '\0') {
44!
503
        Py_DECREF(status);
504
        PyErr_SetString(PyExc_RuntimeError, "No index path available");
×
505
        return std::nullopt;
×
506
    }
507
    std::string result(path);
66!
508
    Py_DECREF(status);
22!
509
    return result;
44!
510
}
44✔
511

512
static PyObject* Indexer_get_hash_table(IndexerObject* self, PyObject* args) {
12✔
513
    const char* type_str = nullptr;
12✔
514
    if (!PyArg_ParseTuple(args, "s", &type_str)) {
12!
515
        return nullptr;
×
516
    }
517

518
    using dftracer::utils::utilities::indexer::IndexDatabase;
519
    using HashType = IndexDatabase::HashType;
520

521
    HashType type;
522
    if (std::strcmp(type_str, "file") == 0) {
12✔
523
        type = HashType::FILE;
4✔
524
    } else if (std::strcmp(type_str, "host") == 0) {
10✔
525
        type = HashType::HOST;
4✔
526
    } else if (std::strcmp(type_str, "string") == 0) {
6✔
527
        type = HashType::STRING;
2✔
528
    } else if (std::strcmp(type_str, "proc") == 0) {
3✔
529
        type = HashType::PROC;
×
530
    } else {
531
        PyErr_SetString(PyExc_ValueError,
2!
532
                        "type must be 'file', 'host', 'string', or 'proc'");
533
        return nullptr;
2✔
534
    }
535

536
    auto idx_opt = resolve_index_path(self);
10!
537
    if (!idx_opt) return nullptr;
10✔
538
    std::string index_path = std::move(*idx_opt);
10✔
539

540
    std::unordered_map<std::string, std::string> hash_map;
10✔
541
    if (!run_blocking_r(
10!
542
            [&] {
15✔
543
                IndexDatabase db(index_path,
10✔
544
                                 dftracer::utils::rocksdb::RocksDatabase::
545
                                     OpenMode::ReadOnly);
5!
546
                return db.query_hash_table(type);
15!
547
            },
10✔
548
            hash_map)) {
549
        return nullptr;
×
550
    }
551

552
    PyObject* dict = PyDict_New();
10!
553
    if (!dict) return nullptr;
10✔
554

555
    for (const auto& [hash, name] : hash_map) {
10!
556
        PyObject* key = PyUnicode_FromStringAndSize(hash.data(), hash.size());
×
557
        PyObject* val = PyUnicode_FromStringAndSize(name.data(), name.size());
×
558
        PyDict_SetItem(dict, key, val);
×
559
        Py_DECREF(key);
×
560
        Py_DECREF(val);
×
561
    }
562

563
    return dict;
10✔
564
}
11✔
565

566
static PyObject* Indexer_query_file_pids(IndexerObject* self, PyObject* args) {
4✔
567
    int file_id;
568
    if (!PyArg_ParseTuple(args, "i", &file_id)) {
4!
569
        return nullptr;
×
570
    }
571

572
    using dftracer::utils::utilities::indexer::IndexDatabase;
573

574
    auto idx_opt = resolve_index_path(self);
4!
575
    if (!idx_opt) return nullptr;
4✔
576
    std::string index_path = std::move(*idx_opt);
4✔
577

578
    std::unordered_set<std::uint64_t> pids;
4✔
579
    if (!run_blocking_r(
4!
580
            [&] {
6✔
581
                IndexDatabase db(index_path,
4✔
582
                                 dftracer::utils::rocksdb::RocksDatabase::
583
                                     OpenMode::ReadOnly);
2!
584
                return db.query_file_pids(file_id);
6!
585
            },
4✔
586
            pids)) {
587
        return nullptr;
×
588
    }
589

590
    PyObject* set = PySet_New(nullptr);
4!
591
    if (!set) return nullptr;
4✔
592

593
    for (auto pid : pids) {
6!
594
        PyObject* val = PyLong_FromUnsignedLongLong(pid);
2!
595
        PySet_Add(set, val);
2!
596
        Py_DECREF(val);
1!
597
    }
598

599
    return set;
4✔
600
}
4✔
601

602
static PyObject* Indexer_query_all_file_pids(IndexerObject* self,
6✔
603
                                             PyObject* Py_UNUSED(ignored)) {
604
    using dftracer::utils::utilities::indexer::IndexDatabase;
605

606
    auto idx_opt = resolve_index_path(self);
6!
607
    if (!idx_opt) return nullptr;
6!
608
    std::string index_path = std::move(*idx_opt);
6✔
609

610
    std::unordered_map<int, std::unordered_set<std::uint64_t>> all_pids;
6✔
611
    if (!run_blocking_r(
6!
612
            [&] {
9✔
613
                IndexDatabase db(index_path,
6✔
614
                                 dftracer::utils::rocksdb::RocksDatabase::
615
                                     OpenMode::ReadOnly);
3!
616
                return db.query_all_file_pids();
9!
617
            },
6✔
618
            all_pids)) {
619
        return nullptr;
×
620
    }
621

622
    PyObject* dict = PyDict_New();
6!
623
    if (!dict) return nullptr;
6✔
624

625
    for (const auto& [file_id, pids] : all_pids) {
12!
626
        PyObject* key = PyLong_FromLong(file_id);
6!
627
        PyObject* set = PySet_New(nullptr);
6!
628
        for (auto pid : pids) {
12✔
629
            PyObject* val = PyLong_FromUnsignedLongLong(pid);
6!
630
            PySet_Add(set, val);
6!
631
            Py_DECREF(val);
3!
632
        }
633
        PyDict_SetItem(dict, key, set);
6!
634
        Py_DECREF(key);
3!
635
        Py_DECREF(set);
3!
636
    }
637

638
    return dict;
6✔
639
}
6✔
640

641
static PyObject* Indexer_query_file_info(IndexerObject* self,
×
642
                                         PyObject* Py_UNUSED(ignored)) {
643
    using dftracer::utils::utilities::indexer::IndexDatabase;
644

645
    auto idx_opt = resolve_index_path(self);
×
646
    if (!idx_opt) return nullptr;
×
647
    std::string index_path = std::move(*idx_opt);
×
648

649
    std::unordered_map<std::string, int> file_ids;
×
650
    std::unordered_map<int, std::unordered_set<std::uint64_t>> all_pids;
×
651

652
    if (!run_blocking([&] {
×
653
            IndexDatabase db(
654
                index_path,
×
655
                dftracer::utils::rocksdb::RocksDatabase::OpenMode::ReadOnly);
×
656
            file_ids = db.query_all_file_info_ids();
×
657
            all_pids = db.query_all_file_pids();
×
658
        })) {
×
659
        return nullptr;
×
660
    }
661

662
    auto data_dir = fs::weakly_canonical(fs::path(index_path)).parent_path();
×
663

664
    PyObject* id_to_path = PyDict_New();
×
665
    if (!id_to_path) return nullptr;
×
666
    for (const auto& [logical_name, fid] : file_ids) {
×
667
        auto resolved = (data_dir / logical_name).string();
×
668
        PyObject* key = PyLong_FromLong(fid);
×
669
        PyObject* val = PyUnicode_FromStringAndSize(
×
670
            resolved.data(), static_cast<Py_ssize_t>(resolved.size()));
×
671
        PyDict_SetItem(id_to_path, key, val);
×
672
        Py_DECREF(key);
×
673
        Py_DECREF(val);
×
674
    }
×
675

676
    PyObject* pid_dict = PyDict_New();
×
677
    if (!pid_dict) {
×
678
        Py_DECREF(id_to_path);
×
679
        return nullptr;
×
680
    }
681
    for (const auto& [file_id, pids] : all_pids) {
×
682
        PyObject* key = PyLong_FromLong(file_id);
×
683
        PyObject* set = PySet_New(nullptr);
×
684
        for (auto pid : pids) {
×
685
            PyObject* val = PyLong_FromUnsignedLongLong(pid);
×
686
            PySet_Add(set, val);
×
687
            Py_DECREF(val);
×
688
        }
689
        PyDict_SetItem(pid_dict, key, set);
×
690
        Py_DECREF(key);
×
691
        Py_DECREF(set);
×
692
    }
693

694
    PyObject* result = PyTuple_Pack(2, id_to_path, pid_dict);
×
695
    Py_DECREF(id_to_path);
×
696
    Py_DECREF(pid_dict);
×
697
    return result;
×
698
}
×
699

700
#ifdef DFTRACER_UTILS_ENABLE_ARROW
701
#include <dftracer/utils/python/trace_reader_iterator.h>
702
#include <dftracer/utils/utilities/common/arrow/column_builder.h>
703
#include <dftracer/utils/utilities/composites/dft/dfanalyzer/dfanalyzer_scan.h>
704

705
static PyObject* create_arrow_batch_capsule(
77✔
706
    dftracer::utils::utilities::common::arrow::ArrowExportResult&& result) {
707
    auto* obj = (ArrowBatchCapsuleObject*)ArrowBatchCapsuleType.tp_alloc(
77✔
708
        &ArrowBatchCapsuleType, 0);
709
    if (!obj) return nullptr;
77✔
710
    obj->result =
77✔
711
        new dftracer::utils::utilities::common::arrow::ArrowExportResult(
66!
712
            std::move(result));
77!
713
    return (PyObject*)obj;
77✔
714
}
33✔
715

716
namespace {
717

718
using dftracer::utils::utilities::common::arrow::ArrowExportResult;
719

720
namespace dfanalyzer = dftracer::utils::utilities::composites::dft::dfanalyzer;
721
using dfanalyzer::AggScanInput;
722
using dfanalyzer::AggScanOutput;
723
using dfanalyzer::DfanalyzerScanInput;
724
using dfanalyzer::DfanalyzerScanOutput;
725
using dfanalyzer::GroupByConfig;
726
using dfanalyzer::open_agg_db;
727
using dfanalyzer::parse_group_by_name;
728
using dfanalyzer::scan_aggregation_shard_range;
729
using dfanalyzer::scan_dfanalyzer_shards;
730
using dfanalyzer::scan_system_metrics_buffer;
731

732
static bool parse_agg_type_str(const char* type_str, AggMapType& out) {
4✔
733
    if (strcmp(type_str, "events") == 0) {
4✔
734
        out = AggMapType::EVENT;
4✔
735
        return true;
4✔
736
    }
737
    if (strcmp(type_str, "profiles") == 0) {
×
738
        out = AggMapType::PROFILE;
×
739
        return true;
×
740
    }
741
    if (strcmp(type_str, "system") == 0) {
×
742
        out = AggMapType::SYSTEM;
×
743
        return true;
×
744
    }
745
    PyErr_SetString(PyExc_ValueError,
×
746
                    "type must be 'events', 'profiles', or 'system'");
747
    return false;
×
748
}
2✔
749

750
static std::optional<dftracer::utils::utilities::common::query::Query>
751
parse_query_arg(const char* query_str) {
26✔
752
    if (!query_str || query_str[0] == '\0') return std::nullopt;
26!
753
    auto result = dftracer::utils::utilities::common::query::Query::from_string(
9✔
754
        query_str);
18!
755
    if (!result) {
18✔
756
        PyErr_SetString(PyExc_ValueError, result.error().message.c_str());
2!
757
        return std::nullopt;
2✔
758
    }
759
    return std::move(*result);
16!
760
}
22✔
761

762
constexpr std::uint16_t DFT_NUM_SHARDS = 4096;
763

764
template <typename Output, typename ScanFn>
765
void parallel_shard_scan_range(Runtime* rt, std::uint16_t outer_begin,
24✔
766
                               std::uint16_t outer_end, ScanFn&& scan_fn,
767
                               std::vector<Output>& outputs) {
768
    if (outer_end <= outer_begin) return;
24!
769
    const std::size_t span = static_cast<std::size_t>(outer_end - outer_begin);
24✔
770
    const std::size_t num_tasks = std::min<std::size_t>(rt->threads(), span);
24!
771
    const std::size_t shards_per_task = (span + num_tasks - 1) / num_tasks;
24✔
772
    rt->submit(run_coro_scope(
48!
773
                   rt->executor(),
12✔
774
                   [&](CoroScope& scope) -> CoroTask<void> {
188!
775
                       std::vector<dftracer::utils::coro::SpawnFuture<Output>>
12✔
776
                           futures;
12✔
777
                       futures.reserve(num_tasks);
12!
778
                       for (std::size_t t = 0; t < num_tasks; ++t) {
48!
779
                           auto shard_begin = static_cast<std::uint16_t>(
72✔
780
                               outer_begin + t * shards_per_task);
36✔
781
                           auto shard_end =
36✔
782
                               static_cast<std::uint16_t>(std::min<std::size_t>(
36!
783
                                   outer_begin + (t + 1) * shards_per_task,
36✔
784
                                   outer_end));
36✔
785
                           futures.push_back(
36!
786
                               scope.spawn([&scan_fn, shard_begin, shard_end](
293!
787
                                               CoroScope&) -> CoroTask<Output> {
37!
788
                                   co_return scan_fn(shard_begin, shard_end);
36!
789
                               }));
790
                       }
36✔
791
                       outputs.reserve(num_tasks);
12!
792
                       for (auto& f : futures) {
140!
793
                           outputs.push_back(co_await f);
105!
794
                       }
36!
795
                   }),
104!
796
               "parallel-shard-scan-range")
12!
797
        .get();
24!
798
}
12✔
799

800
template <typename Output, typename ScanFn>
801
void parallel_shard_scan(Runtime* rt, ScanFn&& scan_fn,
4✔
802
                         std::vector<Output>& outputs) {
803
    parallel_shard_scan_range<Output>(rt, 0, DFT_NUM_SHARDS,
6✔
804
                                      std::forward<ScanFn>(scan_fn), outputs);
2✔
805
}
4✔
806

807
static void append_results_to_list(PyObject* list,
64✔
808
                                   std::vector<ArrowExportResult>& results) {
809
    for (auto& r : results) {
141✔
810
        PyObject* capsule = create_arrow_batch_capsule(std::move(r));
77!
811
        if (capsule) {
77✔
812
            PyList_Append(list, capsule);
77!
813
            Py_DECREF(capsule);
33✔
814
        }
33✔
815
    }
816
}
64✔
817

818
}  // namespace
819

820
static PyObject* Indexer_iter_aggregation(IndexerObject* self, PyObject* args,
×
821
                                          PyObject* kwds) {
822
    static const char* kwlist[] = {"type", "batch_size", nullptr};
823
    const char* type_str = "events";
×
824
    Py_ssize_t batch_size = 10000;
×
825

826
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|sn", (char**)kwlist,
×
827
                                     &type_str, &batch_size)) {
828
        return nullptr;
×
829
    }
830

831
    AggMapType target_type;
832
    if (!parse_agg_type_str(type_str, target_type)) return nullptr;
×
833

834
    AggregationBatchType batch_type;
835
    if (target_type == AggMapType::EVENT)
×
836
        batch_type = AggregationBatchType::EVENT;
×
837
    else if (target_type == AggMapType::PROFILE)
×
838
        batch_type = AggregationBatchType::PROFILE;
×
839
    else
840
        batch_type = AggregationBatchType::SYSTEM;
×
841

842
    auto idx_opt = resolve_index_path(self);
×
843
    if (!idx_opt) return nullptr;
×
844
    std::string index_path = std::move(*idx_opt);
×
845

846
    PyObject* batch_list = PyList_New(0);
×
847
    if (!batch_list) return nullptr;
×
848

849
    std::string error_msg;
×
850
    std::vector<dftracer::utils::utilities::common::arrow::ArrowExportResult>
851
        results;
×
852

853
    Py_BEGIN_ALLOW_THREADS try {
×
854
        auto handle = open_agg_db(index_path, error_msg);
×
855
        if (handle) {
×
856
            Runtime* rt = get_batch_indexer_runtime(self);
×
857
            std::vector<AggScanOutput> outputs;
×
858
            parallel_shard_scan<AggScanOutput>(
×
859
                rt,
860
                [&](std::uint16_t shard_begin, std::uint16_t shard_end) {
×
861
                    AggScanInput input;
862
                    input.agg = handle->agg.get();
×
863
                    input.target_type = target_type;
×
864
                    input.batch_type = batch_type;
×
865
                    input.batch_size = batch_size;
×
866
                    input.shard_begin = shard_begin;
×
867
                    input.shard_end = shard_end;
×
868
                    return scan_aggregation_shard_range(input);
×
869
                },
870
                outputs);
871

872
            for (auto& out : outputs) {
×
873
                for (auto& r : out.results) {
×
874
                    results.push_back(std::move(r));
×
875
                }
876
            }
877
        }
×
878
    } catch (const std::exception& e) {
×
879
        error_msg = e.what();
×
880
    }
×
881
    Py_END_ALLOW_THREADS
×
882

883
        if (!error_msg.empty()) {
×
884
        Py_DECREF(batch_list);
×
885
        PyErr_SetString(PyExc_RuntimeError, error_msg.c_str());
×
886
        return nullptr;
×
887
    }
888

889
    append_results_to_list(batch_list, results);
×
890

891
    PyObject* iter = PyObject_GetIter(batch_list);
×
892
    Py_DECREF(batch_list);
×
893
    return iter;
×
894
}
×
895

896
static PyObject* Indexer_iter_arrow_dfanalyzer(IndexerObject* self,
4✔
897
                                               PyObject* args, PyObject* kwds) {
898
    static const char* kwlist[] = {
899
        "type",  "batch_size", "time_granularity", "time_resolution",
900
        "query", nullptr};
901
    const char* type_str = "events";
4✔
902
    Py_ssize_t batch_size = 10000;
4✔
903
    double time_granularity = 1.0;
4✔
904
    double time_resolution = 1000000.0;
4✔
905
    const char* query_str = nullptr;
4✔
906

907
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|snddz", (char**)kwlist,
4!
908
                                     &type_str, &batch_size, &time_granularity,
909
                                     &time_resolution, &query_str)) {
910
        return nullptr;
×
911
    }
912

913
    AggMapType target_type;
914
    if (!parse_agg_type_str(type_str, target_type)) return nullptr;
4!
915

916
    auto query_opt = parse_query_arg(query_str);
4!
917
    if (!query_opt && PyErr_Occurred()) return nullptr;
4!
918

919
    auto idx_opt = resolve_index_path(self);
4!
920
    if (!idx_opt) return nullptr;
4✔
921
    std::string index_path = std::move(*idx_opt);
4✔
922

923
    PyObject* batch_list = PyList_New(0);
4!
924
    if (!batch_list) return nullptr;
4✔
925

926
    std::string error_msg;
4✔
927
    std::vector<ArrowExportResult> results;
4✔
928

929
    Py_BEGIN_ALLOW_THREADS try {
4!
930
        auto handle = open_agg_db(index_path, error_msg);
4!
931
        if (handle) {
4✔
932
            dftracer::utils::utilities::indexer::IndexDatabase idx_db(
2!
933
                index_path,
934
                dftracer::utils::rocksdb::RocksDatabase::OpenMode::ReadOnly);
2!
935
            auto time_bounds = handle->agg->query_time_bounds();
4!
936
            std::uint64_t time_origin =
4✔
937
                time_bounds.valid ? time_bounds.min_time_bucket : 0;
4!
938

939
            DfanalyzerContext ctx;
4✔
940
            // Resolve on demand: ingesting both hash tables costs the whole
941
            // trace's file list, and this runs once per dfanalyzer task.
942
            ctx.hash_db = &idx_db;
4✔
943
            ctx.query_filter = query_opt ? &*query_opt : nullptr;
4!
944
            ctx.time_origin = time_origin;
4✔
945
            ctx.time_resolution = time_resolution;
4✔
946
            ctx.time_granularity = time_granularity;
4✔
947

948
            Runtime* rt = get_batch_indexer_runtime(self);
4!
949
            std::vector<DfanalyzerScanOutput> outputs;
4✔
950
            parallel_shard_scan<DfanalyzerScanOutput>(
4!
951
                rt,
2✔
952
                [&](std::uint16_t shard_begin, std::uint16_t shard_end) {
13✔
953
                    DfanalyzerScanInput input;
11✔
954
                    input.agg = handle->agg.get();
13✔
955
                    input.ctx = &ctx;
12✔
956
                    input.type_filter = target_type;
12✔
957
                    input.batch_size = batch_size;
13✔
958
                    input.shard_begin = shard_begin;
13✔
959
                    input.shard_end = shard_end;
13✔
960
                    return scan_dfanalyzer_shards(input);
20!
961
                },
962
                outputs);
963

964
            for (auto& out : outputs) {
18✔
965
                for (auto& r : out.events) results.push_back(std::move(r));
28✔
966
                for (auto& r : out.profiles) results.push_back(std::move(r));
14!
967
                for (auto& r : out.system) results.push_back(std::move(r));
14!
968
            }
969
        }
4✔
970
    } catch (const std::exception& e) {
4!
971
        error_msg = e.what();
×
972
    }
×
973
    Py_END_ALLOW_THREADS
4!
974

975
        if (!error_msg.empty()) {
4!
976
        Py_DECREF(batch_list);
×
977
        PyErr_SetString(PyExc_RuntimeError, error_msg.c_str());
×
978
        return nullptr;
×
979
    }
980

981
    append_results_to_list(batch_list, results);
4!
982

983
    PyObject* iter = PyObject_GetIter(batch_list);
4!
984
    Py_DECREF(batch_list);
2!
985
    return iter;
4✔
986
}
4✔
987

988
static bool parse_group_by_arg(PyObject* obj, GroupByConfig& out) {
20✔
989
    if (!obj || obj == Py_None) return true;
20!
990
    if (!PySequence_Check(obj)) {
×
991
        PyErr_SetString(PyExc_TypeError,
×
992
                        "group_by must be a sequence of strings or None");
993
        return false;
×
994
    }
995
    Py_ssize_t n = PySequence_Length(obj);
×
996
    for (Py_ssize_t i = 0; i < n; ++i) {
×
997
        PyObject* item = PySequence_GetItem(obj, i);
×
998
        if (!item) return false;
×
999
        if (!PyUnicode_Check(item)) {
×
1000
            Py_DECREF(item);
1001
            PyErr_SetString(PyExc_TypeError,
×
1002
                            "group_by entries must be strings");
1003
            return false;
×
1004
        }
1005
        Py_ssize_t sz = 0;
×
1006
        const char* s = PyUnicode_AsUTF8AndSize(item, &sz);
×
1007
        if (!s) {
×
1008
            Py_DECREF(item);
1009
            return false;
×
1010
        }
1011
        std::string_view sv(s, static_cast<std::size_t>(sz));
×
1012
        auto field = parse_group_by_name(sv);
×
1013
        if (!field) {
×
1014
            std::string msg = "unsupported group_by field: ";
×
1015
            msg.append(sv);
×
1016
            Py_DECREF(item);
×
1017
            PyErr_SetString(PyExc_ValueError, msg.c_str());
×
1018
            return false;
×
1019
        }
×
1020
        if (!(out.mask & *field)) {
×
1021
            out.mask |= *field;
×
1022
            out.order.push_back(*field);
×
1023
            out.names.emplace_back(sv);
×
1024
        }
1025
        Py_DECREF(item);
1026
    }
1027
    return true;
×
1028
}
10✔
1029

1030
static PyObject* Indexer_iter_arrow_dfanalyzer_all(IndexerObject* self,
22✔
1031
                                                   PyObject* args,
1032
                                                   PyObject* kwds) {
1033
    static const char* kwlist[] = {
1034
        "batch_size", "time_granularity", "time_resolution",
1035
        "query",      "group_by",         "shard_begin",
1036
        "shard_end",  "progress",         nullptr};
1037
    Py_ssize_t batch_size = 10000;
22✔
1038
    double time_granularity = 1.0;
22✔
1039
    double time_resolution = 1000000.0;
22✔
1040
    const char* query_str = nullptr;
22✔
1041
    PyObject* group_by_obj = nullptr;
22✔
1042
    int shard_begin_i = 0;
22✔
1043
    int shard_end_i = DFT_NUM_SHARDS;
22✔
1044
    PyObject* progress_obj = nullptr;
22✔
1045

1046
    if (!PyArg_ParseTupleAndKeywords(
22!
1047
            args, kwds, "|nddzOiiO", (char**)kwlist, &batch_size,
11✔
1048
            &time_granularity, &time_resolution, &query_str, &group_by_obj,
1049
            &shard_begin_i, &shard_end_i, &progress_obj)) {
NEW
1050
        return nullptr;
×
1051
    }
1052
    if (shard_begin_i < 0 || shard_end_i > DFT_NUM_SHARDS ||
22!
1053
        shard_begin_i > shard_end_i) {
22!
NEW
1054
        PyErr_Format(PyExc_ValueError,
×
1055
                     "shard range [%d, %d) is outside [0, %d)", shard_begin_i,
1056
                     shard_end_i, static_cast<int>(DFT_NUM_SHARDS));
UNCOV
1057
        return nullptr;
×
1058
    }
1059

1060
    auto query_opt = parse_query_arg(query_str);
22!
1061
    if (!query_opt && PyErr_Occurred()) return nullptr;
22!
1062

1063
    GroupByConfig group_by_cfg;
20✔
1064
    if (!parse_group_by_arg(group_by_obj, group_by_cfg)) return nullptr;
20!
1065
    const GroupByConfig* group_by_ptr =
20✔
1066
        group_by_cfg.mask != 0 ? &group_by_cfg : nullptr;
20!
1067

1068
    auto idx_opt = resolve_index_path(self);
20!
1069
    if (!idx_opt) return nullptr;
20✔
1070
    std::string index_path = std::move(*idx_opt);
20✔
1071

1072
    PyObject* result_dict = PyDict_New();
20!
1073
    if (!result_dict) return nullptr;
20!
1074

1075
    PyObject* events_list = PyList_New(0);
20!
1076
    PyObject* profiles_list = PyList_New(0);
20!
1077
    PyObject* system_list = PyList_New(0);
20!
1078
    if (!events_list || !profiles_list || !system_list) {
20!
1079
        Py_XDECREF(events_list);
×
1080
        Py_XDECREF(profiles_list);
×
1081
        Py_XDECREF(system_list);
×
1082
        Py_DECREF(result_dict);
×
1083
        return nullptr;
×
1084
    }
1085

1086
    std::string error_msg;
20✔
1087
    std::vector<ArrowExportResult> events_results, profiles_results,
20✔
1088
        system_results;
20✔
1089

1090
    // The scan runs with the GIL released; re-acquire it per call. Built here
1091
    // while the GIL is held so the INCREF is safe.
1092
    std::function<void(std::size_t, std::size_t)> scan_progress;
20✔
1093
    if (progress_obj && progress_obj != Py_None) {
20!
NEW
1094
        Py_INCREF(progress_obj);
×
NEW
1095
        std::shared_ptr<PyObject> cb(progress_obj, [](PyObject* p) {
×
NEW
1096
            PyGILState_STATE g = PyGILState_Ensure();
×
1097
            Py_DECREF(p);
NEW
1098
            PyGILState_Release(g);
×
NEW
1099
        });
×
NEW
1100
        scan_progress = [cb](std::size_t done, std::size_t total) {
×
NEW
1101
            PyGILState_STATE g = PyGILState_Ensure();
×
NEW
1102
            PyObject* r = PyObject_CallFunction(cb.get(), "nn",
×
1103
                                                static_cast<Py_ssize_t>(done),
1104
                                                static_cast<Py_ssize_t>(total));
NEW
1105
            if (r) {
×
1106
                Py_DECREF(r);
1107
            } else {
NEW
1108
                PyErr_Clear();
×
1109
            }
NEW
1110
            PyGILState_Release(g);
×
NEW
1111
        };
×
NEW
1112
    }
×
1113
    const std::function<void(std::size_t, std::size_t)>* scan_progress_ptr =
10✔
1114
        scan_progress ? &scan_progress : nullptr;
20!
1115

1116
    Py_BEGIN_ALLOW_THREADS try {
20!
1117
        auto handle = open_agg_db(index_path, error_msg);
20!
1118
        if (handle) {
20✔
1119
            dftracer::utils::utilities::indexer::IndexDatabase idx_db(
10!
1120
                index_path,
1121
                dftracer::utils::rocksdb::RocksDatabase::OpenMode::ReadOnly);
10!
1122
            auto time_bounds = handle->agg->query_time_bounds();
20!
1123
            std::uint64_t time_origin =
20✔
1124
                time_bounds.valid ? time_bounds.min_time_bucket : 0;
20!
1125

1126
            DfanalyzerContext ctx;
20✔
1127
            // Resolve on demand: ingesting both hash tables costs the whole
1128
            // trace's file list, and this runs once per dfanalyzer task.
1129
            ctx.hash_db = &idx_db;
20✔
1130
            ctx.query_filter = query_opt ? &*query_opt : nullptr;
20✔
1131
            ctx.time_origin = time_origin;
20✔
1132
            ctx.time_resolution = time_resolution;
20✔
1133
            ctx.time_granularity = time_granularity;
20✔
1134
            std::atomic<std::size_t> scan_shards_done{0};
20✔
1135
            ctx.shards_done = &scan_shards_done;
20✔
1136
            ctx.total_shards =
20✔
1137
                static_cast<std::size_t>(shard_end_i - shard_begin_i);
20✔
1138
            ctx.progress = scan_progress_ptr;
20✔
1139

1140
            Runtime* rt = get_batch_indexer_runtime(self);
20!
1141
            std::vector<DfanalyzerScanOutput> outputs;
20✔
1142
            parallel_shard_scan_range<DfanalyzerScanOutput>(
20!
1143
                rt, static_cast<std::uint16_t>(shard_begin_i),
10✔
1144
                static_cast<std::uint16_t>(shard_end_i),
10✔
1145
                [&](std::uint16_t shard_begin, std::uint16_t shard_end) {
71✔
1146
                    DfanalyzerScanInput input;
61✔
1147
                    input.agg = handle->agg.get();
65✔
1148
                    input.ctx = &ctx;
64✔
1149
                    input.type_filter = std::nullopt;
64✔
1150
                    input.batch_size = batch_size;
67✔
1151
                    input.shard_begin = shard_begin;
67✔
1152
                    input.shard_end = shard_end;
67✔
1153
                    input.group_by = group_by_ptr;
67✔
1154
                    return scan_dfanalyzer_shards(input);
106!
1155
                },
1156
                outputs);
1157

1158
            for (auto& out : outputs) {
90✔
1159
                for (auto& r : out.events)
133✔
1160
                    events_results.push_back(std::move(r));
63!
1161
                for (auto& r : out.profiles)
70✔
1162
                    profiles_results.push_back(std::move(r));
×
1163
                for (auto& r : out.system)
70✔
1164
                    system_results.push_back(std::move(r));
×
1165
            }
1166

1167
            auto sys_buf =
1168
                scan_system_metrics_buffer(handle->agg.get(), &ctx, batch_size);
20!
1169
            for (auto& r : sys_buf) system_results.push_back(std::move(r));
20!
1170
        }
20✔
1171
    } catch (const std::exception& e) {
20!
1172
        error_msg = e.what();
×
1173
    }
×
1174
    Py_END_ALLOW_THREADS
20!
1175

1176
        if (!error_msg.empty()) {
20!
1177
        Py_DECREF(events_list);
×
1178
        Py_DECREF(profiles_list);
×
1179
        Py_DECREF(system_list);
×
1180
        Py_DECREF(result_dict);
×
1181
        PyErr_SetString(PyExc_RuntimeError, error_msg.c_str());
×
1182
        return nullptr;
×
1183
    }
1184

1185
    append_results_to_list(events_list, events_results);
20!
1186
    append_results_to_list(profiles_list, profiles_results);
20!
1187
    append_results_to_list(system_list, system_results);
20!
1188

1189
    PyDict_SetItemString(result_dict, "events", events_list);
20!
1190
    PyDict_SetItemString(result_dict, "profiles", profiles_list);
20!
1191
    PyDict_SetItemString(result_dict, "system", system_list);
20!
1192
    Py_DECREF(events_list);
10!
1193
    Py_DECREF(profiles_list);
10!
1194
    Py_DECREF(system_list);
10!
1195

1196
    return result_dict;
20✔
1197
}
22✔
1198

1199
// ---------------------------------------------------------------------------
1200
// scan_aggregation_manifest — module-level entry point for analyze_trace.
1201
//
1202
// Each Dask worker calls this with its slice of the agg manifest
1203
// (agg_ssts + sys_ssts) and optionally a [shard_begin, shard_end) range.
1204
// The function opens a scratch IndexDatabase at `scratch_dir`, ingests the
1205
// SSTs into its AGGREGATION/SYSTEM_METRICS CFs (nearly free when SSTs live
1206
// on the same filesystem as `scratch_dir` — RocksDB hard-links them), then
1207
// runs the same parallel shard scan that `iter_arrow_dfanalyzer_all` uses.
1208
//
1209
// AGG_GLOBAL_CONFIG_KEY is not written by worker SSTs, so we construct the
1210
// EventAggregator with config_hash=0 directly instead of going through
1211
// `open_agg_db` (which requires the config key). The config hash is used
1212
// by the aggregator only for write-time validation, not for reads.
1213
//
1214
// The scratch DB is NOT cleaned up here — the Python caller owns
1215
// `scratch_dir` lifetime and should remove it after gathering results.
1216
// ---------------------------------------------------------------------------
1217

1218
static bool collect_string_string_dict(
×
1219
    PyObject* obj, const char* name,
1220
    std::unordered_map<std::string, std::string>& out) {
1221
    if (!obj || obj == Py_None) return true;
×
1222
    if (!PyDict_Check(obj)) {
×
1223
        PyErr_Format(PyExc_TypeError, "%s must be a dict[str, str] or None",
×
1224
                     name);
1225
        return false;
×
1226
    }
1227
    PyObject *k, *v;
1228
    Py_ssize_t pos = 0;
×
1229
    while (PyDict_Next(obj, &pos, &k, &v)) {
×
1230
        if (!PyUnicode_Check(k) || !PyUnicode_Check(v)) {
×
1231
            PyErr_Format(PyExc_TypeError, "%s must map str -> str", name);
×
1232
            return false;
×
1233
        }
1234
        const char* ks = PyUnicode_AsUTF8(k);
×
1235
        const char* vs = PyUnicode_AsUTF8(v);
×
1236
        if (!ks || !vs) return false;
×
1237
        out.emplace(ks, vs);
×
1238
    }
1239
    return true;
×
1240
}
1241

1242
static PyObject* scan_aggregation_manifest_fn(PyObject* /*self*/,
×
1243
                                              PyObject* args, PyObject* kwds) {
1244
    static const char* kwlist[] = {
1245
        "agg_ssts",        "sys_ssts",    "scratch_dir",
1246
        "meta_index_path", "batch_size",  "time_granularity",
1247
        "time_resolution", "query",       "group_by",
1248
        "shard_begin",     "shard_end",   "runtime",
1249
        "file_hashes",     "host_hashes", nullptr};
1250

1251
    PyObject* agg_ssts_obj = nullptr;
×
1252
    PyObject* sys_ssts_obj = nullptr;
×
1253
    const char* scratch_dir = nullptr;
×
1254
    const char* meta_index_path = nullptr;
×
1255
    Py_ssize_t batch_size = 10000;
×
1256
    double time_granularity = 1.0;
×
1257
    double time_resolution = 1000000.0;
×
1258
    const char* query_str = nullptr;
×
1259
    PyObject* group_by_obj = nullptr;
×
1260
    int shard_begin_i = 0;
×
1261
    int shard_end_i = DFT_NUM_SHARDS;
×
1262
    PyObject* runtime_obj = nullptr;
×
1263
    PyObject* file_hashes_obj = nullptr;
×
1264
    PyObject* host_hashes_obj = nullptr;
×
1265

1266
    if (!PyArg_ParseTupleAndKeywords(
×
1267
            args, kwds, "OOss|nddzOiiOOO", (char**)kwlist, &agg_ssts_obj,
1268
            &sys_ssts_obj, &scratch_dir, &meta_index_path, &batch_size,
1269
            &time_granularity, &time_resolution, &query_str, &group_by_obj,
1270
            &shard_begin_i, &shard_end_i, &runtime_obj, &file_hashes_obj,
1271
            &host_hashes_obj)) {
1272
        return nullptr;
×
1273
    }
1274

1275
    if (shard_begin_i < 0 || shard_end_i > DFT_NUM_SHARDS ||
×
1276
        shard_begin_i >= shard_end_i) {
×
1277
        PyErr_Format(PyExc_ValueError,
×
1278
                     "shard range [%d, %d) invalid (must be within [0, %d))",
1279
                     shard_begin_i, shard_end_i, (int)DFT_NUM_SHARDS);
1280
        return nullptr;
×
1281
    }
1282

1283
    std::vector<std::string> agg_ssts;
×
1284
    std::vector<std::string> sys_ssts;
×
1285
    if (!parse_str_list(agg_ssts_obj, "agg_ssts", agg_ssts)) return nullptr;
×
1286
    if (!parse_str_list(sys_ssts_obj, "sys_ssts", sys_ssts)) return nullptr;
×
1287

1288
    std::unordered_map<std::string, std::string> preloaded_file_hashes;
×
1289
    std::unordered_map<std::string, std::string> preloaded_host_hashes;
×
1290
    const bool hashes_preloaded =
×
1291
        (file_hashes_obj && file_hashes_obj != Py_None) ||
×
1292
        (host_hashes_obj && host_hashes_obj != Py_None);
×
1293
    if (!collect_string_string_dict(file_hashes_obj, "file_hashes",
×
1294
                                    preloaded_file_hashes))
1295
        return nullptr;
×
1296
    if (!collect_string_string_dict(host_hashes_obj, "host_hashes",
×
1297
                                    preloaded_host_hashes))
1298
        return nullptr;
×
1299

1300
    auto query_opt = parse_query_arg(query_str);
×
1301
    if (!query_opt && PyErr_Occurred()) return nullptr;
×
1302

1303
    GroupByConfig group_by_cfg;
×
1304
    if (!parse_group_by_arg(group_by_obj, group_by_cfg)) return nullptr;
×
1305
    const GroupByConfig* group_by_ptr =
×
1306
        group_by_cfg.mask != 0 ? &group_by_cfg : nullptr;
×
1307

1308
    Runtime* rt = nullptr;
×
1309
    if (runtime_obj && runtime_obj != Py_None) {
×
1310
        if (!PyObject_TypeCheck(runtime_obj, &RuntimeType)) {
×
1311
            PyErr_SetString(PyExc_TypeError,
×
1312
                            "runtime must be a Runtime instance or None");
1313
            return nullptr;
×
1314
        }
1315
        rt = ((RuntimeObject*)runtime_obj)->runtime.get();
×
1316
    } else {
1317
        rt = get_default_runtime();
×
1318
    }
1319

1320
    PyObject* result_dict = PyDict_New();
×
1321
    if (!result_dict) return nullptr;
×
1322
    PyObject* events_list = PyList_New(0);
×
1323
    PyObject* profiles_list = PyList_New(0);
×
1324
    PyObject* system_list = PyList_New(0);
×
1325
    if (!events_list || !profiles_list || !system_list) {
×
1326
        Py_XDECREF(events_list);
×
1327
        Py_XDECREF(profiles_list);
×
1328
        Py_XDECREF(system_list);
×
1329
        Py_DECREF(result_dict);
×
1330
        return nullptr;
×
1331
    }
1332

1333
    std::string error_msg;
×
1334
    std::vector<ArrowExportResult> events_results, profiles_results,
×
1335
        system_results;
×
1336
    std::string scratch_index_path = std::string(scratch_dir) + "/.dftindex";
×
1337
    std::string meta_index_path_str(meta_index_path);
×
1338

1339
    Py_BEGIN_ALLOW_THREADS try {
×
1340
        namespace rcf = dftracer::utils::rocksdb::cf;
1341
        using clock = std::chrono::steady_clock;
1342
        auto ms = [](clock::time_point a, clock::time_point b) -> long long {
×
1343
            return std::chrono::duration_cast<std::chrono::milliseconds>(b - a)
×
1344
                .count();
×
1345
        };
1346

1347
        auto t_start = clock::now();
×
1348
        dftracer::utils::utilities::indexer::IndexDatabase scratch_db(
×
1349
            scratch_index_path);
×
1350
        auto t_scratch_open = clock::now();
×
1351

1352
        auto raw_db = scratch_db.db();
×
1353
        for (const auto& p : agg_ssts) {
×
1354
            auto st = raw_db->ingest_external_files(rcf::AGGREGATION, {p},
×
1355
                                                    /*ingest_behind=*/false);
×
1356
            if (!st.ok()) {
×
1357
                error_msg =
1358
                    "ingest AGGREGATION sst '" + p + "': " + st.ToString();
×
1359
                break;
×
1360
            }
1361
        }
×
1362
        if (error_msg.empty()) {
×
1363
            for (const auto& p : sys_ssts) {
×
1364
                auto st = raw_db->ingest_external_files(
×
1365
                    rcf::SYSTEM_METRICS, {p}, /*ingest_behind=*/false);
×
1366
                if (!st.ok()) {
×
1367
                    error_msg = "ingest SYSTEM_METRICS sst '" + p +
×
1368
                                "': " + st.ToString();
×
1369
                    break;
×
1370
                }
1371
            }
×
1372
        }
1373
        auto t_ingest = clock::now();
×
1374

1375
        if (error_msg.empty()) {
×
1376
            auto agg =
1377
                std::make_unique<EventAggregator>(raw_db, /*cfg_hash=*/0);
×
1378

1379
            // If the caller passed pre-loaded hash tables, skip opening
1380
            // the meta DB on lustre. When many dask workers run
1381
            // scan_aggregation_manifest in parallel, loading the hash
1382
            // tables N times from the same file is significant lustre
1383
            // metadata pressure; loading once on the coordinator and
1384
            // passing them in eliminates the redundant reads.
1385
            std::unordered_map<std::string, std::string> loaded_file_hashes;
×
1386
            std::unordered_map<std::string, std::string> loaded_host_hashes;
×
1387
            std::unique_ptr<dftracer::utils::utilities::indexer::IndexDatabase>
1388
                meta_db;
×
1389
            if (!hashes_preloaded) {
×
1390
                meta_db = std::make_unique<
×
1391
                    dftracer::utils::utilities::indexer::IndexDatabase>(
1392
                    meta_index_path_str, dftracer::utils::rocksdb::
1393
                                             RocksDatabase::OpenMode::ReadOnly);
×
1394
                loaded_file_hashes = meta_db->query_hash_table(
×
1395
                    dftracer::utils::utilities::indexer::IndexDatabase::
1396
                        HashType::FILE);
1397
                loaded_host_hashes = meta_db->query_hash_table(
×
1398
                    dftracer::utils::utilities::indexer::IndexDatabase::
1399
                        HashType::HOST);
1400
            }
1401
            const auto& file_hashes =
×
1402
                hashes_preloaded ? preloaded_file_hashes : loaded_file_hashes;
×
1403
            const auto& host_hashes =
×
1404
                hashes_preloaded ? preloaded_host_hashes : loaded_host_hashes;
×
1405
            auto t_hash_tables = clock::now();
×
1406

1407
            auto time_bounds = agg->query_time_bounds();
×
1408
            std::uint64_t time_origin =
×
1409
                time_bounds.valid ? time_bounds.min_time_bucket : 0;
×
1410

1411
            DfanalyzerContext ctx;
×
1412
            ctx.file_hashes = &file_hashes;
×
1413
            ctx.host_hashes = &host_hashes;
×
1414
            ctx.query_filter = query_opt ? &*query_opt : nullptr;
×
1415
            ctx.time_origin = time_origin;
×
1416
            ctx.time_resolution = time_resolution;
×
1417
            ctx.time_granularity = time_granularity;
×
1418

1419
            std::vector<DfanalyzerScanOutput> outputs;
×
1420
            parallel_shard_scan_range<DfanalyzerScanOutput>(
×
1421
                rt, static_cast<std::uint16_t>(shard_begin_i),
1422
                static_cast<std::uint16_t>(shard_end_i),
1423
                [&](std::uint16_t sb, std::uint16_t se) {
×
1424
                    DfanalyzerScanInput input;
×
1425
                    input.agg = agg.get();
×
1426
                    input.ctx = &ctx;
×
1427
                    input.type_filter = std::nullopt;
×
1428
                    input.batch_size = batch_size;
×
1429
                    input.shard_begin = sb;
×
1430
                    input.shard_end = se;
×
1431
                    input.group_by = group_by_ptr;
×
1432
                    return scan_dfanalyzer_shards(input);
×
1433
                },
1434
                outputs);
1435
            auto t_scan = clock::now();
×
1436

1437
            for (auto& out : outputs) {
×
1438
                for (auto& r : out.events)
×
1439
                    events_results.push_back(std::move(r));
×
1440
                for (auto& r : out.profiles)
×
1441
                    profiles_results.push_back(std::move(r));
×
1442
                for (auto& r : out.system)
×
1443
                    system_results.push_back(std::move(r));
×
1444
            }
1445

1446
            std::fprintf(
×
1447
                stderr,
1448
                "[scan_aggregation_manifest] n_agg=%zu n_sys=%zu "
1449
                "scratch_open=%lldms ingest=%lldms hash_tables=%lldms "
1450
                "scan=%lldms\n",
1451
                agg_ssts.size(), sys_ssts.size(), ms(t_start, t_scratch_open),
×
1452
                ms(t_scratch_open, t_ingest), ms(t_ingest, t_hash_tables),
×
1453
                ms(t_hash_tables, t_scan));
×
1454
            std::fflush(stderr);
×
1455
        }
×
1456
    } catch (const std::exception& e) {
×
1457
        error_msg = e.what();
×
1458
    }
×
1459
    Py_END_ALLOW_THREADS
×
1460

1461
        if (!error_msg.empty()) {
×
1462
        Py_DECREF(events_list);
×
1463
        Py_DECREF(profiles_list);
×
1464
        Py_DECREF(system_list);
×
1465
        Py_DECREF(result_dict);
×
1466
        PyErr_SetString(PyExc_RuntimeError, error_msg.c_str());
×
1467
        return nullptr;
×
1468
    }
1469

1470
    append_results_to_list(events_list, events_results);
×
1471
    append_results_to_list(profiles_list, profiles_results);
×
1472
    append_results_to_list(system_list, system_results);
×
1473

1474
    PyDict_SetItemString(result_dict, "events", events_list);
×
1475
    PyDict_SetItemString(result_dict, "profiles", profiles_list);
×
1476
    PyDict_SetItemString(result_dict, "system", system_list);
×
1477
    Py_DECREF(events_list);
×
1478
    Py_DECREF(profiles_list);
×
1479
    Py_DECREF(system_list);
×
1480

1481
    return result_dict;
×
1482
}
×
1483

NEW
1484
static PyObject* count_hash_entries_fn(PyObject* /*self*/, PyObject* args) {
×
NEW
1485
    const char* index_path = nullptr;
×
NEW
1486
    const char* type_str = nullptr;
×
NEW
1487
    if (!PyArg_ParseTuple(args, "ss", &index_path, &type_str)) return nullptr;
×
1488

1489
    using dftracer::utils::utilities::indexer::IndexDatabase;
1490
    using HashType = IndexDatabase::HashType;
1491

1492
    HashType type;
NEW
1493
    if (std::strcmp(type_str, "file") == 0) {
×
NEW
1494
        type = HashType::FILE;
×
NEW
1495
    } else if (std::strcmp(type_str, "host") == 0) {
×
NEW
1496
        type = HashType::HOST;
×
NEW
1497
    } else if (std::strcmp(type_str, "string") == 0) {
×
NEW
1498
        type = HashType::STRING;
×
NEW
1499
    } else if (std::strcmp(type_str, "proc") == 0) {
×
NEW
1500
        type = HashType::PROC;
×
1501
    } else {
NEW
1502
        PyErr_SetString(PyExc_ValueError,
×
1503
                        "type must be 'file', 'host', 'string', or 'proc'");
NEW
1504
        return nullptr;
×
1505
    }
1506

NEW
1507
    std::uint64_t count = 0;
×
NEW
1508
    if (!run_blocking_r(
×
NEW
1509
            [&] {
×
NEW
1510
                IndexDatabase db(index_path,
×
1511
                                 dftracer::utils::rocksdb::RocksDatabase::
1512
                                     OpenMode::ReadOnly);
×
NEW
1513
                return db.count_hash_entries(type);
×
NEW
1514
            },
×
1515
            count)) {
NEW
1516
        return nullptr;
×
1517
    }
NEW
1518
    return PyLong_FromUnsignedLongLong(count);
×
1519
}
1520

1521
static PyMethodDef BatchIndexerModuleMethods[] = {
1522
    {"count_hash_entries", (PyCFunction)count_hash_entries_fn, METH_VARARGS,
1523
     "count_hash_entries(index_path, type)\n"
1524
     "--\n\n"
1525
     "Number of hashes of `type` ('file', 'host', 'string', 'proc') in the\n"
1526
     "index at `index_path`. Counted by iteration, so a table holding tens\n"
1527
     "of millions of entries is not materialised to take its length.\n"},
1528
    {"scan_aggregation_manifest", (PyCFunction)scan_aggregation_manifest_fn,
1529
     METH_VARARGS | METH_KEYWORDS,
1530
     "scan_aggregation_manifest(agg_ssts, sys_ssts, scratch_dir, "
1531
     "meta_index_path, batch_size=10000, time_granularity=1.0, "
1532
     "time_resolution=1e6, query=None, group_by=None, shard_begin=0, "
1533
     "shard_end=4096, runtime=None) -> dict\n"
1534
     "--\n\n"
1535
     "Scan a worker's slice of the distributed aggregation manifest.\n\n"
1536
     "Ingests agg_ssts + sys_ssts into a scratch IndexDatabase at "
1537
     "scratch_dir (caller owns the directory lifecycle) and runs the "
1538
     "dfanalyzer aggregation scan over [shard_begin, shard_end). "
1539
     "meta_index_path is the unified .dftindex used to resolve file / "
1540
     "host hashes. Returns the same dict shape as "
1541
     "Indexer.iter_arrow_dfanalyzer_all."},
1542
    {nullptr, nullptr, 0, nullptr}};
1543
#endif
1544

1545
static PyMethodDef Indexer_methods[] = {
1546
    {"get_checkpoint_indexer", (PyCFunction)Indexer_get_checkpoint_indexer,
1547
     METH_VARARGS,
1548
     "get_checkpoint_indexer(file_path)\n"
1549
     "--\n\n"
1550
     "Get a checkpoint indexer for a specific file.\n\n"
1551
     "Args:\n"
1552
     "    file_path: Path to the trace file (.pfw/.pfw.gz)\n\n"
1553
     "Returns:\n"
1554
     "    Indexer instance for checkpoint-level operations.\n"},
1555
    {"resolve", (PyCFunction)Indexer_resolve, METH_NOARGS,
1556
     "resolve()\n"
1557
     "--\n\n"
1558
     "Check what files exist vs need indexing.\n\n"
1559
     "Returns:\n"
1560
     "    dict with 'total_files', 'ready', 'needs_work', 'index_path'\n"},
1561
    {"build", (PyCFunction)Indexer_build, METH_NOARGS,
1562
     "build()\n"
1563
     "--\n\n"
1564
     "Build all missing index tiers based on require_* flags.\n"},
1565
    {"ensure_indexed", (PyCFunction)Indexer_ensure_indexed, METH_NOARGS,
1566
     "ensure_indexed()\n"
1567
     "--\n\n"
1568
     "Resolve and build if needed.\n\n"
1569
     "Returns:\n"
1570
     "    dict with index status after building.\n"},
1571
    {"get_hash_table", (PyCFunction)Indexer_get_hash_table, METH_VARARGS,
1572
     "get_hash_table(type)\n"
1573
     "--\n\n"
1574
     "Query hash table mappings.\n\n"
1575
     "Args:\n"
1576
     "    type: 'file', 'host', 'string', or 'proc'\n\n"
1577
     "Returns:\n"
1578
     "    dict mapping hash values to resolved names.\n"},
1579
    {"query_file_pids", (PyCFunction)Indexer_query_file_pids, METH_VARARGS,
1580
     "query_file_pids(file_id)\n"
1581
     "--\n\n"
1582
     "Query PIDs observed in a specific file.\n\n"
1583
     "Args:\n"
1584
     "    file_id: Integer file ID from index.\n\n"
1585
     "Returns:\n"
1586
     "    set of PIDs.\n"},
1587
    {"query_all_file_pids", (PyCFunction)Indexer_query_all_file_pids,
1588
     METH_NOARGS,
1589
     "query_all_file_pids()\n"
1590
     "--\n\n"
1591
     "Query PIDs for all indexed files.\n\n"
1592
     "Returns:\n"
1593
     "    dict mapping file_id to set of PIDs.\n"},
1594
    {"query_file_info", (PyCFunction)Indexer_query_file_info, METH_NOARGS,
1595
     "query_file_info()\n"
1596
     "--\n\n"
1597
     "Query file ID to path mapping and per-file PIDs in one call.\n\n"
1598
     "Returns:\n"
1599
     "    tuple of (dict[int, str], dict[int, set[int]]).\n"},
1600
#ifdef DFTRACER_UTILS_ENABLE_ARROW
1601
    {"iter_aggregation", (PyCFunction)Indexer_iter_aggregation,
1602
     METH_VARARGS | METH_KEYWORDS,
1603
     "iter_aggregation(type='events', batch_size=10000)\n"
1604
     "--\n\n"
1605
     "Iterate over aggregation data as Arrow batches.\n\n"
1606
     "Args:\n"
1607
     "    type: 'events', 'profiles', or 'system'\n"
1608
     "    batch_size: Number of entries per batch (default 10000)\n\n"
1609
     "Returns:\n"
1610
     "    Iterator over Arrow batch capsules.\n"},
1611
    {"iter_arrow_dfanalyzer", (PyCFunction)Indexer_iter_arrow_dfanalyzer,
1612
     METH_VARARGS | METH_KEYWORDS,
1613
     "iter_arrow_dfanalyzer(type='events', batch_size=10000, "
1614
     "time_granularity=1.0, time_resolution=1e6, query=None)\n"
1615
     "--\n\n"
1616
     "Iterate over aggregation data as dfanalyzer-compatible Arrow batches.\n\n"
1617
     "Output schema matches dfanalyzer expectations with resolved hashes,\n"
1618
     "normalized time_range, and computed columns (proc_name, io_cat).\n\n"
1619
     "Args:\n"
1620
     "    type: 'events', 'profiles', or 'system'\n"
1621
     "    batch_size: Number of entries per batch (default 10000)\n"
1622
     "    time_granularity: Bucket width in seconds (default 1.0)\n"
1623
     "    time_resolution: Microseconds per output time unit (default 1e6)\n"
1624
     "    query: Optional query filter string (e.g., \"pid == 1234\")\n\n"
1625
     "Returns:\n"
1626
     "    Iterator over Arrow batch capsules.\n"},
1627
    {"iter_arrow_dfanalyzer_all",
1628
     (PyCFunction)Indexer_iter_arrow_dfanalyzer_all,
1629
     METH_VARARGS | METH_KEYWORDS,
1630
     "iter_arrow_dfanalyzer_all(batch_size=10000, time_granularity=1.0, "
1631
     "time_resolution=1e6, query=None, group_by=None)\n"
1632
     "--\n\n"
1633
     "Iterate over all aggregation types in a single scan.\n\n"
1634
     "Returns a dict with 'events', 'profiles', 'system' keys, each "
1635
     "containing\n"
1636
     "a list of Arrow batch capsules. This is ~3x faster than calling\n"
1637
     "iter_arrow_dfanalyzer separately for each type.\n\n"
1638
     "When group_by is provided, the scan collapses dimensions during "
1639
     "aggregation\n"
1640
     "and emits a reduced schema containing only the requested columns plus\n"
1641
     "aggregated metrics (count, time, size, time_sq, size_sq, time_min,\n"
1642
     "time_max, size_min, size_max, time_call_min, time_call_max, "
1643
     "size_call_min,\n"
1644
     "size_call_max, time_start, time_end). Supported group_by columns: "
1645
     "cat,\n"
1646
     "func_name, pid, tid, file_hash, host_hash, file_name, host_name, "
1647
     "proc_name,\n"
1648
     "io_cat, acc_pat, time_range.\n\n"
1649
     "Args:\n"
1650
     "    batch_size: Number of entries per batch (default 10000)\n"
1651
     "    time_granularity: Bucket width in seconds (default 1.0)\n"
1652
     "    time_resolution: Microseconds per output time unit (default 1e6)\n"
1653
     "    query: Optional query filter string\n"
1654
     "    group_by: Optional list of columns to group by; enables coarse\n"
1655
     "        in-scan aggregation (default None = full granularity)\n\n"
1656
     "Returns:\n"
1657
     "    dict with 'events', 'profiles', 'system' lists of Arrow capsules.\n"},
1658
#endif
1659
    {nullptr}};
1660

1661
static PyGetSetDef Indexer_getsetters[] = {{nullptr}};
1662

1663
PyTypeObject IndexerType = {
1664
    PyVarObject_HEAD_INIT(nullptr, 0) "dftracer_utils_ext.Indexer",
1665
    sizeof(IndexerObject),
1666
    0,
1667
    (destructor)Indexer_dealloc,
1668
    0,
1669
    0,
1670
    0,
1671
    0,
1672
    0,
1673
    0,
1674
    0,
1675
    0,
1676
    0,
1677
    0,
1678
    0,
1679
    0,
1680
    0,
1681
    0,
1682
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
1683
    "BatchIndexer(directory='', files=None, index_dir='',\n"
1684
    "             require_checkpoint=True, require_bloom=True,\n"
1685
    "             require_manifest=True, require_aggregation=False,\n"
1686
    "             time_interval_ms=5000.0, group_keys=None,\n"
1687
    "             custom_metric_fields=None, compute_percentiles=False,\n"
1688
    "             parallelism=0, force_rebuild=False, runtime=None)\n"
1689
    "--\n\n"
1690
    "Indexer with tiered index building.\n\n"
1691
    "At least one of 'directory' or 'files' must be provided.\n"
1692
    "- directory: scan for .pfw/.pfw.gz files\n"
1693
    "- files: list of specific file paths\n\n"
1694
    "Supports:\n"
1695
    "- Tier 1: Checkpoints (require_checkpoint)\n"
1696
    "- Tier 2: Bloom filters (require_bloom), Manifests (require_manifest)\n"
1697
    "- Tier 3: Aggregation (require_aggregation + config params)\n",
1698
    0,
1699
    0,
1700
    0,
1701
    0,
1702
    0,
1703
    0,
1704
    Indexer_methods,
1705
    0,
1706
    Indexer_getsetters,
1707
    0,
1708
    0,
1709
    0,
1710
    0,
1711
    0,
1712
    (initproc)Indexer_init,
1713
    0,
1714
    Indexer_new,
1715
};
1716

1717
int init_indexer(PyObject* m) {
2✔
1718
    if (register_type(m, &IndexerType, "Indexer") < 0) return -1;
2✔
1719

1720
#ifdef DFTRACER_UTILS_ENABLE_ARROW
1721
    if (PyModule_AddFunctions(m, BatchIndexerModuleMethods) < 0) return -1;
2✔
1722
#endif
1723

1724
    return 0;
2✔
1725
}
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