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

llnl / dftracer-utils / 30074477855

24 Jul 2026 07:08AM UTC coverage: 52.727% (+0.07%) from 52.66%
30074477855

Pull #99

github

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

42091 of 103330 branches covered (40.73%)

Branch coverage included in aggregate %.

2472 of 3143 new or added lines in 65 files covered. (78.65%)

146 existing lines in 14 files now uncovered.

37235 of 47117 relevant lines covered (79.03%)

68905.24 hits per line

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

56.49
/src/dftracer/utils/python/trace_reader.cpp
1
#define PY_SSIZE_T_CLEAN
2
#include <Python.h>
3
#include <dftracer/utils/core/common/config.h>
4
#include <dftracer/utils/core/common/constants.h>
5
#include <dftracer/utils/core/common/filesystem.h>
6
#include <dftracer/utils/core/common/memory_budget.h>
7
#include <dftracer/utils/core/coro/channel.h>
8
#include <dftracer/utils/core/coro/task.h>
9
#include <dftracer/utils/core/coro/when_all.h>
10
#include <dftracer/utils/core/tasks/coro_scope.h>
11
#include <dftracer/utils/core/utils/string.h>
12
#include <dftracer/utils/python/arrow_helpers.h>
13
#include <dftracer/utils/python/batch_byte_size.h>
14
#include <dftracer/utils/python/json.h>
15
#include <dftracer/utils/python/py_dict_helpers.h>
16
#include <dftracer/utils/python/py_errors.h>
17
#include <dftracer/utils/python/py_list_helpers.h>
18
#include <dftracer/utils/python/py_runtime_mixin.h>
19
#include <dftracer/utils/python/py_type_helpers.h>
20
#include <dftracer/utils/python/runtime.h>
21
#include <dftracer/utils/python/trace_reader.h>
22
#include <dftracer/utils/python/trace_reader_iterator.h>
23
#include <dftracer/utils/utilities/common/query/query.h>
24
#include <dftracer/utils/utilities/composites/dft/indexing/chunk_pruner_utility.h>
25
#include <dftracer/utils/utilities/composites/dft/internal/utils.h>
26
#include <dftracer/utils/utilities/filesystem/pattern_directory_scanner_utility.h>
27
#include <dftracer/utils/utilities/indexer/index_database.h>
28
#include <dftracer/utils/utilities/indexer/internal/helpers.h>
29
#include <dftracer/utils/utilities/reader/internal/arrow_row_builder.h>
30
#include <dftracer/utils/utilities/reader/internal/chunk_geometry.h>
31
#include <dftracer/utils/utilities/reader/internal/json_dict_builder.h>
32
#include <dftracer/utils/utilities/reader/trace_reader.h>
33

34
#include <algorithm>
35
#include <cctype>
36
#include <cstddef>
37
#include <cstdio>
38
#include <cstring>
39
#include <exception>
40
#include <memory>
41
#include <string>
42
#include <unordered_map>
43
#include <vector>
44
#ifdef DFTRACER_UTILS_ENABLE_ARROW
45
#include <dftracer/utils/python/arrow_stream_capsule.h>
46
#include <dftracer/utils/utilities/common/arrow/column_builder.h>
47
#include <dftracer/utils/utilities/common/json/parser.h>
48
#endif
49
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
50
#include <dftracer/utils/utilities/common/arrow/ipc_writer.h>
51
#include <dftracer/utils/utilities/common/arrow/partition_writer.h>
52
#include <dftracer/utils/utilities/composites/dft/internal/utils.h>
53
#include <dftracer/utils/utilities/composites/dft/metadata_collector_utility.h>
54
#include <dftracer/utils/utilities/composites/dft/views/view_builder_utility.h>
55
#include <dftracer/utils/utilities/composites/dft/views/view_definition.h>
56
#include <dftracer/utils/utilities/composites/dft/views/view_reader_utility.h>
57
#endif
58

59
namespace {
60

61
using dftracer::utils::CoroScope;
62
using dftracer::utils::Runtime;
63
using dftracer::utils::coro::CoroTask;
64
using dftracer::utils::coro::when_all;
65
using dftracer::utils::utilities::filesystem::PatternDirectoryScannerUtility;
66
using dftracer::utils::utilities::filesystem::
67
    PatternDirectoryScannerUtilityInput;
68
using dftracer::utils::utilities::reader::ReadConfig;
69
using dftracer::utils::utilities::reader::TraceReader;
70
using dftracer::utils::utilities::reader::TraceReaderConfig;
71
#ifdef DFTRACER_UTILS_ENABLE_ARROW
72
using dftracer::utils::utilities::common::arrow::RecordBatchBuilder;
73
#endif
74
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
75
using dftracer::utils::utilities::common::arrow::IpcCompression;
76
using dftracer::utils::utilities::common::arrow::PartitionWriter;
77
using dftracer::utils::utilities::common::arrow::PartitionWriteStats;
78
using dftracer::utils::utilities::composites::dft::MetadataCollectorUtility;
79
using dftracer::utils::utilities::composites::dft::
80
    MetadataCollectorUtilityInput;
81
using dftracer::utils::utilities::composites::dft::views::ViewBuilderInput;
82
using dftracer::utils::utilities::composites::dft::views::ViewBuilderUtility;
83
using dftracer::utils::utilities::composites::dft::views::ViewDefinition;
84
using dftracer::utils::utilities::composites::dft::views::ViewReaderInput;
85
using dftracer::utils::utilities::composites::dft::views::ViewReaderUtility;
86
#endif
87

88
using dftracer::utils::python::MemoryViewBatchData;
89
using dftracer::utils::python::MemoryViewBatchIteratorState;
90

91
CoroTask<void> produce_lines_batched(
4,724!
92
    std::shared_ptr<MemoryViewBatchIteratorState> state,
93
    dftracer::utils::coro::ChannelProducer<MemoryViewBatchData> producer,
94
    TraceReaderConfig cfg, ReadConfig rc, std::size_t batch_size) {
58!
95
    auto guard = producer.guard();
58!
96
    try {
97
        TraceReader reader(std::move(cfg));
58!
98
        auto gen = reader.read_lines(rc);
58!
99
        MemoryViewBatchData batch;
58✔
100
        std::size_t count = 0;
58✔
101

102
        while (auto opt = co_await gen.next()) {
4,232!
103
            if (state->cancelled.load(std::memory_order_acquire)) break;
986!
104
            auto sv = opt->content;
986✔
105
            Py_ssize_t offset = static_cast<Py_ssize_t>(batch.buffer.size());
986✔
106
            batch.buffer.insert(batch.buffer.end(), sv.begin(), sv.end());
986!
107
            batch.offsets.push_back(offset);
986!
108
            batch.lengths.push_back(static_cast<Py_ssize_t>(sv.size()));
986!
109
            ++count;
986✔
110

111
            if (count >= batch_size) {
986!
112
                auto batch_bytes = dftracer::utils::python::byte_size(batch);
×
113
                state->bytes_in_queue.fetch_add(batch_bytes,
114
                                                std::memory_order_acq_rel);
115
                if (!co_await producer.send(std::move(batch))) break;
×
116
                batch = MemoryViewBatchData{};
117
                count = 0;
118
            }
×
119
        }
1,044!
120
        if (count > 0 && !state->cancelled.load(std::memory_order_acquire)) {
159✔
121
            auto batch_bytes = dftracer::utils::python::byte_size(batch);
162!
122
            state->bytes_in_queue.fetch_add(batch_bytes,
162✔
123
                                            std::memory_order_acq_rel);
124
            co_await producer.send(std::move(batch));
216!
125
        }
54!
126
    } catch (...) {
2,456✔
127
        state->set_error(std::current_exception());
1!
128
    }
1!
129
}
6,751!
130

131
CoroTask<void> produce_raw_batched(
100!
132
    std::shared_ptr<MemoryViewBatchIteratorState> state,
133
    dftracer::utils::coro::ChannelProducer<MemoryViewBatchData> producer,
134
    TraceReaderConfig cfg, ReadConfig rc) {
10!
135
    auto guard = producer.guard();
10!
136
    try {
137
        TraceReader reader(std::move(cfg));
10!
138
        auto gen = reader.read_raw(rc);
10!
139
        while (auto opt = co_await gen.next()) {
638!
140
            if (state->cancelled.load(std::memory_order_acquire)) break;
442!
141
            MemoryViewBatchData batch;
442✔
142
            batch.buffer.assign(opt->data(), opt->data() + opt->size());
442!
143
            batch.offsets.push_back(0);
442!
144
            batch.lengths.push_back(static_cast<Py_ssize_t>(opt->size()));
442!
145
            auto batch_bytes = dftracer::utils::python::byte_size(batch);
442✔
146
            state->bytes_in_queue.fetch_add(batch_bytes,
442✔
147
                                            std::memory_order_acq_rel);
148
            if (!co_await producer.send(std::move(batch))) break;
589!
149
        }
451✔
150
    } catch (...) {
30✔
151
        state->set_error(std::current_exception());
×
152
    }
×
153
}
1,266!
154

155
using dftracer::utils::utilities::composites::dft::TimeScaleState;
156
using dftracer::utils::utilities::reader::time_normalization_target;
157
using dftracer::utils::utilities::reader::TimeNormalization;
158
using dftracer::utils::utilities::reader::internal::parse_json_to_event;
159

160
// Map a Python normalize_time string to the reader enum. Empty/None/"native"
161
// mean no scaling. Returns false on an unrecognized value.
162
static bool parse_normalize_time(const char *s, TimeNormalization &out) {
148✔
163
    if (!s || !*s) {
148!
164
        out = TimeNormalization::None;
128✔
165
        return true;
128✔
166
    }
167
    std::string v;
20✔
168
    for (const char *p = s; *p; ++p)
76✔
169
        v.push_back(
56!
170
            static_cast<char>(std::tolower(static_cast<unsigned char>(*p))));
56!
171
    if (v == "none" || v == "native") {
20!
NEW
172
        out = TimeNormalization::None;
×
173
    } else if (v == "ns" || v == "nanoseconds") {
20!
174
        out = TimeNormalization::Nanoseconds;
2✔
175
    } else if (v == "us" || v == "microseconds") {
19!
176
        out = TimeNormalization::Microseconds;
14✔
177
    } else if (v == "ms" || v == "milliseconds") {
11!
178
        out = TimeNormalization::Milliseconds;
2✔
179
    } else if (v == "s" || v == "sec" || v == "seconds") {
3!
NEW
180
        out = TimeNormalization::Seconds;
×
181
    } else {
182
        return false;
2✔
183
    }
184
    return true;
18✔
185
}
84✔
186

187
static constexpr std::size_t ESTIMATED_BYTES_PER_LINE = 256;
188
static constexpr std::size_t ESTIMATED_BYTES_PER_RAW_CHUNK = 4 * 1024 * 1024;
189
static constexpr std::size_t ESTIMATED_BYTES_PER_JSON_EVENT = 512;
190
static constexpr std::size_t ESTIMATED_BYTES_PER_ARROW_ROW = 1024;
191

192
CoroTask<void> produce_json_dicts(
206!
193
    std::shared_ptr<JsonDictIteratorState> state,
194
    dftracer::utils::coro::ChannelProducer<JsonDictBatch> producer,
195
    TraceReaderConfig cfg, ReadConfig rc, std::size_t batch_size) {
9!
196
    auto guard = producer.guard();
161!
197
    try {
198
        TraceReader reader(std::move(cfg));
161!
199
        TimeScaleState time_scale;
161✔
200
        if (rc.normalize_time != TimeNormalization::None) {
161✔
201
            time_scale.target = time_normalization_target(rc.normalize_time);
21!
202
            time_scale.metric = co_await reader.read_time_metric();
37!
203
        }
7✔
204
        auto gen = reader.read_json(rc);
147!
205
        JsonDictBatch batch;
147✔
206
        batch.events.reserve(batch_size);
147!
207

208
        while (auto opt = co_await gen.next()) {
240!
209
            if (state->cancelled.load(std::memory_order_acquire)) break;
51!
210

211
            JsonDictEvent ev;
51!
212
            parse_json_to_event(*opt->parser, ev, time_scale);
51!
213
            batch.events.push_back(std::move(ev));
51!
214

215
            if (batch.events.size() >= batch_size) {
51!
216
                auto batch_bytes = dftracer::utils::python::byte_size(batch);
×
217
                state->bytes_in_queue.fetch_add(batch_bytes,
218
                                                std::memory_order_acq_rel);
219
                if (!co_await producer.send(std::move(batch))) break;
×
220
                batch = JsonDictBatch{};
221
                batch.events.reserve(batch_size);
×
222
            }
×
223
        }
60!
224
        if (!batch.events.empty() &&
27✔
225
            !state->cancelled.load(std::memory_order_acquire)) {
9✔
226
            auto batch_bytes = dftracer::utils::python::byte_size(batch);
27!
227
            state->bytes_in_queue.fetch_add(batch_bytes,
27✔
228
                                            std::memory_order_acq_rel);
229
            co_await producer.send(std::move(batch));
36!
230
        }
9!
231
    } catch (...) {
45!
232
        state->set_error(std::current_exception());
×
233
    }
×
234
}
317!
235

236
static CoroTask<void> send_files_to_channel(
144!
237
    std::shared_ptr<dftracer::utils::coro::Channel<std::string>> file_chan,
238
    const std::vector<std::string> *files, std::atomic<bool> *cancelled) {
8!
239
    for (const auto &fp : *files) {
128✔
240
        if (cancelled->load(std::memory_order_acquire)) break;
72!
241
        if (!co_await file_chan->send(fp)) break;
104!
242
    }
24✔
243
    file_chan->close();
8!
244
    co_return;
8✔
245
}
112!
246

247
static CoroTask<void> json_dict_file_worker(
965!
248
    std::shared_ptr<dftracer::utils::coro::Channel<std::string>> file_chan,
249
    dftracer::utils::coro::Channel<JsonDictBatch> *out_chan,
250
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
251
    ReadConfig rc, std::size_t batch_size, std::atomic<bool> *cancelled) {
9!
252
    dftracer::utils::coro::ChannelProducer<JsonDictBatch> producer(out_chan);
9!
253
    auto guard = producer.guard();
9!
254

255
    while (auto file_path = co_await file_chan->receive()) {
59!
256
        if (cancelled->load(std::memory_order_acquire)) co_return;
488!
257
        TraceReaderConfig cfg;
488✔
258
        cfg.file_path = std::move(*file_path);
488✔
259
        cfg.index_dir = index_dir;
488!
260
        cfg.checkpoint_size = checkpoint_size;
488✔
261
        cfg.auto_build_index = auto_build_index;
488✔
262

263
        TraceReader reader(std::move(cfg));
488!
264
        TimeScaleState time_scale;
488✔
265
        if (rc.normalize_time != TimeNormalization::None) {
488!
266
            time_scale.target = time_normalization_target(rc.normalize_time);
×
267
            time_scale.metric = co_await reader.read_time_metric();
×
268
        }
269
        auto gen = reader.read_json(rc);
488!
270
        JsonDictBatch batch;
488✔
271
        batch.events.reserve(batch_size);
488!
272

273
        while (auto opt = co_await gen.next()) {
893!
274
            if (cancelled->load(std::memory_order_acquire)) co_return;
210!
275
            JsonDictEvent ev;
210!
276
            parse_json_to_event(*opt->parser, ev, time_scale);
210!
277
            batch.events.push_back(std::move(ev));
210!
278
            if (batch.events.size() >= batch_size) {
210!
279
                if (!co_await producer.send(std::move(batch))) co_return;
×
280
                batch = JsonDictBatch{};
281
                batch.events.reserve(batch_size);
×
282
            }
283
        }
224!
284
        if (!batch.events.empty()) {
42!
285
            if (!co_await producer.send(std::move(batch))) co_return;
56!
286
        }
14✔
287
    }
497!
288
    co_return;
9✔
289
}
1,466!
290

291
static CoroTask<void> spawn_json_dict_producers(
30!
292
    CoroScope &child, dftracer::utils::coro::Channel<JsonDictBatch> *out_chan,
293
    const std::vector<std::string> *files, const std::string *index_dir,
294
    std::size_t checkpoint_size, bool auto_build_index, const ReadConfig *rc,
295
    std::size_t batch_size, std::atomic<bool> *cancelled_ptr,
296
    std::size_t max_workers) {
5!
297
    std::size_t num_workers = std::min(files->size(), max_workers);
5!
298
    auto file_chan =
5✔
299
        dftracer::utils::coro::make_channel<std::string>(num_workers);
5!
300

301
    for (std::size_t i = 0; i < num_workers; ++i) {
14✔
302
        child.spawn([out_chan, fc = file_chan, idx = *index_dir,
27!
303
                     checkpoint_size, auto_build_index, r = *rc, batch_size,
27!
304
                     cancelled_ptr](CoroScope &) {
36✔
305
            return json_dict_file_worker(fc, out_chan, idx, checkpoint_size,
27!
306
                                         auto_build_index, r, batch_size,
18!
307
                                         cancelled_ptr);
18!
308
        });
309
    }
9✔
310

311
    child.spawn([fc = file_chan, files, cancelled_ptr](CoroScope &) {
15!
312
        return send_files_to_channel(fc, files, cancelled_ptr);
10!
313
    });
314
    co_return;
10✔
315
}
15!
316

317
static CoroTask<void> produce_json_dicts_parallel(
68!
318
    CoroScope &scope, JsonDictIteratorState *sp, std::string dir_path,
319
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
320
    ReadConfig rc, std::size_t batch_size, std::size_t max_workers) {
6!
321
    try {
322
        PatternDirectoryScannerUtility scanner;
18!
323
        auto scan_input = PatternDirectoryScannerUtilityInput(
36!
324
            dir_path, {".pfw", ".pfw.gz"}, true, false);
18!
325
        auto entries = co_await scope.spawn(scanner, scan_input);
30!
326

327
        std::vector<std::string> files;
16✔
328
        files.reserve(entries.size());
16✔
329
        for (auto &e : entries) files.push_back(e.path.string());
20!
330
        std::sort(files.begin(), files.end());
6✔
331

332
        if (files.empty()) {
16✔
333
            sp->channel->close();
1!
334
            co_return;
1✔
335
        }
336

337
        auto *chan_ptr = sp->channel.get();
15✔
338
        auto *cancelled_ptr = &sp->cancelled;
15✔
339

340
        co_await scope.scope([chan_ptr, &files, &index_dir, checkpoint_size,
120!
341
                              auto_build_index, &rc, batch_size, cancelled_ptr,
45✔
342
                              max_workers](CoroScope &child) -> CoroTask<void> {
20!
343
            co_await spawn_json_dict_producers(
40!
344
                child, chan_ptr, &files, &index_dir, checkpoint_size,
15✔
345
                auto_build_index, &rc, batch_size, cancelled_ptr, max_workers);
15✔
346
        });
20!
347
    } catch (...) {
16✔
348
        sp->set_error(std::current_exception());
×
349
    }
×
350
}
78!
351

352
static CoroTask<void> lines_file_worker(
544!
353
    std::shared_ptr<dftracer::utils::coro::Channel<std::string>> file_chan,
354
    dftracer::utils::coro::Channel<MemoryViewBatchData> *out_chan,
355
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
356
    ReadConfig rc, std::size_t batch_size, std::atomic<bool> *cancelled) {
4!
357
    dftracer::utils::coro::ChannelProducer<MemoryViewBatchData> producer(
8!
358
        out_chan);
4✔
359
    auto guard = producer.guard();
4!
360

361
    while (auto file_path = co_await file_chan->receive()) {
21!
362
        if (cancelled->load(std::memory_order_acquire)) co_return;
7!
363
        TraceReaderConfig cfg;
7✔
364
        cfg.file_path = std::move(*file_path);
7✔
365
        cfg.index_dir = index_dir;
7!
366
        cfg.checkpoint_size = checkpoint_size;
7✔
367
        cfg.auto_build_index = auto_build_index;
7✔
368

369
        TraceReader reader(std::move(cfg));
7!
370
        auto gen = reader.read_lines(rc);
7!
371
        MemoryViewBatchData batch;
7✔
372
        std::size_t count = 0;
7✔
373

374
        while (auto opt = co_await gen.next()) {
505!
375
            if (cancelled->load(std::memory_order_acquire)) co_return;
120!
376
            auto sv = opt->content;
120✔
377
            Py_ssize_t offset = static_cast<Py_ssize_t>(batch.buffer.size());
120✔
378
            batch.buffer.insert(batch.buffer.end(), sv.begin(), sv.end());
120!
379
            batch.offsets.push_back(offset);
120!
380
            batch.lengths.push_back(static_cast<Py_ssize_t>(sv.size()));
120!
381
            ++count;
120✔
382
            if (count >= batch_size) {
120!
383
                if (!co_await producer.send(std::move(batch))) co_return;
×
384
                batch = MemoryViewBatchData{};
385
                count = 0;
386
            }
387
        }
128!
388
        if (count > 0) {
21!
389
            if (!co_await producer.send(std::move(batch))) co_return;
28!
390
        }
7✔
391
    }
275✔
392
    co_return;
4✔
393
}
812!
394

395
static CoroTask<void> spawn_lines_producers(
12!
396
    CoroScope &child,
397
    dftracer::utils::coro::Channel<MemoryViewBatchData> *out_chan,
398
    const std::vector<std::string> *files, const std::string *index_dir,
399
    std::size_t checkpoint_size, bool auto_build_index, const ReadConfig *rc,
400
    std::size_t batch_size, std::atomic<bool> *cancelled_ptr,
401
    std::size_t max_workers) {
2!
402
    std::size_t num_workers = std::min(files->size(), max_workers);
2!
403
    auto file_chan =
2✔
404
        dftracer::utils::coro::make_channel<std::string>(num_workers);
2!
405

406
    for (std::size_t i = 0; i < num_workers; ++i) {
6✔
407
        child.spawn([out_chan, fc = file_chan, idx = *index_dir,
12!
408
                     checkpoint_size, auto_build_index, r = *rc, batch_size,
12!
409
                     cancelled_ptr](CoroScope &) {
16✔
410
            return lines_file_worker(fc, out_chan, idx, checkpoint_size,
12!
411
                                     auto_build_index, r, batch_size,
8!
412
                                     cancelled_ptr);
8!
413
        });
414
    }
4✔
415

416
    child.spawn([fc = file_chan, files, cancelled_ptr](CoroScope &) {
6!
417
        return send_files_to_channel(fc, files, cancelled_ptr);
4!
418
    });
419
    co_return;
4✔
420
}
6!
421

422
static CoroTask<void> produce_lines_parallel(
32!
423
    CoroScope &scope, MemoryViewBatchIteratorState *sp, std::string dir_path,
424
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
425
    ReadConfig rc, std::size_t batch_size, std::size_t max_workers) {
3!
426
    try {
427
        PatternDirectoryScannerUtility scanner;
9!
428
        auto scan_input = PatternDirectoryScannerUtilityInput(
18!
429
            dir_path, {".pfw", ".pfw.gz"}, true, false);
9!
430
        auto entries = co_await scope.spawn(scanner, scan_input);
15!
431

432
        std::vector<std::string> files;
7✔
433
        files.reserve(entries.size());
7✔
434
        for (auto &e : entries) files.push_back(e.path.string());
10!
435
        std::sort(files.begin(), files.end());
3✔
436

437
        if (files.empty()) {
7✔
438
            sp->channel->close();
1!
439
            co_return;
1✔
440
        }
441

442
        auto *chan_ptr = sp->channel.get();
6✔
443
        auto *cancelled_ptr = &sp->cancelled;
6✔
444

445
        co_await scope.scope([chan_ptr, &files, &index_dir, checkpoint_size,
48!
446
                              auto_build_index, &rc, batch_size, cancelled_ptr,
18✔
447
                              max_workers](CoroScope &child) -> CoroTask<void> {
8!
448
            co_await spawn_lines_producers(
16!
449
                child, chan_ptr, &files, &index_dir, checkpoint_size,
6✔
450
                auto_build_index, &rc, batch_size, cancelled_ptr, max_workers);
6✔
451
        });
8!
452
    } catch (...) {
7✔
453
        sp->set_error(std::current_exception());
×
454
    }
×
455
}
36!
456

457
static CoroTask<void> raw_file_worker(
20!
458
    std::shared_ptr<dftracer::utils::coro::Channel<std::string>> file_chan,
459
    dftracer::utils::coro::Channel<MemoryViewBatchData> *out_chan,
460
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
461
    ReadConfig rc, std::atomic<bool> *cancelled) {
2!
462
    dftracer::utils::coro::ChannelProducer<MemoryViewBatchData> producer(
4!
463
        out_chan);
2✔
464
    auto guard = producer.guard();
2!
465

466
    while (auto file_path = co_await file_chan->receive()) {
13!
467
        if (cancelled->load(std::memory_order_acquire)) co_return;
2!
468
        TraceReaderConfig cfg;
2✔
469
        cfg.file_path = std::move(*file_path);
2✔
470
        cfg.index_dir = index_dir;
2!
471
        cfg.checkpoint_size = checkpoint_size;
2✔
472
        cfg.auto_build_index = auto_build_index;
2✔
473

474
        TraceReader reader(std::move(cfg));
2!
475
        auto gen = reader.read_raw(rc);
2!
476
        while (auto opt = co_await gen.next()) {
252!
477
            if (cancelled->load(std::memory_order_acquire)) co_return;
180!
478
            MemoryViewBatchData batch;
180✔
479
            batch.buffer.assign(opt->data(), opt->data() + opt->size());
180!
480
            batch.offsets.push_back(0);
180!
481
            batch.lengths.push_back(static_cast<Py_ssize_t>(opt->size()));
180!
482
            if (!co_await producer.send(std::move(batch))) co_return;
240!
483
        }
183!
484
    }
13✔
485
    co_return;
2✔
486
}
510!
487

488
static CoroTask<void> spawn_raw_producers(
6!
489
    CoroScope &child,
490
    dftracer::utils::coro::Channel<MemoryViewBatchData> *out_chan,
491
    const std::vector<std::string> *files, const std::string *index_dir,
492
    std::size_t checkpoint_size, bool auto_build_index, const ReadConfig *rc,
493
    std::atomic<bool> *cancelled_ptr, std::size_t max_workers) {
1!
494
    std::size_t num_workers = std::min(files->size(), max_workers);
1!
495
    auto file_chan =
1✔
496
        dftracer::utils::coro::make_channel<std::string>(num_workers);
1!
497

498
    for (std::size_t i = 0; i < num_workers; ++i) {
3✔
499
        child.spawn([out_chan, fc = file_chan, idx = *index_dir,
6!
500
                     checkpoint_size, auto_build_index, r = *rc,
6!
501
                     cancelled_ptr](CoroScope &) {
8✔
502
            return raw_file_worker(fc, out_chan, idx, checkpoint_size,
6!
503
                                   auto_build_index, r, cancelled_ptr);
4!
504
        });
505
    }
2✔
506

507
    child.spawn([fc = file_chan, files, cancelled_ptr](CoroScope &) {
3!
508
        return send_files_to_channel(fc, files, cancelled_ptr);
2!
509
    });
510
    co_return;
2✔
511
}
3!
512

513
static CoroTask<void> produce_raw_parallel(
20!
514
    CoroScope &scope, MemoryViewBatchIteratorState *sp, std::string dir_path,
515
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
516
    ReadConfig rc, std::size_t max_workers) {
2!
517
    try {
518
        PatternDirectoryScannerUtility scanner;
6!
519
        auto scan_input = PatternDirectoryScannerUtilityInput(
12!
520
            dir_path, {".pfw", ".pfw.gz"}, true, false);
6!
521
        auto entries = co_await scope.spawn(scanner, scan_input);
10!
522

523
        std::vector<std::string> files;
4✔
524
        files.reserve(entries.size());
4✔
525
        for (auto &e : entries) files.push_back(e.path.string());
5!
526
        std::sort(files.begin(), files.end());
2✔
527

528
        if (files.empty()) {
4✔
529
            sp->channel->close();
1!
530
            co_return;
1✔
531
        }
532

533
        auto *chan_ptr = sp->channel.get();
3✔
534
        auto *cancelled_ptr = &sp->cancelled;
3✔
535

536
        co_await scope.scope([chan_ptr, &files, &index_dir, checkpoint_size,
21!
537
                              auto_build_index, &rc, cancelled_ptr,
6✔
538
                              max_workers](CoroScope &child) -> CoroTask<void> {
4!
539
            co_await spawn_raw_producers(child, chan_ptr, &files, &index_dir,
8!
540
                                         checkpoint_size, auto_build_index, &rc,
3✔
541
                                         cancelled_ptr, max_workers);
3✔
542
        });
4!
543
    } catch (...) {
4✔
544
        sp->set_error(std::current_exception());
×
545
    }
×
546
}
22!
547

548
#ifdef DFTRACER_UTILS_ENABLE_ARROW
549

550
using dftracer::utils::utilities::common::arrow::ArrowExportResult;
551
using dftracer::utils::utilities::common::arrow::RecordBatchBuilder;
552

553
using dftracer::utils::StringArena;
554
using dftracer::utils::utilities::reader::TimeNormalization;
555
using dftracer::utils::utilities::reader::internal::build_arrow_row;
556
using dftracer::utils::utilities::reader::internal::TimeScaleState;
557

558
static CoroTask<void> produce_arrow_for_file(
×
559
    dftracer::utils::coro::Channel<ArrowExportResult> *chan,
560
    std::string file_path, std::string index_dir, std::size_t checkpoint_size,
561
    bool auto_build_index, ReadConfig rc, std::size_t batch_size,
562
    bool normalize, std::atomic<bool> *cancelled) {
563
    dftracer::utils::coro::ChannelProducer<ArrowExportResult> producer(chan);
564
    auto guard = producer.guard();
565

566
    TraceReaderConfig cfg;
567
    cfg.file_path = std::move(file_path);
568
    cfg.index_dir = std::move(index_dir);
569
    cfg.checkpoint_size = checkpoint_size;
570
    cfg.auto_build_index = auto_build_index;
571

572
    TraceReader reader(std::move(cfg));
573

574
    // Fast path: non-normalized Arrow build happens inside TraceReader.
575
    // Normalize still goes through read_json + build_arrow_row for the
576
    // richer schema derivation.
577
    if (!normalize) {
578
        auto batch_gen = reader.read_arrow(rc, batch_size);
579
        while (auto batch_opt = co_await batch_gen.next()) {
580
            if (cancelled->load(std::memory_order_acquire)) co_return;
581
            if (!co_await producer.send(std::move(*batch_opt))) co_return;
582
        }
583
        co_return;
584
    }
585

586
    auto gen = reader.read_json(rc);
587
    RecordBatchBuilder builder;
588
    builder.reserve(batch_size);
589
    StringArena arena;
590
    TimeScaleState time_scale;
591
    if (rc.normalize_time != TimeNormalization::None) {
592
        time_scale.target = time_normalization_target(rc.normalize_time);
593
        time_scale.metric = co_await reader.read_time_metric();
594
    }
595

596
    while (auto opt = co_await gen.next()) {
597
        if (cancelled->load(std::memory_order_acquire)) co_return;
598
        if (!build_arrow_row(builder, *opt->parser, arena, normalize,
599
                             time_scale))
600
            continue;
601
        if (builder.num_rows() >= batch_size) {
602
            auto result = builder.finish();
603
            arena.clear();
604
            if (!co_await producer.send(std::move(result))) co_return;
605
            if (!builder.is_schema_locked()) builder.lock_schema();
606
            builder.reset(true);
607
            builder.reserve(batch_size);
608
        }
609
    }
610
    if (builder.num_rows() > 0) {
611
        co_await producer.send(builder.finish());
612
    }
613
    co_return;
614
}
×
615

616
static CoroTask<void> file_worker(
×
617
    std::shared_ptr<dftracer::utils::coro::Channel<std::string>> file_chan,
618
    dftracer::utils::coro::Channel<ArrowExportResult> *out_chan,
619
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
620
    ReadConfig rc, std::size_t batch_size, bool normalize,
621
    std::atomic<bool> *cancelled) {
622
    dftracer::utils::coro::ChannelProducer<ArrowExportResult> producer(
623
        out_chan);
624
    auto guard = producer.guard();
625

626
    while (auto file_path = co_await file_chan->receive()) {
627
        if (cancelled->load(std::memory_order_acquire)) co_return;
628
        TraceReaderConfig cfg;
629
        cfg.file_path = std::move(*file_path);
630
        cfg.index_dir = index_dir;
631
        cfg.checkpoint_size = checkpoint_size;
632
        cfg.auto_build_index = auto_build_index;
633

634
        TraceReader reader(std::move(cfg));
635

636
        if (!normalize) {
637
            auto batch_gen = reader.read_arrow(rc, batch_size);
638
            while (auto batch_opt = co_await batch_gen.next()) {
639
                if (cancelled->load(std::memory_order_acquire)) co_return;
640
                if (!co_await producer.send(std::move(*batch_opt))) co_return;
641
            }
642
            continue;
643
        }
644

645
        auto gen = reader.read_json(rc);
646
        RecordBatchBuilder builder;
647
        builder.reserve(batch_size);
648
        StringArena arena;
649
        TimeScaleState time_scale;
650
        if (rc.normalize_time != TimeNormalization::None) {
651
            time_scale.target = time_normalization_target(rc.normalize_time);
652
            time_scale.metric = co_await reader.read_time_metric();
653
        }
654

655
        while (auto opt = co_await gen.next()) {
656
            if (cancelled->load(std::memory_order_acquire)) co_return;
657
            if (!build_arrow_row(builder, *opt->parser, arena, normalize,
658
                                 time_scale))
659
                continue;
660
            if (builder.num_rows() >= batch_size) {
661
                auto result = builder.finish();
662
                arena.clear();
663
                if (!co_await producer.send(std::move(result))) co_return;
664
                if (!builder.is_schema_locked()) builder.lock_schema();
665
                builder.reset(true);
666
                builder.reserve(batch_size);
667
            }
668
        }
669
        if (builder.num_rows() > 0) {
670
            if (!co_await producer.send(builder.finish())) co_return;
671
        }
672
    }
673
    co_return;
674
}
×
675

676
using dftracer::utils::utilities::reader::internal::ArrowWorkItem;
677
using dftracer::utils::utilities::reader::internal::enumerate_work_items;
678

679
static CoroTask<void> send_work_items_to_channel(
464!
680
    std::shared_ptr<dftracer::utils::coro::Channel<ArrowWorkItem>> chan,
681
    const std::vector<ArrowWorkItem> *items, std::atomic<bool> *cancelled) {
36!
682
    for (const auto &it : *items) {
346✔
683
        if (cancelled->load(std::memory_order_acquire)) break;
186!
684
        if (!co_await chan->send(it)) break;
284!
685
    }
62✔
686
    chan->close();
36!
687
    co_return;
36✔
688
}
320!
689

690
static CoroTask<void> checkpoint_worker(
320!
691
    std::shared_ptr<dftracer::utils::coro::Channel<ArrowWorkItem>> work_chan,
692
    dftracer::utils::coro::Channel<ArrowExportResult> *out_chan,
693
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
694
    ReadConfig rc, std::size_t batch_size, bool normalize,
695
    std::atomic<bool> *cancelled) {
52✔
696
    dftracer::utils::coro::ChannelProducer<ArrowExportResult> producer(
104!
697
        out_chan);
52✔
698
    auto guard = producer.guard();
52!
699

700
    // Cache readers keyed by file path so we don't re-probe the same file
701
    // when successive work items land on it.
702
    std::unordered_map<std::string, std::shared_ptr<TraceReader>> readers;
52✔
703

704
    while (auto item = co_await work_chan->receive()) {
346!
705
        if (cancelled->load(std::memory_order_acquire)) co_return;
186!
706

707
        auto &reader_ptr = readers[item->file_path];
186!
708
        if (!reader_ptr) {
186✔
709
            TraceReaderConfig cfg;
62✔
710
            cfg.file_path = item->file_path;
62!
711
            cfg.index_dir = index_dir;
62!
712
            cfg.checkpoint_size = checkpoint_size;
62✔
713
            cfg.auto_build_index = auto_build_index;
62✔
714
            reader_ptr = std::make_shared<TraceReader>(std::move(cfg));
62!
715
        }
62✔
716

717
        ReadConfig local_rc = rc;
186!
718
        if (item->start_line > 0 || item->end_line > 0) {
186!
719
            // Line-range work items: the read drives off LINE_RANGE; the
720
            // gzip stream resolves it back to byte offsets via checkpoints.
721
            local_rc.start_line = item->start_line;
127✔
722
            local_rc.end_line = item->end_line;
127✔
723
            local_rc.start_byte = 0;
127✔
724
            local_rc.end_byte = 0;
127✔
725
            local_rc.start_at_checkpoint = false;
127✔
726
            local_rc.end_at_checkpoint = false;
127✔
727
        } else {
127✔
728
            local_rc.start_byte = item->start_byte;
59✔
729
            local_rc.end_byte = item->end_byte;
59✔
730
            local_rc.start_at_checkpoint = item->start_at_checkpoint;
59✔
731
            local_rc.end_at_checkpoint = item->end_at_checkpoint;
59✔
732
        }
733
        // Pruning already happened at enumeration time; avoid the per-
734
        // work-item RocksDB opens that would otherwise dwarf the actual
735
        // read cost at directory scale (256 files * N ranges).
736
        local_rc.skip_pruning = true;
186✔
737
        // chunks pre-classified as uniform-matching skip per-event eval.
738
        if (item->chunk_prune_only) local_rc.chunk_prune_only = true;
186!
739

740
        if (!normalize) {
186!
741
            auto batch_gen = reader_ptr->read_arrow(local_rc, batch_size);
186!
742
            while (auto batch_opt = co_await batch_gen.next()) {
571!
743
                if (cancelled->load(std::memory_order_acquire)) co_return;
243!
744
                if (!co_await producer.send(std::move(*batch_opt))) co_return;
323!
745
            }
304!
746
            continue;
62✔
747
        }
62✔
748

749
        auto gen = reader_ptr->read_json(local_rc);
×
750
        RecordBatchBuilder builder;
×
751
        builder.reserve(batch_size);
×
752
        StringArena arena;
×
753
        TimeScaleState time_scale;
754
        if (local_rc.normalize_time != TimeNormalization::None) {
×
755
            time_scale.target =
756
                time_normalization_target(local_rc.normalize_time);
×
757
            time_scale.metric = co_await reader_ptr->read_time_metric();
×
758
        }
759

760
        while (auto opt = co_await gen.next()) {
×
761
            if (cancelled->load(std::memory_order_acquire)) co_return;
×
762
            if (!build_arrow_row(builder, *opt->parser, arena, normalize,
×
763
                                 time_scale))
764
                continue;
765
            if (builder.num_rows() >= batch_size) {
×
766
                auto result = builder.finish();
×
767
                arena.clear();
×
768
                if (!co_await producer.send(std::move(result))) co_return;
×
769
                if (!builder.is_schema_locked()) builder.lock_schema();
×
770
                builder.reset(true);
×
771
                builder.reserve(batch_size);
×
772
            }
×
773
        }
×
774
        if (builder.num_rows() > 0) {
×
775
            if (!co_await producer.send(builder.finish())) co_return;
×
776
        }
777
    }
238!
778
    co_return;
52✔
779
}
1,293!
780

781
static CoroTask<void> spawn_arrow_producers(
216!
782
    CoroScope &child,
783
    dftracer::utils::coro::Channel<ArrowExportResult> *out_chan,
784
    const std::vector<ArrowWorkItem> *work_items, const std::string *index_dir,
785
    std::size_t checkpoint_size, bool auto_build_index, const ReadConfig *rc,
786
    std::size_t batch_size, bool normalize, std::atomic<bool> *cancelled_ptr,
787
    std::size_t max_workers) {
36!
788
    std::size_t num_workers = std::min(work_items->size(), max_workers);
36!
789
    if (num_workers == 0) num_workers = 1;
36!
790
    auto work_chan =
36✔
791
        dftracer::utils::coro::make_channel<ArrowWorkItem>(num_workers);
36!
792

793
    for (std::size_t i = 0; i < num_workers; ++i) {
88✔
794
        child.spawn([out_chan, wc = work_chan, idx = *index_dir,
155!
795
                     checkpoint_size, auto_build_index, r = *rc, batch_size,
156!
796
                     normalize, cancelled_ptr](CoroScope &) {
258✔
797
            return checkpoint_worker(wc, out_chan, idx, checkpoint_size,
153!
798
                                     auto_build_index, r, batch_size, normalize,
103!
799
                                     cancelled_ptr);
103!
800
        });
801
    }
52✔
802

803
    child.spawn([wc = work_chan, work_items, cancelled_ptr](CoroScope &) {
108!
804
        return send_work_items_to_channel(wc, work_items, cancelled_ptr);
72!
805
    });
806
    co_return;
72✔
807
}
108!
808

809
static CoroTask<void> produce_arrow_batches_for_files(
294!
810
    CoroScope &scope, ArrowIteratorState *sp, std::vector<std::string> files,
811
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
812
    ReadConfig rc, std::size_t batch_size, bool normalize,
813
    std::size_t max_workers) {
37!
814
    try {
815
        if (files.empty()) {
109✔
816
            sp->channel->close();
1!
817
            co_return;
38✔
818
        }
819

820
        auto work_items = enumerate_work_items(
216!
821
            files, index_dir, rc.query, max_workers, rc.start_byte, rc.end_byte,
108✔
822
            rc.start_line, rc.end_line);
108✔
823
        if (work_items.empty()) {
108!
824
            sp->channel->close();
×
825
            co_return;
826
        }
827

828
        auto *chan_ptr = sp->channel.get();
108✔
829
        auto *cancelled_ptr = &sp->cancelled;
108✔
830

831
        co_await scope.scope([chan_ptr, &work_items, &index_dir,
1,080!
832
                              checkpoint_size, auto_build_index, &rc,
216✔
833
                              batch_size, normalize, cancelled_ptr,
324✔
834
                              max_workers](CoroScope &child) -> CoroTask<void> {
144!
835
            co_await spawn_arrow_producers(
288!
836
                child, chan_ptr, &work_items, &index_dir, checkpoint_size,
108✔
837
                auto_build_index, &rc, batch_size, normalize, cancelled_ptr,
108✔
838
                max_workers);
108✔
839
        });
144!
840
    } catch (...) {
36!
841
        sp->set_error(std::current_exception());
×
842
    }
×
843
}
146!
844

845
static CoroTask<void> produce_arrow_batches_parallel(
180!
846
    CoroScope &scope, ArrowIteratorState *sp, std::string dir_path,
847
    std::string index_dir, std::size_t checkpoint_size, bool auto_build_index,
848
    ReadConfig rc, std::size_t batch_size, bool normalize,
849
    std::size_t max_workers) {
15!
850
    try {
851
        PatternDirectoryScannerUtility scanner;
45!
852
        auto scan_input = PatternDirectoryScannerUtilityInput(
90!
853
            dir_path, {".pfw", ".pfw.gz"}, true, false);
45!
854
        auto entries = co_await scope.spawn(scanner, scan_input);
75!
855

856
        std::vector<std::string> files;
45✔
857
        files.reserve(entries.size());
45✔
858
        for (auto &e : entries) files.push_back(e.path.string());
54!
859
        std::sort(files.begin(), files.end());
15✔
860

861
        co_await produce_arrow_batches_for_files(
105!
862
            scope, sp, std::move(files), std::move(index_dir), checkpoint_size,
45✔
863
            auto_build_index, std::move(rc), batch_size, normalize,
45✔
864
            max_workers);
45✔
865
    } catch (...) {
45✔
866
        sp->set_error(std::current_exception());
×
867
    }
×
868
}
210!
869

870
CoroTask<void> produce_arrow_batches(
384!
871
    std::shared_ptr<ArrowIteratorState> state,
872
    dftracer::utils::coro::ChannelProducer<ArrowExportResult> producer,
873
    TraceReaderConfig cfg, ReadConfig rc, std::size_t batch_size,
874
    bool flatten_objects = false, bool normalize = false) {
21!
875
    (void)flatten_objects;
876

877
    auto guard = producer.guard();
59!
878
    try {
879
        TraceReader reader(std::move(cfg));
59!
880

881
        if (!normalize) {
59✔
882
            auto batch_gen = reader.read_arrow(rc, batch_size);
51!
883
            while (auto batch_opt = co_await batch_gen.next()) {
217!
884
                if (state->cancelled.load(std::memory_order_acquire)) break;
97✔
885
                auto result_bytes =
96✔
886
                    dftracer::utils::python::byte_size(*batch_opt);
96!
887
                state->bytes_in_queue.fetch_add(result_bytes,
96✔
888
                                                std::memory_order_acq_rel);
889
                if (!co_await producer.send(std::move(*batch_opt))) break;
128!
890
            }
113!
891
            co_return;
17✔
892
        }
17✔
893

894
        auto gen = reader.read_json(rc);
8!
895
        RecordBatchBuilder builder;
8!
896
        builder.reserve(batch_size);
8!
897

898
        StringArena arena;
8!
899
        TimeScaleState time_scale;
8✔
900
        if (rc.normalize_time != TimeNormalization::None) {
8✔
901
            time_scale.target = time_normalization_target(rc.normalize_time);
6!
902
            time_scale.metric = co_await reader.read_time_metric();
8!
903
        }
2✔
904

905
        while (auto opt = co_await gen.next()) {
204!
906
            if (state->cancelled.load(std::memory_order_acquire)) break;
47!
907
            if (!build_arrow_row(builder, *opt->parser, arena, normalize,
47!
908
                                 time_scale))
909
                continue;
910

911
            if (builder.num_rows() >= batch_size) {
47!
912
                auto result = builder.finish();
×
913
                arena.clear();
×
914
                auto result_bytes = dftracer::utils::python::byte_size(result);
×
915
                state->bytes_in_queue.fetch_add(result_bytes,
916
                                                std::memory_order_acq_rel);
917
                if (!co_await producer.send(std::move(result))) break;
×
918
                if (!builder.is_schema_locked()) {
×
919
                    builder.lock_schema();
920
                }
921
                builder.reset(true);
×
922
                builder.reserve(batch_size);
×
923
            }
×
924
        }
51✔
925

926
        if (builder.num_rows() > 0 &&
12✔
927
            !state->cancelled.load(std::memory_order_acquire)) {
4✔
928
            auto result = builder.finish();
12!
929
            auto result_bytes = dftracer::utils::python::byte_size(result);
12!
930
            state->bytes_in_queue.fetch_add(result_bytes,
12✔
931
                                            std::memory_order_acq_rel);
932
            co_await producer.send(std::move(result));
16!
933
        }
4!
934
    } catch (...) {
147✔
935
        state->set_error(std::current_exception());
×
936
    }
×
937
}
721!
938

939
#endif  // DFTRACER_UTILS_ENABLE_ARROW
940

941
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
942

943
struct WriteArrowStats {
39✔
944
    std::unordered_map<std::string, PartitionWriteStats> partitions;
945
    int64_t total_rows = 0;
39✔
946
    int64_t total_uncompressed_bytes = 0;
39✔
947
};
948

949
struct WriteArrowResult {
52✔
950
    WriteArrowStats stats;
951
    std::string error;
952
    std::uint64_t chunks_scanned = 0;
39✔
953
    std::uint64_t chunks_skipped = 0;
39✔
954
};
955

956
CoroTask<WriteArrowResult> write_arrow_pipeline(
190!
957
    std::string file_path, std::string index_path, std::size_t checkpoint_size,
958
    std::vector<ViewDefinition> views, std::string output_path,
959
    int64_t chunk_size_bytes, IpcCompression compression,
960
    std::size_t event_batch_size) {
13!
961
    namespace dft_internal =
962
        dftracer::utils::utilities::composites::dft::internal;
963
    WriteArrowResult result;
13✔
964

965
    try {
966
        if (views.empty()) {
13✔
967
            views.push_back(ViewDefinition().with_name("all"));
8!
968
        }
8✔
969

970
        std::string resolved_index =
13✔
971
            index_path.empty()
26!
972
                ? dft_internal::determine_index_path(file_path, "")
13!
973
                : index_path;
×
974

975
        auto meta_input = MetadataCollectorUtilityInput::from_file(file_path)
26!
976
                              .with_checkpoint_size(checkpoint_size)
13!
977
                              .with_index(resolved_index);
13!
978
        auto metadata = co_await MetadataCollectorUtility{}.process(meta_input);
39!
979
        if (!metadata.success) {
13!
980
            result.error =
981
                "Failed to collect metadata: " + metadata.error_message;
×
982
            co_return result;
×
983
        }
984

985
        for (const auto &view : views) {
99✔
986
            std::string view_output = output_path;
30!
987
            if (views.size() > 1 || view.name != "all") {
30✔
988
                view_output = output_path + "/" + view.name;
42!
989
            }
6✔
990

991
            PartitionWriter writer;
42!
992
            int rc_open = co_await writer.open(view_output, chunk_size_bytes,
98!
993
                                               compression);
42✔
994
            if (rc_open != 0) {
42!
995
                result.error =
996
                    "Failed to open partition writer for view: " + view.name;
×
997
                co_return result;
×
998
            }
999

1000
            ViewBuilderInput builder_input;
42✔
1001
            builder_input.with_view(view)
42✔
1002
                .with_file_path(file_path)
14!
1003
                .with_index_path(resolved_index)
14!
1004
                .with_uncompressed_size(metadata.uncompressed_size)
14!
1005
                .with_num_checkpoints(metadata.num_checkpoints);
14✔
1006

1007
            auto build_output =
42✔
1008
                co_await ViewBuilderUtility{}.process(builder_input);
56!
1009
            if (!build_output) {
58!
1010
                result.error = "ViewBuilder failed for view: " + view.name;
×
1011
                co_return result;
×
1012
            }
1013

1014
            result.chunks_skipped += build_output->skipped_checkpoints;
58✔
1015

1016
            if (!build_output->file_may_match) {
14✔
1017
                auto stats = co_await writer.close();
24!
1018
                result.stats.partitions[view.name] = std::move(stats);
6!
1019
                continue;
1020
            }
6✔
1021

1022
            RecordBatchBuilder builder;
40!
1023
            bool schema_locked = false;
40✔
1024

1025
            for (const auto &candidate : build_output->candidates) {
64!
1026
                ViewReaderInput reader_input;
40✔
1027
                reader_input.with_file_path(file_path)
40✔
1028
                    .with_index_path(resolved_index)
8!
1029
                    .with_checkpoint_size(checkpoint_size)
8!
1030
                    .with_byte_range(candidate.start_byte, candidate.end_byte)
8!
1031
                    .with_checkpoint_idx(candidate.checkpoint_idx)
8!
1032
                    .with_event_batch_size(event_batch_size)
8!
1033
                    .with_view(view);
8!
1034
                reader_input.query = view.query;
8✔
1035

1036
                ViewReaderUtility reader;
40!
1037
                auto gen = reader.process(reader_input);
40!
1038
                while (auto opt = co_await gen.next()) {
64!
1039
                    auto arrow_batch = opt->to_arrow(builder);
24!
1040
                    int rc_write = co_await writer.write_batch(arrow_batch);
32!
1041
                    if (rc_write != 0) {
8!
1042
                        result.error =
1043
                            "Failed to write batch for view: " + view.name;
×
1044
                        co_return result;
×
1045
                    }
1046
                    if (!schema_locked) {
8!
1047
                        builder.lock_schema();
8✔
1048
                        schema_locked = true;
8✔
1049
                    }
8✔
1050
                    builder.reset(true);
8!
1051
                }
32!
1052
                result.chunks_scanned++;
8✔
1053
            }
24✔
1054

1055
            auto stats = co_await writer.close();
32!
1056
            result.stats.partitions[view.name] = std::move(stats);
8!
1057
            result.stats.total_rows +=
8✔
1058
                result.stats.partitions[view.name].total_rows;
8!
1059
            result.stats.total_uncompressed_bytes +=
8✔
1060
                result.stats.partitions[view.name].total_uncompressed_bytes;
8!
1061
        }
86✔
1062
    } catch (const std::exception &e) {
69!
1063
        result.error = e.what();
×
1064
    }
×
1065
    co_return result;
13!
1066
}
493!
1067

1068
struct ViewChunkInfo {
1069
    std::uint64_t checkpoint_idx;
1070
    std::size_t start_byte;
1071
    std::size_t end_byte;
1072
};
1073

1074
struct GetViewChunksResult {
12✔
1075
    std::vector<ViewChunkInfo> chunks;
1076
    std::uint64_t total_checkpoints = 0;
9✔
1077
    std::uint64_t skipped_checkpoints = 0;
9✔
1078
    bool file_may_match = false;
9✔
1079
    std::string error;
1080
};
1081

1082
CoroTask<GetViewChunksResult> get_view_chunks_pipeline(
30!
1083
    std::string file_path, std::string index_path, std::size_t checkpoint_size,
1084
    ViewDefinition view) {
3!
1085
    namespace dft_internal =
1086
        dftracer::utils::utilities::composites::dft::internal;
1087
    GetViewChunksResult result;
3✔
1088

1089
    try {
1090
        std::string resolved_index =
3✔
1091
            index_path.empty()
6!
1092
                ? dft_internal::determine_index_path(file_path, "")
3!
1093
                : index_path;
×
1094

1095
        auto meta_input = MetadataCollectorUtilityInput::from_file(file_path)
3!
1096
                              .with_checkpoint_size(checkpoint_size)
3✔
1097
                              .with_index(resolved_index);
3!
1098
        auto metadata = co_await MetadataCollectorUtility{}.process(meta_input);
9!
1099
        if (!metadata.success) {
9!
1100
            result.error =
1101
                "Failed to collect metadata: " + metadata.error_message;
×
1102
            co_return result;
×
1103
        }
1104

1105
        ViewBuilderInput builder_input;
9✔
1106
        builder_input.with_view(view)
9✔
1107
            .with_file_path(file_path)
3!
1108
            .with_index_path(resolved_index)
3!
1109
            .with_uncompressed_size(metadata.uncompressed_size)
3!
1110
            .with_num_checkpoints(metadata.num_checkpoints);
3✔
1111

1112
        auto build_output =
9✔
1113
            co_await ViewBuilderUtility{}.process(builder_input);
12!
1114
        if (!build_output) {
3!
1115
            result.error = "ViewBuilder failed";
×
1116
            co_return result;
×
1117
        }
1118

1119
        result.file_may_match = build_output->file_may_match;
3!
1120
        result.total_checkpoints = build_output->total_checkpoints;
3!
1121
        result.skipped_checkpoints = build_output->skipped_checkpoints;
3!
1122

1123
        for (const auto &candidate : build_output->candidates) {
7!
1124
            result.chunks.push_back({candidate.checkpoint_idx,
12!
1125
                                     candidate.start_byte, candidate.end_byte});
8✔
1126
        }
4✔
1127
    } catch (const std::exception &e) {
9!
1128
        result.error = e.what();
×
1129
    }
×
1130
    co_return result;
3!
1131
}
39!
1132

1133
struct WriteViewChunkResult {
16✔
1134
    std::string output_file;
1135
    std::uint64_t events_matched = 0;
12✔
1136
    std::uint64_t events_scanned = 0;
12✔
1137
    int64_t rows_written = 0;
12✔
1138
    int64_t bytes_written = 0;
12✔
1139
    std::string error;
1140
};
1141

1142
CoroTask<WriteViewChunkResult> write_view_chunk_pipeline(
52!
1143
    std::string file_path, std::string index_path, std::size_t checkpoint_size,
1144
    ViewDefinition view, std::uint64_t checkpoint_idx, std::size_t start_byte,
1145
    std::size_t end_byte, std::string output_file, IpcCompression compression,
1146
    std::size_t event_batch_size) {
4!
1147
    namespace dft_internal =
1148
        dftracer::utils::utilities::composites::dft::internal;
1149
    WriteViewChunkResult result;
4✔
1150
    result.output_file = output_file;
4!
1151

1152
    try {
1153
        std::string resolved_index =
4✔
1154
            index_path.empty()
8!
1155
                ? dft_internal::determine_index_path(file_path, "")
4!
1156
                : index_path;
×
1157

1158
        dftracer::utils::utilities::common::arrow::IpcWriter writer;
4!
1159
        int rc_open = co_await writer.open(output_file, compression);
12!
1160
        if (rc_open != 0) {
18!
1161
            result.error = "Failed to open output file";
×
1162
            co_return result;
×
1163
        }
1164

1165
        ViewReaderInput reader_input;
18✔
1166
        reader_input.with_file_path(file_path)
18✔
1167
            .with_index_path(resolved_index)
4!
1168
            .with_checkpoint_size(checkpoint_size)
4!
1169
            .with_byte_range(start_byte, end_byte)
4!
1170
            .with_checkpoint_idx(checkpoint_idx)
4!
1171
            .with_event_batch_size(event_batch_size)
4!
1172
            .with_view(view);
4!
1173
        reader_input.query = view.query;
4✔
1174

1175
        RecordBatchBuilder builder;
18!
1176
        bool schema_locked = false;
18✔
1177

1178
        ViewReaderUtility reader;
18!
1179
        auto gen = reader.process(reader_input);
18!
1180
        while (auto opt = co_await gen.next()) {
29!
1181
            result.events_matched += opt->events_matched;
12✔
1182
            result.events_scanned += opt->events_scanned;
12✔
1183
            auto batch = opt->to_arrow(builder);
12!
1184
            if (batch.valid()) {
12!
1185
                result.rows_written += batch.num_rows();
12✔
1186
                int rc = co_await writer.write_batch(batch);
16!
1187
                if (rc != 0) {
4!
1188
                    result.error = "Failed to write batch";
×
1189
                    co_return result;
×
1190
                }
1191
                if (!schema_locked) {
4!
1192
                    builder.lock_schema();
4✔
1193
                    schema_locked = true;
4✔
1194
                }
4✔
1195
                builder.reset(true);
4!
1196
            }
4!
1197
        }
16!
1198

1199
        int rc = co_await writer.close();
16!
1200
        if (rc != 0) {
4!
1201
            result.error = "Failed to close output file";
×
1202
        }
1203
    } catch (const std::exception &e) {
18!
1204
        result.error = e.what();
×
1205
    }
×
1206
    co_return result;
4!
1207
}
106!
1208

1209
struct ChunkDescriptor {
1210
    std::uint64_t checkpoint_idx;
1211
    std::size_t start_byte;
1212
    std::size_t end_byte;
1213
    std::string output_file;
1214
};
1215

1216
struct WriteViewChunksResult {
3✔
1217
    std::vector<WriteViewChunkResult> results;
1218
    int64_t total_rows = 0;
3✔
1219
    int64_t total_events_matched = 0;
3✔
1220
};
1221

1222
CoroTask<WriteViewChunksResult> write_view_chunks_pipeline(
8!
1223
    std::string file_path, std::string index_path, std::size_t checkpoint_size,
1224
    ViewDefinition view, std::vector<ChunkDescriptor> chunks,
1225
    IpcCompression compression, std::size_t event_batch_size) {
1!
1226
    WriteViewChunksResult result;
3✔
1227

1228
    if (chunks.empty()) {
3!
1229
        co_return result;
1!
1230
    }
1231

1232
    std::vector<CoroTask<WriteViewChunkResult>> tasks;
3✔
1233
    tasks.reserve(chunks.size());
3!
1234

1235
    for (const auto &chunk : chunks) {
6✔
1236
        tasks.push_back(write_view_chunk_pipeline(
6!
1237
            file_path, index_path, checkpoint_size, view, chunk.checkpoint_idx,
3!
1238
            chunk.start_byte, chunk.end_byte, chunk.output_file, compression,
3!
1239
            event_batch_size));
3✔
1240
    }
3✔
1241

1242
    result.results = co_await when_all(std::move(tasks));
4!
1243

1244
    for (const auto &r : result.results) {
4✔
1245
        result.total_rows += r.rows_written;
3✔
1246
        result.total_events_matched += r.events_matched;
3✔
1247
    }
3✔
1248

1249
    co_return result;
1!
1250
}
7!
1251

1252
#endif  // DFTRACER_UTILS_ENABLE_ARROW_IPC
1253

1254
TraceReaderConfig build_config(TraceReaderObject *self) {
330✔
1255
    TraceReaderConfig cfg;
330✔
1256
    cfg.file_path = PyUnicode_AsUTF8(self->file_path);
330!
1257
    const char *idx = PyUnicode_AsUTF8(self->index_dir);
330!
1258
    if (idx) cfg.index_dir = idx;
330!
1259
    cfg.checkpoint_size = self->checkpoint_size;
330✔
1260
    cfg.auto_build_index = self->auto_build_index != 0;
330✔
1261
    return cfg;
330✔
1262
}
165!
1263

1264
static Runtime *get_runtime(TraceReaderObject *self) {
328✔
1265
    if (self->runtime_obj) {
328✔
1266
        return ((RuntimeObject *)self->runtime_obj)->runtime.get();
68✔
1267
    }
1268
    return get_default_runtime();
260✔
1269
}
164✔
1270

1271
static TraceReaderIteratorObject *make_memoryview_iterator(
146✔
1272
    std::shared_ptr<MemoryViewBatchIteratorState> state) {
1273
    TraceReaderIteratorObject *it =
73✔
1274
        (TraceReaderIteratorObject *)TraceReaderIteratorType.tp_alloc(
146✔
1275
            &TraceReaderIteratorType, 0);
1276
    if (!it) return NULL;
146✔
1277
    new (&it->batch_state)
146✔
1278
        std::shared_ptr<MemoryViewBatchIteratorState>(std::move(state));
146✔
1279
    it->current_batch = NULL;
146✔
1280
    it->batch_index = 0;
146✔
1281
    new (&it->json_dict_state) std::shared_ptr<JsonDictIteratorState>();
146✔
1282
    new (&it->json_dict_current_batch) std::shared_ptr<JsonDictBatch>();
146✔
1283
    it->json_dict_index = 0;
146✔
1284
#ifdef DFTRACER_UTILS_ENABLE_ARROW
1285
    new (&it->arrow_state) std::shared_ptr<ArrowIteratorState>();
146✔
1286
#endif
1287
    it->mode = IteratorMode::MEMORYVIEW;
146✔
1288
    return it;
146✔
1289
}
73✔
1290

1291
static TraceReaderIteratorObject *make_json_dict_iterator(
30✔
1292
    std::shared_ptr<JsonDictIteratorState> state) {
1293
    TraceReaderIteratorObject *it =
15✔
1294
        (TraceReaderIteratorObject *)TraceReaderIteratorType.tp_alloc(
30✔
1295
            &TraceReaderIteratorType, 0);
1296
    if (!it) return NULL;
30✔
1297
    new (&it->batch_state) std::shared_ptr<MemoryViewBatchIteratorState>();
30✔
1298
    it->current_batch = NULL;
30✔
1299
    it->batch_index = 0;
30✔
1300
    new (&it->json_dict_state)
30✔
1301
        std::shared_ptr<JsonDictIteratorState>(std::move(state));
30✔
1302
    new (&it->json_dict_current_batch) std::shared_ptr<JsonDictBatch>();
30✔
1303
    it->json_dict_index = 0;
30✔
1304
#ifdef DFTRACER_UTILS_ENABLE_ARROW
1305
    new (&it->arrow_state) std::shared_ptr<ArrowIteratorState>();
30✔
1306
#endif
1307
    it->mode = IteratorMode::JSON_DICT;
30✔
1308
    return it;
30✔
1309
}
15✔
1310

1311
#ifdef DFTRACER_UTILS_ENABLE_ARROW
1312
static TraceReaderIteratorObject *make_arrow_iterator(
60✔
1313
    std::shared_ptr<ArrowIteratorState> state) {
1314
    TraceReaderIteratorObject *it =
30✔
1315
        (TraceReaderIteratorObject *)TraceReaderIteratorType.tp_alloc(
60✔
1316
            &TraceReaderIteratorType, 0);
1317
    if (!it) return NULL;
60✔
1318
    new (&it->batch_state) std::shared_ptr<MemoryViewBatchIteratorState>();
60✔
1319
    it->current_batch = NULL;
60✔
1320
    it->batch_index = 0;
60✔
1321
    new (&it->json_dict_state) std::shared_ptr<JsonDictIteratorState>();
60✔
1322
    new (&it->json_dict_current_batch) std::shared_ptr<JsonDictBatch>();
60✔
1323
    it->json_dict_index = 0;
60✔
1324
    new (&it->arrow_state)
60✔
1325
        std::shared_ptr<ArrowIteratorState>(std::move(state));
60✔
1326
    it->mode = IteratorMode::ARROW;
60✔
1327
    return it;
60✔
1328
}
30✔
1329
#endif
1330

1331
}  // namespace
1332

1333
static void TraceReader_dealloc(TraceReaderObject *self) {
346✔
1334
    Py_XDECREF(self->file_path);
346✔
1335
    Py_XDECREF(self->index_dir);
346✔
1336
    Py_XDECREF(self->runtime_obj);
346✔
1337
    Py_TYPE(self)->tp_free((PyObject *)self);
346✔
1338
}
346✔
1339

1340
static PyObject *TraceReader_new(PyTypeObject *type, PyObject *args,
346✔
1341
                                 PyObject *kwds) {
1342
    TraceReaderObject *self = (TraceReaderObject *)type->tp_alloc(type, 0);
346✔
1343
    if (self) {
346✔
1344
        self->file_path = NULL;
346✔
1345
        self->index_dir = NULL;
346✔
1346
        self->checkpoint_size =
346✔
1347
            dftracer::utils::constants::indexer::DEFAULT_CHECKPOINT_SIZE;
1348
        self->auto_build_index = 0;
346✔
1349
        self->has_index = 0;
346✔
1350
        self->runtime_obj = NULL;
346✔
1351
    }
173✔
1352
    return (PyObject *)self;
346✔
1353
}
1354

1355
static int TraceReader_init(TraceReaderObject *self, PyObject *args,
346✔
1356
                            PyObject *kwds) {
1357
    static const char *kwlist[] = {
1358
        "path",    "index_dir", "checkpoint_size", "auto_build_index",
1359
        "runtime", NULL};
1360

1361
    const char *file_path;
1362
    const char *index_dir = "";
346✔
1363
    std::size_t checkpoint_size =
346✔
1364
        dftracer::utils::constants::indexer::DEFAULT_CHECKPOINT_SIZE;
1365
    int auto_build_index = 0;
346✔
1366
    PyObject *runtime_arg = NULL;
346✔
1367

1368
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|snpO", (char **)kwlist,
346!
1369
                                     &file_path, &index_dir, &checkpoint_size,
1370
                                     &auto_build_index, &runtime_arg)) {
1371
        return -1;
×
1372
    }
1373

1374
    if (runtime_arg && runtime_arg != Py_None) {
346✔
1375
        if (PyObject_TypeCheck(runtime_arg, &RuntimeType)) {
64!
1376
            // Direct C++ Runtime object
1377
            Py_INCREF(runtime_arg);
×
1378
            self->runtime_obj = runtime_arg;
×
1379
        } else {
1380
            // Python wrapper, extract _native attribute
1381
            PyObject *native = PyObject_GetAttrString(runtime_arg, "_native");
64!
1382
            if (native && PyObject_TypeCheck(native, &RuntimeType)) {
64!
1383
                self->runtime_obj = native;  // already incref'd by GetAttr
64✔
1384
            } else {
32✔
1385
                Py_XDECREF(native);
×
1386
                PyErr_SetString(PyExc_TypeError,
×
1387
                                "runtime must be a Runtime instance or None");
1388
                return -1;
×
1389
            }
1390
        }
1391
    }
32✔
1392

1393
    self->file_path = PyUnicode_FromString(file_path);
346!
1394
    if (!self->file_path) return -1;
346✔
1395

1396
    self->index_dir = PyUnicode_FromString(index_dir);
346!
1397
    if (!self->index_dir) {
346✔
1398
        Py_DECREF(self->file_path);
×
1399
        self->file_path = NULL;
×
1400
        return -1;
×
1401
    }
1402

1403
    self->checkpoint_size = checkpoint_size;
346✔
1404
    self->auto_build_index = auto_build_index;
346✔
1405

1406
    try {
1407
        TraceReaderConfig cfg;
346✔
1408
        cfg.file_path = file_path;
346!
1409
        cfg.index_dir = index_dir;
346!
1410
        cfg.checkpoint_size = checkpoint_size;
346✔
1411
        cfg.auto_build_index = auto_build_index != 0;
346✔
1412
        TraceReader probe(std::move(cfg));
346!
1413
        self->has_index = probe.has_index() ? 1 : 0;
346!
1414
    } catch (const std::exception &e) {
346!
1415
        set_typed_py_error(e);
×
1416
        Py_DECREF(self->file_path);
×
1417
        Py_DECREF(self->index_dir);
×
1418
        self->file_path = NULL;
×
1419
        self->index_dir = NULL;
×
1420
        return -1;
×
1421
    }
×
1422

1423
    return 0;
346✔
1424
}
173✔
1425

1426
static PyObject *TraceReader_iter_lines(TraceReaderObject *self, PyObject *args,
128✔
1427
                                        PyObject *kwds) {
1428
    static const char *kwlist[] = {"start_line",    "end_line",    "start_byte",
1429
                                   "end_byte",      "buffer_size", "query",
1430
                                   "memory_budget", NULL};
1431
    Py_ssize_t start_line = 0, end_line = 0;
128✔
1432
    Py_ssize_t start_byte = 0, end_byte = 0;
128✔
1433
    Py_ssize_t buffer_size = 4 * 1024 * 1024;
128✔
1434
    const char *query_str = NULL;
128✔
1435
    Py_ssize_t memory_budget = 0;
128✔
1436

1437
    if (!PyArg_ParseTupleAndKeywords(
128!
1438
            args, kwds, "|nnnnnzn", (char **)kwlist, &start_line, &end_line,
64✔
1439
            &start_byte, &end_byte, &buffer_size, &query_str, &memory_budget)) {
1440
        return NULL;
×
1441
    }
1442

1443
    if (start_line < 0 || end_line < 0 || start_byte < 0 || end_byte < 0 ||
128!
1444
        buffer_size <= 0) {
122!
1445
        PyErr_SetString(
6!
1446
            PyExc_ValueError,
3✔
1447
            "range arguments must be >= 0; buffer_size must be > 0");
1448
        return NULL;
6✔
1449
    }
1450

1451
    TraceReaderConfig cfg;
122✔
1452
    try {
1453
        cfg = build_config(self);
122!
1454
    } catch (const std::exception &e) {
61!
1455
        set_typed_py_error(e);
×
1456
        return NULL;
×
1457
    }
×
1458

1459
    ReadConfig rc;
122✔
1460
    rc.start_line = static_cast<std::size_t>(start_line);
122✔
1461
    rc.end_line = static_cast<std::size_t>(end_line);
122✔
1462
    rc.start_byte = static_cast<std::size_t>(start_byte);
122✔
1463
    rc.end_byte = static_cast<std::size_t>(end_byte);
122✔
1464
    rc.buffer_size = static_cast<std::size_t>(buffer_size);
122✔
1465
    if (query_str) rc.query = query_str;
122!
1466

1467
    auto state = std::make_shared<MemoryViewBatchIteratorState>();
122!
1468
    state->memory_budget_bytes = dftracer::utils::compute_memory_budget(
122!
1469
        static_cast<std::size_t>(memory_budget));
61✔
1470

1471
    Runtime *rt = get_runtime(self);
122!
1472
    std::size_t max_workers = rt->threads();
122!
1473
    constexpr std::size_t LINE_BATCH_SIZE = 1024;
122✔
1474
    std::size_t capacity = dftracer::utils::compute_channel_capacity(
122!
1475
        state->memory_budget_bytes, LINE_BATCH_SIZE * ESTIMATED_BYTES_PER_LINE,
122✔
1476
        max_workers);
61✔
1477
    state->channel =
122✔
1478
        dftracer::utils::coro::make_channel<MemoryViewBatchData>(capacity);
183!
1479
    auto *sp = state.get();
122✔
1480

1481
    try {
1482
        bool is_dir = fs::is_directory(cfg.file_path);
122!
1483
        if (is_dir) {
122✔
1484
            auto handle = rt->scope(
6!
1485
                "iter_lines_parallel",
3!
1486
                [sp, dir_path = cfg.file_path, index_dir = cfg.index_dir,
27!
1487
                 checkpoint_size = cfg.checkpoint_size,
6✔
1488
                 auto_build_index = cfg.auto_build_index, rc,
9!
1489
                 max_workers](CoroScope &scope) -> CoroTask<void> {
6!
1490
                    co_await produce_lines_parallel(
24!
1491
                        scope, sp, dir_path, index_dir, checkpoint_size,
9!
1492
                        auto_build_index, rc, LINE_BATCH_SIZE, max_workers);
9!
1493
                });
18!
1494
            state->task_future = handle.future;
6!
1495
        } else {
6✔
1496
            auto handle = rt->submit(
116!
1497
                produce_lines_batched(state, state->channel->producer(), cfg,
232!
1498
                                      rc, LINE_BATCH_SIZE),
58!
1499
                "iter_lines");
232!
1500
            state->task_future = handle.future;
116!
1501
        }
116✔
1502
    } catch (const std::exception &e) {
61!
1503
        set_typed_py_error(e);
×
1504
        return NULL;
×
1505
    }
×
1506

1507
    TraceReaderIteratorObject *it = make_memoryview_iterator(std::move(state));
122!
1508
    return (PyObject *)it;
122✔
1509
}
125✔
1510

1511
static PyObject *TraceReader_iter_raw(TraceReaderObject *self, PyObject *args,
26✔
1512
                                      PyObject *kwds) {
1513
    static const char *kwlist[] = {"start_line", "end_line",    "start_byte",
1514
                                   "end_byte",   "buffer_size", "line_aligned",
1515
                                   "multi_line", "query",       "memory_budget",
1516
                                   NULL};
1517
    Py_ssize_t start_line = 0, end_line = 0;
26✔
1518
    Py_ssize_t start_byte = 0, end_byte = 0;
26✔
1519
    Py_ssize_t buffer_size = 4 * 1024 * 1024;
26✔
1520
    int line_aligned = 1;
26✔
1521
    int multi_line = 1;
26✔
1522
    const char *query_str = NULL;
26✔
1523
    Py_ssize_t memory_budget = 0;
26✔
1524

1525
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnnnnppzn", (char **)kwlist,
26!
1526
                                     &start_line, &end_line, &start_byte,
1527
                                     &end_byte, &buffer_size, &line_aligned,
1528
                                     &multi_line, &query_str, &memory_budget)) {
1529
        return NULL;
×
1530
    }
1531

1532
    if (start_line < 0 || end_line < 0 || start_byte < 0 || end_byte < 0 ||
26!
1533
        buffer_size <= 0) {
24!
1534
        PyErr_SetString(
2!
1535
            PyExc_ValueError,
1✔
1536
            "range arguments must be >= 0; buffer_size must be > 0");
1537
        return NULL;
2✔
1538
    }
1539

1540
    TraceReaderConfig cfg;
24✔
1541
    try {
1542
        cfg = build_config(self);
24!
1543
    } catch (const std::exception &e) {
12!
1544
        set_typed_py_error(e);
×
1545
        return NULL;
×
1546
    }
×
1547

1548
    ReadConfig rc;
24✔
1549
    rc.start_line = static_cast<std::size_t>(start_line);
24✔
1550
    rc.end_line = static_cast<std::size_t>(end_line);
24✔
1551
    rc.start_byte = static_cast<std::size_t>(start_byte);
24✔
1552
    rc.end_byte = static_cast<std::size_t>(end_byte);
24✔
1553
    rc.buffer_size = static_cast<std::size_t>(buffer_size);
24✔
1554
    rc.line_aligned = line_aligned != 0;
24✔
1555
    rc.multi_line = multi_line != 0;
24✔
1556
    if (query_str) rc.query = query_str;
24!
1557

1558
    auto state = std::make_shared<MemoryViewBatchIteratorState>();
24!
1559
    state->memory_budget_bytes = dftracer::utils::compute_memory_budget(
24!
1560
        static_cast<std::size_t>(memory_budget));
12✔
1561

1562
    Runtime *rt = get_runtime(self);
24!
1563
    std::size_t max_workers = rt->threads();
24!
1564
    std::size_t capacity = dftracer::utils::compute_channel_capacity(
24!
1565
        state->memory_budget_bytes, ESTIMATED_BYTES_PER_RAW_CHUNK, max_workers);
24✔
1566
    state->channel =
24✔
1567
        dftracer::utils::coro::make_channel<MemoryViewBatchData>(capacity);
36!
1568
    auto *sp = state.get();
24✔
1569

1570
    try {
1571
        bool is_dir = fs::is_directory(cfg.file_path);
24!
1572
        if (is_dir) {
24✔
1573
            auto handle = rt->scope(
4!
1574
                "iter_raw_parallel",
2!
1575
                [sp, dir_path = cfg.file_path, index_dir = cfg.index_dir,
18!
1576
                 checkpoint_size = cfg.checkpoint_size,
4✔
1577
                 auto_build_index = cfg.auto_build_index, rc,
6!
1578
                 max_workers](CoroScope &scope) -> CoroTask<void> {
4!
1579
                    co_await produce_raw_parallel(
16!
1580
                        scope, sp, dir_path, index_dir, checkpoint_size,
6!
1581
                        auto_build_index, rc, max_workers);
6!
1582
                });
12!
1583
            state->task_future = handle.future;
4!
1584
        } else {
4✔
1585
            auto handle = rt->submit(
20!
1586
                produce_raw_batched(state, state->channel->producer(), cfg, rc),
30!
1587
                "iter_raw");
40!
1588
            state->task_future = handle.future;
20!
1589
        }
20✔
1590
    } catch (const std::exception &e) {
12!
1591
        set_typed_py_error(e);
×
1592
        return NULL;
×
1593
    }
×
1594

1595
    TraceReaderIteratorObject *it = make_memoryview_iterator(std::move(state));
24!
1596
    return (PyObject *)it;
24✔
1597
}
25✔
1598

1599
static PyObject *TraceReader_read_lines(TraceReaderObject *self, PyObject *args,
92✔
1600
                                        PyObject *kwds) {
1601
    PyObject *iter = TraceReader_iter_lines(self, args, kwds);
92✔
1602
    if (!iter) return NULL;
92✔
1603
    PyObject *list = PySequence_List(iter);
88✔
1604
    Py_DECREF(iter);
44✔
1605
    return list;
88✔
1606
}
46✔
1607

1608
static PyObject *TraceReader_iter_json(TraceReaderObject *self, PyObject *args,
32✔
1609
                                       PyObject *kwds) {
1610
    static const char *kwlist[] = {
1611
        "start_line",     "end_line", "start_byte", "end_byte",
1612
        "buffer_size",    "query",    "batch_size", "memory_budget",
1613
        "normalize_time", NULL};
1614
    Py_ssize_t start_line = 0, end_line = 0;
32✔
1615
    Py_ssize_t start_byte = 0, end_byte = 0;
32✔
1616
    Py_ssize_t buffer_size = 4 * 1024 * 1024;
32✔
1617
    const char *query_str = NULL;
32✔
1618
    Py_ssize_t batch_size = 1024;
32✔
1619
    Py_ssize_t memory_budget = 0;
32✔
1620
    const char *normalize_time_str = NULL;
32✔
1621

1622
    if (!PyArg_ParseTupleAndKeywords(
32!
1623
            args, kwds, "|nnnnnznnz", (char **)kwlist, &start_line, &end_line,
16✔
1624
            &start_byte, &end_byte, &buffer_size, &query_str, &batch_size,
1625
            &memory_budget, &normalize_time_str)) {
NEW
1626
        return NULL;
×
1627
    }
1628

1629
    TimeNormalization normalize_time = TimeNormalization::None;
32✔
1630
    if (!parse_normalize_time(normalize_time_str, normalize_time)) {
32✔
1631
        PyErr_SetString(PyExc_ValueError,
2!
1632
                        "normalize_time must be one of None, 'ns', 'us', 'ms', "
1633
                        "'sec'");
1634
        return NULL;
2✔
1635
    }
1636

1637
    if (start_line < 0 || end_line < 0 || start_byte < 0 || end_byte < 0 ||
30!
1638
        buffer_size <= 0 || batch_size <= 0) {
30!
1639
        PyErr_SetString(PyExc_ValueError,
×
1640
                        "range arguments must be >= 0; buffer_size and "
1641
                        "batch_size must be > 0");
1642
        return NULL;
×
1643
    }
1644

1645
    TraceReaderConfig cfg;
30✔
1646
    try {
1647
        cfg = build_config(self);
30!
1648
    } catch (const std::exception &e) {
15!
1649
        set_typed_py_error(e);
×
1650
        return NULL;
×
1651
    }
×
1652

1653
    ReadConfig rc;
30✔
1654
    rc.start_line = static_cast<std::size_t>(start_line);
30✔
1655
    rc.end_line = static_cast<std::size_t>(end_line);
30✔
1656
    rc.start_byte = static_cast<std::size_t>(start_byte);
30✔
1657
    rc.end_byte = static_cast<std::size_t>(end_byte);
30✔
1658
    rc.buffer_size = static_cast<std::size_t>(buffer_size);
30✔
1659
    rc.normalize_time = normalize_time;
30✔
1660
    if (query_str) rc.query = query_str;
30!
1661

1662
    auto state = std::make_shared<JsonDictIteratorState>();
30!
1663
    state->memory_budget_bytes = dftracer::utils::compute_memory_budget(
30!
1664
        static_cast<std::size_t>(memory_budget));
15✔
1665

1666
    Runtime *rt = get_runtime(self);
30!
1667
    std::size_t max_workers = rt->threads();
30!
1668
    auto bs = static_cast<std::size_t>(batch_size);
30✔
1669
    std::size_t capacity = dftracer::utils::compute_channel_capacity(
30!
1670
        state->memory_budget_bytes, bs * ESTIMATED_BYTES_PER_JSON_EVENT,
30✔
1671
        max_workers);
15✔
1672
    state->channel =
30✔
1673
        dftracer::utils::coro::make_channel<JsonDictBatch>(capacity);
45!
1674
    auto *sp = state.get();
30✔
1675

1676
    try {
1677
        bool is_dir = fs::is_directory(cfg.file_path);
30!
1678
        if (is_dir) {
30✔
1679
            auto handle = rt->scope(
12!
1680
                "iter_json_parallel",
6!
1681
                [sp, dir_path = cfg.file_path, index_dir = cfg.index_dir,
54!
1682
                 checkpoint_size = cfg.checkpoint_size,
12✔
1683
                 auto_build_index = cfg.auto_build_index, rc, bs,
18!
1684
                 max_workers](CoroScope &scope) -> CoroTask<void> {
12!
1685
                    co_await produce_json_dicts_parallel(
48!
1686
                        scope, sp, dir_path, index_dir, checkpoint_size,
18!
1687
                        auto_build_index, rc, bs, max_workers);
18!
1688
                });
36!
1689
            state->task_future = handle.future;
12!
1690
        } else {
12✔
1691
            auto handle =
1692
                rt->submit(produce_json_dicts(state, state->channel->producer(),
45!
1693
                                              cfg, rc, bs),
9!
1694
                           "iter_json");
36!
1695
            state->task_future = handle.future;
18!
1696
        }
18✔
1697
    } catch (const std::exception &e) {
15!
1698
        set_typed_py_error(e);
×
1699
        return NULL;
×
1700
    }
×
1701

1702
    TraceReaderIteratorObject *it = make_json_dict_iterator(std::move(state));
30!
1703
    return (PyObject *)it;
30✔
1704
}
31✔
1705

1706
static PyObject *TraceReader_read_json_py(TraceReaderObject *self,
20✔
1707
                                          PyObject *args, PyObject *kwds) {
1708
    PyObject *iter = TraceReader_iter_json(self, args, kwds);
20✔
1709
    if (!iter) return NULL;
20✔
1710
    PyObject *list = PySequence_List(iter);
18✔
1711
    Py_DECREF(iter);
9✔
1712
    return list;
18✔
1713
}
10✔
1714

1715
static PyObject *TraceReader_read_raw(TraceReaderObject *self, PyObject *args,
8✔
1716
                                      PyObject *kwds) {
1717
    PyObject *iter = TraceReader_iter_raw(self, args, kwds);
8✔
1718
    if (!iter) return NULL;
8✔
1719
    PyObject *list = PySequence_List(iter);
8✔
1720
    Py_DECREF(iter);
4✔
1721
    return list;
8✔
1722
}
4✔
1723

1724
#ifdef DFTRACER_UTILS_ENABLE_ARROW
1725

1726
static PyObject *TraceReader_iter_arrow(TraceReaderObject *self, PyObject *args,
60✔
1727
                                        PyObject *kwds) {
1728
    static const char *kwlist[] = {
1729
        "batch_size", "start_line",    "end_line",       "start_byte",
1730
        "end_byte",   "buffer_size",   "query",          "flatten_objects",
1731
        "normalize",  "memory_budget", "normalize_time", NULL};
1732
    Py_ssize_t batch_size = 10000;
60✔
1733
    Py_ssize_t start_line = 0, end_line = 0;
60✔
1734
    Py_ssize_t start_byte = 0, end_byte = 0;
60✔
1735
    Py_ssize_t buffer_size = 4 * 1024 * 1024;
60✔
1736
    const char *query_str = NULL;
60✔
1737
    int flatten_objects = 1;  // default: expand top-level objects
60✔
1738
    int normalize = 0;
60✔
1739
    Py_ssize_t memory_budget = 0;
60✔
1740
    const char *normalize_time_str = NULL;
60✔
1741

1742
    if (!PyArg_ParseTupleAndKeywords(
60!
1743
            args, kwds, "|nnnnnnzppnz", (char **)kwlist, &batch_size,
30✔
1744
            &start_line, &end_line, &start_byte, &end_byte, &buffer_size,
1745
            &query_str, &flatten_objects, &normalize, &memory_budget,
1746
            &normalize_time_str)) {
NEW
1747
        return NULL;
×
1748
    }
1749

1750
    TimeNormalization normalize_time = TimeNormalization::None;
60✔
1751
    if (!parse_normalize_time(normalize_time_str, normalize_time)) {
60!
NEW
1752
        PyErr_SetString(PyExc_ValueError,
×
1753
                        "normalize_time must be one of None, 'ns', 'us', 'ms', "
1754
                        "'sec'");
UNCOV
1755
        return NULL;
×
1756
    }
1757

1758
    if (batch_size <= 0) {
60!
1759
        PyErr_SetString(PyExc_ValueError, "batch_size must be > 0");
×
1760
        return NULL;
×
1761
    }
1762
    if (start_line < 0 || end_line < 0 || start_byte < 0 || end_byte < 0 ||
60!
1763
        buffer_size <= 0) {
60!
1764
        PyErr_SetString(
×
1765
            PyExc_ValueError,
1766
            "range arguments must be >= 0; buffer_size must be > 0");
1767
        return NULL;
×
1768
    }
1769

1770
    TraceReaderConfig cfg;
60✔
1771
    try {
1772
        cfg = build_config(self);
60!
1773
    } catch (const std::exception &e) {
30!
1774
        set_typed_py_error(e);
×
1775
        return NULL;
×
1776
    }
×
1777

1778
    ReadConfig rc;
60✔
1779
    rc.start_line = static_cast<std::size_t>(start_line);
60✔
1780
    rc.end_line = static_cast<std::size_t>(end_line);
60✔
1781
    rc.start_byte = static_cast<std::size_t>(start_byte);
60✔
1782
    rc.end_byte = static_cast<std::size_t>(end_byte);
60✔
1783
    rc.buffer_size = static_cast<std::size_t>(buffer_size);
60✔
1784
    rc.flatten_objects = flatten_objects != 0;
60✔
1785
    rc.normalize_time = normalize_time;
60✔
1786
    if (query_str) rc.query = query_str;
60!
1787

1788
    auto state = std::make_shared<ArrowIteratorState>();
60!
1789
    state->memory_budget_bytes = dftracer::utils::compute_memory_budget(
60!
1790
        static_cast<std::size_t>(memory_budget));
30✔
1791

1792
    Runtime *rt = get_runtime(self);
60!
1793
    std::size_t max_workers = rt->threads();
60!
1794
    auto bs = static_cast<std::size_t>(batch_size);
60✔
1795
    std::size_t capacity = dftracer::utils::compute_channel_capacity(
60!
1796
        state->memory_budget_bytes, bs * ESTIMATED_BYTES_PER_ARROW_ROW,
60✔
1797
        max_workers);
30✔
1798
    state->channel =
60✔
1799
        dftracer::utils::coro::make_channel<ArrowIteratorState::BatchType>(
90!
1800
            capacity);
60✔
1801
    auto *sp = state.get();
60✔
1802

1803
    try {
1804
        bool is_dir = fs::is_directory(cfg.file_path);
60!
1805
        if (is_dir) {
60✔
1806
            auto handle = rt->scope(
10!
1807
                "iter_arrow_parallel",
5!
1808
                [sp, dir_path = cfg.file_path, index_dir = cfg.index_dir,
45!
1809
                 checkpoint_size = cfg.checkpoint_size,
10✔
1810
                 auto_build_index = cfg.auto_build_index, rc, bs,
15!
1811
                 norm = normalize != 0,
10✔
1812
                 max_workers](CoroScope &scope) -> CoroTask<void> {
10!
1813
                    co_await produce_arrow_batches_parallel(
40!
1814
                        scope, sp, dir_path, index_dir, checkpoint_size,
15!
1815
                        auto_build_index, rc, bs, norm, max_workers);
15!
1816
                });
30!
1817
            state->task_future = handle.future;
10!
1818
        } else if (normalize) {
60✔
1819
            auto handle = rt->submit(
6!
1820
                produce_arrow_batches(state, state->channel->producer(), cfg,
12!
1821
                                      rc, static_cast<std::size_t>(batch_size),
3!
1822
                                      flatten_objects != 0, normalize != 0),
3✔
1823
                "iter_arrow");
12!
1824
            state->task_future = handle.future;
6!
1825
        } else {
6✔
1826
            std::vector<std::string> files_vec{cfg.file_path};
88!
1827
            auto handle = rt->scope(
44!
1828
                "iter_arrow_parallel",
22!
1829
                [sp, files = std::move(files_vec), index_dir = cfg.index_dir,
198!
1830
                 checkpoint_size = cfg.checkpoint_size,
44✔
1831
                 auto_build_index = cfg.auto_build_index, rc, bs,
66!
1832
                 norm = normalize != 0,
44✔
1833
                 max_workers](CoroScope &scope) mutable -> CoroTask<void> {
44!
1834
                    co_await produce_arrow_batches_for_files(
176!
1835
                        scope, sp, std::move(files), index_dir, checkpoint_size,
66!
1836
                        auto_build_index, rc, bs, norm, max_workers);
66!
1837
                });
132!
1838
            state->task_future = handle.future;
44!
1839
        }
44✔
1840
    } catch (const std::exception &e) {
30!
1841
        set_typed_py_error(e);
×
1842
        return NULL;
×
1843
    }
×
1844

1845
    TraceReaderIteratorObject *it = make_arrow_iterator(std::move(state));
60!
1846
    return (PyObject *)it;
60✔
1847
}
60✔
1848

1849
// Build ArrowIteratorState + spawn the producer task. Same plumbing as
1850
// TraceReader_iter_arrow but returns the state so callers can wrap it as
1851
// either a per-batch iterator or an ArrowArrayStream.
1852
static std::shared_ptr<ArrowIteratorState> spawn_arrow_producer(
56✔
1853
    TraceReaderObject *self, PyObject *args, PyObject *kwds) {
1854
    static const char *kwlist[] = {
1855
        "batch_size", "start_line",    "end_line",       "start_byte",
1856
        "end_byte",   "buffer_size",   "query",          "flatten_objects",
1857
        "normalize",  "memory_budget", "normalize_time", NULL};
1858
    Py_ssize_t batch_size = 10000;
56✔
1859
    Py_ssize_t start_line = 0, end_line = 0;
56✔
1860
    Py_ssize_t start_byte = 0, end_byte = 0;
56✔
1861
    Py_ssize_t buffer_size = 4 * 1024 * 1024;
56✔
1862
    const char *query_str = NULL;
56✔
1863
    int flatten_objects = 1;  // default: expand top-level objects
56✔
1864
    int normalize = 0;
56✔
1865
    Py_ssize_t memory_budget = 0;
56✔
1866
    const char *normalize_time_str = NULL;
56✔
1867

1868
    if (!PyArg_ParseTupleAndKeywords(
56!
1869
            args, kwds, "|nnnnnnzppnz", (char **)kwlist, &batch_size,
28✔
1870
            &start_line, &end_line, &start_byte, &end_byte, &buffer_size,
1871
            &query_str, &flatten_objects, &normalize, &memory_budget,
1872
            &normalize_time_str)) {
NEW
1873
        return nullptr;
×
1874
    }
1875

1876
    TimeNormalization normalize_time = TimeNormalization::None;
56✔
1877
    if (!parse_normalize_time(normalize_time_str, normalize_time)) {
56!
NEW
1878
        PyErr_SetString(PyExc_ValueError,
×
1879
                        "normalize_time must be one of None, 'ns', 'us', 'ms', "
1880
                        "'sec'");
UNCOV
1881
        return nullptr;
×
1882
    }
1883

1884
    if (batch_size <= 0) {
56!
1885
        PyErr_SetString(PyExc_ValueError, "batch_size must be > 0");
×
1886
        return nullptr;
×
1887
    }
1888
    if (start_line < 0 || end_line < 0 || start_byte < 0 || end_byte < 0 ||
56!
1889
        buffer_size <= 0) {
56!
1890
        PyErr_SetString(
×
1891
            PyExc_ValueError,
1892
            "range arguments must be >= 0; buffer_size must be > 0");
1893
        return nullptr;
×
1894
    }
1895

1896
    TraceReaderConfig cfg;
56✔
1897
    try {
1898
        cfg = build_config(self);
56!
1899
    } catch (const std::exception &e) {
28!
1900
        set_typed_py_error(e);
×
1901
        return nullptr;
×
1902
    }
×
1903

1904
    ReadConfig rc;
56✔
1905
    rc.start_line = static_cast<std::size_t>(start_line);
56✔
1906
    rc.end_line = static_cast<std::size_t>(end_line);
56✔
1907
    rc.start_byte = static_cast<std::size_t>(start_byte);
56✔
1908
    rc.end_byte = static_cast<std::size_t>(end_byte);
56✔
1909
    rc.buffer_size = static_cast<std::size_t>(buffer_size);
56✔
1910
    rc.flatten_objects = flatten_objects != 0;
56✔
1911
    rc.normalize_time = normalize_time;
56✔
1912
    if (query_str) rc.query = query_str;
56!
1913

1914
    auto state = std::make_shared<ArrowIteratorState>();
56!
1915
    state->memory_budget_bytes = dftracer::utils::compute_memory_budget(
56!
1916
        static_cast<std::size_t>(memory_budget));
28✔
1917

1918
    Runtime *rt = get_runtime(self);
56!
1919
    std::size_t max_workers = rt->threads();
56!
1920
    auto bs = static_cast<std::size_t>(batch_size);
56✔
1921
    std::size_t capacity = dftracer::utils::compute_channel_capacity(
56!
1922
        state->memory_budget_bytes, bs * ESTIMATED_BYTES_PER_ARROW_ROW,
56✔
1923
        max_workers);
28✔
1924
    state->channel =
56✔
1925
        dftracer::utils::coro::make_channel<ArrowIteratorState::BatchType>(
84!
1926
            capacity);
56✔
1927
    auto *sp = state.get();
56✔
1928

1929
    try {
1930
        bool is_dir = fs::is_directory(cfg.file_path);
56!
1931
        if (is_dir) {
56✔
1932
            auto handle = rt->scope(
20!
1933
                "iter_arrow_parallel",
10!
1934
                [sp, dir_path = cfg.file_path, index_dir = cfg.index_dir,
90!
1935
                 checkpoint_size = cfg.checkpoint_size,
20✔
1936
                 auto_build_index = cfg.auto_build_index, rc, bs,
30!
1937
                 norm = normalize != 0,
20✔
1938
                 max_workers](CoroScope &scope) -> CoroTask<void> {
20!
1939
                    co_await produce_arrow_batches_parallel(
80!
1940
                        scope, sp, dir_path, index_dir, checkpoint_size,
30!
1941
                        auto_build_index, rc, bs, norm, max_workers);
30!
1942
                });
60!
1943
            state->task_future = handle.future;
20!
1944
        } else {
20✔
1945
            auto handle = rt->submit(
36!
1946
                produce_arrow_batches(state, state->channel->producer(), cfg,
72!
1947
                                      rc, static_cast<std::size_t>(batch_size),
18!
1948
                                      flatten_objects != 0, normalize != 0),
18✔
1949
                "iter_arrow");
72!
1950
            state->task_future = handle.future;
36!
1951
        }
36✔
1952
    } catch (const std::exception &e) {
28!
1953
        set_typed_py_error(e);
×
1954
        return nullptr;
×
1955
    }
×
1956

1957
    return state;
56✔
1958
}
56✔
1959

1960
static PyObject *TraceReader_iter_arrow_stream(TraceReaderObject *self,
30✔
1961
                                               PyObject *args, PyObject *kwds) {
1962
    auto state = spawn_arrow_producer(self, args, kwds);
30!
1963
    if (!state) return NULL;
30!
1964
    return make_arrow_batch_stream(std::move(state));
30!
1965
}
30✔
1966

1967
static PyObject *TraceReader_read_arrow(TraceReaderObject *self, PyObject *args,
26✔
1968
                                        PyObject *kwds) {
1969
    auto state = spawn_arrow_producer(self, args, kwds);
26!
1970
    if (!state) return NULL;
26!
1971
    PyObject *stream = make_arrow_batch_stream(std::move(state));
26!
1972
    if (!stream) return NULL;
26✔
1973
    return dftracer::utils::python::wrap_arrow_stream_table(stream);
26!
1974
}
26✔
1975

1976
#endif  // DFTRACER_UTILS_ENABLE_ARROW
1977

1978
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
1979

1980
// Parse a single view spec (string name/preset or dict with optional "name"
1981
// and "query") into `view`. Returns false with a Python error set on failure.
1982
// When `strict`, a value that is neither string nor dict raises TypeError;
1983
// otherwise it is silently ignored (leaving `view` default-constructed).
1984
static bool parse_view_spec(PyObject *view_obj, ViewDefinition &view,
10✔
1985
                            bool strict) {
1986
    if (view_obj && view_obj != Py_None) {
10!
1987
        if (PyUnicode_Check(view_obj)) {
2!
1988
            const char *name = PyUnicode_AsUTF8(view_obj);
×
1989
            if (!name) return false;
×
1990
            std::string name_str(name);
×
1991
            if (name_str == "io") {
×
1992
                view = ViewDefinition::io_view();
×
1993
            } else if (name_str == "compute") {
×
1994
                view = ViewDefinition::compute_view();
×
1995
            } else if (name_str == "dlio") {
×
1996
                view = ViewDefinition::dlio_view();
×
1997
            } else {
1998
                view.with_name(name_str);
×
1999
            }
2000
        } else if (PyDict_Check(view_obj)) {
2!
2001
            PyObject *name_obj = PyDict_GetItemString(view_obj, "name");
2✔
2002
            if (name_obj && PyUnicode_Check(name_obj)) {
2!
2003
                view.with_name(PyUnicode_AsUTF8(name_obj));
2!
2004
            }
1✔
2005
            PyObject *query_obj = PyDict_GetItemString(view_obj, "query");
2✔
2006
            if (query_obj && query_obj != Py_None &&
3!
2007
                PyUnicode_Check(query_obj)) {
2✔
2008
                view.with_query(PyUnicode_AsUTF8(query_obj));
2!
2009
            }
1✔
2010
        } else if (strict) {
1!
2011
            PyErr_SetString(PyExc_TypeError, "view must be a string or dict");
×
2012
            return false;
×
2013
        }
2014
    }
1✔
2015
    return true;
10✔
2016
}
5✔
2017

2018
static PyObject *TraceReader_write_arrow(TraceReaderObject *self,
26✔
2019
                                         PyObject *args, PyObject *kwds) {
2020
    static const char *kwlist[] = {"path",        "views",      "chunk_size_mb",
2021
                                   "compression", "batch_size", NULL};
2022
    const char *path = NULL;
26✔
2023
    PyObject *views_obj = Py_None;
26✔
2024
    int chunk_size_mb = 32;
26✔
2025
    const char *compression_str = "zstd";
26✔
2026
    Py_ssize_t batch_size = 10000;
26✔
2027

2028
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|Oisn", (char **)kwlist,
26!
2029
                                     &path, &views_obj, &chunk_size_mb,
2030
                                     &compression_str, &batch_size)) {
2031
        return NULL;
×
2032
    }
2033

2034
    if (chunk_size_mb < 0) {
26✔
2035
        PyErr_SetString(PyExc_ValueError, "chunk_size_mb must be >= 0");
×
2036
        return NULL;
×
2037
    }
2038

2039
    std::vector<ViewDefinition> views;
26✔
2040
    if (views_obj && views_obj != Py_None) {
26!
2041
        if (!PyList_Check(views_obj)) {
10!
2042
            PyErr_SetString(PyExc_TypeError, "views must be a list or None");
×
2043
            return NULL;
×
2044
        }
2045
        Py_ssize_t n = PyList_Size(views_obj);
10!
2046
        for (Py_ssize_t i = 0; i < n; i++) {
22✔
2047
            PyObject *item = PyList_GetItem(views_obj, i);
12!
2048
            ViewDefinition vd;
12✔
2049

2050
            if (PyUnicode_Check(item)) {
12✔
2051
                const char *name = PyUnicode_AsUTF8(item);
2!
2052
                if (!name) return NULL;
2✔
2053
                std::string name_str(name);
2!
2054
                if (name_str == "io") {
2!
2055
                    vd = ViewDefinition::io_view();
2!
2056
                } else if (name_str == "compute") {
1!
2057
                    vd = ViewDefinition::compute_view();
×
2058
                } else if (name_str == "dlio") {
×
2059
                    vd = ViewDefinition::dlio_view();
×
2060
                } else {
2061
                    vd.with_name(name_str);
×
2062
                }
2063
            } else if (PyDict_Check(item)) {
12!
2064
                PyObject *name_obj = PyDict_GetItemString(item, "name");
10!
2065
                if (!name_obj || !PyUnicode_Check(name_obj)) {
10!
2066
                    PyErr_SetString(PyExc_ValueError,
×
2067
                                    "view dict must have 'name' string");
2068
                    return NULL;
×
2069
                }
2070
                vd.with_name(PyUnicode_AsUTF8(name_obj));
10!
2071

2072
                PyObject *query_obj = PyDict_GetItemString(item, "query");
10!
2073
                if (query_obj && query_obj != Py_None) {
10!
2074
                    if (!PyUnicode_Check(query_obj)) {
10!
2075
                        PyErr_SetString(PyExc_ValueError,
×
2076
                                        "view 'query' must be a string");
2077
                        return NULL;
×
2078
                    }
2079
                    vd.with_query(PyUnicode_AsUTF8(query_obj));
10!
2080
                }
5✔
2081

2082
                PyObject *meta_obj =
5✔
2083
                    PyDict_GetItemString(item, "include_metadata");
10!
2084
                if (meta_obj && meta_obj != Py_None) {
10!
2085
                    vd.with_include_metadata(PyObject_IsTrue(meta_obj));
2!
2086
                }
1✔
2087
            } else {
5✔
2088
                PyErr_SetString(PyExc_TypeError,
×
2089
                                "views list must contain strings or dicts");
2090
                return NULL;
×
2091
            }
2092
            views.push_back(std::move(vd));
12!
2093
        }
12✔
2094
    }
5✔
2095

2096
    IpcCompression compression = IpcCompression::ZSTD;
26✔
2097
    if (compression_str) {
26!
2098
        std::string comp_lower(compression_str);
26!
2099
        for (auto &c : comp_lower) c = std::tolower(c);
130!
2100
        if (comp_lower == "none") {
26✔
2101
            compression = IpcCompression::NONE;
2✔
2102
        } else if (comp_lower == "zstd") {
25✔
2103
#ifdef DFTRACER_UTILS_ENABLE_ZSTD
2104
            compression = IpcCompression::ZSTD;
24✔
2105
#else
2106
            PyErr_SetString(
2107
                PyExc_ValueError,
2108
                "ZSTD compression not available (built without ZSTD)");
2109
            return NULL;
2110
#endif
2111
        } else {
12✔
2112
            PyErr_Format(PyExc_ValueError,
×
2113
                         "Unknown compression: %s (use 'none' or 'zstd')",
2114
                         compression_str);
2115
            return NULL;
×
2116
        }
2117
    }
26✔
2118

2119
    int64_t chunk_size_bytes =
26✔
2120
        static_cast<int64_t>(chunk_size_mb) * 1024 * 1024;
26✔
2121

2122
    std::string file_path = PyUnicode_AsUTF8(self->file_path);
26!
2123
    std::string index_path;
26✔
2124
    const char *idx = PyUnicode_AsUTF8(self->index_dir);
26!
2125
    if (idx && idx[0] != '\0') {
26!
2126
        index_path = idx;
×
2127
    }
2128
    std::size_t checkpoint_size = self->checkpoint_size;
26✔
2129

2130
    std::string output_path(path);
26!
2131
    WriteArrowResult result;
26✔
2132
    if (!run_blocking_r(
26!
2133
            [&] {
39✔
2134
                Runtime *rt = get_runtime(self);
26✔
2135
                return rt
26✔
2136
                    ->submit(
65!
2137
                        write_arrow_pipeline(
52!
2138
                            file_path, index_path, checkpoint_size,
26!
2139
                            std::move(views), output_path, chunk_size_bytes,
26!
2140
                            compression, static_cast<std::size_t>(batch_size)),
26!
2141
                        "write_arrow")
13!
2142
                    .get();
39!
2143
            },
2144
            result)) {
2145
        return NULL;
×
2146
    }
2147

2148
    if (!result.error.empty()) {
26!
2149
        PyErr_SetString(PyExc_RuntimeError, result.error.c_str());
×
2150
        return NULL;
×
2151
    }
2152

2153
    // Build result dict
2154
    PyObject *dict = PyDict_New();
26!
2155
    if (!dict) return NULL;
26✔
2156

2157
    // Build files list per partition
2158
    PyObject *partitions_dict = PyDict_New();
26!
2159
    if (!partitions_dict) {
26!
2160
        Py_DECREF(dict);
×
2161
        return NULL;
×
2162
    }
2163

2164
    for (const auto &[partition_name, partition_stats] :
54!
2165
         result.stats.partitions) {
53✔
2166
        PyObject *partition_dict = PyDict_New();
28!
2167
        if (!partition_dict) {
28!
2168
            Py_DECREF(partitions_dict);
×
2169
            Py_DECREF(dict);
×
2170
            return NULL;
×
2171
        }
2172

2173
        PyObject *files_list = PyList_New(0);
28!
2174
        if (!files_list) {
28!
2175
            Py_DECREF(partition_dict);
×
2176
            Py_DECREF(partitions_dict);
×
2177
            Py_DECREF(dict);
×
2178
            return NULL;
×
2179
        }
2180

2181
        for (const auto &f : partition_stats.files) {
44✔
2182
            PyObject *file_str = PyUnicode_FromString(f.c_str());
16!
2183
            if (!file_str || PyList_Append(files_list, file_str) < 0) {
16!
2184
                Py_XDECREF(file_str);
×
2185
                Py_DECREF(files_list);
×
2186
                Py_DECREF(partition_dict);
×
2187
                Py_DECREF(partitions_dict);
×
2188
                Py_DECREF(dict);
×
2189
                return NULL;
×
2190
            }
2191
            Py_DECREF(file_str);
8!
2192
        }
2193

2194
        PyDict_SetItemString(partition_dict, "files", files_list);
28!
2195
        dict_set_steal(partition_dict, "rows",
28!
2196
                       PyLong_FromLongLong(partition_stats.total_rows));
28!
2197
        dict_set_steal(
28!
2198
            partition_dict, "bytes",
14✔
2199
            PyLong_FromLongLong(partition_stats.total_uncompressed_bytes));
28!
2200
        Py_DECREF(files_list);
14!
2201

2202
        PyObject *key = partition_name.empty()
42!
2203
                            ? PyUnicode_FromString("_default")
14!
2204
                            : PyUnicode_FromString(partition_name.c_str());
28!
2205
        PyDict_SetItem(partitions_dict, key, partition_dict);
28!
2206
        Py_DECREF(key);
14!
2207
        Py_DECREF(partition_dict);
14!
2208
    }
2209

2210
    PyDict_SetItemString(dict, "partitions", partitions_dict);
26!
2211
    dict_set_steal(dict, "total_rows",
26!
2212
                   PyLong_FromLongLong(result.stats.total_rows));
26!
2213
    dict_set_steal(dict, "total_bytes",
26!
2214
                   PyLong_FromLongLong(result.stats.total_uncompressed_bytes));
26!
2215
    dict_set_steal(dict, "chunks_scanned",
26!
2216
                   PyLong_FromUnsignedLongLong(result.chunks_scanned));
26!
2217
    dict_set_steal(dict, "chunks_skipped",
26!
2218
                   PyLong_FromUnsignedLongLong(result.chunks_skipped));
26!
2219
    Py_DECREF(partitions_dict);
13!
2220

2221
    return dict;
26✔
2222
}
26✔
2223

2224
static PyObject *TraceReader_get_view_chunks(TraceReaderObject *self,
6✔
2225
                                             PyObject *args, PyObject *kwds) {
2226
    static const char *kwlist[] = {"view", NULL};
2227
    PyObject *view_obj = Py_None;
6✔
2228

2229
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", (char **)kwlist,
6!
2230
                                     &view_obj)) {
2231
        return NULL;
×
2232
    }
2233

2234
    ViewDefinition view;
6✔
2235
    if (!parse_view_spec(view_obj, view, /*strict=*/true)) return NULL;
6!
2236

2237
    std::string file_path = PyUnicode_AsUTF8(self->file_path);
6!
2238
    std::string index_path;
6✔
2239
    const char *idx = PyUnicode_AsUTF8(self->index_dir);
6!
2240
    if (idx && idx[0] != '\0') {
6!
2241
        index_path = idx;
×
2242
    }
2243
    std::size_t checkpoint_size = self->checkpoint_size;
6✔
2244

2245
    GetViewChunksResult result;
6✔
2246
    if (!run_blocking_r(
6!
2247
            [&] {
9✔
2248
                Runtime *rt = get_runtime(self);
6✔
2249
                return rt
6✔
2250
                    ->submit(get_view_chunks_pipeline(file_path, index_path,
18!
2251
                                                      checkpoint_size, view),
6!
2252
                             "get_view_chunks")
3!
2253
                    .get();
9!
2254
            },
2255
            result)) {
2256
        return NULL;
×
2257
    }
2258

2259
    if (!result.error.empty()) {
6!
2260
        PyErr_SetString(PyExc_RuntimeError, result.error.c_str());
×
2261
        return NULL;
×
2262
    }
2263

2264
    PyObject *dict = PyDict_New();
6!
2265
    if (!dict) return NULL;
6✔
2266

2267
    PyObject *chunks_list = PyList_New(result.chunks.size());
6!
2268
    if (!chunks_list) {
6!
2269
        Py_DECREF(dict);
×
2270
        return NULL;
×
2271
    }
2272

2273
    for (std::size_t i = 0; i < result.chunks.size(); ++i) {
14✔
2274
        const auto &chunk = result.chunks[i];
8✔
2275
        PyObject *chunk_dict = PyDict_New();
8!
2276
        if (!chunk_dict) {
8!
2277
            Py_DECREF(chunks_list);
×
2278
            Py_DECREF(dict);
×
2279
            return NULL;
×
2280
        }
2281
        dict_set_steal(chunk_dict, "checkpoint_idx",
8!
2282
                       PyLong_FromUnsignedLongLong(chunk.checkpoint_idx));
8!
2283
        dict_set_steal(chunk_dict, "start_byte",
8!
2284
                       PyLong_FromSize_t(chunk.start_byte));
8!
2285
        dict_set_steal(chunk_dict, "end_byte",
8!
2286
                       PyLong_FromSize_t(chunk.end_byte));
8!
2287
        PyList_SetItem(chunks_list, i, chunk_dict);
8!
2288
    }
4✔
2289

2290
    PyDict_SetItemString(dict, "chunks", chunks_list);
6!
2291
    dict_set_steal(dict, "total_checkpoints",
6!
2292
                   PyLong_FromUnsignedLongLong(result.total_checkpoints));
6!
2293
    dict_set_steal(dict, "skipped_checkpoints",
6!
2294
                   PyLong_FromUnsignedLongLong(result.skipped_checkpoints));
6!
2295
    dict_set_steal(dict, "file_may_match",
6!
2296
                   PyBool_FromLong(result.file_may_match ? 1 : 0));
6✔
2297
    Py_DECREF(chunks_list);
3!
2298

2299
    return dict;
6✔
2300
}
6✔
2301

2302
static PyObject *TraceReader_write_view_chunk(TraceReaderObject *self,
2✔
2303
                                              PyObject *args, PyObject *kwds) {
2304
    static const char *kwlist[] = {
2305
        "output_file", "checkpoint_idx", "start_byte", "end_byte",
2306
        "view",        "compression",    "batch_size", NULL};
2307
    const char *output_file = NULL;
2✔
2308
    unsigned long long checkpoint_idx = 0;
2✔
2309
    Py_ssize_t start_byte = 0;
2✔
2310
    Py_ssize_t end_byte = 0;
2✔
2311
    PyObject *view_obj = Py_None;
2✔
2312
    const char *compression_str = "zstd";
2✔
2313
    Py_ssize_t batch_size = 10000;
2✔
2314

2315
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "sKnn|Osn", (char **)kwlist,
2!
2316
                                     &output_file, &checkpoint_idx, &start_byte,
2317
                                     &end_byte, &view_obj, &compression_str,
2318
                                     &batch_size)) {
2319
        return NULL;
×
2320
    }
2321

2322
    IpcCompression compression = IpcCompression::ZSTD;
2✔
2323
    if (compression_str) {
2✔
2324
        std::string comp_lower(compression_str);
2!
2325
        for (auto &c : comp_lower) c = std::tolower(c);
10!
2326
        if (comp_lower == "none") {
2!
2327
            compression = IpcCompression::NONE;
×
2328
        } else if (comp_lower == "zstd") {
2✔
2329
#ifdef DFTRACER_UTILS_ENABLE_ZSTD
2330
            compression = IpcCompression::ZSTD;
2✔
2331
#else
2332
            PyErr_SetString(PyExc_ValueError, "ZSTD compression not available");
2333
            return NULL;
2334
#endif
2335
        }
1✔
2336
    }
2✔
2337

2338
    ViewDefinition view;
2✔
2339
    if (!parse_view_spec(view_obj, view, /*strict=*/false)) return NULL;
2!
2340

2341
    std::string file_path = PyUnicode_AsUTF8(self->file_path);
2!
2342
    std::string index_path;
2✔
2343
    const char *idx = PyUnicode_AsUTF8(self->index_dir);
2!
2344
    if (idx && idx[0] != '\0') {
2!
2345
        index_path = idx;
×
2346
    }
2347
    std::size_t checkpoint_size = self->checkpoint_size;
2✔
2348

2349
    WriteViewChunkResult result;
2✔
2350
    if (!run_blocking_r(
2!
2351
            [&] {
3✔
2352
                Runtime *rt = get_runtime(self);
2✔
2353
                return rt
2✔
2354
                    ->submit(write_view_chunk_pipeline(
6!
2355
                                 file_path, index_path, checkpoint_size, view,
2!
2356
                                 checkpoint_idx,
2✔
2357
                                 static_cast<std::size_t>(start_byte),
2✔
2358
                                 static_cast<std::size_t>(end_byte),
2!
2359
                                 std::string(output_file), compression,
3!
2360
                                 static_cast<std::size_t>(batch_size)),
2✔
2361
                             "write_view_chunk")
1!
2362
                    .get();
3!
2363
            },
2364
            result)) {
2365
        return NULL;
×
2366
    }
2367

2368
    if (!result.error.empty()) {
2!
2369
        PyErr_SetString(PyExc_RuntimeError, result.error.c_str());
×
2370
        return NULL;
×
2371
    }
2372

2373
    PyObject *dict = PyDict_New();
2!
2374
    if (!dict) return NULL;
2✔
2375

2376
    dict_set_steal(dict, "output_file",
2!
2377
                   PyUnicode_FromString(result.output_file.c_str()));
1!
2378
    dict_set_steal(dict, "events_matched",
2!
2379
                   PyLong_FromUnsignedLongLong(result.events_matched));
2!
2380
    dict_set_steal(dict, "events_scanned",
2!
2381
                   PyLong_FromUnsignedLongLong(result.events_scanned));
2!
2382
    dict_set_steal(dict, "rows_written",
2!
2383
                   PyLong_FromLongLong(result.rows_written));
2!
2384
    dict_set_steal(dict, "bytes_written",
2!
2385
                   PyLong_FromLongLong(result.bytes_written));
2!
2386

2387
    return dict;
2✔
2388
}
2✔
2389

2390
static PyObject *TraceReader_write_view_chunks(TraceReaderObject *self,
2✔
2391
                                               PyObject *args, PyObject *kwds) {
2392
    static const char *kwlist[] = {"chunks",      "output_dir", "view",
2393
                                   "compression", "batch_size", NULL};
2394
    PyObject *chunks_list = NULL;
2✔
2395
    const char *output_dir = NULL;
2✔
2396
    PyObject *view_obj = Py_None;
2✔
2397
    const char *compression_str = "zstd";
2✔
2398
    Py_ssize_t batch_size = 10000;
2✔
2399

2400
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "Os|Osn", (char **)kwlist,
2!
2401
                                     &chunks_list, &output_dir, &view_obj,
2402
                                     &compression_str, &batch_size)) {
2403
        return NULL;
×
2404
    }
2405

2406
    if (!PyList_Check(chunks_list)) {
2✔
2407
        PyErr_SetString(PyExc_TypeError, "chunks must be a list");
×
2408
        return NULL;
×
2409
    }
2410

2411
    IpcCompression compression = IpcCompression::ZSTD;
2✔
2412
    if (strcmp(compression_str, "none") == 0) {
2✔
2413
        compression = IpcCompression::NONE;
×
2414
    } else if (strcmp(compression_str, "zstd") != 0) {
2!
2415
        PyErr_SetString(PyExc_ValueError,
×
2416
                        "compression must be 'zstd' or 'none'");
2417
        return NULL;
×
2418
    }
2419

2420
    ViewDefinition view;
2✔
2421
    if (!parse_view_spec(view_obj, view, /*strict=*/false)) return NULL;
2!
2422

2423
    std::vector<ChunkDescriptor> chunks;
2✔
2424
    Py_ssize_t num_chunks = PyList_Size(chunks_list);
2!
2425
    chunks.reserve(static_cast<std::size_t>(num_chunks));
2!
2426

2427
    for (Py_ssize_t i = 0; i < num_chunks; i++) {
8✔
2428
        PyObject *chunk_dict = PyList_GetItem(chunks_list, i);
6!
2429
        if (!PyDict_Check(chunk_dict)) {
6!
2430
            PyErr_SetString(PyExc_TypeError, "each chunk must be a dict");
×
2431
            return NULL;
×
2432
        }
2433

2434
        ChunkDescriptor desc;
6✔
2435

2436
        PyObject *cp_idx = PyDict_GetItemString(chunk_dict, "checkpoint_idx");
6!
2437
        PyObject *start = PyDict_GetItemString(chunk_dict, "start_byte");
6!
2438
        PyObject *end = PyDict_GetItemString(chunk_dict, "end_byte");
6!
2439

2440
        if (!cp_idx || !start || !end) {
6!
2441
            PyErr_SetString(
×
2442
                PyExc_KeyError,
2443
                "chunk must have checkpoint_idx, start_byte, end_byte");
2444
            return NULL;
×
2445
        }
2446

2447
        desc.checkpoint_idx =
6✔
2448
            static_cast<std::uint64_t>(PyLong_AsUnsignedLongLong(cp_idx));
6!
2449
        desc.start_byte =
6✔
2450
            static_cast<std::size_t>(PyLong_AsUnsignedLongLong(start));
6!
2451
        desc.end_byte =
6✔
2452
            static_cast<std::size_t>(PyLong_AsUnsignedLongLong(end));
6!
2453

2454
        char filename[64];
2455
        snprintf(filename, sizeof(filename), "chunk-%05llu.arrow",
9✔
2456
                 (unsigned long long)desc.checkpoint_idx);
6✔
2457
        desc.output_file = std::string(output_dir) + "/" + filename;
6!
2458

2459
        chunks.push_back(std::move(desc));
6!
2460
    }
6✔
2461

2462
    std::string file_path = PyUnicode_AsUTF8(self->file_path);
2!
2463
    std::string index_path;
2✔
2464
    const char *idx = PyUnicode_AsUTF8(self->index_dir);
2!
2465
    if (idx && idx[0] != '\0') {
2!
2466
        index_path = idx;
×
2467
    }
2468
    std::size_t checkpoint_size = self->checkpoint_size;
2✔
2469

2470
    WriteViewChunksResult result;
2✔
2471
    if (!run_blocking_r(
2!
2472
            [&] {
3✔
2473
                Runtime *rt = get_runtime(self);
2✔
2474
                return rt
2✔
2475
                    ->submit(write_view_chunks_pipeline(
6!
2476
                                 file_path, index_path, checkpoint_size, view,
2!
2477
                                 std::move(chunks), compression,
2✔
2478
                                 static_cast<std::size_t>(batch_size)),
2✔
2479
                             "write_view_chunks")
1!
2480
                    .get();
3!
2481
            },
2482
            result)) {
2483
        return NULL;
×
2484
    }
2485

2486
    PyObject *dict = PyDict_New();
2!
2487
    if (!dict) return NULL;
2✔
2488

2489
    PyObject *results_list =
1✔
2490
        PyList_New(static_cast<Py_ssize_t>(result.results.size()));
2!
2491
    if (!results_list) {
2!
2492
        Py_DECREF(dict);
×
2493
        return NULL;
×
2494
    }
2495

2496
    for (std::size_t i = 0; i < result.results.size(); i++) {
8✔
2497
        const auto &r = result.results[i];
6✔
2498
        PyObject *item = PyDict_New();
6!
2499
        if (!item) {
6!
2500
            Py_DECREF(results_list);
×
2501
            Py_DECREF(dict);
×
2502
            return NULL;
×
2503
        }
2504
        dict_set_steal(item, "output_file",
6!
2505
                       PyUnicode_FromString(r.output_file.c_str()));
3!
2506
        dict_set_steal(item, "rows_written",
6!
2507
                       PyLong_FromLongLong(r.rows_written));
6!
2508
        dict_set_steal(item, "events_matched",
6!
2509
                       PyLong_FromUnsignedLongLong(r.events_matched));
6!
2510
        if (!r.error.empty()) {
6!
2511
            dict_set_steal(item, "error",
×
2512
                           PyUnicode_FromString(r.error.c_str()));
×
2513
        }
2514
        PyList_SetItem(results_list, static_cast<Py_ssize_t>(i), item);
6!
2515
    }
3✔
2516

2517
    PyDict_SetItemString(dict, "results", results_list);
2!
2518
    Py_DECREF(results_list);
1!
2519
    dict_set_steal(dict, "total_rows", PyLong_FromLongLong(result.total_rows));
2!
2520
    dict_set_steal(dict, "total_events_matched",
2!
2521
                   PyLong_FromLongLong(result.total_events_matched));
2!
2522

2523
    return dict;
2✔
2524
}
2✔
2525

2526
#endif  // DFTRACER_UTILS_ENABLE_ARROW_IPC
2527

2528
static PyObject *TraceReader_enter(TraceReaderObject *self,
142!
2529
                                   PyObject *Py_UNUSED(ignored)) {
2530
    Py_INCREF(self);
71✔
2531
    return (PyObject *)self;
142✔
2532
}
2533

2534
static PyObject *TraceReader_exit(TraceReaderObject *self, PyObject *args) {
140✔
2535
    Py_RETURN_NONE;
140✔
2536
}
2537

2538
static PyObject *TraceReader_get_file_path(TraceReaderObject *self,
14✔
2539
                                           void *closure) {
2540
    Py_INCREF(self->file_path);
14!
2541
    return self->file_path;
14✔
2542
}
2543

2544
static PyObject *TraceReader_get_index_dir(TraceReaderObject *self,
6✔
2545
                                           void *closure) {
2546
    Py_INCREF(self->index_dir);
6✔
2547
    return self->index_dir;
6✔
2548
}
2549

2550
static PyObject *TraceReader_get_has_index(TraceReaderObject *self,
16✔
2551
                                           void *closure) {
2552
    return PyBool_FromLong(self->has_index);
16✔
2553
}
2554

2555
static PyObject *TraceReader_get_num_lines_prop(TraceReaderObject *self,
8✔
2556
                                                void *closure) {
2557
    try {
2558
        TraceReaderConfig cfg = build_config(self);
8!
2559
        TraceReader reader(std::move(cfg));
8!
2560
        std::size_t n = reader.get_num_lines();
8!
2561
        if (n > 0) return PyLong_FromSize_t(n);
8!
2562
    } catch (...) {
8!
2563
    }
×
2564
    PyObject *empty_args = PyTuple_New(0);
8✔
2565
    if (!empty_args) return NULL;
8✔
2566
    PyObject *list = TraceReader_read_lines(self, empty_args, NULL);
8✔
2567
    Py_DECREF(empty_args);
4✔
2568
    if (!list) return NULL;
8✔
2569
    Py_ssize_t n = PyList_GET_SIZE(list);
8✔
2570
    Py_DECREF(list);
4✔
2571
    return PyLong_FromSsize_t(n);
8✔
2572
}
4✔
2573

2574
static PyObject *TraceReader_get_max_bytes(TraceReaderObject *self,
24✔
2575
                                           PyObject *Py_UNUSED(ignored)) {
2576
    try {
2577
        TraceReaderConfig cfg = build_config(self);
24!
2578
        TraceReader reader(std::move(cfg));
24!
2579
        return PyLong_FromSize_t(reader.get_max_bytes());
24!
2580
    } catch (const std::exception &e) {
24!
2581
        set_typed_py_error(e);
×
2582
        return NULL;
×
2583
    }
×
2584
}
12✔
2585

2586
static PyObject *TraceReader_get_num_lines(TraceReaderObject *self,
6✔
2587
                                           PyObject *Py_UNUSED(ignored)) {
2588
    try {
2589
        TraceReaderConfig cfg = build_config(self);
6!
2590
        TraceReader reader(std::move(cfg));
6!
2591
        return PyLong_FromSize_t(reader.get_num_lines());
6!
2592
    } catch (const std::exception &e) {
6!
2593
        set_typed_py_error(e);
×
2594
        return NULL;
×
2595
    }
×
2596
}
3✔
2597

2598
static PyMethodDef TraceReader_methods[] = {
2599
    {"iter_lines", (PyCFunction)TraceReader_iter_lines,
2600
     METH_VARARGS | METH_KEYWORDS,
2601
     "Return an iterator over decoded lines.\n"
2602
     "\n"
2603
     "Args:\n"
2604
     "    start_line (int): First line (0 = beginning).\n"
2605
     "    end_line (int): Last line (0 = end of file).\n"
2606
     "    start_byte (int): First byte offset (0 = beginning).\n"
2607
     "    end_byte (int): Last byte offset (0 = end of file).\n"
2608
     "    buffer_size (int): Internal read buffer size in bytes.\n"},
2609
    {"iter_raw", (PyCFunction)TraceReader_iter_raw,
2610
     METH_VARARGS | METH_KEYWORDS,
2611
     "Return an iterator over raw byte chunks.\n"
2612
     "\n"
2613
     "Args:\n"
2614
     "    start_line (int): First line (0 = beginning).\n"
2615
     "    end_line (int): Last line (0 = end of file).\n"
2616
     "    start_byte (int): First byte offset (0 = beginning).\n"
2617
     "    end_byte (int): Last byte offset (0 = end of file).\n"
2618
     "    buffer_size (int): Internal read buffer size in bytes.\n"
2619
     "    line_aligned (bool): Align chunks to line boundaries.\n"
2620
     "    multi_line (bool): Allow multiple lines per chunk.\n"},
2621
    {"read_lines", (PyCFunction)TraceReader_read_lines,
2622
     METH_VARARGS | METH_KEYWORDS,
2623
     "Read all lines and return as list.\n"
2624
     "\n"
2625
     "Args:\n"
2626
     "    start_line (int): First line (0 = beginning).\n"
2627
     "    end_line (int): Last line (0 = end of file).\n"
2628
     "    start_byte (int): First byte offset (0 = beginning).\n"
2629
     "    end_byte (int): Last byte offset (0 = end of file).\n"
2630
     "    buffer_size (int): Internal read buffer size in bytes.\n"},
2631
    {"iter_json", (PyCFunction)TraceReader_iter_json,
2632
     METH_VARARGS | METH_KEYWORDS,
2633
     "Return an iterator over parsed JSON events as Python dicts.\n"
2634
     "\n"
2635
     "Each event is parsed once in C++ (single-pass simdjson ondemand)\n"
2636
     "and yielded as a Python dict. No double-parsing overhead.\n"
2637
     "\n"
2638
     "Args:\n"
2639
     "    start_line (int): First line (0 = beginning).\n"
2640
     "    end_line (int): Last line (0 = end of file).\n"
2641
     "    start_byte (int): First byte offset (0 = beginning).\n"
2642
     "    end_byte (int): Last byte offset (0 = end of file).\n"
2643
     "    buffer_size (int): Internal read buffer size in bytes.\n"
2644
     "    query (str): Optional query filter.\n"
2645
     "    batch_size (int): Events per internal batch (default 1024).\n"},
2646
    {"read_json", (PyCFunction)TraceReader_read_json_py,
2647
     METH_VARARGS | METH_KEYWORDS,
2648
     "Read all events as parsed Python dicts (list).\n"
2649
     "\n"
2650
     "Equivalent to list(iter_json(...)).\n"},
2651
    {"read_raw", (PyCFunction)TraceReader_read_raw,
2652
     METH_VARARGS | METH_KEYWORDS,
2653
     "Read all raw chunks and return as list.\n"
2654
     "\n"
2655
     "Args:\n"
2656
     "    start_line (int): First line (0 = beginning).\n"
2657
     "    end_line (int): Last line (0 = end of file).\n"
2658
     "    start_byte (int): First byte offset (0 = beginning).\n"
2659
     "    end_byte (int): Last byte offset (0 = end of file).\n"
2660
     "    buffer_size (int): Internal read buffer size in bytes.\n"
2661
     "    line_aligned (bool): Align chunks to line boundaries.\n"
2662
     "    multi_line (bool): Allow multiple lines per chunk.\n"},
2663
#ifdef DFTRACER_UTILS_ENABLE_ARROW
2664
    {"iter_arrow", (PyCFunction)TraceReader_iter_arrow,
2665
     METH_VARARGS | METH_KEYWORDS,
2666
     "Return an iterator over Arrow record batches.\n"
2667
     "\n"
2668
     "Args:\n"
2669
     "    batch_size (int): Maximum rows per Arrow batch.\n"
2670
     "    start_line (int): First line (0 = beginning).\n"
2671
     "    end_line (int): Last line (0 = end of file).\n"
2672
     "    start_byte (int): First byte offset (0 = beginning).\n"
2673
     "    end_byte (int): Last byte offset (0 = end of file).\n"
2674
     "    buffer_size (int): Internal read buffer size in bytes.\n"},
2675
    {"iter_arrow_stream", (PyCFunction)TraceReader_iter_arrow_stream,
2676
     METH_VARARGS | METH_KEYWORDS,
2677
     "Return an _ArrowBatchStream that exposes Arrow record batches\n"
2678
     "via the Arrow C Data Interface stream protocol\n"
2679
     "(__arrow_c_stream__). PyArrow can drain the producer channel\n"
2680
     "with a single call, without per-batch Python iteration.\n"},
2681
    {"read_arrow", (PyCFunction)TraceReader_read_arrow,
2682
     METH_VARARGS | METH_KEYWORDS,
2683
     "Read all events as a materialized ArrowTable.\n"
2684
     "\n"
2685
     "Args:\n"
2686
     "    batch_size (int): Maximum rows per Arrow batch.\n"
2687
     "    start_line (int): First line (0 = beginning).\n"
2688
     "    end_line (int): Last line (0 = end of file).\n"
2689
     "    start_byte (int): First byte offset (0 = beginning).\n"
2690
     "    end_byte (int): Last byte offset (0 = end of file).\n"
2691
     "    buffer_size (int): Internal read buffer size in bytes.\n"},
2692
#endif
2693
#ifdef DFTRACER_UTILS_ENABLE_ARROW_IPC
2694
    {"write_arrow", (PyCFunction)TraceReader_write_arrow,
2695
     METH_VARARGS | METH_KEYWORDS,
2696
     "Write trace data to partitioned Arrow IPC files.\n"
2697
     "\n"
2698
     "Args:\n"
2699
     "    path (str): Output directory path.\n"
2700
     "    partition_by (list[str] or None): Column names to partition by.\n"
2701
     "    num_buckets (int): Number of hash buckets (0 = no bucketing).\n"
2702
     "    chunk_size_mb (int): Max uncompressed MB per file (default 32).\n"
2703
     "    compression (str): 'zstd' or 'none' (default 'zstd').\n"
2704
     "    batch_size (int): Rows per internal batch (default 10000).\n"
2705
     "    normalize (bool): Use normalized schema (default False).\n"
2706
     "\n"
2707
     "Returns:\n"
2708
     "    dict: Statistics including partitions, total_rows, total_bytes.\n"},
2709
    {"get_view_chunks", (PyCFunction)TraceReader_get_view_chunks,
2710
     METH_VARARGS | METH_KEYWORDS,
2711
     "Get candidate chunks for a view after bloom filter pruning.\n"
2712
     "\n"
2713
     "Args:\n"
2714
     "    view (str or dict): View name ('io', 'compute', 'dlio') or\n"
2715
     "                        dict with 'name' and optional 'query'.\n"
2716
     "\n"
2717
     "Returns:\n"
2718
     "    dict: chunks list, total_checkpoints, skipped_checkpoints.\n"},
2719
    {"write_view_chunk", (PyCFunction)TraceReader_write_view_chunk,
2720
     METH_VARARGS | METH_KEYWORDS,
2721
     "Write a single chunk to an Arrow IPC file.\n"
2722
     "\n"
2723
     "Args:\n"
2724
     "    output_file (str): Path to output Arrow IPC file.\n"
2725
     "    checkpoint_idx (int): Checkpoint index.\n"
2726
     "    start_byte (int): Start byte offset.\n"
2727
     "    end_byte (int): End byte offset.\n"
2728
     "    view (str or dict): View definition.\n"
2729
     "    compression (str): 'zstd' or 'none' (default 'zstd').\n"
2730
     "    batch_size (int): Events per batch (default 10000).\n"
2731
     "\n"
2732
     "Returns:\n"
2733
     "    dict: output_file, events_matched, rows_written, bytes_written.\n"},
2734
    {"write_view_chunks", (PyCFunction)TraceReader_write_view_chunks,
2735
     METH_VARARGS | METH_KEYWORDS,
2736
     "Write multiple chunks to Arrow IPC files in parallel.\n"
2737
     "\n"
2738
     "All chunks are processed concurrently on the Runtime thread pool.\n"
2739
     "\n"
2740
     "Args:\n"
2741
     "    chunks (list): List of dicts with checkpoint_idx, start_byte, "
2742
     "end_byte.\n"
2743
     "    output_dir (str): Directory for output Arrow IPC files.\n"
2744
     "    view (str or dict): View definition.\n"
2745
     "    compression (str): 'zstd' or 'none' (default 'zstd').\n"
2746
     "    batch_size (int): Events per batch (default 10000).\n"
2747
     "\n"
2748
     "Returns:\n"
2749
     "    dict: results list, total_rows, total_events_matched.\n"},
2750
#endif
2751
    {"get_max_bytes", (PyCFunction)TraceReader_get_max_bytes, METH_NOARGS,
2752
     "Get the maximum byte position (0 if unknown for compressed\n"
2753
     "files without index)."},
2754
    {"get_num_lines", (PyCFunction)TraceReader_get_num_lines, METH_NOARGS,
2755
     "Get the total number of lines (0 if unknown for files without\n"
2756
     "index)."},
2757
    {"__enter__", (PyCFunction)TraceReader_enter, METH_NOARGS,
2758
     "Enter the runtime context for the with statement."},
2759
    {"__exit__", (PyCFunction)TraceReader_exit, METH_VARARGS,
2760
     "Exit the runtime context for the with statement.\n"
2761
     "\n"
2762
     "TraceReader does not own the shared RocksDB instance for an index path;\n"
2763
     "any shared DB lifetime remains manager-owned on the native side."},
2764
    {NULL}};
2765

2766
static PyGetSetDef TraceReader_getsetters[] = {
2767
    {"path", (getter)TraceReader_get_file_path, NULL,
2768
     "Path to the trace file or directory", NULL},
2769
    {"index_dir", (getter)TraceReader_get_index_dir, NULL,
2770
     "Directory for index files", NULL},
2771
    {"has_index", (getter)TraceReader_get_has_index, NULL,
2772
     "True if a checkpoint index was found", NULL},
2773
    {"num_lines", (getter)TraceReader_get_num_lines_prop, NULL,
2774
     "Total line count (reads all lines if needed)", NULL},
2775
    {NULL}};
2776

2777
PyTypeObject TraceReaderType = {
2778
    PyVarObject_HEAD_INIT(NULL, 0) "dftracer_utils_ext.TraceReader",
2779
    sizeof(TraceReaderObject),                /* tp_basicsize */
2780
    0,                                        /* tp_itemsize */
2781
    (destructor)TraceReader_dealloc,          /* tp_dealloc */
2782
    0,                                        /* tp_vectorcall_offset */
2783
    0,                                        /* tp_getattr */
2784
    0,                                        /* tp_setattr */
2785
    0,                                        /* tp_as_async */
2786
    0,                                        /* tp_repr */
2787
    0,                                        /* tp_as_number */
2788
    0,                                        /* tp_as_sequence */
2789
    0,                                        /* tp_as_mapping */
2790
    0,                                        /* tp_hash */
2791
    0,                                        /* tp_call */
2792
    0,                                        /* tp_str */
2793
    0,                                        /* tp_getattro */
2794
    0,                                        /* tp_setattro */
2795
    0,                                        /* tp_as_buffer */
2796
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
2797
    "TraceReader(file_path: str, index_dir: str = '',\n"
2798
    "            checkpoint_size: int = 33554432,\n"
2799
    "            auto_build_index: bool = False,\n"
2800
    "            runtime: Runtime | None = None)\n"
2801
    "--\n"
2802
    "\n"
2803
    "Smart trace file reader that auto-selects sequential or indexed\n"
2804
    "reading based on whether a ``.dftindex`` store exists.\n"
2805
    "\n"
2806
    "Args:\n"
2807
    "    file_path (str): Path to the trace file (.pfw.gz or plain "
2808
    "text).\n"
2809
    "    index_dir (str): Directory to search for ``.dftindex`` "
2810
    "stores.\n"
2811
    "        Empty string (default) searches next to the trace file.\n"
2812
    "    checkpoint_size (int): Checkpoint interval in bytes for index\n"
2813
    "        building (default 32 MB).\n"
2814
    "    auto_build_index (bool): If True, automatically build an "
2815
    "index\n"
2816
    "        when none exists.\n"
2817
    "    runtime (Runtime or None): Runtime instance for thread pool "
2818
    "control.\n"
2819
    "        If None, uses the default global Runtime.\n"
2820
    "\n"
2821
    "Raises:\n"
2822
    "    RuntimeError: If *file_path* does not exist or cannot be "
2823
    "opened.\n",                /* tp_doc */
2824
    0,                          /* tp_traverse */
2825
    0,                          /* tp_clear */
2826
    0,                          /* tp_richcompare */
2827
    0,                          /* tp_weaklistoffset */
2828
    0,                          /* tp_iter */
2829
    0,                          /* tp_iternext */
2830
    TraceReader_methods,        /* tp_methods */
2831
    0,                          /* tp_members */
2832
    TraceReader_getsetters,     /* tp_getset */
2833
    0,                          /* tp_base */
2834
    0,                          /* tp_dict */
2835
    0,                          /* tp_descr_get */
2836
    0,                          /* tp_descr_set */
2837
    0,                          /* tp_dictoffset */
2838
    (initproc)TraceReader_init, /* tp_init */
2839
    0,                          /* tp_alloc */
2840
    TraceReader_new,            /* tp_new */
2841
};
2842

2843
int init_trace_reader(PyObject *m) {
2✔
2844
    if (register_type(m, &TraceReaderType, "TraceReader") < 0) return -1;
2✔
2845

2846
    return 0;
2✔
2847
}
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